package controllers import ( "encoding/base64" "encoding/json" "fmt" "oc-datacenter/conf" "oc-datacenter/infrastructure" "oc-datacenter/models" "slices" "time" beego "github.com/beego/beego/v2/server/web" jwt "github.com/golang-jwt/jwt/v5" "gopkg.in/yaml.v2" v1 "k8s.io/api/core/v1" ) type KubeInfo struct { Url *string KubeCA *string KubeCert *string KubeKey *string } type RemoteKubeconfig struct { Data *string } type KubeUser struct { Name string User struct { Token string } } type KubeconfigToken struct { ApiVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Preferences string `yaml:"preferences"` CurrentContext string `yaml:"current-context"` Clusters []struct{ Cluster struct{ CA string `yaml:"certificate-authority-data"` Server string `yaml:"server"` } `yaml:"cluster"` Name string `yaml:"name"` } `yaml:"clusters"` Contexts []struct{ Context struct{ Cluster string `yaml:"cluster"` User string `yaml:"user"` } `yaml:"context"` Name string `yaml:"name"` } `yaml:"contexts"` Users []struct{ Name string `yaml:"name"` User struct { Token string `yaml:"token"` } `yaml:"user"` } `yaml:"users"` } // Operations about the admiralty objects of the datacenter type AdmiraltyController struct { beego.Controller } // @Title GetAllTargets // @Description find all Admiralty Target // @Success 200 // @router /targets [get] func (c *AdmiraltyController) GetAllTargets() { serv, err := infrastructure.NewService() if err != nil { // change code to 500 HandleControllerErrors(c.Controller,500,&err,nil) // c.Ctx.Output.SetStatus(500) // c.ServeJSON() // c.Data["json"] = map[string]string{"error": err.Error()} return } res, err := serv.GetTargets(c.Ctx.Request.Context()) c.Data["json"] = res c.ServeJSON() } // @Title GetOneTarget // @Description find one Admiralty Target // @Param id path string true "the name of the target to get" // @Success 200 // @router /targets/:execution [get] func (c *AdmiraltyController) GetOneTarget() { id := c.Ctx.Input.Param(":execution") serv, err := infrastructure.NewService() if err != nil { // change code to 500 c.Ctx.Output.SetStatus(500) c.ServeJSON() c.Data["json"] = map[string]string{"error": err.Error()} return } res, err := serv.GetTargets(c.Ctx.Request.Context()) id = "target-"+id found := slices.Contains(res,id) if !found { c.Ctx.Output.SetStatus(404) c.ServeJSON() } c.Data["json"] = id c.ServeJSON() } // @Title CreateSource // @Description Create an Admiralty Source on remote cluster // @Param execution path string true "execution id of the workflow" // @Success 201 // @router /source/:execution [post] func (c *AdmiraltyController) CreateSource() { execution := c.Ctx.Input.Param(":execution") fmt.Println("execution :: ", execution) fmt.Println("input :: ", c.Ctx.Input) serv, err := infrastructure.NewKubernetesService() 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(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 } // TODO : Return a description of the created resource var respData map[string]interface{} err = json.Unmarshal(res,&respData) c.Ctx.Output.SetStatus(201) c.Data["json"] = respData c.ServeJSON() } // @Title CreateAdmiraltyTarget // @Description Create an Admiralty Target in the namespace associated to the executionID // @Param execution path string true "execution id of the workflow" // @Success 201 // @router /target/:execution [post] func (c *AdmiraltyController) CreateAdmiraltyTarget(){ var data map[string]interface{} execution := c.Ctx.Input.Param(":execution") 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(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 { 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.Ctx.Output.SetStatus(201) 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 execution path string true "execution id of the workflow" // @Success 200 // @router /secret/:execution [get] func(c *AdmiraltyController) GetKubeSecret() { var data map[string]interface{} execution := c.Ctx.Input.Param(":execution") 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(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(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 execution path string true "execution id of the workflow" // @Param kubeconfig body controllers.RemoteKubeconfig true "Kubeconfig to use when creating secret" // @Success 201 // @router /secret/:execution [post] func (c *AdmiraltyController) CreateKubeSecret() { var kubeconfig RemoteKubeconfig 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 } execution := c.Ctx.Input.Param(":execution") 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(c.Ctx.Request.Context(),*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.Ctx.Output.SetStatus(201) 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 execution path string true "execution id of the workflow" // @Success 200 // @router /node/:execution [get] func (c *AdmiraltyController) GetNodeReady(){ var secret v1.Secret execution := c.Ctx.Input.Param(":execution") 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{} 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 } token, err := retrieveTokenFromKonfig(editedKubeconfig) if err != nil { fmt.Println("Error while trying to retrieve token for kubeconfing") fmt.Println(err) HandleControllerErrors(c.Controller,500,&err,nil) } // Decode token isExpired, err := isTokenExpired(token) if err != nil { fmt.Println("Error veryfing token's expiration") c.Ctx.Output.SetStatus(500) c.Data["json"] = err c.ServeJSON() } if *isExpired { c.Data["json"] = map[string]string{ "token" : "token in the secret is expired and must be regenerated", } c.Ctx.Output.SetStatus(410) c.ServeJSON() } c.Data["json"] = map[string]bool{"ok": true} c.ServeJSON() } func retrieveTokenFromKonfig(editedKubeconfig map[string]interface{}) (string,error) { var kubeUsers []KubeUser b, err := yaml.Marshal(editedKubeconfig["users"]) if err != nil { fmt.Println("Error while retrieving the users attribute from the Kubeconfig") fmt.Println(err) return "", err } err = yaml.Unmarshal(b,&kubeUsers) if err != nil { fmt.Println("Error while unmarshalling users attribute from kubeconfig") fmt.Println(err) return "", nil } fmt.Println(kubeUsers) token := kubeUsers[0].User.Token return token, nil } func isTokenExpired(token string) (*bool, error){ t, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{}) if err != nil { fmt.Println("couldn't decode token") return nil, err } expiration, err := t.Claims.GetExpirationTime() if err != nil { fmt.Println("Error while checking token's expiration time") return nil, err } fmt.Println("Expiration date : " + expiration.UTC().Format("2006-01-02T15:04:05")) expired := expiration.Unix() < time.Now().Unix() return &expired, nil } // @name Get Admiralty Kubeconfig // @description Retrieve a kubeconfig from the host with the token to authenticate as the SA from the namespace identified with execution id // @Param execution path string true "execution id of the workflow" // @Success 200 // @router /kubeconfig/:execution [get] func (c *AdmiraltyController) GetAdmiraltyKubeconfig() { execution := c.Ctx.Input.Param(":execution") 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 } generatedToken, err := serv.GenerateToken(c.Ctx.Request.Context(),execution,3600) if err != nil { fmt.Println("Couldn't generate a token for ns-", execution) fmt.Println(err) c.Ctx.Output.SetStatus(500) c.Data["json"] = map[string]string{"error": err.Error()} c.ServeJSON() return } kubeconfig, err := NewHostKubeWithToken(generatedToken) if err != nil { fmt.Println("Could not retrieve the Kubeconfig edited with token") fmt.Println(err) c.Ctx.Output.SetStatus(500) c.Data["json"] = map[string]string{"error": err.Error()} c.ServeJSON() return } b, err := yaml.Marshal(kubeconfig) if err != nil { fmt.Println("Error while marshalling kubeconfig") c.Ctx.Output.SetStatus(500) c.Data["json"] = map[string]string{"error": err.Error()} c.ServeJSON() } encodedKubeconfig := base64.StdEncoding.EncodeToString(b) c.Data["json"] = map[string]string{ "data": encodedKubeconfig, } c.ServeJSON() } func NewHostKubeWithToken(token string) (*models.KubeConfigValue, error){ if len(token) == 0 { return nil, fmt.Errorf("you didn't provide a token to be inserted in the Kubeconfig") } encodedCA := base64.StdEncoding.EncodeToString([]byte(conf.GetConfig().KubeCA)) hostKube := models.KubeConfigValue{ APIVersion: "v1", CurrentContext: "default", Kind: "Config", Preferences: struct{}{}, Clusters: []models.KubeconfigNamedCluster{ { Name: "default", Cluster: models.KubeconfigCluster{ Server: conf.GetConfig().KubeHost, CertificateAuthorityData: encodedCA, }, }, }, Contexts: []models.KubeconfigNamedContext{ { Name: "default", Context: models.KubeconfigContext{ Cluster: "default", User: "default", }, }, }, Users: []models.KubeconfigUser{ models.KubeconfigUser{ Name: "default", User: models.KubeconfigUserKeyPair{ Token: token, }, }, }, } return &hostKube, nil }