Kaynağa Gözat

add create cluster candidate endpoint

Alexander Belanger 4 yıl önce
ebeveyn
işleme
347a1152e1

+ 42 - 0
api/server/handlers/cluster/create_manual.go → api/server/handlers/cluster/create.go

@@ -11,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes/resolver"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 )
@@ -113,3 +114,44 @@ func getClusterModelFromManualRequest(
 		CertificateAuthorityData: cert,
 	}, nil
 }
+
+func createClusterFromCandidate(
+	repo repository.Repository,
+	project *models.Project,
+	user *models.User,
+	candidate *models.ClusterCandidate,
+) (*models.Cluster, *models.ClusterCandidate, error) {
+	// we query the repo again to get the decrypted version of the cluster candidate
+	cc, err := repo.Cluster().ReadClusterCandidate(project.ID, candidate.ID)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cResolver := &resolver.CandidateResolver{
+		Resolver:           &types.ClusterResolverAll{},
+		ClusterCandidateID: cc.ID,
+		ProjectID:          project.ID,
+		UserID:             user.ID,
+	}
+
+	err = cResolver.ResolveIntegration(repo)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cluster, err := cResolver.ResolveCluster(repo)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cc, err = repo.Cluster().UpdateClusterCandidateCreatedClusterID(cc.ID, cluster.ID)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return cluster, cc, nil
+}

+ 97 - 0
api/server/handlers/cluster/create_candidate.go

