From f60474681b8b37c08f95550818decaffb0bee617 Mon Sep 17 00:00:00 2001 From: pb Date: Tue, 25 Feb 2025 13:03:52 +0100 Subject: [PATCH] Added routes and methods to create admiralty resources : secrets, target sources --- controllers/admiralty.go | 224 ++++++++++++++++++++++++++++++ infrastructure/interface.go | 5 + infrastructure/kubernetes.go | 254 +++++++++++++++++++++++++++++++++-- 3 files changed, 474 insertions(+), 9 deletions(-) diff --git a/controllers/admiralty.go b/controllers/admiralty.go index 80dfee1..6b8f50d 100644 --- a/controllers/admiralty.go +++ b/controllers/admiralty.go @@ -1,12 +1,25 @@ package controllers import ( + "encoding/json" + "fmt" "oc-datacenter/infrastructure" "slices" beego "github.com/beego/beego/v2/server/web" ) +type KubeInfo struct { + Url *string + KubeCA *string + KubeCert *string + KubeKey *string +} + +type Kubeconfig struct { + Data *string +} + // Operations about the admiralty objects of the datacenter type AdmiraltyController struct { beego.Controller @@ -57,4 +70,215 @@ func (c *AdmiraltyController) GetOneTarget() { c.Data["json"] = id c.ServeJSON() +} + +// @Title CreateSource +// @Description Create an Admiralty Source on remote cluster +// @Param dc_id path string true "which dc to contact" +// @Param execution path string true "execution id of the workflow" +// @Param serviceAccount body controllers.KubeInfo true "url and serviceAccount to use with the source formatted as json object" +// @Success 200 +// @router /sources/:dc_id/:execution [post] +func (c *AdmiraltyController) CreateSource() { + var data KubeInfo + json.Unmarshal(c.Ctx.Input.CopyBody(10000000),&data) + if data.Url == nil || data.KubeCA == nil || data.KubeCert == nil|| data.KubeKey == nil { + c.Ctx.Output.SetStatus(500) + c.ServeJSON() + missingData := fmt.Sprint(data) + c.Data["json"] = map[string]string{"error" : "Missing something in " + missingData} + c.ServeJSON() + } + fmt.Println("") + fmt.Println("URL : %v", data.Url) + fmt.Println("") + fmt.Println("CA : %v", data.KubeCA) + fmt.Println("") + fmt.Println("Key : ", data.KubeKey) + + dc_id := c.Ctx.Input.Param(":dc_id") + execution := c.Ctx.Input.Param(":execution") + _ = dc_id + serv, err := infrastructure.NewRemoteKubernetesService( + *data.Url, + *data.KubeCA, + *data.KubeCert, + *data.KubeKey, + ) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.ServeJSON() + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + res, err := serv.CreateAdmiraltySource(execution) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + // TODO : Return a description of the created resource + var respData map[string]interface{} + err = json.Unmarshal(res,&respData) + c.Data["json"] = respData + c.ServeJSON() + +} + +// @Title CreateAdmiraltyTarget +// @Description Create an Admiralty Target in the namespace associated to the executionID +// @Param dc_id path string true "which dc to contact" +// @Param execution path string true "execution id of the workflow" +// @Success 201 +// @router /target/:dc_id/:execution [post] +func (c *AdmiraltyController) CreateAdmiraltyTarget(){ + var data map[string]interface{} + dc_id := c.Ctx.Input.Param(":dc_id") + execution := c.Ctx.Input.Param(":execution") + _ = dc_id + + serv, err := infrastructure.NewService() + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + resp, err := serv.CreateAdmiraltyTarget(execution) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + if resp == nil { + fmt.Println("Error while trying to create Admiralty target") + fmt.Println(resp) + fmt.Println(err) + c.Ctx.Output.SetStatus(401) + c.Data["json"] = map[string]string{"error" : "Could not perform the action" } + c.ServeJSON() + return + } + + err = json.Unmarshal(resp,&data) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.ServeJSON() + c.Data["json"] = map[string]string{"error": err.Error()} + return + } + c.Data["json"] = data + c.ServeJSON() + +} + +// @Title GetKubeSecret +// @Description Retrieve the secret created from a Kubeconfig that will be associated to an Admiralty Target +// @Param dc_id path string true "which dc to contact" +// @Param execution path string true "execution id of the workflow" +// @Success 200 +// @router /secret/:dc_id/:execution [get] +func(c *AdmiraltyController) GetKubeSecret() { + var data map[string]interface{} + dc_id := c.Ctx.Input.Param(":dc_id") + execution := c.Ctx.Input.Param(":execution") + _ = dc_id + + serv, err := infrastructure.NewService() + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + resp, err := serv.GetKubeconfigSecret(execution) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + if resp == nil { + c.Ctx.Output.SetStatus(404) + c.ServeJSON() + return + } + + err = json.Unmarshal(resp,&data) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.ServeJSON() + c.Data["json"] = map[string]string{"error": err.Error()} + return + } + + c.Data["json"] = data + c.ServeJSON() +} + + +// @Title CreateKubeSecret +// @Description Creat a secret from a Kubeconfig that will be associated to an Admiralty Target +// @Param dc_id path string true "which dc to contact" +// @Param execution path string true "execution id of the workflow" +// @Param kubeconfig body controllers.Kubeconfig true "Kubeconfig to use when creating secret" +// @Success 200 +// @router /secret/:dc_id/:execution [post] +func (c *AdmiraltyController) CreateKubeSecret() { + var kubeconfig Kubeconfig + var respData map[string]interface{} + + data := c.Ctx.Input.CopyBody(100000) + + err := json.Unmarshal(data, &kubeconfig) + if err != nil { + fmt.Println("Error when retrieving the data for kubeconfig from request") + fmt.Println(err) + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + dc_id := c.Ctx.Input.Param(":dc_id") + execution := c.Ctx.Input.Param(":execution") + _ = dc_id + + serv, err := infrastructure.NewService() + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + resp, err := serv.CreateKubeconfigSecret(*kubeconfig.Data,execution) + if err != nil { + // change code to 500 + c.Ctx.Output.SetStatus(500) + c.Data["json"] = map[string]string{"error": err.Error()} + c.ServeJSON() + return + } + + err = json.Unmarshal(resp,&respData) + c.Data["json"] = respData + c.ServeJSON() + } \ No newline at end of file diff --git a/infrastructure/interface.go b/infrastructure/interface.go index f152602..fe9d170 100644 --- a/infrastructure/interface.go +++ b/infrastructure/interface.go @@ -14,6 +14,10 @@ type Infrastructure interface { CreateRoleBinding(ctx context.Context, ns string, roleBinding string, role string) error CreateRole(ctx context.Context, ns string, role string, groups [][]string, resources [][]string, verbs [][]string) error GetTargets(ctx context.Context) ([]string,error) + CreateAdmiraltySource(executionId string) ([]byte, error) + CreateKubeconfigSecret(kubeconfig string, executionId string) ([]byte, error) + GetKubeconfigSecret(executionId string) ([]byte, error) + CreateAdmiraltyTarget(executionId string)([]byte,error) } var _service = map[string]func() (Infrastructure, error){ @@ -27,3 +31,4 @@ func NewService() (Infrastructure, error) { } return service() } + diff --git a/infrastructure/kubernetes.go b/infrastructure/kubernetes.go index 0c99268..6fa24b5 100644 --- a/infrastructure/kubernetes.go +++ b/infrastructure/kubernetes.go @@ -1,15 +1,19 @@ package infrastructure import ( + "bytes" "context" + "encoding/base64" "encoding/json" "errors" "fmt" + "html/template" "oc-datacenter/conf" authv1 "k8s.io/api/authentication/v1" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -43,6 +47,34 @@ func NewKubernetesService() (Infrastructure, error) { }, nil } +func NewRemoteKubernetesService(url string, ca string, cert string, key string) (Infrastructure, error) { + decodedCa, _ := base64.StdEncoding.DecodeString(ca) + decodedCert, _ := base64.StdEncoding.DecodeString(cert) + decodedKey, _ := base64.StdEncoding.DecodeString(key) + + config := &rest.Config{ + Host: url + ":6443", + TLSClientConfig: rest.TLSClientConfig{ + CAData: decodedCa, + CertData: decodedCert, + KeyData: decodedKey, + }, + } + // Create clientset + clientset, err := kubernetes.NewForConfig(config) + fmt.Println("NewForConfig", clientset, err) + if err != nil { + return nil, errors.New("Error creating Kubernetes client: " + err.Error()) + } + if clientset == nil { + return nil, errors.New("Error creating Kubernetes client: clientset is nil") + } + + return &KubernetesService{ + Set: clientset, + }, nil +} + func (k *KubernetesService) CreateNamespace(ctx context.Context, ns string) error { // Define the namespace namespace := &v1.Namespace{ @@ -158,19 +190,16 @@ func (k *KubernetesService) GetToken(ctx context.Context, ns string, duration in return token.Status.Token, nil } +// Needs refactoring : +// - Retrieving the metada (in a method that Unmarshall the part of the json in a metadata object) func (k *KubernetesService) GetTargets(ctx context.Context) ([]string,error){ var listTargets []string - resp, err := k.Set.RESTClient(). - Get(). - AbsPath("/apis/multicluster.admiralty.io/v1alpha1/targets"). - DoRaw(ctx) // from https://stackoverflow.com/questions/60764908/how-to-access-kubernetes-crd-using-client-go - + resp, err := getCDRapiKube(*k.Set, ctx,"/apis/multicluster.admiralty.io/v1alpha1/targets") if err != nil { - fmt.Println("TODO : handle the error generated when contacting kube API") - fmt.Println("Error from k8s API : ", err) return nil,err } + fmt.Println(string(resp)) var targetDict map[string]interface{} err = json.Unmarshal(resp,&targetDict) @@ -201,8 +230,215 @@ func (k *KubernetesService) GetTargets(ctx context.Context) ([]string,error){ listTargets = append(listTargets, metadata.Name) } - // parse targets to retrieve the info we need - // fmt.Println(targetDict) + return listTargets,nil +} + +// Admiralty Target allows a cluster to deploy pods to remote cluster +// +// The remote cluster must : +// +// - have declared a Source resource +// +// - have declared the same namespace as the one where the pods are created in the local cluster +// +// - have delcared a serviceAccount with sufficient permission to create pods +func (k *KubernetesService) CreateAdmiraltyTarget(executionId string)([]byte,error){ + exists, err := k.GetKubeconfigSecret(executionId) + if err != nil { + fmt.Println("Error verifying kube-secret before creating target") + return nil, err + } + + if exists == nil { + fmt.Println("Target needs to be binded to a secret in ns-",executionId) + return nil, nil // Maybe we could create a wrapper for errors and add more info to have + } + + var targetManifest string + var tpl bytes.Buffer + tmpl, err := template.New("target"). + Parse("{\"apiVersion\": \"multicluster.admiralty.io/v1alpha1\", \"kind\": \"Target\", \"metadata\": {\"name\": \"target-{{.ExecutionId}}\"}, \"spec\": { \"kubeconfigSecret\" :{\"name\": \"kube-secret-{{.ExecutionId}}\"}} }") + if err != nil { + fmt.Println("Error creating the template for the target Manifest") + return nil, err + } + + err = tmpl.Execute(&tpl, map[string]string{"ExecutionId":executionId}) + targetManifest = tpl.String() + resp, err := postCDRapiKube( + *k.Set, + context.TODO(), + "/apis/multicluster.admiralty.io/v1alpha1/namespaces/ns-"+ executionId +"/targets", + []byte(targetManifest), + map[string]string{"fieldManager":"kubectl-client-side-apply"}, + map[string]string{"fieldValidation":"Strict"}, + ) + + if err != nil { + fmt.Println("Error trying to create a Source on remote cluster : ", err , " : ", resp) + return nil, err + } + + return resp, nil +} + + +// Admiralty Source allows a cluster to receive pods from a remote cluster +// +// The source must be associated to a serviceAccount, which will execute the pods locally. +// This serviceAccount must have sufficient permission to create and patch pods +// +// This method is temporary to implement the use of Admiralty, but must be edited +// to rather contact the oc-datacenter from the remote cluster to create the source +// locally and retrieve the token for the serviceAccount +func (k *KubernetesService) CreateAdmiraltySource(executionId string) ([]byte, error) { + var sourceManifest string + var tpl bytes.Buffer + tmpl, err := template.New("source"). + Parse("{\"apiVersion\": \"multicluster.admiralty.io/v1alpha1\", \"kind\": \"Source\", \"metadata\": {\"name\": \"source-{{.ExecutionId}}\"}, \"spec\": {\"serviceAccountName\": \"sa-{{.ExecutionId}}\"} }") + if err != nil { + fmt.Println("Error creating the template for the source Manifest") + return nil, err + } + + err = tmpl.Execute(&tpl, map[string]string{"ExecutionId":executionId}) + sourceManifest = tpl.String() + + resp, err := postCDRapiKube( + *k.Set, + context.TODO(), + "/apis/multicluster.admiralty.io/v1alpha1/namespaces/ns-"+ executionId +"/sources", + []byte(sourceManifest), + map[string]string{"fieldManager":"kubectl-client-side-apply"}, + map[string]string{"fieldValidation":"Strict"}, + ) + + // We can add more info to the log with the content of resp if not nil + if err != nil { + fmt.Println("Error trying to create a Source on remote cluster : ", err , " : ", resp) + return nil, err + } + + return resp, nil +} + +// Create a secret from a kubeconfing. Use it to create the secret binded to an Admiralty +// target, which must contain the serviceAccount's token value +func (k *KubernetesService) CreateKubeconfigSecret(kubeconfig string, executionId string) ([]byte, error) { + config, err := base64.StdEncoding.DecodeString(kubeconfig) + // config, err := base64.RawStdEncoding.DecodeString(kubeconfig) + if err != nil { + fmt.Println("Error while encoding kubeconfig") + fmt.Println(err) + return nil, err + } + + secretManifest := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-secret-" + executionId, + Namespace: "ns-" + executionId, + }, + Data: map[string][]byte{ + "config": config, + }, + } + + exists, err := k.GetKubeconfigSecret(executionId) + if err != nil { + fmt.Println("Error verifying if kube secret exists in ns-", executionId) + return nil, err + } + if exists != nil { + fmt.Println("kube-secret already exists in ns-", executionId) + fmt.Println("Overriding existing kube-secret with a newer resource") + // TODO : implement DeleteKubeConfigSecret(executionID) + deleted, err := k.DeleteKubeConfigSecret(executionId) + _ = deleted + _ = err + } + resp, err := k.Set.CoreV1(). + Secrets("ns-"+executionId). + Create(context.TODO(),secretManifest,metav1.CreateOptions{}) + + if err != nil { + fmt.Println("Error while trying to contact API to get secret kube-secret-"+executionId) + fmt.Println(err) + return nil, err + } + + data, err := json.Marshal(resp) + if err != nil { + fmt.Println("Couldn't marshal resp from : ", data) + fmt.Println(err) + return nil, err + } + return data, nil +} + +func (k *KubernetesService) GetKubeconfigSecret(executionId string) ([]byte, error) { + resp, err := k.Set.CoreV1(). + Secrets("ns-"+executionId). + Get(context.TODO(),"kube-secret-"+executionId,metav1.GetOptions{}) + + if err != nil { + if(apierrors.IsNotFound(err)){ + fmt.Println("kube-secret not found for execution ", executionId) + return nil, nil + } + fmt.Println("Error while trying to contact API to get secret kube-secret-"+executionId) + fmt.Println(err) + return nil, err + } + + data, err := json.Marshal(resp) + + if err != nil { + fmt.Println("Couldn't marshal resp from : ", data) + fmt.Println(err) + return nil, err + } + + return data, nil +} + +func (k *KubernetesService) DeleteKubeConfigSecret(executionID string) ([]byte, error){ + + return []byte{}, nil +} + +func getCDRapiKube(client kubernetes.Clientset, ctx context.Context, path string) ([]byte,error) { + resp, err := client.RESTClient().Get(). + AbsPath(path). + DoRaw(ctx) // from https://stackoverflow.com/questions/60764908/how-to-access-kubernetes-crd-using-client-go + + if err != nil { + fmt.Println("Error from k8s API when getting " + path + " : " , err) + return nil,err + } + + return resp, nil +} + +func postCDRapiKube(client kubernetes.Clientset, ctx context.Context, path string, body []byte, params ...map[string]string) ([]byte, error){ + req := client.RESTClient(). + Post(). + AbsPath(path). + Body(body) + + for _, param := range params { + for k,v := range param { + req = req.Param(k,v) + } + } + + resp, err := req.DoRaw(ctx) + + if err != nil { + fmt.Println("Error from k8s API when posting " + string(body) + " to " + path + " : " , err) + return nil,err + } + + return resp, nil } \ No newline at end of file