From affabc3c039b2bd4dae94dc2766bd569ead9ee33 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Wed, 3 Apr 2024 20:05:23 +0200 Subject: [PATCH] Create an env using api --- api/v1/accounts.go | 3 +- api/v1/email.go | 34 +++++++--- api/v1/environments.go | 33 ++++++++-- dataexample.yaml | 4 ++ go.mod | 6 +- go.sum | 4 +- internal/consts/consts.go | 9 +++ internal/controllers/accounts.go | 11 ++-- internal/controllers/email.go | 56 +++++++++-------- internal/controllers/environments.go | 92 +++++++++++++++++++++++++--- internal/helpers/kube/kube.go | 13 ++-- internal/helpers/kube/kube_test.go | 4 -- internal/providers/hetzner.go | 9 +++ main.go | 11 ++-- 14 files changed, 213 insertions(+), 76 deletions(-) create mode 100644 dataexample.yaml create mode 100644 internal/consts/consts.go create mode 100644 internal/providers/hetzner.go diff --git a/api/v1/accounts.go b/api/v1/accounts.go index e7bf7a9..ab87953 100644 --- a/api/v1/accounts.go +++ b/api/v1/accounts.go @@ -21,7 +21,7 @@ func NewAccountRPCImpl(contoller ctrl.Manager, hashCost int16) *AccountsServer { type AccountsServer struct { accounts.UnimplementedAccountsServer Controller ctrl.Manager - Params *controllers.AccountParams + Params *controllers.AccountParams } func (a *AccountsServer) SignUp(ctx context.Context, in *accounts.AccountWithPassword) (*accounts.AccountFullWithToken, error) { @@ -78,4 +78,3 @@ func populateAccount(data *controllers.AccountData, controller ctrl.Manager) *co Data: data, } } - diff --git a/api/v1/email.go b/api/v1/email.go index 85001e4..0b33083 100644 --- a/api/v1/email.go +++ b/api/v1/email.go @@ -7,47 +7,61 @@ import ( "git.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email" proto_email "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/email" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/emptypb" ctrl "sigs.k8s.io/controller-runtime" ) type EmailServer struct { proto_email.UnimplementedEmailValidationServer - emailConfig email.EmailConf - controller ctrl.Manager - + emailConfig email.EmailConf + controller ctrl.Manager + devMode bool } -func InitEmailServer(controller ctrl.Manager, emailConfig *email.EmailConf) *EmailServer { +func InitEmailServer(controller ctrl.Manager, emailConfig *email.EmailConf, devMode bool) *EmailServer { return &EmailServer{ - controller: controller, + controller: controller, emailConfig: *emailConfig, + devMode: devMode, } } func (c *EmailServer) SendRequest(ctx context.Context, in *proto_email.RequestValidation) (*emptypb.Empty, error) { - emailSvc := controllers.EmailSvc { + emailSvc := controllers.EmailSvc{ Data: controllers.EmailData{ UserID: in.GetUserId(), }, EmailConfig: c.emailConfig, - Controller: c.controller, + Controller: c.controller, + DevMode: c.devMode, } err := emailSvc.SendVerification(ctx) if err != nil { return nil, err } + if c.devMode { + header := metadata.Pairs("code", emailSvc.Data.Code) + if err := grpc.SendHeader(ctx, header); err != nil { + return nil, err + } + trailer := metadata.Pairs("trailer-key", "val") + if err := grpc.SetTrailer(ctx, trailer); err != nil { + return nil, err + } + } return &emptypb.Empty{}, nil } func (c *EmailServer) ValidateEmail(ctx context.Context, in *proto_email.ConfirmValidation) (*emptypb.Empty, error) { - emailSvc := controllers.EmailSvc { + emailSvc := controllers.EmailSvc{ Data: controllers.EmailData{ UserID: in.GetUserId(), - Code: fmt.Sprintf("%d", in.GetCode()), + Code: fmt.Sprintf("%d", in.GetCode()), }, EmailConfig: c.emailConfig, - Controller: c.controller, + Controller: c.controller, } err := emailSvc.ConfirmVerification(ctx) if err != nil { diff --git a/api/v1/environments.go b/api/v1/environments.go index 606ad74..2ed1090 100644 --- a/api/v1/environments.go +++ b/api/v1/environments.go @@ -2,28 +2,51 @@ package v1 import ( "context" + "errors" "git.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" proto "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments" "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc/metadata" + ctrl "sigs.k8s.io/controller-runtime" ) -func NewapiGrpcImpl() *EnvironmentsServer { - return &EnvironmentsServer{} +func NewapiGrpcImpl(controller ctrl.Manager) *EnvironmentsServer { + return &EnvironmentsServer{ + controller: controller, + } } type EnvironmentsServer struct { proto.UnimplementedEnvironmentsServer + controller ctrl.Manager } func (e *EnvironmentsServer) Create(ctx context.Context, in *proto.EnvironmentData) (*proto.EnvironmentFull, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, errors.New("metadata is not provided") + } + token, ok := md["token"] + if !ok { + return nil, errors.New("token is not sent via metadata") + } + + uuid, ok := md["uuid"] + if !ok { + return nil, errors.New("used id is not sent via metadata") + } + data := &controllers.EnvironemntData{ - Name: in.GetName(), - Provider: in.GetProvider().String(), + Name: in.GetName(), + Provider: in.GetProvider().String(), + Kubernetes: in.GetKubernetes().String(), } environment := &controllers.Environemnt{ - Controller: nil, + UserID: uuid[0], + Controller: e.controller, Data: data, + Token: token[0], } err := environment.Create(ctx) if err != nil { diff --git a/dataexample.yaml b/dataexample.yaml new file mode 100644 index 0000000..1857901 --- /dev/null +++ b/dataexample.yaml @@ -0,0 +1,4 @@ +provider: hetzner +servers: + - name: some name + kind: cax11 diff --git a/go.mod b/go.mod index d760a14..bba42f7 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( golang.org/x/crypto v0.21.0 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 + k8s.io/client-go v0.29.3 sigs.k8s.io/controller-runtime v0.17.2 ) @@ -55,7 +56,6 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.3 // indirect - k8s.io/client-go v0.29.3 // indirect k8s.io/component-base v0.29.3 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect @@ -66,12 +66,12 @@ require ( ) require ( - git.badhouseplants.net/softplayer/softplayer-go-proto v0.1.2 + git.badhouseplants.net/softplayer/softplayer-go-proto v0.1.3 github.com/golang/protobuf v1.5.4 golang.org/x/net v0.22.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/grpc v1.62.1 - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.33.0 ) diff --git a/go.sum b/go.sum index 370d8df..f9379f8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.badhouseplants.net/softplayer/softplayer-go-proto v0.1.2 h1:kKL9lOWIRzzkf9mJCVQWOCj6AAodwm/IeOaIDBs5J+8= -git.badhouseplants.net/softplayer/softplayer-go-proto v0.1.2/go.mod h1:OU+833cHwvecr+gsnPEKQYlAJbpL8bqSJVLobdw63qI= +git.badhouseplants.net/softplayer/softplayer-go-proto v0.1.3 h1:c0q0nz9bT1TXUaMI7G+RII6mzZI1Tg0xWn2ToKekh4c= +git.badhouseplants.net/softplayer/softplayer-go-proto v0.1.3/go.mod h1:OU+833cHwvecr+gsnPEKQYlAJbpL8bqSJVLobdw63qI= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..c01687d --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1,9 @@ +package consts + +const ( + USERNAME_LABEL_KEY = "username" + EMAIL_VERIFIED_LABEL_KEY = "email-verified" + EMAIL_VERIFIED_LABEL_TRUE = "true" + EMAIL_VERIFIED_LABEL_FALSE = "false" + SOFTPLAYER_ACCOUNTS_NAMESPACE = "softplayer-accounts" +) diff --git a/internal/controllers/accounts.go b/internal/controllers/accounts.go index 04e5212..bd34ee7 100644 --- a/internal/controllers/accounts.go +++ b/internal/controllers/accounts.go @@ -47,13 +47,13 @@ func (acc *Account) Create(ctx context.Context) error { ObjectMeta: metav1.ObjectMeta{ Name: acc.Data.UUID, Labels: map[string]string{ - "username": acc.Data.Username, + "username": acc.Data.Username, "email-verified": "false", }, }, } - - if err := kube.Create(ctx,client, namespace, true); err != nil { + + if err := kube.Create(ctx, client, namespace, true); err != nil { return err } @@ -85,7 +85,7 @@ func (acc *Account) Create(ctx context.Context) error { } return err } - + // Prepare RBAC resources for the account role := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{Name: acc.Data.Username, Namespace: acc.Data.UUID}, @@ -105,7 +105,7 @@ func (acc *Account) Create(ctx context.Context) error { Namespace: acc.Data.UUID, }, } - + if err := client.Create(ctx, sa); err != nil { if err := client.Delete(ctx, namespace); err != nil { return err @@ -113,7 +113,6 @@ func (acc *Account) Create(ctx context.Context) error { return err } - rb := &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: acc.Data.UUID, diff --git a/internal/controllers/email.go b/internal/controllers/email.go index b93c9c2..05e49d6 100644 --- a/internal/controllers/email.go +++ b/internal/controllers/email.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "errors" + "fmt" "io" "log" @@ -17,21 +18,22 @@ import ( ) type EmailSvc struct { - Controller ctrl.Manager - Data EmailData - EmailConfig email.EmailConf + Controller ctrl.Manager + Data EmailData + EmailConfig email.EmailConf + DevMode bool } type EmailData struct { UserID string - Code string + Code string } func (svc *EmailSvc) SendVerification(ctx context.Context) error { - client := svc.Controller.GetClient() + client := svc.Controller.GetClient() userns := &corev1.Namespace{} if err := client.Get(ctx, types.NamespacedName{ - Name: svc.Data.UserID, + Name: svc.Data.UserID, }, userns); err != nil { return err } @@ -48,17 +50,20 @@ func (svc *EmailSvc) SendVerification(ctx context.Context) error { return err } number := encodeToString(6) - email := string(accountData.Data["email"]) - if err := svc.EmailConfig.SendEmail(email, number); err != nil { - return err + svc.Data.Code = number + if !svc.DevMode { + emailContent := "Subject: Softplayer verification code\r\n" + "\r\n" + fmt.Sprintf("Your verification code is %s", number) + email := string(accountData.Data["email"]) + if err := svc.EmailConfig.SendEmail(email, emailContent); err != nil { + return err + } } - emailCode := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: "email-verification-code", - Namespace: svc.Data.UserID, + Name: "email-verification-code", + Namespace: svc.Data.UserID, }, - Data: map[string]string{ + Data: map[string]string{ "code": number, }, } @@ -66,7 +71,7 @@ func (svc *EmailSvc) SendVerification(ctx context.Context) error { if err := kube.Create(ctx, client, &emailCode, true); err != nil { return err } - return nil + return nil } func (svc *EmailSvc) ConfirmVerification(ctx context.Context) error { @@ -74,7 +79,7 @@ func (svc *EmailSvc) ConfirmVerification(ctx context.Context) error { emailCode := &corev1.ConfigMap{} if err := client.Get(ctx, types.NamespacedName{ Namespace: svc.Data.UserID, - Name: "email-verification-code", + Name: "email-verification-code", }, emailCode); err != nil { return err } @@ -90,10 +95,11 @@ func (svc *EmailSvc) ConfirmVerification(ctx context.Context) error { userns := &corev1.Namespace{} if err := client.Get(ctx, types.NamespacedName{ - Name: svc.Data.UserID, + Name: svc.Data.UserID, }, userns); err != nil { return err } + userns.Labels["email-verified"] = "true" if err := client.Update(ctx, userns); err != nil { return err @@ -102,15 +108,15 @@ func (svc *EmailSvc) ConfirmVerification(ctx context.Context) error { } func encodeToString(max int) string { - b := make([]byte, max) - n, err := io.ReadAtLeast(rand.Reader, b, max) - if n != max { - panic(err) - } - for i := 0; i < len(b); i++ { - b[i] = table[int(b[i])%len(table)] - } - return string(b) + b := make([]byte, max) + n, err := io.ReadAtLeast(rand.Reader, b, max) + if n != max { + panic(err) + } + for i := 0; i < len(b); i++ { + b[i] = table[int(b[i])%len(table)] + } + return string(b) } var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} diff --git a/internal/controllers/environments.go b/internal/controllers/environments.go index b40253b..ecdb75e 100644 --- a/internal/controllers/environments.go +++ b/internal/controllers/environments.go @@ -2,26 +2,104 @@ package controllers import ( "context" + "errors" + "fmt" "log" + "strings" + "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/apimachinery/pkg/types" + "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" ) type Environemnt struct { Controller ctrl.Manager + UserID string Data *EnvironemntData + Token string } type EnvironemntData struct { - Name string - Provider string + Name string + Provider string + Kubernetes string + HetznerData HetznerData } -func (env *Environemnt) Create(ctx context.Context) error { - log.Printf("%s", env.Data.Name) - log.Printf("%s", env.Data.Provider) +type HetznerData struct { + ServerLocation string + ServerType string +} + +func (e *EnvironemntData) buildVars() string { + vars := fmt.Sprintf("SP_PROVIDER=%s\nSP_KUBERNETES=%s", e.providerFmt(), e.kubernetesFmt()) + return vars +} + +func (e *EnvironemntData) providerFmt() string { + res := strings.Replace(e.Provider, "PROVIDER_", "", -1) + return strings.ToLower(res) +} + +func (e *EnvironemntData) kubernetesFmt() string { + res := strings.Replace(e.Kubernetes, "KUBERNETES", "", -1) + return strings.ToLower(res) +} + +func (env *Environemnt) isNsVerified(ctx context.Context) error { + client := env.Controller.GetClient() + ns := &corev1.Namespace{} + if err := client.Get(ctx, types.NamespacedName{Name: env.UserID}, ns); err != nil { + return err + } + + 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 { + if err := env.isNsVerified(ctx); err != nil { + log.Println("Can't verify ns") + return err + } + 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 { + return err + } + + obj := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: env.Data.Name, + Namespace: env.UserID, + Labels: map[string]string{ + "component": "bootstrap", + }, + }, + Data: map[string]string{ + "vars": env.Data.buildVars(), + }, + } + if err := kube.Create(ctx, controller.GetClient(), &obj, false); err != nil { + return err + } - // Create a configmap - // return nil } diff --git a/internal/helpers/kube/kube.go b/internal/helpers/kube/kube.go index 6e6b303..c8c9af8 100644 --- a/internal/helpers/kube/kube.go +++ b/internal/helpers/kube/kube.go @@ -14,7 +14,7 @@ func Create(ctx context.Context, client client.Client, obj client.Object, wait b if err := client.Create(ctx, obj); err != nil { return err } - if wait{ + if wait { if err := WaitUntilCreated(ctx, client, obj, 10, time.Millisecond*50); err != nil { return err } @@ -23,13 +23,13 @@ func Create(ctx context.Context, client client.Client, obj client.Object, wait b } func SetOwnerRef(ctx context.Context, client client.Client, obj client.Object, owner client.Object) client.Object { - apiVersion := fmt.Sprintf("%s/%s", owner.GetObjectKind().GroupVersionKind().Group, owner.GetObjectKind().GroupVersionKind().Version) + apiVersion := fmt.Sprintf("%s/%s", owner.GetObjectKind().GroupVersionKind().Group, owner.GetObjectKind().GroupVersionKind().Version) ownerReference := []metav1.OwnerReference{ { - APIVersion: apiVersion, - Kind: owner.GetObjectKind().GroupVersionKind().GroupKind().Kind, - Name: owner.GetName(), - UID: owner.GetUID(), + APIVersion: apiVersion, + Kind: owner.GetObjectKind().GroupVersionKind().GroupKind().Kind, + Name: owner.GetName(), + UID: owner.GetUID(), }, } obj.SetOwnerReferences(ownerReference) @@ -52,4 +52,3 @@ func WaitUntilCreated(ctx context.Context, client client.Client, obj client.Obje } return nil } - diff --git a/internal/helpers/kube/kube_test.go b/internal/helpers/kube/kube_test.go index 6983d5d..4122d93 100644 --- a/internal/helpers/kube/kube_test.go +++ b/internal/helpers/kube/kube_test.go @@ -1,12 +1,8 @@ package kube_test - import ( "testing" "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube" "github.com/alecthomas/assert/v2" ) - - - diff --git a/internal/providers/hetzner.go b/internal/providers/hetzner.go new file mode 100644 index 0000000..39aedfd --- /dev/null +++ b/internal/providers/hetzner.go @@ -0,0 +1,9 @@ +package providers + +// Hetzner supported regions +const ( + HETZNER_REG_FINLAND = "fn" +) + +type Hetzner struct { +} diff --git a/main.go b/main.go index 3aa8aa6..f24a848 100644 --- a/main.go +++ b/main.go @@ -8,8 +8,8 @@ import ( v1 "git.badhouseplants.net/softplayer/softplayer-backend/api/v1" "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email" "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts" - "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments" email_proto "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/email" + "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments" "github.com/alecthomas/kong" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -17,10 +17,11 @@ import ( ) type Serve struct { - Port int16 `short:"p" env:"SOFTPLAYER_PORT" default:"4020"` + Port int64 `short:"p" env:"SOFTPLAYER_PORT" default:"4020"` Host string `env:"SOFTPLAYER_HOST" default:"0.0.0.0"` HashCost int16 `env:"SOFTPLAYER_HASH_COST" default:"1"` Reflection bool `env:"SOFTPLAYER_REFLECTION" default:"false"` + DevMode bool `env:"SOFTPLAYER_DEV_MODE" default:"false"` SmtpHost string `env:"SOFTPLAYER_SMTP_HOST"` SmtpPort string `env:"SOFTPLAYER_SMTP_PORT" default:"587"` SmtpFrom string `env:"SOFTPLAYER_SMTP_FROM" default:"overlord@badhouseplants.net"` @@ -62,6 +63,7 @@ func server(params Serve) error { SmtpHost: params.SmtpHost, SmtpPort: params.SmtpPort, } + address := fmt.Sprintf("%s:%d", params.Host, params.Port) lis, err := net.Listen("tcp", address) if err != nil { @@ -72,10 +74,9 @@ func server(params Serve) error { reflection.Register(grpcServer) } - - environments.RegisterEnvironmentsServer(grpcServer, v1.NewapiGrpcImpl()) + environments.RegisterEnvironmentsServer(grpcServer, v1.NewapiGrpcImpl(controller)) accounts.RegisterAccountsServer(grpcServer, v1.NewAccountRPCImpl(controller, params.HashCost)) - email_proto.RegisterEmailValidationServer(grpcServer, v1.InitEmailServer(controller,&emailConfig)) + email_proto.RegisterEmailValidationServer(grpcServer, v1.InitEmailServer(controller, &emailConfig, params.DevMode)) if err := grpcServer.Serve(lis); err != nil { return err }