فهرست منبع

rewrite kubeconfig resolvers

Alexander Belanger 5 سال پیش
والد
کامیت
0767731cd3

+ 1 - 0
go.mod

@@ -30,6 +30,7 @@ require (
 	github.com/gorilla/securecookie v1.1.1
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.4.2
+	github.com/hashicorp/consul/api v1.3.0
 	github.com/imdario/mergo v0.3.11 // indirect
 	github.com/jinzhu/gorm v1.9.16
 	github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd

+ 7 - 0
go.sum

@@ -160,6 +160,7 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy
 github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
@@ -675,20 +676,24 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
 github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/api v1.3.0 h1:HXNYlRkkM/t+Y/Yhxtwcy02dlYwIaoxzvxPnS+cqy78=
 github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
 github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
 github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
 github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
+github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
 github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
 github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
 github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -707,6 +712,7 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2 h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
 github.com/heketi/heketi v9.0.1-0.20190917153846-c2e2a4ab7ab9+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o=
 github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4=
@@ -948,6 +954,7 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT
 github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
 github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=

+ 0 - 376
internal/forms/action.go

@@ -1,376 +0,0 @@
-package forms
-
-import (
-	"encoding/base64"
-	"net/url"
-	"strings"
-
-	"github.com/porter-dev/porter/internal/kubernetes"
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-)
-
-// ActionResolver exposes an interface for resolving an action as a ServiceAccount.
-// So that actions can be chained together, a pointer to a serviceAccount can be
-// used -- if this points to nil, a new service account is created
-type ActionResolver interface {
-	PopulateServiceAccount(repo repository.ServiceAccountRepository) error
-}
-
-// ServiceAccountActionResolver is the base type for resolving a ServiceAccountAction
-// that belongs to a given ServiceAccountCandidate
-type ServiceAccountActionResolver struct {
-	ServiceAccountCandidateID uint `json:"sa_candidate_id" form:"required"`
-	SA                        *models.ServiceAccount
-	SACandidate               *models.ServiceAccountCandidate
-}
-
-// PopulateServiceAccount will create a service account if it does not exist,
-// or will append a new cluster given by a ServiceAccountCandidate to the
-// ServiceAccount
-func (sar *ServiceAccountActionResolver) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	var err error
-	id := sar.ServiceAccountCandidateID
-
-	if sar.SACandidate == nil {
-		sar.SACandidate, err = repo.ReadServiceAccountCandidate(id)
-
-		if err != nil {
-			return err
-		}
-	}
-
-	rawConf, err := kubernetes.GetRawConfigFromBytes(sar.SACandidate.Kubeconfig)
-
-	if err != nil {
-		return err
-	}
-
-	context := rawConf.Contexts[rawConf.CurrentContext]
-
-	authInfoName := context.AuthInfo
-	authInfo := rawConf.AuthInfos[authInfoName]
-
-	clusterName := context.Cluster
-	cluster := rawConf.Clusters[clusterName]
-
-	modelCluster := models.Cluster{
-		Name:                  clusterName,
-		LocationOfOrigin:      cluster.LocationOfOrigin,
-		Server:                cluster.Server,
-		TLSServerName:         cluster.TLSServerName,
-		InsecureSkipTLSVerify: cluster.InsecureSkipTLSVerify,
-	}
-
-	if len(cluster.CertificateAuthorityData) > 0 {
-		modelCluster.CertificateAuthorityData = cluster.CertificateAuthorityData
-	}
-
-	if sar.SA == nil {
-		sar.SA = &models.ServiceAccount{
-			ProjectID:         sar.SACandidate.ProjectID,
-			Kind:              sar.SACandidate.Kind,
-			Clusters:          []models.Cluster{modelCluster},
-			Integration:     sar.SACandidate.Integration,
-			LocationOfOrigin:  authInfo.LocationOfOrigin,
-			Impersonate:       authInfo.Impersonate,
-			ImpersonateGroups: strings.Join(authInfo.ImpersonateGroups, ","),
-		}
-	} else {
-		doesClusterExist := false
-
-		for _, cluster := range sar.SA.Clusters {
-			if cluster.Name == sar.SACandidate.ClusterName && cluster.Server == sar.SACandidate.ClusterEndpoint {
-				doesClusterExist = true
-			}
-		}
-
-		if !doesClusterExist {
-			sar.SA.Clusters = append(sar.SA.Clusters, modelCluster)
-		}
-	}
-
-	// if auth mechanism is local, just write the kubeconfig and return: rest of config is
-	// unnecessary
-	if sar.SACandidate.Integration == models.Local && len(sar.SACandidate.Kubeconfig) > 0 {
-		sar.SA.Kubeconfig = sar.SACandidate.Kubeconfig
-		return nil
-	}
-
-	if len(authInfo.ClientCertificateData) > 0 {
-		sar.SA.ClientCertificateData = authInfo.ClientCertificateData
-	}
-
-	if len(authInfo.ClientKeyData) > 0 {
-		sar.SA.ClientKeyData = authInfo.ClientKeyData
-	}
-
-	if len(authInfo.Token) > 0 {
-		sar.SA.Token = []byte(authInfo.Token)
-	}
-
-	if len(authInfo.Username) > 0 {
-		sar.SA.Username = []byte(authInfo.Username)
-	}
-
-	if len(authInfo.Password) > 0 {
-		sar.SA.Password = []byte(authInfo.Password)
-	}
-
-	if authInfo.AuthProvider != nil && authInfo.AuthProvider.Name == "oidc" {
-		if url, ok := authInfo.AuthProvider.Config["idp-issuer-url"]; ok {
-			sar.SA.OIDCIssuerURL = []byte(url)
-		}
-
-		if clientID, ok := authInfo.AuthProvider.Config["client-id"]; ok {
-			sar.SA.OIDCClientID = []byte(clientID)
-		}
-
-		if clientSecret, ok := authInfo.AuthProvider.Config["client-secret"]; ok {
-			sar.SA.OIDCClientSecret = []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
-			sar.SA.OIDCCertificateAuthorityData = []byte(caData)
-		}
-
-		if idToken, ok := authInfo.AuthProvider.Config["id-token"]; ok {
-			sar.SA.OIDCIDToken = []byte(idToken)
-		}
-
-		if refreshToken, ok := authInfo.AuthProvider.Config["refresh-token"]; ok {
-			sar.SA.OIDCRefreshToken = []byte(refreshToken)
-		}
-	}
-
-	return nil
-}
-
-// ClusterCADataAction contains the base64 encoded cluster CA data
-type ClusterCADataAction struct {
-	*ServiceAccountActionResolver
-	ClusterCAData string `json:"cluster_ca_data" form:"required"`
-}
-
-// PopulateServiceAccount will add cluster ca data to a cluster in the ServiceAccount's
-// list of clusters
-func (cda *ClusterCADataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := cda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	saCandidate := cda.ServiceAccountActionResolver.SACandidate
-
-	for i, cluster := range cda.ServiceAccountActionResolver.SA.Clusters {
-		if cluster.Name == saCandidate.ClusterName && cluster.Server == saCandidate.ClusterEndpoint {
-			decoded, err := base64.StdEncoding.DecodeString(cda.ClusterCAData)
-
-			// skip if decoding error
-			if err != nil {
-				return err
-			}
-
-			(&cluster).CertificateAuthorityData = decoded
-			cda.ServiceAccountActionResolver.SA.Clusters[i] = cluster
-		}
-	}
-
-	return nil
-}
-
-// ClusterLocalhostAction contains the non-localhost server
-type ClusterLocalhostAction struct {
-	*ServiceAccountActionResolver
-	ClusterHostname string `json:"cluster_hostname" form:"required"`
-}
-
-// PopulateServiceAccount will add cluster ca data to a cluster in the ServiceAccount's
-// list of clusters
-func (cla *ClusterLocalhostAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := cla.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	saCandidate := cla.ServiceAccountActionResolver.SACandidate
-
-	for i, cluster := range cla.ServiceAccountActionResolver.SA.Clusters {
-		if cluster.Name == saCandidate.ClusterName && cluster.Server == saCandidate.ClusterEndpoint {
-			serverURL, err := url.Parse(cluster.Server)
-
-			if err != nil {
-				continue
-			}
-
-			if serverURL.Port() == "" {
-				serverURL.Host = cla.ClusterHostname
-			} else {
-				serverURL.Host = cla.ClusterHostname + ":" + serverURL.Port()
-			}
-
-			(&cluster).Server = serverURL.String()
-			cla.ServiceAccountActionResolver.SA.Clusters[i] = cluster
-		}
-	}
-
-	return nil
-}
-
-// ClientCertDataAction contains the base64 encoded cluster cert data
-type ClientCertDataAction struct {
-	*ServiceAccountActionResolver
-	ClientCertData string `json:"client_cert_data" form:"required"`
-}
-
-// PopulateServiceAccount will add client CA data to a ServiceAccount
-func (ccda *ClientCertDataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := ccda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	decoded, err := base64.StdEncoding.DecodeString(ccda.ClientCertData)
-
-	// skip if decoding error
-	if err != nil {
-		return err
-	}
-
-	ccda.ServiceAccountActionResolver.SA.ClientCertificateData = decoded
-
-	return nil
-}
-
-// ClientKeyDataAction contains the base64 encoded cluster key data
-type ClientKeyDataAction struct {
-	*ServiceAccountActionResolver
-	ClientKeyData string `json:"client_key_data" form:"required"`
-}
-
-// PopulateServiceAccount will add client CA data to a ServiceAccount
-func (ckda *ClientKeyDataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := ckda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	decoded, err := base64.StdEncoding.DecodeString(ckda.ClientKeyData)
-
-	// skip if decoding error
-	if err != nil {
-		return err
-	}
-
-	ckda.ServiceAccountActionResolver.SA.ClientKeyData = decoded
-
-	return nil
-}
-
-// OIDCIssuerDataAction contains the base64 encoded IDP issuer CA data
-type OIDCIssuerDataAction struct {
-	*ServiceAccountActionResolver
-	OIDCIssuerCAData string `json:"oidc_idp_issuer_ca_data" form:"required"`
-}
-
-// PopulateServiceAccount will add OIDC issuer CA data to a ServiceAccount
-func (oida *OIDCIssuerDataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := oida.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	// 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
-	oida.ServiceAccountActionResolver.SA.OIDCCertificateAuthorityData = []byte(oida.OIDCIssuerCAData)
-
-	return nil
-}
-
-// TokenDataAction contains the token data to use
-type TokenDataAction struct {
-	*ServiceAccountActionResolver
-	TokenData string `json:"token_data" form:"required"`
-}
-
-// PopulateServiceAccount will add bearer token data to a ServiceAccount
-func (tda *TokenDataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := tda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	tda.ServiceAccountActionResolver.SA.Token = []byte(tda.TokenData)
-
-	return nil
-}
-
-// GCPKeyDataAction contains the GCP key data
-type GCPKeyDataAction struct {
-	*ServiceAccountActionResolver
-	GCPKeyData string `json:"gcp_key_data" form:"required"`
-}
-
-// PopulateServiceAccount will add GCP key data to a ServiceAccount
-func (gkda *GCPKeyDataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := gkda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	gkda.ServiceAccountActionResolver.SA.GCPKeyData = []byte(gkda.GCPKeyData)
-
-	return nil
-}
-
-// AWSDataAction contains the AWS data (access id, key)
-type AWSDataAction struct {
-	*ServiceAccountActionResolver
-	AWSAccessKeyID     string `json:"aws_access_key_id" form:"required"`
-	AWSSecretAccessKey string `json:"aws_secret_access_key" form:"required"`
-	AWSClusterID       string `json:"aws_cluster_id" form:"required"`
-}
-
-// PopulateServiceAccount will add GCP key data to a ServiceAccount
-func (akda *AWSDataAction) PopulateServiceAccount(
-	repo repository.ServiceAccountRepository,
-) error {
-	err := akda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
-
-	if err != nil {
-		return err
-	}
-
-	akda.ServiceAccountActionResolver.SA.AWSAccessKeyID = []byte(akda.AWSAccessKeyID)
-	akda.ServiceAccountActionResolver.SA.AWSSecretAccessKey = []byte(akda.AWSSecretAccessKey)
-	akda.ServiceAccountActionResolver.SA.AWSClusterID = []byte(akda.AWSClusterID)
-
-	return nil
-}

