package controllers import ( "context" "errors" "fmt" "strings" "github.com/go-logr/logr" "github.com/go-logr/zapr" "github.com/google/uuid" "github.com/joho/godotenv" "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "git.badhouseplants.net/softplayer/softplayer-backend/internal/consts" "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" ) type Environemnt struct { Controller ctrl.Manager Config *rest.Config UserID string Data *EnvironemntData Token string } type EnvironemntData struct { UUID string Name string Description string Provider string Kubernetes string Location string ServerType string DiskSize int } func (e *EnvironemntData) buildVars() (string, error) { // Please make sure that the same variables are used by ansible vars := fmt.Sprintf(`# -- Generated by the softplayer controller SP_PROVIDER=%s SP_KUBERNETES=%s SP_SERVER_TYPE=%s SP_SERVER_LOCATION=%s SP_DISK_SIZE=%d`, e.Provider, e.Kubernetes, e.ServerType, e.Location, e.DiskSize, ) return vars, nil } // Check whether used has passed the email verification func (env *Environemnt) isNsVerified(ctx context.Context) error { log, err := logr.FromContext(ctx) if err != nil { zapLog, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } log = zapr.NewLogger(zapLog) } clientset, err := kubernetes.NewForConfig(env.Config) if err != nil { log.Error(err, "Couldn't create a new clientset") return consts.ErrSystemError } ns, err := clientset.CoreV1().Namespaces().Get(ctx, env.UserID, metav1.GetOptions{}) if err != nil { log.Error(err, "Couldn't get a user's namespace") return consts.ErrSystemError } val, ok := ns.GetLabels()["email-verified"] if !ok || val == "false" { return errors.New("user email is not verified, can't create an new env") } return nil } // Create environment should create a new configmap in the user's namespace // using a token that belongs to the user. func (env *Environemnt) Create(ctx context.Context) error { log, err := logr.FromContext(ctx) if err != nil { zapLog, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } log = zapr.NewLogger(zapLog) } if err := env.isNsVerified(ctx); err != nil { return status.Error(codes.Unauthenticated, err.Error()) } // Prepare a new ID for a enironment env.Data.UUID = uuid.New().String() env.Controller.GetClient() conf := &rest.Config{ Host: "https://kubernetes.default.svc.cluster.local:443", BearerToken: env.Token, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, }, } controller, err := ctrl.NewManager(conf, ctrl.Options{}) if err != nil { log.Error(err, "Couldn't init a controller") return consts.ErrSystemError } vars, err := env.Data.buildVars() if err != nil { log.Error(err, "Couldn't build the environment's dotenv file", "environment_id", env.Data.UUID) return consts.ErrSystemError } obj := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: env.Data.UUID, Namespace: env.UserID, Labels: map[string]string{ "component": "bootstrap", "kind": "environment", }, }, Data: map[string]string{ "name": env.Data.Name, "description": env.Data.Description, "vars": vars, }, } if err := kube.Create(ctx, controller.GetClient(), &obj, false); err != nil { log.Error(err, "Couln't create the environment's configmap", "environment_id", env.Data.UUID) return consts.ErrSystemError } return nil } func (env *Environemnt) Update(ctx context.Context) error { log, err := logr.FromContext(ctx) if err != nil { zapLog, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } log = zapr.NewLogger(zapLog) } env.Controller.GetClient() conf := &rest.Config{ Host: "https://kubernetes.default.svc.cluster.local:443", BearerToken: env.Token, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, }, } controller, err := ctrl.NewManager(conf, ctrl.Options{}) if err != nil { log.Error(err, "Couldn't init a controller") return consts.ErrSystemError } oldEnv := &Environemnt{ Controller: env.Controller, UserID: env.UserID, Token: env.Token, Data: &EnvironemntData{ UUID: env.Data.UUID, }, } if err := oldEnv.Get(ctx); err != nil { log.Error(err, "Couldn't get environment's configmap", "environment_id", env.Data.UUID) return consts.ErrSystemError } // Check whter immutable fields are changed if oldEnv.Data.Provider != env.Data.Provider { return errors.New("provider can't be changed") } if oldEnv.Data.Location != env.Data.Location { return errors.New("location can't be changed") } vars, err := env.Data.buildVars() if err != nil { log.Error(err, "Couldn't build the environment's dotenv file", "environment_id", env.Data.UUID) return consts.ErrSystemError } obj := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: env.Data.UUID, Namespace: env.UserID, Labels: map[string]string{ "component": "bootstrap", "kind": "environment", }, }, Data: map[string]string{ "name": env.Data.Name, "description": env.Data.Description, "vars": vars, }, } if err := kube.Update(ctx, controller.GetClient(), &obj); err != nil { log.Error(err, "Couln't update the environment's configmap", "environment_id", env.Data.UUID) return consts.ErrSystemError } return nil } func (env *Environemnt) Delete(ctx context.Context) error { log, err := logr.FromContext(ctx) if err != nil { zapLog, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } log = zapr.NewLogger(zapLog) } env.Controller.GetClient() conf := &rest.Config{ Host: "https://kubernetes.default.svc.cluster.local:443", BearerToken: env.Token, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, }, } controller, err := ctrl.NewManager(conf, ctrl.Options{}) if err != nil { log.Error(err, "couldn't init a controller") return consts.ErrSystemError } obj := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: env.Data.UUID, Namespace: env.UserID, Labels: map[string]string{ "component": "bootstrap", }, }, } if err := kube.Delete(ctx, controller.GetClient(), &obj, false); err != nil { log.Error(err, "Couln't remove environment's configmap", "environment_id", env.Data.UUID) return consts.ErrSystemError } return nil } func (env *Environemnt) List(ctx context.Context, searchString string) ([]*Environemnt, error) { log, err := logr.FromContext(ctx) if err != nil { zapLog, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } log = zapr.NewLogger(zapLog) } env.Controller.GetClient() conf := &rest.Config{ Host: "https://kubernetes.default.svc.cluster.local:443", BearerToken: env.Token, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, }, } clientset, err := kubernetes.NewForConfig(conf) if err != nil { log.Error(err, "Couldn't create a new clientset") return nil, consts.ErrSystemError } configmaps, err := clientset.CoreV1().ConfigMaps(env.UserID).List(ctx, metav1.ListOptions{LabelSelector: "kind=environment"}) if err != nil { log.Error(err, "Couldn't list configmaps") return nil, consts.ErrSystemError } result := []*Environemnt{} for _, cm := range configmaps.Items { i := &Environemnt{} data := &EnvironemntData{ UUID: cm.GetName(), } i.Token = env.Token i.UserID = env.UserID i.Data = data i.Controller = env.Controller if err := i.Get(ctx); err != nil { log.Error(err, "Couldn't get an environment", "environment_id", i.Data.UUID) return nil, consts.ErrSystemError } if len(searchString) > 0 { if strings.Contains(i.Data.Name, searchString) { result = append(result, i) } if strings.Contains(i.Data.Description, searchString) { result = append(result, i) } } else { result = append(result, i) } } return result, nil } func (env *Environemnt) Get(ctx context.Context) error { log, err := logr.FromContext(ctx) if err != nil { zapLog, err := zap.NewDevelopment() if err != nil { panic(fmt.Sprintf("who watches the watchmen (%v)?", err)) } log = zapr.NewLogger(zapLog) } env.Controller.GetClient() conf := &rest.Config{ Host: "https://kubernetes.default.svc.cluster.local:443", BearerToken: env.Token, TLSClientConfig: rest.TLSClientConfig{ Insecure: true, }, } clientset, err := kubernetes.NewForConfig(conf) if err != nil { log.Error(err, "Couldn't create a new clientset") return consts.ErrSystemError } envData, err := clientset.CoreV1().ConfigMaps(env.UserID).Get(ctx, env.Data.UUID, metav1.GetOptions{}) if err != nil { log.Error(err, "Couldn't get an environment's configmap", "environment_id", env.Data.UUID) return consts.ErrSystemError } res, err := godotenv.Unmarshal(envData.Data["vars"]) if err != nil { log.Error(err, "Couldn't parse environment's dotenv file from a configmap", "environment_id", env.Data.UUID) return consts.ErrSystemError } if val, ok := envData.Data["name"]; ok { env.Data.Name = val } else { env.Data.Name = "" } if val, ok := envData.Data["description"]; ok { env.Data.Description = val } else { env.Data.Description = "" } if val, ok := res["SP_PROVIDER"]; ok { env.Data.Provider = val } else { env.Data.Provider = "" } if val, ok := res["SP_KUBERNETES"]; ok { env.Data.Kubernetes = val } else { env.Data.Kubernetes = "" } if val, ok := res["SP_SERVER_TYPE"]; ok { env.Data.ServerType = val } else { env.Data.ServerType = "" } if val, ok := res["SP_SERVER_LOCATION"]; ok { env.Data.Location = val } else { env.Data.Location = "" } return nil }