@@ -1 +1,98 @@
 package cluster
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type CreateClusterCandidateHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreateClusterCandidateHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateClusterCandidateHandler {
+	return &CreateClusterCandidateHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *CreateClusterCandidateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// read the project from context
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+
+	request := &types.CreateClusterCandidateRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	ccs, err := getClusterCandidateModelsFromRequest(c.Repo(), proj, request, c.Config().ServerConf.IsLocal)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := make([]*types.ClusterCandidate, 0)
+
+	for _, cc := range ccs {
+		// handle write to the database
+		cc, err = c.Repo().Cluster().CreateClusterCandidate(cc)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		// if the ClusterCandidate does not have any actions to perform, create the Cluster
+		// automatically
+		if len(cc.Resolvers) == 0 {
+			_, cc, err = createClusterFromCandidate(c.Repo(), proj, user, cc)
+
+			if err != nil {
+				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+		}
+
+		res = append(res, cc.ToClusterCandidateType())
+	}
+
+	c.WriteResult(w, r, res)
+}
+
+func getClusterCandidateModelsFromRequest(
+	repo repository.Repository,
+	project *models.Project,
+	request *types.CreateClusterCandidateRequest,
+	isServerLocal bool,
+) ([]*models.ClusterCandidate, error) {
+	candidates, err := kubernetes.GetClusterCandidatesFromKubeconfig(
+		[]byte(request.Kubeconfig),
+		project.ID,
+		// can only use "local" auth mechanism if the server is running locally
+		isServerLocal && request.IsLocal,
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, cc := range candidates {
+		cc.ProjectID = project.ID
+	}
+
+	return candidates, nil
+}

+ 28 - 0
api/server/router/cluster.go

@@ -82,6 +82,34 @@ func getClusterRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/candidates -> project.NewCreateClusterCandidateHandler
+	createCandidateEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/clusters/candidates",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	createCandidateHandler := cluster.NewCreateClusterCandidateHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: createCandidateEndpoint,
+		Handler:  createCandidateHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id} -> project.NewClusterGetHandler
 	getEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 143 - 0
api/types/cluster.go

@@ -27,6 +27,124 @@ type Cluster struct {
 	AWSIntegrationID uint `json:"aws_integration_id"`
 }
 
+type ClusterCandidate struct {
+	ID uint `json:"id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// CreatedClusterID is the ID of the cluster that's eventually
+	// created
+	CreatedClusterID uint `json:"created_cluster_id"`
+
+	// Name of the cluster
+	Name string `json:"name"`
+
+	// Server endpoint for the cluster
+	Server string `json:"server"`
+
+	// Name of the context that this was created from, if it exists
+	ContextName string `json:"context_name"`
+
+	// Resolvers are the list of resolvers: once all resolvers are "resolved," the
+	// cluster will be created
+	Resolvers []ClusterResolver `json:"resolvers"`
+
+	// The best-guess for the AWSClusterID, which is required by aws auth mechanisms
+	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
+	AWSClusterIDGuess string `json:"aws_cluster_id_guess"`
+}
+
+type ClusterResolver struct {
+	ID uint `json:"id"`
+
+	// The ClusterCandidate that this is resolving
+	ClusterCandidateID uint `json:"cluster_candidate_id"`
+
+	// One of the ClusterResolverNames
+	Name ClusterResolverName `json:"name"`
+
+	// Resolved is true if this has been resolved, false otherwise
+	Resolved bool `json:"resolved"`
+
+	// Docs is a link to documentation that helps resolve this manually
+	Docs string `json:"docs"`
+
+	// Fields is a list of fields that must be sent with the resolving request
+	Fields string `json:"fields"`
+
+	// Data is additional data for resolving the action, for example a file name,
+	// context name, etc
+	Data ClusterResolverData `json:"data,omitempty"`
+}
+
+// ClusterResolverAll is a helper type that contains the fields for
+// all possible resolvers, so that raw bytes can be unmarshaled in a single
+// read
+type ClusterResolverAll struct {
+	ClusterCAData      string `json:"cluster_ca_data,omitempty"`
+	ClusterHostname    string `json:"cluster_hostname,omitempty"`
+	ClientCertData     string `json:"client_cert_data,omitempty"`
+	ClientKeyData      string `json:"client_key_data,omitempty"`
+	OIDCIssuerCAData   string `json:"oidc_idp_issuer_ca_data,omitempty"`
+	TokenData          string `json:"token_data,omitempty"`
+	GCPKeyData         string `json:"gcp_key_data,omitempty"`
+	AWSAccessKeyID     string `json:"aws_access_key_id"`
+	AWSSecretAccessKey string `json:"aws_secret_access_key"`
+	AWSClusterID       string `json:"aws_cluster_id"`
+}
+
+// ClusterResolverInfo contains the information for actions to be
+// performed in order to initialize a cluster
+type ClusterResolverInfo struct {
+	// Docs is a link to documentation that helps resolve this manually
+	Docs string `json:"docs"`
+
+	// a comma-separated list of required fields to send in an action request
+	Fields string `json:"fields"`
+}
+
+// ClusterResolverInfos is a map of the information for actions to be
+// performed in order to initialize a cluster
+var ClusterResolverInfos = map[ClusterResolverName]ClusterResolverInfo{
+	ClusterCAData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "cluster_ca_data",
+	},
+	ClusterLocalhost: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "cluster_hostname",
+	},
+	ClientCertData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "client_cert_data",
+	},
+	ClientKeyData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "client_key_data",
+	},
+	OIDCIssuerData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "oidc_idp_issuer_ca_data",
+	},
+	TokenData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "token_data",
+	},
+	GCPKeyData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "gcp_key_data",
+	},
+	AWSData: {
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "aws_access_key_id,aws_secret_access_key,aws_cluster_id",
+	},
+}
+
+// ClusterResolverData is a map of key names to fields, which gets marshaled from
+// the raw JSON bytes stored in the ClusterResolver
+type ClusterResolverData map[string]string
+
 type ClusterGetResponse struct {
 	*Cluster
 
@@ -46,6 +164,21 @@ const (
 	Kube ClusterService = "kube"
 )
 
+// ClusterResolverName is the name for a cluster resolve
+type ClusterResolverName string
+
+// Options for the cluster resolver names
+const (
+	ClusterCAData    ClusterResolverName = "upload-cluster-ca-data"
+	ClusterLocalhost ClusterResolverName = "rewrite-cluster-localhost"
+	ClientCertData   ClusterResolverName = "upload-client-cert-data"
+	ClientKeyData    ClusterResolverName = "upload-client-key-data"
+	OIDCIssuerData   ClusterResolverName = "upload-oidc-idp-issuer-ca-data"
+	TokenData        ClusterResolverName = "upload-token-data"
+	GCPKeyData       ClusterResolverName = "upload-gcp-key-data"
+	AWSData          ClusterResolverName = "upload-aws-data"
+)
+
 type ListNamespacesResponse struct {
 	*v1.NamespaceList
 }
@@ -89,3 +222,13 @@ type CreateClusterManualRequest struct {
 
 	CertificateAuthorityData string `json:"certificate_authority_data,omitempty"`
 }
+
+type CreateClusterCandidateRequest struct {
+	ProjectID  uint   `json:"project_id"`
+	Kubeconfig string `json:"kubeconfig"`
+
+	// Represents whether the auth mechanism should be designated as
+	// "local": if so, the auth mechanism uses local plugins/mechanisms purely from the
+	// kubeconfig.
+	IsLocal bool `json:"is_local"`
+}

+ 1 - 1
docs/developing/backend-refactor-status.md

@@ -38,7 +38,7 @@
 | <li>- [ ] `POST /api/projects/{project_id}/ci/actions/generate`                                                             |             |                 |             |                  |
 | <li>- [x] `GET /api/projects/{project_id}/clusters`                                                                         | AB          |                 |             | yes              |
 | <li>- [X] `POST /api/projects/{project_id}/clusters`                                                                        | AB          |                 |             |                  |
-| <li>- [ ] `POST /api/projects/{project_id}/clusters/candidates`                                                             |             |                 |             |                  |
+| <li>- [X] `POST /api/projects/{project_id}/clusters/candidates`                                                             | AB          |                 |             |                  |
 | <li>- [ ] `GET /api/projects/{project_id}/clusters/candidates`                                                              |             |                 |             |                  |
 | <li>- [ ] `POST /api/projects/{project_id}/clusters/candidates/{candidate_id}/resolve`                                      |             |                 |             |                  |
 | <li>- [x] `GET /api/projects/{project_id}/clusters/{cluster_id}`                                                            | AB          |                 |             | yes              |

+ 13 - 86
internal/kubernetes/kubeconfig.go

@@ -5,6 +5,7 @@ import (
 	"errors"
 	"net/url"
 
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/clientcmd/api"
@@ -85,80 +86,6 @@ func GetClusterCandidatesFromKubeconfig(
 	return res, nil
 }
 
-// GetServiceAccountCandidates parses a kubeconfig for a list of service account
-// candidates.
-//
-// The local boolean represents whether the auth mechanism should be designated as
-// "local": if so, the auth mechanism uses local plugins/mechanisms purely from the
-// kubeconfig.
-// func GetServiceAccountCandidates(kubeconfig []byte, local bool) ([]*models.ServiceAccountCandidate, error) {
-// 	config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
-
-// 	if err != nil {
-// 		return nil, err
-// 	}
-
-// 	rawConf, err := config.RawConfig()
-
-// 	if err != nil {
-// 		return nil, err
-// 	}
-
-// 	res := make([]*models.ServiceAccountCandidate, 0)
-
-// 	for contextName, context := range rawConf.Contexts {
-// 		clusterName := context.Cluster
-// 		awsClusterID := ""
-// 		authInfoName := context.AuthInfo
-
-// 		resolvers := make([]models.ServiceAccountResolver, 0)
-// 		var integration string
-
-// 		if local {
-// 			integration = models.Local
-// 		} else {
-// 			// get the auth mechanism and resolvers
-// 			integration, resolvers = parseAuthInfoForResolvers(rawConf.AuthInfos[authInfoName])
-// 			clusterResolvers := parseClusterForResolvers(rawConf.Clusters[clusterName])
-// 			resolvers = append(resolvers, clusterResolvers...)
-
-// 			// if auth mechanism is unsupported, we'll skip it
-// 			if integration == models.NotAvailable {
-// 				continue
-// 			} else if integration == models.AWS {
-// 				// if the auth mechanism is AWS, we need to parse more explicitly
-// 				// for the cluster id
-// 				awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
-// 			}
-// 		}
-
-// 		// construct the raw kubeconfig that's relevant for that context
-// 		contextConf, err := getConfigForContext(&rawConf, contextName)
-
-// 		if err != nil {
-// 			continue
-// 		}
-
-// 		rawBytes, err := clientcmd.Write(*contextConf)
-
-// 		if err == nil {
-// 			// create the candidate service account
-// 			res = append(res, &models.ServiceAccountCandidate{
-// 				Resolvers:           resolvers,
-// 				Kind:              "connector",
-// 				ContextName:       contextName,
-// 				ClusterName:       clusterName,
-// 				ClusterEndpoint:   rawConf.Clusters[clusterName].Server,
-// 				Integration:     integration,
-// 				AWSClusterIDGuess: awsClusterID,
-// 				Kubeconfig:        rawBytes,
-// 			})
-// 		}
-// 	}
-
-// 	return res, nil
-// }
-
 // GetRawConfigFromBytes returns the clientcmdapi.Config from kubeconfig
 // bytes
 func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
@@ -198,7 +125,7 @@ func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.Clu
 			fnBytes, _ := json.Marshal(&fn)
 
 			resolvers = append(resolvers, models.ClusterResolver{
-				Name:     models.ClientCertData,
+				Name:     types.ClientCertData,
 				Resolved: false,
 				Data:     fnBytes,
 			})
@@ -212,7 +139,7 @@ func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.Clu
 			fnBytes, _ := json.Marshal(&fn)
 
 			resolvers = append(resolvers, models.ClusterResolver{
-				Name:     models.ClientKeyData,
+				Name:     types.ClientKeyData,
 				Resolved: false,
 				Data:     fnBytes,
 			})
@@ -235,8 +162,8 @@ func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.Clu
 				fnBytes, _ := json.Marshal(&fn)
 
 				return models.OIDC, []models.ClusterResolver{
-					models.ClusterResolver{
-						Name:     models.OIDCIssuerData,
+					{
+						Name:     types.OIDCIssuerData,
 						Resolved: false,
 						Data:     fnBytes,
 					},
@@ -246,8 +173,8 @@ func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.Clu
 			return models.OIDC, resolvers
 		case "gcp":
 			return models.GCP, []models.ClusterResolver{
-				models.ClusterResolver{
-					Name:     models.GCPKeyData,
+				{
+					Name:     types.GCPKeyData,
 					Resolved: false,
 				},
 			}
@@ -257,8 +184,8 @@ func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.Clu
 	if authInfo.Exec != nil {
 		if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
 			return models.AWS, []models.ClusterResolver{
-				models.ClusterResolver{
-					Name:     models.AWSData,
+				{
+					Name:     types.AWSData,
 					Resolved: false,
 				},
 			}
@@ -274,8 +201,8 @@ func parseAuthInfoForResolvers(authInfo *api.AuthInfo) (authMechanism models.Clu
 			fnBytes, _ := json.Marshal(&fn)
 
 			return models.Bearer, []models.ClusterResolver{
-				models.ClusterResolver{
-					Name:     models.TokenData,
+				{
+					Name:     types.TokenData,
 					Resolved: false,
 					Data:     fnBytes,
 				},
@@ -305,7 +232,7 @@ func parseClusterForResolvers(cluster *api.Cluster) (resolvers []models.ClusterR
 		fnBytes, _ := json.Marshal(&fn)
 
 		resolvers = append(resolvers, models.ClusterResolver{
-			Name:     models.ClusterCAData,
+			Name:     types.ClusterCAData,
 			Resolved: false,
 			Data:     fnBytes,
 		})
@@ -316,7 +243,7 @@ func parseClusterForResolvers(cluster *api.Cluster) (resolvers []models.ClusterR
 	if err == nil {
 		if hostname := serverURL.Hostname(); hostname == "127.0.0.1" || hostname == "localhost" {
 			resolvers = append(resolvers, models.ClusterResolver{
-				Name:     models.ClusterLocalhost,
+				Name:     types.ClusterLocalhost,
 				Resolved: false,
 			})
 		}

+ 42 - 41
internal/kubernetes/kubeconfig_test.go

@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/fixtures"
 	"github.com/porter-dev/porter/internal/models"
@@ -17,16 +18,16 @@ type ccsTest struct {
 }
 
 var ClusterCandidatesTests = []ccsTest{
-	ccsTest{
+	{
 		name: "test without cluster ca data",
 		raw:  []byte(fixtures.ClusterCAWithoutData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.X509,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
-						Name:     models.ClusterCAData,
+					{
+						Name:     types.ClusterCAData,
 						Resolved: false,
 						Data:     []byte(`{"filename":"/fake/path/to/ca.pem"}`),
 					},
@@ -39,16 +40,16 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "test cluster localhost",
 		raw:  []byte(fixtures.ClusterLocalhost),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.X509,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
-						Name:     models.ClusterLocalhost,
+					{
+						Name:     types.ClusterLocalhost,
 						Resolved: false,
 					},
 				},
@@ -60,11 +61,11 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "x509 test with cert and key data",
 		raw:  []byte(fixtures.X509WithData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism:     models.X509,
 				ProjectID:         1,
 				Resolvers:         []models.ClusterResolver{},
@@ -76,15 +77,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "x509 test without cert data",
 		raw:  []byte(fixtures.X509WithoutCertData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.X509,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-client-cert-data",
 						Resolved: false,
 						Data:     []byte(`{"filename":"/fake/path/to/cert.pem"}`),
@@ -98,15 +99,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "x509 test without key data",
 		raw:  []byte(fixtures.X509WithoutKeyData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.X509,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-client-key-data",
 						Resolved: false,
 						Data:     []byte(`{"filename":"/fake/path/to/key.pem"}`),
@@ -120,20 +121,20 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "x509 test without cert and key data",
 		raw:  []byte(fixtures.X509WithoutCertAndKeyData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.X509,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-client-cert-data",
 						Resolved: false,
 						Data:     []byte(`{"filename":"/fake/path/to/cert.pem"}`),
 					},
-					models.ClusterResolver{
+					{
 						Name:     "upload-client-key-data",
 						Resolved: false,
 						Data:     []byte(`{"filename":"/fake/path/to/key.pem"}`),
@@ -147,11 +148,11 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "bearer token test with data",
 		raw:  []byte(fixtures.BearerTokenWithData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism:     models.Bearer,
 				ProjectID:         1,
 				Resolvers:         []models.ClusterResolver{},
@@ -163,15 +164,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "bearer token test without data",
 		raw:  []byte(fixtures.BearerTokenWithoutData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.Bearer,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-token-data",
 						Resolved: false,
 						Data:     []byte(`{"filename":"/path/to/token/file.txt"}`),
@@ -185,15 +186,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "gcp test",
 		raw:  []byte(fixtures.GCPPlugin),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.GCP,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-gcp-key-data",
 						Resolved: false,
 					},
@@ -206,15 +207,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "aws iam authenticator test",
 		raw:  []byte(fixtures.AWSIamAuthenticatorExec),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.AWS,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-aws-data",
 						Resolved: false,
 					},
@@ -227,15 +228,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "aws eks get-token test",
 		raw:  []byte(fixtures.AWSEKSGetTokenExec),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.AWS,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-aws-data",
 						Resolved: false,
 					},
@@ -248,15 +249,15 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "oidc without ca data",
 		raw:  []byte(fixtures.OIDCAuthWithoutData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism: models.OIDC,
 				ProjectID:     1,
 				Resolvers: []models.ClusterResolver{
-					models.ClusterResolver{
+					{
 						Name:     "upload-oidc-idp-issuer-ca-data",
 						Resolved: false,
 						Data:     []byte(`{"filename":"/fake/path/to/ca.pem"}`),
@@ -270,11 +271,11 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "oidc with ca data",
 		raw:  []byte(fixtures.OIDCAuthWithData),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism:     models.OIDC,
 				ProjectID:         1,
 				Resolvers:         []models.ClusterResolver{},
@@ -286,11 +287,11 @@ var ClusterCandidatesTests = []ccsTest{
 			},
 		},
 	},
-	ccsTest{
+	{
 		name: "basic auth test",
 		raw:  []byte(fixtures.BasicAuth),
 		expected: []*models.ClusterCandidate{
-			&models.ClusterCandidate{
+			{
 				AuthMechanism:     models.Basic,
 				ProjectID:         1,
 				Resolvers:         []models.ClusterResolver{},

+ 441 - 0
internal/kubernetes/resolver/resolver.go

@@ -0,0 +1,441 @@
+package resolver
+
+import (
+	"encoding/base64"
+	"errors"
+	"net/url"
+	"strings"
+
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"k8s.io/client-go/tools/clientcmd/api"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// CandidateResolver will resolve a cluster candidate to create a new cluster
+type CandidateResolver struct {
+	Resolver *types.ClusterResolverAll
+
+	ClusterCandidateID uint
+	ProjectID          uint
+	UserID             uint
+
+	// populated during the ResolveIntegration step
+	integrationID    uint
+	clusterCandidate *models.ClusterCandidate
+	rawConf          *api.Config
+}
+
+// ResolveIntegration creates an integration in the DB
+func (rcf *CandidateResolver) ResolveIntegration(
+	repo repository.Repository,
+) error {
+	cc, err := repo.Cluster().ReadClusterCandidate(rcf.ProjectID, rcf.ClusterCandidateID)
+
+	if err != nil {
+		return err
+	}
+
+	rcf.clusterCandidate = cc
+
+	rawConf, err := kubernetes.GetRawConfigFromBytes(cc.Kubeconfig)
+
+	if err != nil {
+		return err
+	}
+
+	rcf.rawConf = rawConf
+
+	context := rawConf.Contexts[rawConf.CurrentContext]
+
+	authInfoName := context.AuthInfo
+	authInfo := rawConf.AuthInfos[authInfoName]
+
+	// iterate through the resolvers, and use the ClusterResolverAll to populate
+	// the required fields
+	var id uint
+
+	switch cc.AuthMechanism {
+	case models.X509:
+		id, err = rcf.resolveX509(repo, authInfo)
+	case models.Bearer:
+		id, err = rcf.resolveToken(repo, authInfo)
+	case models.Basic:
+		id, err = rcf.resolveBasic(repo, authInfo)
+	case models.Local:
+		id, err = rcf.resolveLocal(repo, authInfo)
+	case models.OIDC:
+		id, err = rcf.resolveOIDC(repo, authInfo)
+	case models.GCP:
+		id, err = rcf.resolveGCP(repo, authInfo)
+	case models.AWS:
+		id, err = rcf.resolveAWS(repo, authInfo)
+	}
+
+	if err != nil {
+		return err
+	}
+
+	rcf.integrationID = id
+
+	return nil
+}
+
+func (rcf *CandidateResolver) resolveX509(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	ki := &ints.KubeIntegration{
+		Mechanism: ints.KubeX509,
+		UserID:    rcf.UserID,
+		ProjectID: rcf.ProjectID,
+	}
+
+	// attempt to construct cert and key from raw config
+	if len(authInfo.ClientCertificateData) > 0 {
+		ki.ClientCertificateData = authInfo.ClientCertificateData
+	}
+
+	if len(authInfo.ClientKeyData) > 0 {
+		ki.ClientKeyData = authInfo.ClientKeyData
+	}
+
+	// override with resolver
+	if rcf.Resolver.ClientCertData != "" {
+		decoded, err := base64.StdEncoding.DecodeString(rcf.Resolver.ClientCertData)
+
+		if err != nil {
+			return 0, err
+		}
+
+		ki.ClientCertificateData = decoded
+	}
+
+	if rcf.Resolver.ClientKeyData != "" {
+		decoded, err := base64.StdEncoding.DecodeString(rcf.Resolver.ClientKeyData)
+
+		if err != nil {
+			return 0, err
+		}
+
+		ki.ClientKeyData = decoded
+	}
+
+	// if resolvable, write kube integration to repo
+	if len(ki.ClientCertificateData) == 0 || len(ki.ClientKeyData) == 0 {
+		return 0, errors.New("could not resolve kube integration (x509)")
+	}
+
+	// return integration id if exists
+	ki, err := repo.KubeIntegration().CreateKubeIntegration(ki)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return ki.Model.ID, nil
+}
+
+func (rcf *CandidateResolver) resolveToken(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	ki := &ints.KubeIntegration{
+		Mechanism: ints.KubeBearer,
+		UserID:    rcf.UserID,
+		ProjectID: rcf.ProjectID,
+	}
+
+	// attempt to construct token from raw config
+	if len(authInfo.Token) > 0 {
+		ki.Token = []byte(authInfo.Token)
+	}
+
+	// supplement with resolver
+	if rcf.Resolver.TokenData != "" {
+		ki.Token = []byte(rcf.Resolver.TokenData)
+	}
+
+	// if resolvable, write kube integration to repo
+	if len(ki.Token) == 0 {
+		return 0, errors.New("could not resolve kube integration (token)")
+	}
+
+	// return integration id if exists
+	ki, err := repo.KubeIntegration().CreateKubeIntegration(ki)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return ki.Model.ID, nil
+}
+
+func (rcf *CandidateResolver) resolveBasic(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	ki := &ints.KubeIntegration{
+		Mechanism: ints.KubeBasic,
+		UserID:    rcf.UserID,
+		ProjectID: rcf.ProjectID,
+	}
+
+	if len(authInfo.Username) > 0 {
+		ki.Username = []byte(authInfo.Username)
+	}
+
+	if len(authInfo.Password) > 0 {
+		ki.Password = []byte(authInfo.Password)
+	}
+
+	if len(ki.Username) == 0 || len(ki.Password) == 0 {
+		return 0, errors.New("could not resolve kube integration (basic)")
+	}
+
+	// return integration id if exists
+	ki, err := repo.KubeIntegration().CreateKubeIntegration(ki)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return ki.Model.ID, nil
+}
+
+func (rcf *CandidateResolver) resolveLocal(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	ki := &ints.KubeIntegration{
+		Mechanism:  ints.KubeLocal,
+		UserID:     rcf.UserID,
+		ProjectID:  rcf.ProjectID,
+		Kubeconfig: rcf.clusterCandidate.Kubeconfig,
+	}
+
+	// return integration id if exists
+	ki, err := repo.KubeIntegration().CreateKubeIntegration(ki)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return ki.Model.ID, nil
+}
+
+func (rcf *CandidateResolver) resolveOIDC(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	oidc := &ints.OIDCIntegration{
+		Client:    ints.OIDCKube,
+		UserID:    rcf.UserID,
+		ProjectID: rcf.ProjectID,
+	}
+
+	if url, ok := authInfo.AuthProvider.Config["idp-issuer-url"]; ok {
+		oidc.IssuerURL = []byte(url)
+	}
+
+	if clientID, ok := authInfo.AuthProvider.Config["client-id"]; ok {
+		oidc.ClientID = []byte(clientID)
+	}
+
+	if clientSecret, ok := authInfo.AuthProvider.Config["client-secret"]; ok {
+		oidc.ClientSecret = []byte(clientSecret)
+	}
+
+	if caData, ok := authInfo.AuthProvider.Config["idp-certificate-authority-data"]; ok {
+		// based on the implementation, the oidc plugin expects the data to be base64 encoded,
+		// which means we will not decode it here
+		// reference: https://github.com/kubernetes/kubernetes/blob/9dfb4c876bfca7a5ae84259fae2bc337ed90c2d7/staging/src/k8s.io/client-go/plugin/pkg/client/auth/oidc/oidc.go#L135
+		oidc.CertificateAuthorityData = []byte(caData)
+	}
+
+	if idToken, ok := authInfo.AuthProvider.Config["id-token"]; ok {
+		oidc.IDToken = []byte(idToken)
+	}
+
+	if refreshToken, ok := authInfo.AuthProvider.Config["refresh-token"]; ok {
+		oidc.RefreshToken = []byte(refreshToken)
+	}
+
+	// override with resolver
+	if rcf.Resolver.OIDCIssuerCAData != "" {
+		// based on the implementation, the oidc plugin expects the data to be base64 encoded,
+		// which means we will not decode it here
+		// reference: https://github.com/kubernetes/kubernetes/blob/9dfb4c876bfca7a5ae84259fae2bc337ed90c2d7/staging/src/k8s.io/client-go/plugin/pkg/client/auth/oidc/oidc.go#L135
+		oidc.CertificateAuthorityData = []byte(rcf.Resolver.OIDCIssuerCAData)
+	}
+
+	// return integration id if exists
+	oidc, err := repo.OIDCIntegration().CreateOIDCIntegration(oidc)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return oidc.Model.ID, nil
+}
+
+func (rcf *CandidateResolver) resolveGCP(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	// TODO -- add GCP project ID and GCP email so that source is trackable
+	gcp := &ints.GCPIntegration{
+		UserID:    rcf.UserID,
+		ProjectID: rcf.ProjectID,
+	}
+
+	// supplement with resolver
+	if rcf.Resolver.GCPKeyData != "" {
+		gcp.GCPKeyData = []byte(rcf.Resolver.GCPKeyData)
+	}
+
+	// throw error if no data
+	if len(gcp.GCPKeyData) == 0 {
+		return 0, errors.New("could not resolve gcp integration")
+	}
+
+	// return integration id if exists
+	gcp, err := repo.GCPIntegration().CreateGCPIntegration(gcp)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return gcp.Model.ID, nil
+}
+
+func (rcf *CandidateResolver) resolveAWS(
+	repo repository.Repository,
+	authInfo *api.AuthInfo,
+) (uint, error) {
+	// TODO -- add AWS session token as an optional param
+	// TODO -- add AWS entity and user ARN
+	aws := &ints.AWSIntegration{
+		UserID:    rcf.UserID,
+		ProjectID: rcf.ProjectID,
+	}
+
+	// override with resolver
+	if rcf.Resolver.AWSClusterID != "" {
+		aws.AWSClusterID = []byte(rcf.Resolver.AWSClusterID)
+	}
+
+	if rcf.Resolver.AWSAccessKeyID != "" {
+		aws.AWSAccessKeyID = []byte(rcf.Resolver.AWSAccessKeyID)
+	}
+
+	if rcf.Resolver.AWSSecretAccessKey != "" {
+		aws.AWSSecretAccessKey = []byte(rcf.Resolver.AWSSecretAccessKey)
+	}
+
+	// throw error if no data
+	if len(aws.AWSClusterID) == 0 || len(aws.AWSAccessKeyID) == 0 || len(aws.AWSSecretAccessKey) == 0 {
+		return 0, errors.New("could not resolve aws integration")
+	}
+
+	// return integration id if exists
+	aws, err := repo.AWSIntegration().CreateAWSIntegration(aws)
+
+	if err != nil {
+		return 0, err
+	}
+
+	return aws.Model.ID, nil
+}
+
+// ResolveCluster writes a new cluster to the DB -- this must be called after
+// rcf.ResolveIntegration, since it relies on the previously created integration.
+func (rcf *CandidateResolver) ResolveCluster(
+	repo repository.Repository,
+) (*models.Cluster, error) {
+	// build a cluster from the candidate
+	cluster, err := rcf.buildCluster()
+
+	if err != nil {
+		return nil, err
+	}
+
+	// save cluster to db
+	return repo.Cluster().CreateCluster(cluster)
+}
+
+func (rcf *CandidateResolver) buildCluster() (*models.Cluster, error) {
+	rawConf := rcf.rawConf
+
+	kcContext := rawConf.Contexts[rawConf.CurrentContext]
+
+	kcAuthInfoName := kcContext.AuthInfo
+	kcAuthInfo := rawConf.AuthInfos[kcAuthInfoName]
+
+	kcClusterName := kcContext.Cluster
+	kcCluster := rawConf.Clusters[kcClusterName]
+
+	cc := rcf.clusterCandidate
+
+	cluster := &models.Cluster{
+		AuthMechanism:           cc.AuthMechanism,
+		ProjectID:               cc.ProjectID,
+		Name:                    cc.Name,
+		Server:                  cc.Server,
+		ClusterLocationOfOrigin: kcCluster.LocationOfOrigin,
+		TLSServerName:           kcCluster.TLSServerName,
+		InsecureSkipTLSVerify:   kcCluster.InsecureSkipTLSVerify,
+		UserLocationOfOrigin:    kcAuthInfo.LocationOfOrigin,
+		UserImpersonate:         kcAuthInfo.Impersonate,
+	}
+
+	if len(kcAuthInfo.ImpersonateGroups) > 0 {
+		cluster.UserImpersonateGroups = strings.Join(kcAuthInfo.ImpersonateGroups, ",")
+	}
+
+	if len(kcCluster.CertificateAuthorityData) > 0 {
+		cluster.CertificateAuthorityData = kcCluster.CertificateAuthorityData
+	}
+
+	if rcf.Resolver.ClusterCAData != "" {
+		decoded, err := base64.StdEncoding.DecodeString(rcf.Resolver.ClusterCAData)
+
+		// skip if decoding error
+		if err != nil {
+			return nil, err
+		}
+
+		cluster.CertificateAuthorityData = decoded
+	}
+
+	if rcf.Resolver.ClusterHostname != "" {
+		serverURL, err := url.Parse(cluster.Server)
+		if err != nil {
+			return nil, err
+		}
+
+		if serverURL.Port() == "" {
+			serverURL.Host = rcf.Resolver.ClusterHostname
+		} else {
+			serverURL.Host = rcf.Resolver.ClusterHostname + ":" + serverURL.Port()
+		}
+
+		cluster.Server = serverURL.String()
+	}
+
+	switch cc.AuthMechanism {
+	case models.X509, models.Bearer, models.Basic, models.Local:
+		cluster.KubeIntegrationID = rcf.integrationID
+	case models.OIDC:
+		cluster.OIDCIntegrationID = rcf.integrationID
+	case models.GCP:
+		cluster.GCPIntegrationID = rcf.integrationID
+	case models.AWS:
+		cluster.AWSIntegrationID = rcf.integrationID
+	}
+
+	return cluster, nil
+}

+ 9 - 211
internal/models/cluster.go

@@ -69,52 +69,6 @@ type Cluster struct {
 	CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
 }
 
-// ClusterExternal is an external Cluster to be shared over REST
-type ClusterExternal struct {
-	ID uint `json:"id"`
-
-	// The project that this integration belongs to
-	ProjectID uint `json:"project_id"`
-
-	// Name of the cluster
-	Name string `json:"name"`
-
-	// Server endpoint for the cluster
-	Server string `json:"server"`
-
-	// The integration service for this cluster
-	Service integrations.IntegrationService `json:"service"`
-
-	// The infra id, if cluster was provisioned with Porter
-	InfraID uint `json:"infra_id"`
-
-	// (optional) The aws integration id, if available
-	AWSIntegrationID uint `json:"aws_integration_id"`
-}
-
-// Externalize generates an external Cluster to be shared over REST
-func (c *Cluster) Externalize() *ClusterExternal {
-	serv := integrations.Kube
-
-	if c.AWSIntegrationID != 0 {
-		serv = integrations.EKS
-	} else if c.GCPIntegrationID != 0 {
-		serv = integrations.GKE
-	} else if c.DOIntegrationID != 0 {
-		serv = integrations.DOKS
-	}
-
-	return &ClusterExternal{
-		ID:               c.ID,
-		ProjectID:        c.ProjectID,
-		Name:             c.Name,
-		Server:           c.Server,
-		Service:          serv,
-		InfraID:          c.InfraID,
-		AWSIntegrationID: c.AWSIntegrationID,
-	}
-}
-
 // ToProjectType generates an external types.Project to be shared over REST
 func (c *Cluster) ToClusterType() *types.Cluster {
 	serv := types.Kube
@@ -138,25 +92,6 @@ func (c *Cluster) ToClusterType() *types.Cluster {
 	}
 }
 
-type ClusterDetailedExternal struct {
-	// Simple cluster external data
-	ClusterExternal
-
-	// The NGINX Ingress IP to access the cluster
-	IngressIP string `json:"ingress_ip"`
-
-	// Error displayed in case couldn't get the IP
-	IngressError error `json:"ingress_error"`
-}
-
-func (c *Cluster) DetailedExternalize() *ClusterDetailedExternal {
-	clusterExt := c.Externalize()
-
-	return &ClusterDetailedExternal{
-		ClusterExternal: *clusterExt,
-	}
-}
-
 // ClusterCandidate is a cluster integration that requires additional action
 // from the user to set up.
 type ClusterCandidate struct {
@@ -197,44 +132,14 @@ type ClusterCandidate struct {
 	Kubeconfig []byte `json:"kubeconfig"`
 }
 
-// ClusterCandidateExternal represents the ClusterCandidate to be sent over REST
-type ClusterCandidateExternal struct {
-	ID uint `json:"id"`
-
-	// The project that this integration belongs to
-	ProjectID uint `json:"project_id"`
-
-	// CreatedClusterID is the ID of the cluster that's eventually
-	// created
-	CreatedClusterID uint `json:"created_cluster_id"`
-
-	// Name of the cluster
-	Name string `json:"name"`
-
-	// Server endpoint for the cluster
-	Server string `json:"server"`
-
-	// Name of the context that this was created from, if it exists
-	ContextName string `json:"context_name"`
-
-	// Resolvers are the list of resolvers: once all resolvers are "resolved," the
-	// cluster will be created
-	Resolvers []ClusterResolverExternal `json:"resolvers"`
-
-	// The best-guess for the AWSClusterID, which is required by aws auth mechanisms
-	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
-	AWSClusterIDGuess string `json:"aws_cluster_id_guess"`
-}
-
-// Externalize generates an external ClusterCandidateExternal to be shared over REST
-func (cc *ClusterCandidate) Externalize() *ClusterCandidateExternal {
-	resolvers := make([]ClusterResolverExternal, 0)
+func (cc *ClusterCandidate) ToClusterCandidateType() *types.ClusterCandidate {
+	resolvers := make([]types.ClusterResolver, 0)
 
 	for _, resolver := range cc.Resolvers {
-		resolvers = append(resolvers, *resolver.Externalize())
+		resolvers = append(resolvers, *resolver.ToClusterResolverType())
 	}
 
-	return &ClusterCandidateExternal{
+	return &types.ClusterCandidate{
 		ID:                cc.ID,
 		ProjectID:         cc.ProjectID,
 		CreatedClusterID:  cc.CreatedClusterID,
@@ -246,84 +151,6 @@ func (cc *ClusterCandidate) Externalize() *ClusterCandidateExternal {
 	}
 }
 
-// ClusterResolverName is the name for a cluster resolve
-type ClusterResolverName string
-
-// Options for the cluster resolver names
-const (
-	ClusterCAData    ClusterResolverName = "upload-cluster-ca-data"
-	ClusterLocalhost                     = "rewrite-cluster-localhost"
-	ClientCertData                       = "upload-client-cert-data"
-	ClientKeyData                        = "upload-client-key-data"
-	OIDCIssuerData                       = "upload-oidc-idp-issuer-ca-data"
-	TokenData                            = "upload-token-data"
-	GCPKeyData                           = "upload-gcp-key-data"
-	AWSData                              = "upload-aws-data"
-)
-
-// ClusterResolverInfo contains the information for actions to be
-// performed in order to initialize a cluster
-type ClusterResolverInfo struct {
-	// Docs is a link to documentation that helps resolve this manually
-	Docs string `json:"docs"`
-
-	// a comma-separated list of required fields to send in an action request
-	Fields string `json:"fields"`
-}
-
-// ClusterResolverInfos is a map of the information for actions to be
-// performed in order to initialize a cluster
-var ClusterResolverInfos = map[ClusterResolverName]ClusterResolverInfo{
-	ClusterCAData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "cluster_ca_data",
-	},
-	ClusterLocalhost: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "cluster_hostname",
-	},
-	ClientCertData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "client_cert_data",
-	},
-	ClientKeyData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "client_key_data",
-	},
-	OIDCIssuerData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "oidc_idp_issuer_ca_data",
-	},
-	TokenData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "token_data",
-	},
-	GCPKeyData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "gcp_key_data",
-	},
-	AWSData: {
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "aws_access_key_id,aws_secret_access_key,aws_cluster_id",
-	},
-}
-
-// ClusterResolverAll is a helper type that contains the fields for
-// all possible resolvers, so that raw bytes can be unmarshaled in a single
-// read
-type ClusterResolverAll struct {
-	ClusterCAData      string `json:"cluster_ca_data,omitempty"`
-	ClusterHostname    string `json:"cluster_hostname,omitempty"`
-	ClientCertData     string `json:"client_cert_data,omitempty"`
-	ClientKeyData      string `json:"client_key_data,omitempty"`
-	OIDCIssuerCAData   string `json:"oidc_idp_issuer_ca_data,omitempty"`
-	TokenData          string `json:"token_data,omitempty"`
-	GCPKeyData         string `json:"gcp_key_data,omitempty"`
-	AWSAccessKeyID     string `json:"aws_access_key_id"`
-	AWSSecretAccessKey string `json:"aws_secret_access_key"`
-	AWSClusterID       string `json:"aws_cluster_id"`
-}
-
 // ClusterResolver is an action that must be resolved to set up
 // a Cluster
 type ClusterResolver struct {
@@ -333,7 +160,7 @@ type ClusterResolver struct {
 	ClusterCandidateID uint `json:"cluster_candidate_id"`
 
 	// One of the ClusterResolverNames
-	Name ClusterResolverName `json:"name"`
+	Name types.ClusterResolverName `json:"name"`
 
 	// Resolved is true if this has been resolved, false otherwise
 	Resolved bool `json:"resolved"`
@@ -343,43 +170,14 @@ type ClusterResolver struct {
 	Data []byte `json:"data,omitempty"`
 }
 
-// ClusterResolverData is a map of key names to fields, which gets marshaled from
-// the raw JSON bytes stored in the ClusterResolver
-type ClusterResolverData map[string]string
-
-// ClusterResolverExternal is an external ClusterResolver to be shared over REST
-type ClusterResolverExternal struct {
-	ID uint `json:"id"`
-
-	// The ClusterCandidate that this is resolving
-	ClusterCandidateID uint `json:"cluster_candidate_id"`
-
-	// One of the ClusterResolverNames
-	Name ClusterResolverName `json:"name"`
-
-	// Resolved is true if this has been resolved, false otherwise
-	Resolved bool `json:"resolved"`
-
-	// Docs is a link to documentation that helps resolve this manually
-	Docs string `json:"docs"`
-
-	// Fields is a list of fields that must be sent with the resolving request
-	Fields string `json:"fields"`
-
-	// Data is additional data for resolving the action, for example a file name,
-	// context name, etc
-	Data ClusterResolverData `json:"data,omitempty"`
-}
-
-// Externalize generates an external ClusterResolver to be shared over REST
-func (cr *ClusterResolver) Externalize() *ClusterResolverExternal {
-	info := ClusterResolverInfos[cr.Name]
+func (cr *ClusterResolver) ToClusterResolverType() *types.ClusterResolver {
+	info := types.ClusterResolverInfos[cr.Name]
 
-	data := make(ClusterResolverData)
+	data := make(types.ClusterResolverData)
 
 	json.Unmarshal(cr.Data, &data)
 
-	return &ClusterResolverExternal{
+	return &types.ClusterResolver{
 		ID:                 cr.ID,
 		ClusterCandidateID: cr.ClusterCandidateID,
 		Name:               cr.Name,

+ 3 - 2
internal/models/cluster_test.go

@@ -5,11 +5,12 @@ import (
 	"testing"
 
 	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
 
 func TestClusterResolverExternalize(t *testing.T) {
-	crData := models.ClusterResolverData{
+	crData := types.ClusterResolverData{
 		"filename": "/hello/there.pem",
 		"key":      "value",
 	}
@@ -25,7 +26,7 @@ func TestClusterResolverExternalize(t *testing.T) {
 		Data: bytes,
 	}
 
-	crExternal := cr.Externalize()
+	crExternal := cr.ToClusterResolverType()
 
 	if diff := deep.Equal(crExternal.Data, crData); diff != nil {
 		t.Errorf("incorrect cluster resolver data")

+ 2 - 1
internal/repository/gorm/cluster_test.go

@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 	orm "gorm.io/gorm"
@@ -74,7 +75,7 @@ func TestCreateClusterCandidateWithResolvers(t *testing.T) {
 		CreatedClusterID: 0,
 		Resolvers: []models.ClusterResolver{
 			{
-				Name:     models.ClusterLocalhost,
+				Name:     types.ClusterLocalhost,
 				Resolved: false,
 			},
 		},