From 74ac2b6d9c6a80cc71ba69124209c44881c730ca Mon Sep 17 00:00:00 2001 From: pb Date: Thu, 27 Feb 2025 17:00:36 +0100 Subject: [PATCH] Uniformisation and verification of admiralty link with nodes + token --- controllers/admiralty.go | 119 +++++++++++++++++++++++++++++++++-- controllers/booking.go | 10 ++- controllers/session.go | 2 +- infrastructure/interface.go | 13 ++-- infrastructure/kubernetes.go | 64 +++++++++++++------ 5 files changed, 176 insertions(+), 32 deletions(-) diff --git a/controllers/admiralty.go b/controllers/admiralty.go index 6b8f50d..7a2c61d 100644 --- a/controllers/admiralty.go +++ b/controllers/admiralty.go @@ -5,8 +5,12 @@ import ( "fmt" "oc-datacenter/infrastructure" "slices" + "time" beego "github.com/beego/beego/v2/server/web" + jwt "github.com/golang-jwt/jwt/v5" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" ) type KubeInfo struct { @@ -20,6 +24,12 @@ type Kubeconfig struct { Data *string } +type KubeUser struct { + Name string + User struct { + Token string + } +} // Operations about the admiralty objects of the datacenter type AdmiraltyController struct { beego.Controller @@ -76,9 +86,9 @@ func (c *AdmiraltyController) GetOneTarget() { // @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" +// @Param kubeconfigInfo body controllers.KubeInfo true "url and serviceAccount to use with the source formatted as json object" // @Success 200 -// @router /sources/:dc_id/:execution [post] +// @router /source/:dc_id/:execution [post] func (c *AdmiraltyController) CreateSource() { var data KubeInfo json.Unmarshal(c.Ctx.Input.CopyBody(10000000),&data) @@ -114,7 +124,7 @@ func (c *AdmiraltyController) CreateSource() { return } - res, err := serv.CreateAdmiraltySource(execution) + res, err := serv.CreateAdmiraltySource(c.Ctx.Request.Context(),execution) if err != nil { // change code to 500 c.Ctx.Output.SetStatus(500) @@ -152,7 +162,7 @@ func (c *AdmiraltyController) CreateAdmiraltyTarget(){ return } - resp, err := serv.CreateAdmiraltyTarget(execution) + resp, err := serv.CreateAdmiraltyTarget(c.Ctx.Request.Context(),execution) if err != nil { // change code to 500 c.Ctx.Output.SetStatus(500) @@ -204,7 +214,7 @@ func(c *AdmiraltyController) GetKubeSecret() { return } - resp, err := serv.GetKubeconfigSecret(execution) + resp, err := serv.GetKubeconfigSecret(c.Ctx.Request.Context(),execution) if err != nil { // change code to 500 c.Ctx.Output.SetStatus(500) @@ -268,7 +278,7 @@ func (c *AdmiraltyController) CreateKubeSecret() { return } - resp, err := serv.CreateKubeconfigSecret(*kubeconfig.Data,execution) + resp, err := serv.CreateKubeconfigSecret(c.Ctx.Request.Context(),*kubeconfig.Data,execution) if err != nil { // change code to 500 c.Ctx.Output.SetStatus(500) @@ -281,4 +291,101 @@ func (c *AdmiraltyController) CreateKubeSecret() { c.Data["json"] = respData c.ServeJSON() +} + +// @name GetAdmiraltyNodes +// @description Allows user to test if an admiralty connection has already been established : Target and valid Secret set up on the local host and Source set up on remote host +// @Param dc_id path string true "which dc to contact" +// @Param execution path string true "execution id of the workflow" +// @Success 200 +// @Success 203 +// @router /node/:dc_id/:execution [get] +func (c *AdmiraltyController) GetNodeReady(){ + var secret v1.Secret + 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 + } + + node, err := serv.GetOneNode(c.Ctx.Request.Context(),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 node == nil { + c.Ctx.Output.SetStatus(404) + c.Data["json"] = map[string]string{ + "error" : "the node for " + execution + " can't be found, make sure both target and source resources are set up on local and remote hosts", + } + c.ServeJSON() + return + } + + resp, err := serv.GetKubeconfigSecret(c.Ctx.Request.Context(),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(500) + c.Data["json"] = map[string]string{"error": "Nodes was up but the secret can't be found"} + c.ServeJSON() + return + } + + + // Extract JWT token RS265 encoded + var editedKubeconfig map[string]interface{} + var kubeUsers []KubeUser + json.Unmarshal(resp,&secret) + byteEditedKubeconfig := secret.Data["config"] + err = yaml.Unmarshal(byteEditedKubeconfig,&editedKubeconfig) + // err = json.Unmarshal(byteEditedKubeconfig,&editedKubeconfig) + if err != nil { + fmt.Println("Error while retrieving the kubeconfig from secret-",execution) + fmt.Println(err) + c.Ctx.Output.SetStatus(500) + c.Data["json"] = err + c.ServeJSON() + return + } + b, err := json.Marshal(editedKubeconfig["users"]) + err = yaml.Unmarshal(b,&kubeUsers) + token := kubeUsers[0].User.Token + + // Decode token + t, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{}) + if err != nil { + fmt.Println("couldn't decode token") + c.Data["json"] = false + c.ServeJSON() + } + + expiration, err := t.Claims.GetExpirationTime() + fmt.Println("Expiration date : " + expiration.UTC().Format("2006-01-02T15:04:05")) + + if expiration.Add(1 * time.Hour).Unix() < time.Now().Unix() { + c.Data["json"] = map[string]string{ + "token" : "token in the secret is expired and must be regenerated", + } + c.ServeJSON() + } + + c.Data["json"] = map[string]bool{"ok": true} + c.ServeJSON() + } \ No newline at end of file diff --git a/controllers/booking.go b/controllers/booking.go index c2ce1ee..c9885de 100644 --- a/controllers/booking.go +++ b/controllers/booking.go @@ -21,6 +21,8 @@ type BookingController struct { beego.Controller } +var BookingExample booking.Booking + // @Title Search // @Description search bookings by execution // @Param id path string true "id execution" @@ -209,7 +211,13 @@ func (o *BookingController) Post() { */ var resp booking.Booking user, peerID, groups := oclib.ExtractTokenInfo(*o.Ctx.Request) - json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &resp) + err := json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &resp) + if err != nil { + fmt.Println("Error unmarshalling") + fmt.Println(err) + fmt.Println(resp) + } + dc_id := resp.ResourceID // delete all previous bookings isDraft := o.Ctx.Input.Query("is_draft") diff --git a/controllers/session.go b/controllers/session.go index e2acf89..69aa51d 100644 --- a/controllers/session.go +++ b/controllers/session.go @@ -40,7 +40,7 @@ func (o *SessionController) GetToken() { return } fmt.Println("BLAPO", id, duration) - token, err := serv.GetToken(o.Ctx.Request.Context(), id, duration) + token, err := serv.GenerateToken(o.Ctx.Request.Context(), id, duration) if err != nil { // change code to 500 o.Ctx.Output.SetStatus(500) diff --git a/infrastructure/interface.go b/infrastructure/interface.go index fe9d170..2f8fbf1 100644 --- a/infrastructure/interface.go +++ b/infrastructure/interface.go @@ -4,20 +4,23 @@ import ( "context" "errors" "oc-datacenter/conf" + + v1 "k8s.io/api/core/v1" ) type Infrastructure interface { CreateNamespace(ctx context.Context, ns string) error DeleteNamespace(ctx context.Context, ns string) error - GetToken(ctx context.Context, ns string, duration int) (string, error) + GenerateToken(ctx context.Context, ns string, duration int) (string, error) CreateServiceAccount(ctx context.Context, ns string) error 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) + CreateAdmiraltySource(context context.Context,executionId string) ([]byte, error) + CreateKubeconfigSecret(context context.Context,kubeconfig string, executionId string) ([]byte, error) + GetKubeconfigSecret(context context.Context,executionId string) ([]byte, error) + CreateAdmiraltyTarget(context context.Context,executionId string)([]byte,error) + GetOneNode(context context.Context,executionID string) (*v1.Node, error) } var _service = map[string]func() (Infrastructure, error){ diff --git a/infrastructure/kubernetes.go b/infrastructure/kubernetes.go index 6fa24b5..327dfc3 100644 --- a/infrastructure/kubernetes.go +++ b/infrastructure/kubernetes.go @@ -9,6 +9,7 @@ import ( "fmt" "html/template" "oc-datacenter/conf" + "strings" authv1 "k8s.io/api/authentication/v1" v1 "k8s.io/api/core/v1" @@ -77,6 +78,7 @@ func NewRemoteKubernetesService(url string, ca string, cert string, key string) func (k *KubernetesService) CreateNamespace(ctx context.Context, ns string) error { // Define the namespace + fmt.Println("ExecutionID in CreateNamespace() : ", ns) namespace := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: ns, @@ -172,7 +174,7 @@ func (k *KubernetesService) DeleteNamespace(ctx context.Context, ns string) erro return nil } -func (k *KubernetesService) GetToken(ctx context.Context, ns string, duration int) (string, error) { +func (k *KubernetesService) GenerateToken(ctx context.Context, ns string, duration int) (string, error) { // Define TokenRequest (valid for 1 hour) d := int64(duration) tokenRequest := &authv1.TokenRequest{ @@ -190,6 +192,8 @@ 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){ @@ -244,15 +248,15 @@ func (k *KubernetesService) GetTargets(ctx context.Context) ([]string,error){ // - 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) +func (k *KubernetesService) CreateAdmiraltyTarget(context context.Context,executionId string)([]byte,error){ + exists, err := k.GetKubeconfigSecret(context,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) + fmt.Println("Target needs to be binded to a secret in namespace ",executionId) return nil, nil // Maybe we could create a wrapper for errors and add more info to have } @@ -269,8 +273,8 @@ func (k *KubernetesService) CreateAdmiraltyTarget(executionId string)([]byte,err targetManifest = tpl.String() resp, err := postCDRapiKube( *k.Set, - context.TODO(), - "/apis/multicluster.admiralty.io/v1alpha1/namespaces/ns-"+ executionId +"/targets", + context, + "/apis/multicluster.admiralty.io/v1alpha1/namespaces/"+ executionId +"/targets", []byte(targetManifest), map[string]string{"fieldManager":"kubectl-client-side-apply"}, map[string]string{"fieldValidation":"Strict"}, @@ -293,7 +297,7 @@ func (k *KubernetesService) CreateAdmiraltyTarget(executionId string)([]byte,err // 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) { +func (k *KubernetesService) CreateAdmiraltySource(context context.Context,executionId string) ([]byte, error) { var sourceManifest string var tpl bytes.Buffer tmpl, err := template.New("source"). @@ -308,8 +312,8 @@ func (k *KubernetesService) CreateAdmiraltySource(executionId string) ([]byte, e resp, err := postCDRapiKube( *k.Set, - context.TODO(), - "/apis/multicluster.admiralty.io/v1alpha1/namespaces/ns-"+ executionId +"/sources", + context, + "/apis/multicluster.admiralty.io/v1alpha1/namespaces/"+ executionId +"/sources", []byte(sourceManifest), map[string]string{"fieldManager":"kubectl-client-side-apply"}, map[string]string{"fieldValidation":"Strict"}, @@ -326,7 +330,7 @@ func (k *KubernetesService) CreateAdmiraltySource(executionId string) ([]byte, e // 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) { +func (k *KubernetesService) CreateKubeconfigSecret(context context.Context,kubeconfig string, executionId string) ([]byte, error) { config, err := base64.StdEncoding.DecodeString(kubeconfig) // config, err := base64.RawStdEncoding.DecodeString(kubeconfig) if err != nil { @@ -338,20 +342,20 @@ func (k *KubernetesService) CreateKubeconfigSecret(kubeconfig string, executionI secretManifest := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "kube-secret-" + executionId, - Namespace: "ns-" + executionId, + Namespace: executionId, }, Data: map[string][]byte{ "config": config, }, } - exists, err := k.GetKubeconfigSecret(executionId) + exists, err := k.GetKubeconfigSecret(context,executionId) if err != nil { - fmt.Println("Error verifying if kube secret exists in ns-", executionId) + fmt.Println("Error verifying if kube secret exists in namespace ", executionId) return nil, err } if exists != nil { - fmt.Println("kube-secret already exists in ns-", executionId) + fmt.Println("kube-secret already exists in namespace", executionId) fmt.Println("Overriding existing kube-secret with a newer resource") // TODO : implement DeleteKubeConfigSecret(executionID) deleted, err := k.DeleteKubeConfigSecret(executionId) @@ -359,8 +363,8 @@ func (k *KubernetesService) CreateKubeconfigSecret(kubeconfig string, executionI _ = err } resp, err := k.Set.CoreV1(). - Secrets("ns-"+executionId). - Create(context.TODO(),secretManifest,metav1.CreateOptions{}) + Secrets(executionId). + Create(context,secretManifest,metav1.CreateOptions{}) if err != nil { fmt.Println("Error while trying to contact API to get secret kube-secret-"+executionId) @@ -377,10 +381,10 @@ func (k *KubernetesService) CreateKubeconfigSecret(kubeconfig string, executionI return data, nil } -func (k *KubernetesService) GetKubeconfigSecret(executionId string) ([]byte, error) { +func (k *KubernetesService) GetKubeconfigSecret(context context.Context,executionId string) ([]byte, error) { resp, err := k.Set.CoreV1(). - Secrets("ns-"+executionId). - Get(context.TODO(),"kube-secret-"+executionId,metav1.GetOptions{}) + Secrets(executionId). + Get(context,"kube-secret-"+executionId,metav1.GetOptions{}) if err != nil { if(apierrors.IsNotFound(err)){ @@ -441,4 +445,26 @@ func postCDRapiKube(client kubernetes.Clientset, ctx context.Context, path strin } return resp, nil +} + +func (k *KubernetesService) GetOneNode(context context.Context,executionID string) (*v1.Node, error) { + res, err := k.Set.CoreV1(). + Nodes(). + List( + context, + metav1.ListOptions{}, + ) + if err != nil { + fmt.Println("Error getting the list of nodes from k8s API") + fmt.Println(err) + return nil, err + } + + for _, node := range res.Items { + if isNode := strings.Contains(node.Name,"admiralty-"+executionID+"-target-"+executionID+"-"); isNode { + return &node, nil + } + } + + return nil, nil } \ No newline at end of file