+ 14 - 10
internal/forms/candidate.go

@@ -5,9 +5,9 @@ import (
 	"github.com/porter-dev/porter/internal/models"
 )
 
-// CreateServiceAccountCandidatesForm represents the accepted values for
-// creating a list of ServiceAccountCandidates from a kubeconfig
-type CreateServiceAccountCandidatesForm struct {
+// CreateClusterCandidatesForm represents the accepted values for
+// creating a list of ClusterCandidates from a kubeconfig
+type CreateClusterCandidatesForm struct {
 	ProjectID  uint   `json:"project_id"`
 	Kubeconfig string `json:"kubeconfig"`
 
@@ -17,20 +17,24 @@ type CreateServiceAccountCandidatesForm struct {
 	IsLocal bool `json:"is_local"`
 }
 
-// ToServiceAccountCandidates creates a ServiceAccountCandidate from the kubeconfig and
+// ToClusterCandidates creates a ClusterCandidate from the kubeconfig and
 // project id
-func (csa *CreateServiceAccountCandidatesForm) ToServiceAccountCandidates(
+func (csa *CreateClusterCandidatesForm) ToClusterCandidates(
 	isServerLocal bool,
-) ([]*models.ServiceAccountCandidate, error) {
-	// can only use "local" auth mechanism if the server is running locally
-	candidates, err := kubernetes.GetServiceAccountCandidates([]byte(csa.Kubeconfig), isServerLocal && csa.IsLocal)
+) ([]*models.ClusterCandidate, error) {
+	candidates, err := kubernetes.GetClusterCandidatesFromKubeconfig(
+		[]byte(csa.Kubeconfig),
+		csa.ProjectID,
+		// can only use "local" auth mechanism if the server is running locally
+		isServerLocal && csa.IsLocal,
+	)
 
 	if err != nil {
 		return nil, err
 	}
 
-	for _, saCandidate := range candidates {
-		saCandidate.ProjectID = csa.ProjectID
+	for _, cc := range candidates {
+		cc.ProjectID = csa.ProjectID
 	}
 
 	return candidates, nil

+ 1 - 0
internal/forms/candidate_test.go

@@ -0,0 +1 @@
+package forms_test

+ 634 - 0
internal/forms/cluster.go

@@ -0,0 +1,634 @@
+package forms
+
+import (
+	"encoding/base64"
+	"errors"
+	"net/url"
+	"strings"
+
+	"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"
+)
+
+// ResolveClusterForm will resolve a cluster candidate and create a new cluster
+type ResolveClusterForm struct {
+	Resolver *models.ClusterResolverAll `form:"required"`
+
+	ClusterCandidateID uint `json:"cluster_candidate_id" form:"required"`
+	ProjectID          uint `json:"project_id" form:"required"`
+	UserID             uint `json:"user_id" form:"required"`
+
+	// populated during the ResolveIntegration step
+	IntegrationID    uint
+	ClusterCandidate *models.ClusterCandidate
+	RawConf          *api.Config
+}
+
+// ResolveIntegration creates an integration in the DB
+func (rcf *ResolveClusterForm) ResolveIntegration(
+	repo repository.Repository,
+) error {
+	cc, err := repo.Cluster.ReadClusterCandidate(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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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 *ResolveClusterForm) 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
+}
+
+// // Resolver exposes an interface for resolving a ClusterCandidate.
+// // So that actions can be chained together, a pointer to a cluster can be
+// // used -- if this points to nil, a new cluster is created
+// type Resolver interface {
+// 	PopulateServiceAccount(repo repository.ClusterRepository) error
+// }
+
+// // ClusterResolver is the base type for resolving a ClusterCandidate
+// type ClusterResolver struct {
+// 	ClusterCandidateID uint `json:"cluster_candidate_id" form:"required"`
+// 	ProjectID uint `json:"project_id" form:"required"`
+// 	Cluster            *models.Cluster
+// 	ClusterCandidate   *models.ClusterCandidate
+// }
+
+// // PopulateServiceAccount will create a service account if it does not exist,
+// // or will append a new cluster given by a ServiceAccountCandidate to the
+// // ServiceAccount
+// func (cr *ClusterResolver) PopulateServiceAccount(
+// 	repo repository.ClusterRepository,
+// ) error {
+// 	var err error
+// 	id := cr.ClusterCandidateID
+
+// 	if cr.ClusterCandidate == nil {
+// 		cr.ClusterCandidate, err = repo.ReadClusterCandidate(id)
+
+// 		if err != nil {
+// 			return err
+// 		}
+// 	}
+
+// 	rawConf, err := kubernetes.GetRawConfigFromBytes(cr.ClusterCandidate.Kubeconfig)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	context := rawConf.Contexts[rawConf.CurrentContext]
+
+// 	authInfoName := context.AuthInfo
+// 	authInfo := rawConf.AuthInfos[authInfoName]
+
+// 	clusterName := context.Cluster
+// 	cluster := rawConf.Clusters[clusterName]
+
+// 	cr.Cluster = models.Cluster{
+// 		ProjectID: cr.ProjectID,
+// 		Name:                  clusterName,
+// 		Server:                cluster.Server,
+// 		LocationOfOrigin:      cluster.ClusterLocationOfOrigin,
+// 		TLSServerName:         cluster.TLSServerName,
+// 		InsecureSkipTLSVerify: cluster.InsecureSkipTLSVerify,
+// 	}
+
+// 	if len(cr.Cluster.CertificateAuthorityData) > 0 {
+// 		cr.Cluster.CertificateAuthorityData = cluster.CertificateAuthorityData
+// 	}
+
+// 	// if auth mechanism is local, just write the kubeconfig and return: rest of config is
+// 	// unnecessary
+// 	if sar.SACandidate.Integration == models.Local && len(sar.SACandidate.Kubeconfig) > 0 {
+// 		sar.SA.Kubeconfig = sar.SACandidate.Kubeconfig
+// 		return nil
+// 	}
+
+// 	if len(authInfo.ClientCertificateData) > 0 {
+// 		sar.SA.ClientCertificateData = authInfo.ClientCertificateData
+// 	}
+
+// 	if len(authInfo.ClientKeyData) > 0 {
+// 		sar.SA.ClientKeyData = authInfo.ClientKeyData
+// 	}
+
+// 	if len(authInfo.Token) > 0 {
+// 		sar.SA.Token = []byte(authInfo.Token)
+// 	}
+
+// 	if len(authInfo.Username) > 0 {
+// 		sar.SA.Username = []byte(authInfo.Username)
+// 	}
+
+// 	if len(authInfo.Password) > 0 {
+// 		sar.SA.Password = []byte(authInfo.Password)
+// 	}
+
+// 	if authInfo.AuthProvider != nil && authInfo.AuthProvider.Name == "oidc" {
+// 		if url, ok := authInfo.AuthProvider.Config["idp-issuer-url"]; ok {
+// 			sar.SA.OIDCIssuerURL = []byte(url)
+// 		}
+
+// 		if clientID, ok := authInfo.AuthProvider.Config["client-id"]; ok {
+// 			sar.SA.OIDCClientID = []byte(clientID)
+// 		}
+
+// 		if clientSecret, ok := authInfo.AuthProvider.Config["client-secret"]; ok {
+// 			sar.SA.OIDCClientSecret = []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
+// 			sar.SA.OIDCCertificateAuthorityData = []byte(caData)
+// 		}
+
+// 		if idToken, ok := authInfo.AuthProvider.Config["id-token"]; ok {
+// 			sar.SA.OIDCIDToken = []byte(idToken)
+// 		}
+
+// 		if refreshToken, ok := authInfo.AuthProvider.Config["refresh-token"]; ok {
+// 			sar.SA.OIDCRefreshToken = []byte(refreshToken)
+// 		}
+// 	}
+
+// 	return nil
+// }
+
+// // ClientKeyDataAction contains the base64 encoded cluster key data
+// type ClientKeyDataAction struct {
+// 	*ClusterResolver
+// 	ClientKeyData string `json:"client_key_data" form:"required"`
+// }
+
+// // OIDCIssuerDataAction contains the base64 encoded IDP issuer CA data
+// type OIDCIssuerDataAction struct {
+// 	*ClusterResolver
+// 	OIDCIssuerCAData string `json:"oidc_idp_issuer_ca_data" form:"required"`
+// }
+
+// // PopulateServiceAccount will add OIDC issuer CA data to a ServiceAccount
+// func (oida *OIDCIssuerDataAction) PopulateServiceAccount(
+// 	repo repository.ClusterRepository,
+// ) error {
+// 	err := oida.ServiceAccountActionResolver.PopulateServiceAccount(repo)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	// 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
+// 	oida.ServiceAccountActionResolver.SA.OIDCCertificateAuthorityData = []byte(oida.OIDCIssuerCAData)
+
+// 	return nil
+// }
+
+// // GCPKeyDataAction contains the GCP key data
+// type GCPKeyDataAction struct {
+// 	*ClusterResolver
+// 	GCPKeyData string `json:"gcp_key_data" form:"required"`
+// }
+
+// // PopulateServiceAccount will add GCP key data to a ServiceAccount
+// func (gkda *GCPKeyDataAction) PopulateServiceAccount(
+// 	repo repository.ClusterRepository,
+// ) error {
+// 	err := gkda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	gkda.ServiceAccountActionResolver.SA.GCPKeyData = []byte(gkda.GCPKeyData)
+
+// 	return nil
+// }
+
+// // AWSDataAction contains the AWS data (access id, key)
+// type AWSDataAction struct {
+// 	*ClusterResolver
+// 	AWSAccessKeyID     string `json:"aws_access_key_id" form:"required"`
+// 	AWSSecretAccessKey string `json:"aws_secret_access_key" form:"required"`
+// 	AWSClusterID       string `json:"aws_cluster_id" form:"required"`
+// }
+
+// // PopulateServiceAccount will add GCP key data to a ServiceAccount
+// func (akda *AWSDataAction) PopulateServiceAccount(
+// 	repo repository.ClusterRepository,
+// ) error {
+// 	err := akda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	akda.ServiceAccountActionResolver.SA.AWSAccessKeyID = []byte(akda.AWSAccessKeyID)
+// 	akda.ServiceAccountActionResolver.SA.AWSSecretAccessKey = []byte(akda.AWSSecretAccessKey)
+// 	akda.ServiceAccountActionResolver.SA.AWSClusterID = []byte(akda.AWSClusterID)
+
+// 	return nil
+// }

+ 0 - 0
internal/forms/action_test.go → internal/forms/cluster_test.go


+ 3 - 13
internal/forms/k8s.go

@@ -17,7 +17,7 @@ type K8sForm struct {
 // url.Values (the parsed query params)
 func (kf *K8sForm) PopulateK8sOptionsFromQueryParams(
 	vals url.Values,
-	repo repository.ServiceAccountRepository,
+	repo repository.ClusterRepository,
 ) error {
 	if clusterID, ok := vals["cluster_id"]; ok && len(clusterID) == 1 {
 		id, err := strconv.ParseUint(clusterID[0], 10, 64)
@@ -26,23 +26,13 @@ func (kf *K8sForm) PopulateK8sOptionsFromQueryParams(
 			return err
 		}
 
-		kf.ClusterID = uint(id)
-	}
-
-	if serviceAccountID, ok := vals["service_account_id"]; ok && len(serviceAccountID) == 1 {
-		id, err := strconv.ParseUint(serviceAccountID[0], 10, 64)
-
-		if err != nil {
-			return err
-		}
-
-		sa, err := repo.ReadServiceAccount(uint(id))
+		cluster, err := repo.ReadCluster(uint(id))
 
 		if err != nil {
 			return err
 		}
 
-		kf.ServiceAccount = sa
+		kf.Cluster = cluster
 	}
 
 	return nil

+ 4 - 14
internal/forms/release.go

@@ -17,7 +17,7 @@ type ReleaseForm struct {
 // url.Values (the parsed query params)
 func (rf *ReleaseForm) PopulateHelmOptionsFromQueryParams(
 	vals url.Values,
-	repo repository.ServiceAccountRepository,
+	repo repository.ClusterRepository,
 ) error {
 	if clusterID, ok := vals["cluster_id"]; ok && len(clusterID) == 1 {
 		id, err := strconv.ParseUint(clusterID[0], 10, 64)
@@ -26,23 +26,13 @@ func (rf *ReleaseForm) PopulateHelmOptionsFromQueryParams(
 			return err
 		}
 
-		rf.ClusterID = uint(id)
-	}
-
-	if serviceAccountID, ok := vals["service_account_id"]; ok && len(serviceAccountID) == 1 {
-		id, err := strconv.ParseUint(serviceAccountID[0], 10, 64)
-
-		if err != nil {
-			return err
-		}
-
-		sa, err := repo.ReadServiceAccount(uint(id))
+		cluster, err := repo.ReadCluster(uint(id))
 
 		if err != nil {
 			return err
 		}
 
-		rf.ServiceAccount = sa
+		rf.Cluster = cluster
 	}
 
 	if namespace, ok := vals["namespace"]; ok && len(namespace) == 1 {
@@ -66,7 +56,7 @@ type ListReleaseForm struct {
 // url.Values (the parsed query params)
 func (lrf *ListReleaseForm) PopulateListFromQueryParams(
 	vals url.Values,
-	_ repository.ServiceAccountRepository,
+	_ repository.ClusterRepository,
 ) error {
 	if namespace, ok := vals["namespace"]; ok && len(namespace) == 1 {
 		lrf.ListFilter.Namespace = namespace[0]

+ 7 - 8
internal/helm/config.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/kube"
@@ -18,11 +19,10 @@ import (
 // Form represents the options for connecting to a cluster and
 // creating a Helm agent
 type Form struct {
-	ServiceAccount   *models.ServiceAccount `form:"required"`
-	ClusterID        uint                   `json:"cluster_id" form:"required"`
-	Storage          string                 `json:"storage" form:"oneof=secret configmap memory"`
-	Namespace        string                 `json:"namespace"`
-	UpdateTokenCache kubernetes.UpdateTokenCacheFunc
+	Cluster   *models.Cluster `form:"required"`
+	Repo      *repository.Repository
+	Storage   string `json:"storage" form:"oneof=secret configmap memory"`
+	Namespace string `json:"namespace"`
 }
 
 // GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
@@ -30,9 +30,8 @@ type Form struct {
 func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 	// create a kubernetes agent
 	conf := &kubernetes.OutOfClusterConfig{
-		ServiceAccount:   form.ServiceAccount,
-		ClusterID:        form.ClusterID,
-		UpdateTokenCache: form.UpdateTokenCache,
+		Cluster: form.Cluster,
+		Repo:    form.Repo,
 	}
 
 	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)

+ 20 - 9
internal/kubernetes/kubeconfig.go

@@ -16,7 +16,11 @@ import (
 // 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 GetClusterCandidatesFromKubeconfig(kubeconfig []byte, projectID uint) ([]*models.ClusterCandidate, error) {
+func GetClusterCandidatesFromKubeconfig(
+	kubeconfig []byte,
+	projectID uint,
+	local bool,
+) ([]*models.ClusterCandidate, error) {
 	config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
 
 	if err != nil {
@@ -36,15 +40,22 @@ func GetClusterCandidatesFromKubeconfig(kubeconfig []byte, projectID uint) ([]*m
 		awsClusterID := ""
 		authInfoName := context.AuthInfo
 
-		// get the resolvers, if needed
-		authMechanism, resolvers := parseAuthInfoForResolvers(rawConf.AuthInfos[authInfoName])
-		clusterResolvers := parseClusterForResolvers(rawConf.Clusters[clusterName])
-		resolvers = append(resolvers, clusterResolvers...)
+		resolvers := make([]models.ClusterResolver, 0)
+		var authMechanism models.ClusterAuth
 
-		if authMechanism == models.AWS {
-			// if the auth mechanism is AWS, we need to parse more explicitly
-			// for the cluster id
-			awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
+		if local {
+			authMechanism = models.Local
+		} else {
+			// get the resolvers, if needed
+			authMechanism, resolvers = parseAuthInfoForResolvers(rawConf.AuthInfos[authInfoName])
+			clusterResolvers := parseClusterForResolvers(rawConf.Clusters[clusterName])
+			resolvers = append(resolvers, clusterResolvers...)
+
+			if authMechanism == 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

+ 1 - 1
internal/kubernetes/kubeconfig_test.go

@@ -305,7 +305,7 @@ var ClusterCandidatesTests = []ccsTest{
 
 func TestGetClusterCandidatesNonLocal(t *testing.T) {
 	for _, c := range ClusterCandidatesTests {
-		result, err := kubernetes.GetClusterCandidatesFromKubeconfig(c.raw, 1)
+		result, err := kubernetes.GetClusterCandidatesFromKubeconfig(c.raw, 1, false)
 
 		if err != nil {
 			t.Fatalf("error occurred %v\n", err)

+ 18 - 18
internal/repository/test/auth.go

@@ -137,24 +137,24 @@ func (repo *OIDCIntegrationRepository) ListOIDCIntegrationsByProjectID(
 	return res, nil
 }
 
-// OIntegrationRepository implements repository.OIntegrationRepository
-type OIntegrationRepository struct {
+// OAuthIntegrationRepository implements repository.OAuthIntegrationRepository
+type OAuthIntegrationRepository struct {
 	canQuery      bool
-	oIntegrations []*ints.OIntegration
+	oIntegrations []*ints.OAuthIntegration
 }
 
-// NewOIntegrationRepository will return errors if canQuery is false
-func NewOIntegrationRepository(canQuery bool) repository.OIntegrationRepository {
-	return &OIntegrationRepository{
+// NewOAuthIntegrationRepository will return errors if canQuery is false
+func NewOAuthIntegrationRepository(canQuery bool) repository.OAuthIntegrationRepository {
+	return &OAuthIntegrationRepository{
 		canQuery,
-		[]*ints.OIntegration{},
+		[]*ints.OAuthIntegration{},
 	}
 }
 
-// CreateOIntegration creates a new o auth mechanism
-func (repo *OIntegrationRepository) CreateOIntegration(
-	am *ints.OIntegration,
-) (*ints.OIntegration, error) {
+// CreateOAuthIntegration creates a new o auth mechanism
+func (repo *OAuthIntegrationRepository) CreateOAuthIntegration(
+	am *ints.OAuthIntegration,
+) (*ints.OAuthIntegration, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot write database")
 	}
@@ -165,10 +165,10 @@ func (repo *OIntegrationRepository) CreateOIntegration(
 	return am, nil
 }
 
-// ReadOIntegration finds a o auth mechanism by id
-func (repo *OIntegrationRepository) ReadOIntegration(
+// ReadOAuthIntegration finds a o auth mechanism by id
+func (repo *OAuthIntegrationRepository) ReadOAuthIntegration(
 	id uint,
-) (*ints.OIntegration, error) {
+) (*ints.OAuthIntegration, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot read from database")
 	}
@@ -181,16 +181,16 @@ func (repo *OIntegrationRepository) ReadOIntegration(
 	return repo.oIntegrations[index], nil
 }
 
-// ListOIntegrationsByProjectID finds all o auth mechanisms
+// ListOAuthIntegrationsByProjectID finds all o auth mechanisms
 // for a given project id
-func (repo *OIntegrationRepository) ListOIntegrationsByProjectID(
+func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
 	projectID uint,
-) ([]*ints.OIntegration, error) {
+) ([]*ints.OAuthIntegration, error) {
 	if !repo.canQuery {
 		return nil, errors.New("Cannot read from database")
 	}
 
-	res := make([]*ints.OIntegration, 0)
+	res := make([]*ints.OAuthIntegration, 0)
 
 	for _, oAM := range repo.oIntegrations {
 		if oAM.ProjectID == projectID {