Przeglądaj źródła

basic create sa candidate handler impl

Alexander Belanger 5 lat temu
rodzic
commit
ea098e9908

+ 29 - 0
internal/forms/candidate.go

@@ -0,0 +1,29 @@
+package forms
+
+import (
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// CreateServiceAccountCandidatesForm represents the accepted values for
+// creating a list of ServiceAccountCandidates from a kubeconfig
+type CreateServiceAccountCandidatesForm struct {
+	ProjectID  uint   `json:"project_id"`
+	Kubeconfig []byte `json:"kubeconfig"`
+}
+
+// ToServiceAccountCandidates creates a ServiceAccountCandidate from the kubeconfig and
+// project id
+func (csa *CreateServiceAccountCandidatesForm) ToServiceAccountCandidates() ([]*models.ServiceAccountCandidate, error) {
+	candidates, err := kubernetes.GetServiceAccountCandidates(csa.Kubeconfig)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, saCandidate := range candidates {
+		saCandidate.ProjectID = csa.ProjectID
+	}
+
+	return candidates, nil
+}

+ 51 - 30
internal/kubernetes/kubeconfig.go

@@ -2,7 +2,6 @@ package kubernetes
 
 import (
 	"errors"
-	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
 	"k8s.io/client-go/tools/clientcmd"
@@ -34,18 +33,7 @@ func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCan
 		authMechanism, authInfoActions := parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
 		clusterActions := parseClusterForActions(rawConf.Clusters[clusterName])
 
-		actionsArr := make([]string, 0)
-
-		if authInfoActions != "" {
-			actionsArr = append(actionsArr, strings.Split(authInfoActions, ",")...)
-		}
-
-		if clusterActions != "" {
-			actionsArr = append(actionsArr, strings.Split(clusterActions, ",")...)
-		}
-
-		// join the cluster and auth info actions together
-		actions := strings.Join(actionsArr, ",")
+		actions := append(authInfoActions, clusterActions...)
 
 		// if auth mechanism is unsupported, we'll skip it
 		if authMechanism == models.NotAvailable {
@@ -64,7 +52,7 @@ func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCan
 		if err == nil {
 			// create the candidate service account
 			res = append(res, &models.ServiceAccountCandidate{
-				ActionNames:     actions,
+				Actions:         actions,
 				Kind:            "connector",
 				ClusterName:     clusterName,
 				ClusterEndpoint: rawConf.Clusters[clusterName].Server,
@@ -103,20 +91,26 @@ func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
 // (4) If a username/password exist, uses basic auth mechanism
 // (5) Otherwise, the config gets skipped
 //
-func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, actions string) {
-	actionsArr := make([]string, 0)
+func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, actions []models.ServiceAccountAction) {
+	actions = make([]models.ServiceAccountAction, 0)
 
 	if (authInfo.ClientCertificate != "" || len(authInfo.ClientCertificateData) != 0) &&
 		(authInfo.ClientKey != "" || len(authInfo.ClientKeyData) != 0) {
 		if len(authInfo.ClientCertificateData) == 0 {
-			actionsArr = append(actionsArr, models.ClientCertDataAction)
+			actions = append(actions, models.ServiceAccountAction{
+				Name:     models.ClientCertDataAction,
+				Resolved: false,
+			})
 		}
 
 		if len(authInfo.ClientKeyData) == 0 {
-			actionsArr = append(actionsArr, models.ClientKeyDataAction)
+			actions = append(actions, models.ServiceAccountAction{
+				Name:     models.ClientKeyDataAction,
+				Resolved: false,
+			})
 		}
 
-		return models.X509, strings.Join(actionsArr, ",")
+		return models.X509, actions
 	}
 
 	if authInfo.AuthProvider != nil {
@@ -126,44 +120,71 @@ func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, acti
 			data, isData := authInfo.AuthProvider.Config["idp-certificate-authority-data"]
 
 			if isFile && (!isData || data == "") {
-				return models.OIDC, models.OIDCIssuerDataAction
+				return models.OIDC, []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     models.OIDCIssuerDataAction,
+						Resolved: false,
+					},
+				}
 			}
 
-			return models.OIDC, ""
+			return models.OIDC, actions
 		case "gcp":
-			return models.GCP, models.GCPKeyDataAction
+			return models.GCP, []models.ServiceAccountAction{
+				models.ServiceAccountAction{
+					Name:     models.GCPKeyDataAction,
+					Resolved: false,
+				},
+			}
 		}
 	}
 
 	if authInfo.Exec != nil {
 		if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
-			return models.AWS, models.AWSKeyDataAction
+			return models.AWS, []models.ServiceAccountAction{
+				models.ServiceAccountAction{
+					Name:     models.AWSKeyDataAction,
+					Resolved: false,
+				},
+			}
 		}
 	}
 
 	if authInfo.Token != "" || authInfo.TokenFile != "" {
 		if authInfo.Token == "" {
-			return models.Bearer, models.TokenDataAction
+			return models.Bearer, []models.ServiceAccountAction{
+				models.ServiceAccountAction{
+					Name:     models.TokenDataAction,
+					Resolved: false,
+				},
+			}
 		}
 
-		return models.Bearer, ""
+		return models.Bearer, actions
 	}
 
 	if authInfo.Username != "" && authInfo.Password != "" {
-		return models.Basic, ""
+		return models.Basic, actions
 	}
 
-	return models.NotAvailable, ""
+	return models.NotAvailable, actions
 }
 
 // Parses the cluster object to determine actions -- only currently supported action is
 // population of the cluster certificate authority data
-func parseClusterForActions(cluster *api.Cluster) (actions string) {
+func parseClusterForActions(cluster *api.Cluster) (actions []models.ServiceAccountAction) {
+	actions = make([]models.ServiceAccountAction, 0)
+
 	if cluster.CertificateAuthority != "" && len(cluster.CertificateAuthorityData) == 0 {
-		return models.ClusterCADataAction
+		return []models.ServiceAccountAction{
+			models.ServiceAccountAction{
+				Name:     models.ClusterCADataAction,
+				Resolved: false,
+			},
+		}
 	}
 
-	return ""
+	return actions
 }
 
 // getKubeconfigForContext returns the raw kubeconfig associated with only a

+ 67 - 22
internal/kubernetes/kubeconfig_test.go

@@ -178,7 +178,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(ClusterCAWithoutData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-cluster-ca-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-cluster-ca-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -192,7 +197,7 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(x509WithData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "",
+				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -206,7 +211,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(x509WithoutCertData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-client-cert-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-client-cert-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -220,7 +230,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(x509WithoutKeyData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-client-key-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-client-key-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -234,7 +249,16 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(x509WithoutCertAndKeyData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-client-cert-data,upload-client-key-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-client-cert-data",
+						Resolved: false,
+					},
+					models.ServiceAccountAction{
+						Name:     "upload-client-key-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -248,7 +272,7 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(BearerTokenWithData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "",
+				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -262,7 +286,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(BearerTokenWithoutData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-token-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-token-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -276,7 +305,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(GCPPlugin),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-gcp-key-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-gcp-key-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -290,7 +324,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(AWSIamAuthenticatorExec),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-aws-key-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-aws-key-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -304,7 +343,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(AWSEKSGetTokenExec),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-aws-key-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-aws-key-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -318,7 +362,12 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(OIDCAuthWithoutData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "upload-oidc-idp-issuer-ca-data",
+				Actions: []models.ServiceAccountAction{
+					models.ServiceAccountAction{
+						Name:     "upload-oidc-idp-issuer-ca-data",
+						Resolved: false,
+					},
+				},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -332,7 +381,7 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(OIDCAuthWithData),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "",
+				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -346,7 +395,7 @@ var SACandidatesTests = []saCandidatesTest{
 		raw:  []byte(BasicAuth),
 		expected: []*models.ServiceAccountCandidate{
 			&models.ServiceAccountCandidate{
-				ActionNames:     "",
+				Actions:         []models.ServiceAccountAction{},
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
@@ -396,18 +445,14 @@ func TestGetServiceAccountCandidates(t *testing.T) {
 					c.name, exp.ClusterEndpoint, res.ClusterEndpoint)
 			}
 
-			// compare action names by splitting the arrays (actions may be out of order)
-			resActions := strings.Split(res.ActionNames, ",")
-			expActions := strings.Split(exp.ActionNames, ",")
-
-			if len(resActions) != len(expActions) {
+			if len(res.Actions) != len(exp.Actions) {
 				t.Errorf("%s failed on action names: expected length %d, got length %d\n",
-					c.name, len(expActions), len(resActions))
+					c.name, len(res.Actions), len(exp.Actions))
 			} else {
-				for _, actionName := range expActions {
-					if !strings.Contains(res.ActionNames, actionName) {
+				for i, action := range exp.Actions {
+					if res.Actions[i].Name != action.Name {
 						t.Errorf("%s failed on action names: expected res to contain %s, got %s\n",
-							c.name, actionName, res.ActionNames)
+							c.name, action.Name, res.Actions[i].Name)
 					}
 				}
 			}

+ 52 - 19
internal/models/action.go

@@ -1,14 +1,6 @@
 package models
 
-// ServiceAccountAction is an action that needs to be performed to set up
-// a service account
-type ServiceAccountAction struct {
-	Name string `json:"name"`
-	Docs string `json:"docs"`
-
-	// a comma-separated list of required fields to send in an action request
-	Fields string `json:"fields"`
-}
+import "gorm.io/gorm"
 
 // Action names
 const (
@@ -21,40 +13,81 @@ const (
 	AWSKeyDataAction            = "upload-aws-key-data"
 )
 
-// ServiceAccountsActions are actions must be performed to initialize a
-// ServiceAccount
-var ServiceAccountsActions = map[string]ServiceAccountAction{
-	"upload-cluster-ca-data": ServiceAccountAction{
+// ServiceAccountAction is an action that must be resolved to set up
+// a ServiceAccount
+type ServiceAccountAction struct {
+	gorm.Model
+
+	// One of the constant action names
+	Name     string `json:"name"`
+	Resolved bool   `json:"resolved"`
+}
+
+// Externalize generates an external ServiceAccount to be shared over REST
+func (u *ServiceAccountAction) Externalize() *ServiceAccountActionExternal {
+	info := ServiceAccountActionInfos[u.Name]
+
+	return &ServiceAccountActionExternal{
+		Name:     u.Name,
+		Resolved: u.Resolved,
+		Docs:     info.Docs,
+		Fields:   info.Fields,
+	}
+}
+
+// ServiceAccountActionExternal is an external ServiceAccountAction to be
+// sent over REST
+type ServiceAccountActionExternal struct {
+	Name     string `json:"name"`
+	Docs     string `json:"docs"`
+	Resolved bool   `json:"resolved"`
+	Fields   string `json:"fields"`
+}
+
+// ServiceAccountActionInfo contains the information for actions to be
+// performed in order to initialize a ServiceAccount
+type ServiceAccountActionInfo struct {
+	Name string `json:"name"`
+	Docs string `json:"docs"`
+
+	// a comma-separated list of required fields to send in an action request
+	Fields string `json:"fields"`
+}
+
+// ServiceAccountActionInfos contain the information for actions to be
+// performed in order to initialize a ServiceAccount
+var ServiceAccountActionInfos = map[string]ServiceAccountActionInfo{
+	"upload-cluster-ca-data": ServiceAccountActionInfo{
 		Name:   ClusterCADataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "cluster_ca_data",
 	},
-	"upload-client-cert-data": ServiceAccountAction{
+	"upload-client-cert-data": ServiceAccountActionInfo{
 		Name:   ClientCertDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "client_cert_data",
 	},
-	"upload-client-key-data": ServiceAccountAction{
+	"upload-client-key-data": ServiceAccountActionInfo{
 		Name:   ClientKeyDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "client_key_data",
 	},
-	"upload-oidc-idp-issuer-ca-data": ServiceAccountAction{
+	"upload-oidc-idp-issuer-ca-data": ServiceAccountActionInfo{
 		Name:   OIDCIssuerDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "oidc_idp_issuer_ca_data",
 	},
-	"upload-token-data": ServiceAccountAction{
+	"upload-token-data": ServiceAccountActionInfo{
 		Name:   TokenDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "token_data",
 	},
-	"upload-gcp-key-data": ServiceAccountAction{
+	"upload-gcp-key-data": ServiceAccountActionInfo{
 		Name:   GCPKeyDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "gcp_key_data",
 	},
-	"upload-aws-key-data": ServiceAccountAction{
+	"upload-aws-key-data": ServiceAccountActionInfo{
 		Name:   AWSKeyDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "aws_key_data",

+ 10 - 16
internal/models/serviceaccount.go

@@ -1,8 +1,6 @@
 package models
 
 import (
-	"strings"
-
 	"gorm.io/gorm"
 )
 
@@ -25,8 +23,7 @@ type ServiceAccountCandidate struct {
 	ProjectID uint   `json:"project_id"`
 	Kind      string `json:"kind"`
 
-	// a comma-separated list of action names to perform to create a ServiceAccount
-	ActionNames string `json:"action_names"`
+	Actions []ServiceAccountAction `json:"actions"`
 
 	ClusterName     string `json:"cluster_name"`
 	ClusterEndpoint string `json:"cluster_endpoint"`
@@ -37,23 +34,20 @@ type ServiceAccountCandidate struct {
 // ServiceAccountCandidateExternal represents the ServiceAccountCandidate type that is
 // sent over REST
 type ServiceAccountCandidateExternal struct {
-	Actions         []ServiceAccountAction `json:"actions"`
-	ProjectID       uint                   `json:"project_id"`
-	Kind            string                 `json:"kind"`
-	ClusterName     string                 `json:"cluster_name"`
-	ClusterEndpoint string                 `json:"cluster_endpoint"`
-	AuthMechanism   string                 `json:"auth_mechanism"`
+	Actions         []ServiceAccountActionExternal `json:"actions"`
+	ProjectID       uint                           `json:"project_id"`
+	Kind            string                         `json:"kind"`
+	ClusterName     string                         `json:"cluster_name"`
+	ClusterEndpoint string                         `json:"cluster_endpoint"`
+	AuthMechanism   string                         `json:"auth_mechanism"`
 }
 
 // Externalize generates an external ServiceAccountCandidate to be shared over REST
 func (s *ServiceAccountCandidate) Externalize() *ServiceAccountCandidateExternal {
-	actions := make([]ServiceAccountAction, 0)
-
-	// split the actions string and populate actions
-	actionsArr := strings.Split(s.ActionNames, ",")
+	actions := make([]ServiceAccountActionExternal, 0)
 
-	for _, actionName := range actionsArr {
-		actions = append(actions, ServiceAccountsActions[actionName])
+	for _, action := range s.Actions {
+		actions = append(actions, *action.Externalize())
 	}
 
 	return &ServiceAccountCandidateExternal{

+ 49 - 0
server/api/project_handler.go

@@ -102,3 +102,52 @@ func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 }
+
+// HandleCreateProjectSACandidates handles the creation of ServiceAccountCandidates
+// using a kubeconfig and a project id
+func (app *App) HandleCreateProjectSACandidates(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateServiceAccountCandidatesForm{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a ServiceAccountCandidate
+	saCandidates, err := form.ToServiceAccountCandidates()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	for _, saCandidate := range saCandidates {
+		// handle write to the database
+		saCandidate, err = app.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+
+		if err != nil {
+			app.handleErrorDataWrite(err, w)
+			return
+		}
+
+		app.logger.Info().Msgf("New service account candidate created: %d", saCandidate.ID)
+	}
+
+	w.WriteHeader(http.StatusCreated)
+}