Alexander Belanger 5 лет назад
Родитель
Сommit
ab9c325e8a
42 измененных файлов с 4158 добавлено и 1937 удалено
  1. 8 8
      cli/cmd/api/project_test.go
  2. 2 2
      internal/forms/action.go
  3. 9 9
      internal/forms/action_test.go
  4. 10 10
      internal/kubernetes/kubeconfig.go
  5. 19 19
      internal/kubernetes/kubeconfig_test.go
  6. 0 127
      internal/models/action.go
  7. 288 25
      internal/models/cluster.go
  8. 34 0
      internal/models/cluster_test.go
  9. 0 16
      internal/models/context.go
  10. 40 0
      internal/models/gitrepo.go
  11. 121 0
      internal/models/integrations/aws.go
  12. 95 0
      internal/models/integrations/gcp.go
  13. 71 0
      internal/models/integrations/kube.go
  14. 63 0
      internal/models/integrations/oauth.go
  15. 76 0
      internal/models/integrations/oidc.go
  16. 11 3
      internal/models/integrations/token_cache.go
  17. 25 13
      internal/models/project.go
  18. 0 51
      internal/models/repoclient.go
  19. 0 172
      internal/models/serviceaccount.go
  20. 19 0
      internal/repository/cluster.go
  21. 11 0
      internal/repository/gitrepo.go
  22. 909 0
      internal/repository/gorm/auth.go
  23. 461 0
      internal/repository/gorm/auth_test.go
  24. 64 0
      internal/repository/gorm/gitrepo.go
  25. 104 0
      internal/repository/gorm/gitrepo_test.go
  26. 204 54
      internal/repository/gorm/helpers_test.go
  27. 0 134
      internal/repository/gorm/repoclient.go
  28. 0 112
      internal/repository/gorm/repoclient_test.go
  29. 9 5
      internal/repository/gorm/repository.go
  30. 466 466
      internal/repository/gorm/serviceaccount.go
  31. 416 416
      internal/repository/gorm/serviceaccount_test.go
  32. 45 0
      internal/repository/integrations.go
  33. 0 11
      internal/repository/repoclient.go
  34. 10 5
      internal/repository/repository.go
  35. 0 18
      internal/repository/serviceaccount.go
  36. 330 0
      internal/repository/test/auth.go
  37. 160 0
      internal/repository/test/cluster.go
  38. 67 0
      internal/repository/test/gitrepo.go
  39. 0 66
      internal/repository/test/repoclient.go
  40. 5 4
      internal/repository/test/repository.go
  41. 0 185
      internal/repository/test/serviceaccount.go
  42. 6 6
      server/api/project_handler_test.go

+ 8 - 8
cli/cmd/api/project_test.go

@@ -169,8 +169,8 @@ func TestGetProjectServiceAccount(t *testing.T) {
 		t.Errorf("service account kind incorrect: expected %s, got %s\n", "connector", resp.Kind)
 	}
 
-	if resp.AuthMechanism != models.OIDC {
-		t.Errorf("service account auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp.AuthMechanism)
+	if resp.Integration != models.OIDC {
+		t.Errorf("service account auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp.Integration)
 	}
 
 	// verify clusters
@@ -219,8 +219,8 @@ func TestCreateProjectCandidates(t *testing.T) {
 	}
 
 	// make sure auth mechanism is OIDC, project id is correct, and cluster info is correct
-	if resp[0].AuthMechanism != models.OIDC {
-		t.Errorf("oidc auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp[0].AuthMechanism)
+	if resp[0].Integration != models.OIDC {
+		t.Errorf("oidc auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp[0].Integration)
 	}
 
 	if resp[0].ProjectID != project.ID {
@@ -272,8 +272,8 @@ func TestGetProjectCandidates(t *testing.T) {
 	}
 
 	// make sure auth mechanism is OIDC, project id is correct, and cluster info is correct
-	if resp[0].AuthMechanism != models.OIDC {
-		t.Errorf("oidc auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp[0].AuthMechanism)
+	if resp[0].Integration != models.OIDC {
+		t.Errorf("oidc auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp[0].Integration)
 	}
 
 	if resp[0].ProjectID != project.ID {
@@ -338,8 +338,8 @@ func TestCreateProjectServiceAccount(t *testing.T) {
 		t.Errorf("service account kind incorrect: expected %s, got %s\n", "connector", resp.Kind)
 	}
 
-	if resp.AuthMechanism != models.OIDC {
-		t.Errorf("service account auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp.AuthMechanism)
+	if resp.Integration != models.OIDC {
+		t.Errorf("service account auth mechanism incorrect: expected %s, got %s\n", models.OIDC, resp.Integration)
 	}
 
 	// verify clusters

+ 2 - 2
internal/forms/action.go

@@ -73,7 +73,7 @@ func (sar *ServiceAccountActionResolver) PopulateServiceAccount(
 			ProjectID:         sar.SACandidate.ProjectID,
 			Kind:              sar.SACandidate.Kind,
 			Clusters:          []models.Cluster{modelCluster},
-			AuthMechanism:     sar.SACandidate.AuthMechanism,
+			Integration:     sar.SACandidate.Integration,
 			LocationOfOrigin:  authInfo.LocationOfOrigin,
 			Impersonate:       authInfo.Impersonate,
 			ImpersonateGroups: strings.Join(authInfo.ImpersonateGroups, ","),
@@ -94,7 +94,7 @@ func (sar *ServiceAccountActionResolver) PopulateServiceAccount(
 
 	// if auth mechanism is local, just write the kubeconfig and return: rest of config is
 	// unnecessary
-	if sar.SACandidate.AuthMechanism == models.Local && len(sar.SACandidate.Kubeconfig) > 0 {
+	if sar.SACandidate.Integration == models.Local && len(sar.SACandidate.Kubeconfig) > 0 {
 		sar.SA.Kubeconfig = sar.SACandidate.Kubeconfig
 		return nil
 	}

+ 9 - 9
internal/forms/action_test.go

@@ -57,7 +57,7 @@ func TestPopulateServiceAccountBasic(t *testing.T) {
 			string(sa.Clusters[0].CertificateAuthorityData), string(decodedStr))
 	}
 
-	if sa.AuthMechanism != "x509" {
+	if sa.Integration != "x509" {
 		t.Errorf("service account auth mechanism is not x509")
 	}
 
@@ -122,7 +122,7 @@ func TestPopulateServiceAccountClusterDataAction(t *testing.T) {
 			string(sa.Clusters[0].CertificateAuthorityData), string(decodedStr))
 	}
 
-	if sa.AuthMechanism != "x509" {
+	if sa.Integration != "x509" {
 		t.Errorf("service account auth mechanism is not x509")
 	}
 
@@ -187,7 +187,7 @@ func TestPopulateServiceAccountClusterLocalhostAction(t *testing.T) {
 			"https://host.docker.internal:30000", sa.Clusters[0].Server)
 	}
 
-	if sa.AuthMechanism != "x509" {
+	if sa.Integration != "x509" {
 		t.Errorf("service account auth mechanism is not x509")
 	}
 
@@ -252,7 +252,7 @@ func TestPopulateServiceAccountClientCertAction(t *testing.T) {
 			string(sa.Clusters[0].CertificateAuthorityData), string(decodedStr))
 	}
 
-	if sa.AuthMechanism != "x509" {
+	if sa.Integration != "x509" {
 		t.Errorf("service account auth mechanism is not x509")
 	}
 
@@ -328,7 +328,7 @@ func TestPopulateServiceAccountClientCertAndKeyActions(t *testing.T) {
 			string(sa.Clusters[0].CertificateAuthorityData), string(decodedStr))
 	}
 
-	if sa.AuthMechanism != "x509" {
+	if sa.Integration != "x509" {
 		t.Errorf("service account auth mechanism is not x509")
 	}
 
@@ -388,7 +388,7 @@ func TestPopulateServiceAccountTokenDataAction(t *testing.T) {
 		t.Errorf("service account ID of joined cluster is not 1")
 	}
 
-	if sa.AuthMechanism != models.Bearer {
+	if sa.Integration != models.Bearer {
 		t.Errorf("service account auth mechanism is not %s\n", models.Bearer)
 	}
 
@@ -443,7 +443,7 @@ func TestPopulateServiceAccountGCPKeyDataAction(t *testing.T) {
 		t.Errorf("service account ID of joined cluster is not 1")
 	}
 
-	if sa.AuthMechanism != models.GCP {
+	if sa.Integration != models.GCP {
 		t.Errorf("service account auth mechanism is not %s\n", models.GCP)
 	}
 
@@ -499,7 +499,7 @@ func TestPopulateServiceAccountAWSKeyDataAction(t *testing.T) {
 		t.Errorf("service account ID of joined cluster is not 1")
 	}
 
-	if sa.AuthMechanism != models.AWS {
+	if sa.Integration != models.AWS {
 		t.Errorf("service account auth mechanism is not %s\n", models.AWS)
 	}
 
@@ -563,7 +563,7 @@ func TestPopulateServiceAccountOIDCAction(t *testing.T) {
 		t.Errorf("service account ID of joined cluster is not 1")
 	}
 
-	if sa.AuthMechanism != models.OIDC {
+	if sa.Integration != models.OIDC {
 		t.Errorf("service account auth mechanism is not %s\n", models.OIDC)
 	}
 

+ 10 - 10
internal/kubernetes/kubeconfig.go

@@ -45,20 +45,20 @@ func GetServiceAccountCandidates(kubeconfig []byte, local bool) ([]*models.Servi
 		authInfoName := context.AuthInfo
 
 		actions := make([]models.ServiceAccountAction, 0)
-		var authMechanism string
+		var integration string
 
 		if local {
-			authMechanism = models.Local
+			integration = models.Local
 		} else {
 			// get the auth mechanism and actions
-			authMechanism, actions = parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
+			integration, actions = parseAuthInfoForActions(rawConf.AuthInfos[authInfoName])
 			clusterActions := parseClusterForActions(rawConf.Clusters[clusterName])
 			actions = append(actions, clusterActions...)
 
 			// if auth mechanism is unsupported, we'll skip it
-			if authMechanism == models.NotAvailable {
+			if integration == models.NotAvailable {
 				continue
-			} else if authMechanism == models.AWS {
+			} 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)
@@ -82,7 +82,7 @@ func GetServiceAccountCandidates(kubeconfig []byte, local bool) ([]*models.Servi
 				ContextName:       contextName,
 				ClusterName:       clusterName,
 				ClusterEndpoint:   rawConf.Clusters[clusterName].Server,
-				AuthMechanism:     authMechanism,
+				Integration:     integration,
 				AWSClusterIDGuess: awsClusterID,
 				Kubeconfig:        rawBytes,
 			})
@@ -118,7 +118,7 @@ func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
 // (4) If a username/password exist, uses basic auth mechanism
 // (5) Otherwise, the config gets skipped
 //
-func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, actions []models.ServiceAccountAction) {
+func parseAuthInfoForActions(authInfo *api.AuthInfo) (integration string, actions []models.ServiceAccountAction) {
 	actions = make([]models.ServiceAccountAction, 0)
 
 	if (authInfo.ClientCertificate != "" || len(authInfo.ClientCertificateData) != 0) &&
@@ -292,7 +292,7 @@ func GetClientConfigFromServiceAccount(
 	clusterID uint,
 	updateTokenCache UpdateTokenCacheFunc,
 ) (clientcmd.ClientConfig, error) {
-	if sa.AuthMechanism == models.Local {
+	if sa.Integration == models.Local {
 		return clientcmd.NewClientConfigFromBytes(sa.Kubeconfig)
 	}
 
@@ -338,7 +338,7 @@ func createRawConfigFromServiceAccount(
 	}
 
 	// construct the auth infos
-	authInfoName := cluster.Name + "-" + sa.AuthMechanism
+	authInfoName := cluster.Name + "-" + sa.Integration
 
 	authInfoMap := make(map[string]*api.AuthInfo)
 
@@ -351,7 +351,7 @@ func createRawConfigFromServiceAccount(
 		authInfoMap[authInfoName].ImpersonateGroups = groups
 	}
 
-	switch sa.AuthMechanism {
+	switch sa.Integration {
 	case models.X509:
 		authInfoMap[authInfoName].ClientCertificateData = sa.ClientCertificateData
 		authInfoMap[authInfoName].ClientKeyData = sa.ClientKeyData

+ 19 - 19
internal/kubernetes/kubeconfig_test.go

@@ -188,7 +188,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.X509,
+				Integration:   models.X509,
 				Kubeconfig:      []byte(ClusterCAWithoutData),
 			},
 		},
@@ -207,7 +207,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://localhost",
-				AuthMechanism:   models.X509,
+				Integration:   models.X509,
 				Kubeconfig:      []byte(ClusterLocalhost),
 			},
 		},
@@ -221,7 +221,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.X509,
+				Integration:   models.X509,
 				Kubeconfig:      []byte(x509WithData),
 			},
 		},
@@ -241,7 +241,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.X509,
+				Integration:   models.X509,
 				Kubeconfig:      []byte(x509WithoutCertData),
 			},
 		},
@@ -261,7 +261,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.X509,
+				Integration:   models.X509,
 				Kubeconfig:      []byte(x509WithoutKeyData),
 			},
 		},
@@ -286,7 +286,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.X509,
+				Integration:   models.X509,
 				Kubeconfig:      []byte(x509WithoutCertAndKeyData),
 			},
 		},
@@ -300,7 +300,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.Bearer,
+				Integration:   models.Bearer,
 				Kubeconfig:      []byte(BearerTokenWithData),
 			},
 		},
@@ -320,7 +320,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.Bearer,
+				Integration:   models.Bearer,
 				Kubeconfig:      []byte(BearerTokenWithoutData),
 			},
 		},
@@ -339,7 +339,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.GCP,
+				Integration:   models.GCP,
 				Kubeconfig:      []byte(GCPPlugin),
 			},
 		},
@@ -358,7 +358,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.AWS,
+				Integration:   models.AWS,
 				Kubeconfig:      []byte(AWSIamAuthenticatorExec),
 			},
 		},
@@ -377,7 +377,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.AWS,
+				Integration:   models.AWS,
 				Kubeconfig:      []byte(AWSEKSGetTokenExec),
 			},
 		},
@@ -397,7 +397,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.OIDC,
+				Integration:   models.OIDC,
 				Kubeconfig:      []byte(OIDCAuthWithoutData),
 			},
 		},
@@ -411,7 +411,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.OIDC,
+				Integration:   models.OIDC,
 				Kubeconfig:      []byte(OIDCAuthWithData),
 			},
 		},
@@ -425,7 +425,7 @@ var SACandidatesTests = []saCandidatesTest{
 				Kind:            "connector",
 				ClusterName:     "cluster-test",
 				ClusterEndpoint: "https://10.10.10.10",
-				AuthMechanism:   models.Basic,
+				Integration:   models.Basic,
 				Kubeconfig:      []byte(BasicAuth),
 			},
 		},
@@ -444,21 +444,21 @@ func TestGetServiceAccountCandidatesNonLocal(t *testing.T) {
 		resMap := make(map[string]*models.ServiceAccountCandidate)
 
 		for _, res := range result {
-			resMap[res.Kind+"-"+res.ClusterEndpoint+"-"+res.AuthMechanism] = res
+			resMap[res.Kind+"-"+res.ClusterEndpoint+"-"+res.Integration] = res
 		}
 
 		for _, exp := range c.expected {
-			res, ok := resMap[exp.Kind+"-"+exp.ClusterEndpoint+"-"+exp.AuthMechanism]
+			res, ok := resMap[exp.Kind+"-"+exp.ClusterEndpoint+"-"+exp.Integration]
 
 			if !ok {
 				t.Fatalf("%s failed: no matching result for %s\n", c.name,
-					exp.Kind+"-"+exp.ClusterEndpoint+"-"+exp.AuthMechanism)
+					exp.Kind+"-"+exp.ClusterEndpoint+"-"+exp.Integration)
 			}
 
 			// compare basic string fields
-			if exp.AuthMechanism != res.AuthMechanism {
+			if exp.Integration != res.Integration {
 				t.Errorf("%s failed on auth mechanism: expected %s, got %s\n",
-					c.name, exp.AuthMechanism, res.AuthMechanism)
+					c.name, exp.Integration, res.Integration)
 			}
 
 			if exp.ClusterName != res.ClusterName {

+ 0 - 127
internal/models/action.go

@@ -1,127 +0,0 @@
-package models
-
-import "gorm.io/gorm"
-
-// Action names
-const (
-	ClusterCADataAction    string = "upload-cluster-ca-data"
-	ClusterLocalhostAction        = "fix-cluster-localhost"
-	ClientCertDataAction          = "upload-client-cert-data"
-	ClientKeyDataAction           = "upload-client-key-data"
-	OIDCIssuerDataAction          = "upload-oidc-idp-issuer-ca-data"
-	TokenDataAction               = "upload-token-data"
-	GCPKeyDataAction              = "upload-gcp-key-data"
-	AWSDataAction                 = "upload-aws-data"
-)
-
-// ServiceAccountAction is an action that must be resolved to set up
-// a ServiceAccount
-type ServiceAccountAction struct {
-	gorm.Model
-
-	ServiceAccountCandidateID uint
-
-	// One of the constant action names
-	Name     string `json:"name"`
-	Resolved bool   `json:"resolved"`
-
-	// Filename is an optional filename, if the action requires
-	// data populated from a local file
-	Filename string `json:"filename,omitempty"`
-}
-
-// Externalize generates an external ServiceAccount to be shared over REST
-func (u *ServiceAccountAction) Externalize() *ServiceAccountActionExternal {
-	info := ServiceAccountActionInfos[u.Name]
-
-	return &ServiceAccountActionExternal{
-		Name:     u.Name,
-		Resolved: u.Resolved,
-		Filename: u.Filename,
-		Docs:     info.Docs,
-		Fields:   info.Fields,
-	}
-}
-
-// ServiceAccountActionExternal is an external ServiceAccountAction to be
-// sent over REST
-type ServiceAccountActionExternal struct {
-	Name     string `json:"name"`
-	Docs     string `json:"docs"`
-	Resolved bool   `json:"resolved"`
-	Fields   string `json:"fields"`
-	Filename string `json:"filename,omitempty"`
-}
-
-// ServiceAccountAllActions is a helper type that contains the fields for
-// all possible actions, so that raw bytes can be unmarshaled in a single
-// read
-type ServiceAccountAllActions struct {
-	Name string `json:"name"`
-
-	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"`
-}
-
-// ServiceAccountActionInfo contains the information for actions to be
-// performed in order to initialize a ServiceAccount
-type ServiceAccountActionInfo struct {
-	Name string `json:"name"`
-	Docs string `json:"docs"`
-
-	// a comma-separated list of required fields to send in an action request
-	Fields string `json:"fields"`
-}
-
-// ServiceAccountActionInfos contain the information for actions to be
-// performed in order to initialize a ServiceAccount
-var ServiceAccountActionInfos = map[string]ServiceAccountActionInfo{
-	"upload-cluster-ca-data": ServiceAccountActionInfo{
-		Name:   ClusterCADataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "cluster_ca_data",
-	},
-	"fix-cluster-localhost": ServiceAccountActionInfo{
-		Name:   ClusterLocalhostAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "cluster_hostname",
-	},
-	"upload-client-cert-data": ServiceAccountActionInfo{
-		Name:   ClientCertDataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "client_cert_data",
-	},
-	"upload-client-key-data": ServiceAccountActionInfo{
-		Name:   ClientKeyDataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "client_key_data",
-	},
-	"upload-oidc-idp-issuer-ca-data": ServiceAccountActionInfo{
-		Name:   OIDCIssuerDataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "oidc_idp_issuer_ca_data",
-	},
-	"upload-token-data": ServiceAccountActionInfo{
-		Name:   TokenDataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "token_data",
-	},
-	"upload-gcp-key-data": ServiceAccountActionInfo{
-		Name:   GCPKeyDataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "gcp_key_data",
-	},
-	"upload-aws-data": ServiceAccountActionInfo{
-		Name:   AWSDataAction,
-		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "aws_access_key_id,aws_secret_access_key,aws_cluster_id",
-	},
-}

+ 288 - 25
internal/models/cluster.go

@@ -1,43 +1,306 @@
 package models
 
-import "gorm.io/gorm"
+import (
+	"encoding/json"
 
-// Cluster type that extends gorm.Model
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+)
+
+// Cluster is an integration that can connect to a Kubernetes cluster via
+// a specific auth mechanism
 type Cluster struct {
 	gorm.Model
 
-	Name                  string `json:"name"`
-	ServiceAccountID      uint   `json:"service_account_id"`
-	LocationOfOrigin      string `json:"location_of_origin"`
-	Server                string `json:"server"`
-	TLSServerName         string `json:"tls-server-name,omitempty"`
-	InsecureSkipTLSVerify bool   `json:"insecure-skip-tls-verify,omitempty"`
-	ProxyURL              string `json:"proxy-url,omitempty"`
+	// 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"`
 
-	// CertificateAuthorityData is encrypted at rest
+	// CertificateAuthorityData for the cluster, encrypted at rest
 	CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
+
+	// Additional fields optionally used by the kube client
+	ClusterLocationOfOrigin string `json:"location_of_origin,omitempty"`
+	TLSServerName           string `json:"tls-server-name,omitempty"`
+	InsecureSkipTLSVerify   bool   `json:"insecure-skip-tls-verify,omitempty"`
+	ProxyURL                string `json:"proxy-url,omitempty"`
+	UserLocationOfOrigin    string
+	UserImpersonate         string `json:"act-as,omitempty"`
+	UserImpersonateGroups   string `json:"act-as-groups,omitempty"`
+
+	// The various auth mechanisms available to the integration
+	KubeIntegrationID uint
+	OIDCIntegrationID uint
+	GCPIntegrationID  uint
+	AWSIntegrationID  uint
+
+	// A token cache that can be used by an auth mechanism, if desired
+	TokenCache integrations.TokenCache `json:"token_cache"`
 }
 
-// ClusterExternal is the external cluster type to be sent over REST
+// ClusterExternal is an external Cluster to be shared over REST
 type ClusterExternal struct {
-	ID                    uint   `json:"id"`
-	ServiceAccountID      uint   `json:"service_account_id"`
-	Name                  string `json:"name"`
-	Server                string `json:"server"`
-	TLSServerName         string `json:"tls-server-name,omitempty"`
-	InsecureSkipTLSVerify bool   `json:"insecure-skip-tls-verify,omitempty"`
-	ProxyURL              string `json:"proxy-url,omitempty"`
+	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"`
 }
 
 // Externalize generates an external Cluster to be shared over REST
 func (c *Cluster) Externalize() *ClusterExternal {
 	return &ClusterExternal{
-		ID:                    c.Model.ID,
-		ServiceAccountID:      c.ServiceAccountID,
-		Name:                  c.Name,
-		Server:                c.Server,
-		TLSServerName:         c.TLSServerName,
-		InsecureSkipTLSVerify: c.InsecureSkipTLSVerify,
-		ProxyURL:              c.ProxyURL,
+		ID:        c.ID,
+		ProjectID: c.ProjectID,
+		Name:      c.Name,
+		Server:    c.Server,
+	}
+}
+
+// ClusterCandidate is a cluster integration that requires additional action
+// from the user to set up.
+type ClusterCandidate struct {
+	gorm.Model
+
+	// 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"`
+
+	// Resolvers are the list of resolvers: once all resolvers are "resolved," the
+	// cluster will be created
+	Resolvers []ClusterResolver `json:"resolvers"`
+
+	// 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"`
+
+	// ------------------------------------------------------------------
+	// All fields below this line are encrypted before storage
+	// ------------------------------------------------------------------
+
+	// 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 []byte `json:"aws_cluster_id_guess"`
+
+	// The raw kubeconfig
+	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 []byte `json:"aws_cluster_id_guess"`
+}
+
+// Externalize generates an external ClusterCandidateExternal to be shared over REST
+func (cc *ClusterCandidate) Externalize() *ClusterCandidateExternal {
+	resolvers := make([]ClusterResolverExternal, 0)
+
+	for _, resolver := range cc.Resolvers {
+		resolvers = append(resolvers, *resolver.Externalize())
+	}
+
+	return &ClusterCandidateExternal{
+		ID:                cc.ID,
+		ProjectID:         cc.ProjectID,
+		CreatedClusterID:  cc.CreatedClusterID,
+		Name:              cc.Name,
+		Server:            cc.Server,
+		ContextName:       cc.ContextName,
+		Resolvers:         resolvers,
+		AWSClusterIDGuess: cc.AWSClusterIDGuess,
+	}
+}
+
+// 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[string]ClusterResolverInfo{
+	"upload-cluster-ca-data": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "cluster_ca_data",
+	},
+	"rewrite-cluster-localhost": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "cluster_hostname",
+	},
+	"upload-client-cert-data": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "client_cert_data",
+	},
+	"upload-client-key-data": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "client_key_data",
+	},
+	"upload-oidc-idp-issuer-ca-data": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "oidc_idp_issuer_ca_data",
+	},
+	"upload-token-data": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "token_data",
+	},
+	"upload-gcp-key-data": ClusterResolverInfo{
+		Docs:   "https://github.com/porter-dev/porter",
+		Fields: "gcp_key_data",
+	},
+	"upload-aws-data": ClusterResolverInfo{
+		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 {
+	Name string `json:"name"`
+
+	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 {
+	gorm.Model
+
+	// The ClusterCandidate that this is resolving
+	ClusterCandidateID uint `json:"cluster_candidate_id"`
+
+	// One of the ClusterResolverNames
+	Name string `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 []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 string `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]
+
+	data := make(ClusterResolverData)
+
+	json.Unmarshal(cr.Data, &data)
+
+	return &ClusterResolverExternal{
+		ID:                 cr.ID,
+		ClusterCandidateID: cr.ClusterCandidateID,
+		Name:               cr.Name,
+		Resolved:           cr.Resolved,
+		Docs:               info.Docs,
+		Fields:             info.Fields,
+		Data:               data,
 	}
 }

+ 34 - 0
internal/models/cluster_test.go

@@ -0,0 +1,34 @@
+package models_test
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/go-test/deep"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+func TestClusterResolverExternalize(t *testing.T) {
+	crData := models.ClusterResolverData{
+		"filename": "/hello/there.pem",
+		"key":      "value",
+	}
+
+	bytes, err := json.Marshal(crData)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// test that the data gets unmarshalled properly
+	cr := &models.ClusterResolver{
+		Data: bytes,
+	}
+
+	crExternal := cr.Externalize()
+
+	if diff := deep.Equal(crExternal.Data, crData); diff != nil {
+		t.Errorf("incorrect cluster resolver data")
+		t.Error(diff)
+	}
+}

+ 0 - 16
internal/models/context.go

@@ -1,16 +0,0 @@
-package models
-
-// Context represents the configuration for a single cluster-user pair
-type Context struct {
-	// Name is the name of the context
-	Name string `json:"name"`
-	// Server is the endpoint of the kube apiserver for a cluster
-	Server string `json:"server"`
-	// Cluster is the name of the cluster
-	Cluster string `json:"cluster"`
-	// User is the name of the user for a cluster
-	User string `json:"user"`
-	// Selected determines if the context has been selected for use in the
-	// dashboard
-	Selected bool `json:"selected"`
-}

+ 40 - 0
internal/models/gitrepo.go

@@ -0,0 +1,40 @@
+package models
+
+import (
+	"gorm.io/gorm"
+)
+
+// GitRepo is an integration that can connect to a remote git repo via an auth
+// mechanism (currently only oauth)
+type GitRepo struct {
+	gorm.Model
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The username/organization that this repo integration is linked to
+	RepoEntity string `json:"repo_entity"`
+
+	// The various auth mechanisms available to the integration
+	OIntegrationID uint
+}
+
+// GitRepoExternal is a repository to be shared over REST
+type GitRepoExternal struct {
+	ID uint `json:"id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The username/organization that this repo integration is linked to
+	RepoEntity string `json:"repo_entity"`
+}
+
+// Externalize generates an external Repo to be shared over REST
+func (r *GitRepo) Externalize() *GitRepoExternal {
+	return &GitRepoExternal{
+		ID:         r.Model.ID,
+		ProjectID:  r.ProjectID,
+		RepoEntity: r.RepoEntity,
+	}
+}

+ 121 - 0
internal/models/integrations/aws.go

@@ -0,0 +1,121 @@
+package integrations
+
+import (
+	"gorm.io/gorm"
+
+	"github.com/aws/aws-sdk-go/aws"
+
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	token "sigs.k8s.io/aws-iam-authenticator/pkg/token"
+)
+
+// AWSIntegration is an auth mechanism that uses a AWS IAM user to
+// authenticate
+type AWSIntegration struct {
+	gorm.Model
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The AWS entity this is linked to (individual or organization)
+	AWSEntityID string `json:"aws-entity-id"`
+
+	// The AWS caller identity (ARN) which linked this service
+	AWSCallerID string `json:"aws-caller-id"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// The AWS cluster ID
+	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
+	AWSClusterID []byte `json:"aws_cluster_id"`
+
+	// The AWS access key for this IAM user
+	AWSAccessKeyID []byte `json:"aws_access_key_id"`
+
+	// The AWS secret key for this IAM user
+	AWSSecretAccessKey []byte `json:"aws_secret_access_key"`
+
+	// An optional session token, if the user is assuming a role
+	AWSSessionToken []byte `json:"aws_session_token"`
+}
+
+// AWSIntegrationExternal is a AWSIntegration to be shared over REST
+type AWSIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The AWS entity this is linked to (individual or organization)
+	AWSEntityID string `json:"aws-entity-id"`
+
+	// The AWS caller identity (ARN) which linked this service
+	AWSCallerID string `json:"aws-caller-id"`
+}
+
+// Externalize generates an external KubeIntegration to be shared over REST
+func (a *AWSIntegration) Externalize() *AWSIntegrationExternal {
+	return &AWSIntegrationExternal{
+		ID:          a.ID,
+		UserID:      a.UserID,
+		ProjectID:   a.ProjectID,
+		AWSEntityID: a.AWSEntityID,
+		AWSCallerID: a.AWSCallerID,
+	}
+}
+
+// GetBearerToken retrieves a bearer token for an AWS account
+func (a *AWSIntegration) GetBearerToken(
+	getTokenCache GetTokenCacheFunc,
+	setTokenCache SetTokenCacheFunc,
+) (string, error) {
+	cache, err := getTokenCache()
+
+	// check the token cache for a non-expired token
+	if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
+		return string(tok), nil
+	}
+
+	generator, err := token.NewGenerator(false, false)
+
+	if err != nil {
+		return "", err
+	}
+
+	sess, err := session.NewSessionWithOptions(session.Options{
+		SharedConfigState: session.SharedConfigEnable,
+		Config: aws.Config{
+			Credentials: credentials.NewStaticCredentials(
+				string(a.AWSAccessKeyID),
+				string(a.AWSSecretAccessKey),
+				string(a.AWSSessionToken),
+			),
+		},
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	tok, err := generator.GetWithOptions(&token.GetTokenOptions{
+		Session:   sess,
+		ClusterID: string(a.AWSClusterID),
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	setTokenCache(tok.Token, tok.Expiration)
+
+	return tok.Token, nil
+}

+ 95 - 0
internal/models/integrations/gcp.go

@@ -0,0 +1,95 @@
+package integrations
+
+import (
+	"context"
+
+	"golang.org/x/oauth2/google"
+	"gorm.io/gorm"
+)
+
+// GCPIntegration is an auth mechanism that uses a GCP service account to
+// authenticate
+type GCPIntegration struct {
+	gorm.Model
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The GCP project id where the service account for this auth mechanism persists
+	GCPProjectID string `json:"gcp-project-id"`
+
+	// The GCP user email that linked this service account
+	GCPUserEmail string `json:"gcp-user-email"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// KeyData for a service account for GCP connectors
+	GCPKeyData []byte `json:"gcp_key_data"`
+}
+
+// GCPIntegrationExternal is a GCPIntegration to be shared over REST
+type GCPIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// The GCP project id where the service account for this auth mechanism persists
+	GCPProjectID string `json:"gcp-project-id"`
+
+	// The GCP user email that linked this service account
+	GCPUserEmail string `json:"gcp-user-email"`
+}
+
+// Externalize generates an external KubeIntegration to be shared over REST
+func (g *GCPIntegration) Externalize() *GCPIntegrationExternal {
+	return &GCPIntegrationExternal{
+		ID:           g.ID,
+		UserID:       g.UserID,
+		ProjectID:    g.ProjectID,
+		GCPProjectID: g.GCPProjectID,
+		GCPUserEmail: g.GCPUserEmail,
+	}
+}
+
+// GetBearerToken retrieves a bearer token for a GCP account
+func (g *GCPIntegration) GetBearerToken(
+	getTokenCache GetTokenCacheFunc,
+	setTokenCache SetTokenCacheFunc,
+) (string, error) {
+	cache, err := getTokenCache()
+
+	// check the token cache for a non-expired token
+	if tok := cache.Token; err == nil && !cache.IsExpired() && len(tok) > 0 {
+		return string(tok), nil
+	}
+
+	creds, err := google.CredentialsFromJSON(
+		context.Background(),
+		g.GCPKeyData,
+		"https://www.googleapis.com/auth/cloud-platform",
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	tok, err := creds.TokenSource.Token()
+
+	if err != nil {
+		return "", err
+	}
+
+	// update the token cache
+	setTokenCache(tok.AccessToken, tok.Expiry)
+
+	return tok.AccessToken, nil
+}

+ 71 - 0
internal/models/integrations/kube.go

@@ -0,0 +1,71 @@
+package integrations
+
+import "gorm.io/gorm"
+
+// KubeIntegrationName is the name of a kube auth mechanism
+type KubeIntegrationName string
+
+// The supported kube auth mechanisms
+const (
+	KubeX509   KubeIntegrationName = "x509"
+	KubeBasic                      = "basic"
+	KubeBearer                     = "bearer"
+	KubeLocal                      = "local"
+)
+
+// KubeIntegration represents the kube-native auth mechanisms: using x509 certs,
+// basic (username/password), bearer tokens, or local (using local kubeconfig)
+type KubeIntegration struct {
+	gorm.Model
+
+	// The name of the auth mechanism
+	Mechanism KubeIntegrationName `json:"mechanism"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// Certificate data is used by x509 auth mechanisms over TLS
+	ClientCertificateData []byte `json:"client-certificate-data,omitempty"`
+	ClientKeyData         []byte `json:"client-key-data,omitempty"`
+
+	// Token is used for bearer-token auth mechanisms
+	Token []byte `json:"token,omitempty"`
+
+	// Username/Password for basic authentication to a cluster
+	Username []byte `json:"username,omitempty"`
+	Password []byte `json:"password,omitempty"`
+
+	// The raw kubeconfig, used by local auth mechanisms
+	Kubeconfig []byte `json:"kubeconfig"`
+}
+
+// KubeIntegrationExternal is a KubeIntegration to be shared over REST
+type KubeIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The name of the auth mechanism
+	Mechanism KubeIntegrationName `json:"mechanism"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+}
+
+// Externalize generates an external KubeIntegration to be shared over REST
+func (k *KubeIntegration) Externalize() *KubeIntegrationExternal {
+	return &KubeIntegrationExternal{
+		ID:        k.ID,
+		Mechanism: k.Mechanism,
+		UserID:    k.UserID,
+		ProjectID: k.ProjectID,
+	}
+}

+ 63 - 0
internal/models/integrations/oauth.go

@@ -0,0 +1,63 @@
+package integrations
+
+import "gorm.io/gorm"
+
+// OAuthIntegrationClient is the name of an OAuth mechanism client
+type OAuthIntegrationClient string
+
+// The supported oauth mechanism clients
+const (
+	OAuthGithub OAuthIntegrationClient = "github"
+)
+
+// OAuthIntegration is an auth mechanism that uses oauth
+// https://tools.ietf.org/html/rfc6749
+type OAuthIntegration struct {
+	gorm.Model
+
+	// The name of the auth mechanism
+	Client OAuthIntegrationClient `json:"client"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// The ID issued to the client
+	ClientID []byte `json:"client-id"`
+
+	// The end-users's access token
+	AccessToken []byte `json:"access-token"`
+
+	// The end-user's refresh token
+	RefreshToken []byte `json:"refresh-token"`
+}
+
+// OAuthIntegrationExternal is an OAuthIntegration to be shared over REST
+type OAuthIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The name of the auth mechanism
+	Client OAuthIntegrationClient `json:"client"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+}
+
+// Externalize generates an external KubeIntegration to be shared over REST
+func (o *OAuthIntegration) Externalize() *OAuthIntegrationExternal {
+	return &OAuthIntegrationExternal{
+		ID:        o.ID,
+		Client:    o.Client,
+		UserID:    o.UserID,
+		ProjectID: o.ProjectID,
+	}
+}

+ 76 - 0
internal/models/integrations/oidc.go

@@ -0,0 +1,76 @@
+package integrations
+
+import "gorm.io/gorm"
+
+// OIDCIntegrationClient is the name of an OIDC auth mechanism client
+type OIDCIntegrationClient string
+
+// The supported OIDC auth mechanism clients
+const (
+	OIDCKube OIDCIntegrationClient = "kube"
+)
+
+// OIDCIntegration is an auth mechanism that uses oidc. Spec:
+// https://openid.net/specs/openid-connect-core-1_0.html
+type OIDCIntegration struct {
+	gorm.Model
+
+	// The name of the auth mechanism
+	Client OIDCIntegrationClient `json:"client"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+
+	// ------------------------------------------------------------------
+	// All fields encrypted before storage.
+	// ------------------------------------------------------------------
+
+	// The "Issuer Identifier" of the OIDC spec (16.15)
+	IssuerURL []byte `json:"idp-issuer-url"`
+
+	// The ID issued to the Relying Party
+	ClientID []byte `json:"client-id"`
+
+	// The secret issued to the Relying Party
+	//
+	// This is present because it used to be a required field in a kubeconfig.
+	// However, because the kube apiserver acts as a Relying Party, the client
+	// secret is not necessary.
+	ClientSecret []byte `json:"client-secret"`
+
+	// The CA data -- certificate check must be performed (16.17)
+	CertificateAuthorityData []byte `json:"idp-certificate-authority-data"`
+
+	// The user's JWT id token
+	IDToken []byte `json:"id-token"`
+
+	// The user's refresh token
+	RefreshToken []byte `json:"refresh-token"`
+}
+
+// OIDCIntegrationExternal is a OIDCIntegration to be shared over REST
+type OIDCIntegrationExternal struct {
+	ID uint `json:"id"`
+
+	// The name of the auth mechanism
+	Client OIDCIntegrationClient `json:"client"`
+
+	// The id of the user that linked this auth mechanism
+	UserID uint `json:"user_id"`
+
+	// The project that this integration belongs to
+	ProjectID uint `json:"project_id"`
+}
+
+// Externalize generates an external KubeIntegration to be shared over REST
+func (o *OIDCIntegration) Externalize() *OIDCIntegrationExternal {
+	return &OIDCIntegrationExternal{
+		ID:        o.ID,
+		Client:    o.Client,
+		UserID:    o.UserID,
+		ProjectID: o.ProjectID,
+	}
+}

+ 11 - 3
internal/models/tokencache.go → internal/models/integrations/token_cache.go

@@ -1,4 +1,4 @@
-package models
+package integrations
 
 import (
 	"time"
@@ -6,14 +6,22 @@ import (
 	"gorm.io/gorm"
 )
 
+// GetTokenCacheFunc is a function that retrieves the token and expiry
+// time from the db
+type GetTokenCacheFunc func() (tok *TokenCache, err error)
+
+// SetTokenCacheFunc is a function that updates the token cache
+// with a new token and expiry time
+type SetTokenCacheFunc func(token string, expiry time.Time) error
+
 // TokenCache stores a token and an expiration for the token for a
 // service account. This will never be shared over REST, so no need
 // to externalize.
 type TokenCache struct {
 	gorm.Model
 
-	ServiceAccountID uint      `json:"service_account_id"`
-	Expiry           time.Time `json:"expiry,omitempty"`
+	ClusterID uint      `json:"cluster_id"`
+	Expiry    time.Time `json:"expiry,omitempty"`
 
 	// ------------------------------------------------------------------
 	// All fields below this line are encrypted before storage

+ 25 - 13
internal/models/project.go

@@ -2,26 +2,38 @@ package models
 
 import (
 	"gorm.io/gorm"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 
 // Project type that extends gorm.Model
 type Project struct {
 	gorm.Model
 
-	Name        string       `json:"name"`
-	Roles       []Role       `json:"roles"`
-	RepoClients []RepoClient `json:"repo_clients,omitempty"`
+	Name  string `json:"name"`
+	Roles []Role `json:"roles"`
+
+	// linked repos
+	Repos []GitRepo `json:"git_repos,omitempty"`
+
+	// linked clusters
+	Clusters          []Cluster          `json:"clusters"`
+	ClusterCandidates []ClusterCandidate `json:"cluster_candidates"`
 
-	ServiceAccountCandidates []ServiceAccountCandidate `json:"sa_candidates"`
-	ServiceAccounts          []ServiceAccount          `json:"serviceaccounts"`
+	// auth mechanisms
+	KubeIntegrations  []ints.KubeIntegration  `json:"kube_integrations"`
+	OIDCIntegrations  []ints.OIDCIntegration  `json:"oidc_integrations"`
+	OAuthIntegrations []ints.OAuthIntegration `json:"oauth_integrations"`
+	AWSIntegrations   []ints.AWSIntegration   `json:"aws_integrations"`
+	GCPIntegrations   []ints.GCPIntegration   `json:"gcp_integrations"`
 }
 
 // ProjectExternal represents the Project type that is sent over REST
 type ProjectExternal struct {
-	ID          uint                 `json:"id"`
-	Name        string               `json:"name"`
-	Roles       []RoleExternal       `json:"roles"`
-	RepoClients []RepoClientExternal `json:"repo_clients,omitempty"`
+	ID          uint              `json:"id"`
+	Name        string            `json:"name"`
+	Roles       []RoleExternal    `json:"roles"`
+	RepoClients []GitRepoExternal `json:"git_repos,omitempty"`
 }
 
 // Externalize generates an external Project to be shared over REST
@@ -32,16 +44,16 @@ func (p *Project) Externalize() *ProjectExternal {
 		roles = append(roles, *role.Externalize())
 	}
 
-	repoClients := make([]RepoClientExternal, 0)
+	repos := make([]GitRepoExternal, 0)
 
-	for _, repoClient := range p.RepoClients {
-		repoClients = append(repoClients, *repoClient.Externalize())
+	for _, repo := range p.Repos {
+		repos = append(repos, *repo.Externalize())
 	}
 
 	return &ProjectExternal{
 		ID:          p.ID,
 		Name:        p.Name,
 		Roles:       roles,
-		RepoClients: repoClients,
+		RepoClients: repos,
 	}
 }

+ 0 - 51
internal/models/repoclient.go

@@ -1,51 +0,0 @@
-package models
-
-import (
-	"gorm.io/gorm"
-)
-
-// The allowed repository clients
-const (
-	RepoClientGithub = "github"
-)
-
-// RepoClient is a client for a set of repositories that has been added
-// via a project OAuth flow
-type RepoClient struct {
-	gorm.Model
-
-	ProjectID  uint `json:"project_id"`
-	UserID     uint `json:"user_id"`
-	RepoUserID uint `json:"repo_id"`
-
-	// the kind can be one of the predefined repo kinds
-	Kind string `json:"kind"`
-
-	// ------------------------------------------------------------------
-	// All fields below this line are encrypted before storage
-	// ------------------------------------------------------------------
-
-	AccessToken  []byte `json:"access_token"`
-	RefreshToken []byte `json:"refresh_token"`
-}
-
-// RepoClientExternal is a RepoClient scrubbed of sensitive information to be
-// shared over REST
-type RepoClientExternal struct {
-	ID         uint   `json:"id"`
-	ProjectID  uint   `json:"project_id"`
-	UserID     uint   `json:"user_id"`
-	RepoUserID uint   `json:"repo_id"`
-	Kind       string `json:"kind"`
-}
-
-// Externalize generates an external RepoClient to be shared over REST
-func (r *RepoClient) Externalize() *RepoClientExternal {
-	return &RepoClientExternal{
-		ID:         r.Model.ID,
-		ProjectID:  r.ProjectID,
-		UserID:     r.UserID,
-		RepoUserID: r.RepoUserID,
-		Kind:       r.Kind,
-	}
-}

+ 0 - 172
internal/models/serviceaccount.go

@@ -1,172 +0,0 @@
-package models
-
-import (
-	"gorm.io/gorm"
-)
-
-// Supported auth mechanisms
-const (
-	X509         string = "x509"
-	Basic               = "basic"
-	Bearer              = "bearerToken"
-	OIDC                = "oidc"
-	GCP                 = "gcp-sa"
-	AWS                 = "aws-sa"
-	Local               = "local"
-	NotAvailable        = "n/a"
-)
-
-// ServiceAccountCandidate is a service account that requires an action
-// from the user to set up.
-type ServiceAccountCandidate struct {
-	gorm.Model
-
-	ProjectID uint   `json:"project_id"`
-	Kind      string `json:"kind"`
-
-	Actions []ServiceAccountAction `json:"actions"`
-
-	ContextName     string `json:"context_name"`
-	ClusterName     string `json:"cluster_name"`
-	ClusterEndpoint string `json:"cluster_endpoint"`
-	AuthMechanism   string `json:"auth_mechanism"`
-
-	// CreatedServiceAccountID is the ID of the service account that's eventually
-	// created
-	CreatedServiceAccountID uint `json:"create_sa_id"`
-
-	// 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"`
-
-	// ------------------------------------------------------------------
-	// All fields below this line are encrypted before storage
-	// ------------------------------------------------------------------
-
-	Kubeconfig []byte `json:"kubeconfig"`
-}
-
-// ServiceAccountCandidateExternal represents the ServiceAccountCandidate type that is
-// sent over REST
-type ServiceAccountCandidateExternal struct {
-	ID                      uint                           `json:"id"`
-	Actions                 []ServiceAccountActionExternal `json:"actions"`
-	ProjectID               uint                           `json:"project_id"`
-	Kind                    string                         `json:"kind"`
-	ContextName             string                         `json:"context_name"`
-	ClusterName             string                         `json:"cluster_name"`
-	ClusterEndpoint         string                         `json:"cluster_endpoint"`
-	AuthMechanism           string                         `json:"auth_mechanism"`
-	CreatedServiceAccountID uint                           `json:"created_sa_id"`
-	AWSClusterIDGuess       string                         `json:"aws_cluster_id_guess"`
-}
-
-// Externalize generates an external ServiceAccountCandidate to be shared over REST
-func (s *ServiceAccountCandidate) Externalize() *ServiceAccountCandidateExternal {
-	actions := make([]ServiceAccountActionExternal, 0)
-
-	for _, action := range s.Actions {
-		actions = append(actions, *action.Externalize())
-	}
-
-	return &ServiceAccountCandidateExternal{
-		ID:                      s.ID,
-		Actions:                 actions,
-		ProjectID:               s.ProjectID,
-		Kind:                    s.Kind,
-		ContextName:             s.ContextName,
-		ClusterName:             s.ClusterName,
-		ClusterEndpoint:         s.ClusterEndpoint,
-		AuthMechanism:           s.AuthMechanism,
-		CreatedServiceAccountID: s.CreatedServiceAccountID,
-		AWSClusterIDGuess:       s.AWSClusterIDGuess,
-	}
-}
-
-// ServiceAccount type that extends gorm.Model
-type ServiceAccount struct {
-	gorm.Model
-
-	ProjectID uint `json:"project_id"`
-
-	// Kind can either be "connector" or "provisioner"
-	Kind string `json:"kind"`
-
-	// Clusters is a list of clusters that this ServiceAccount can connect
-	// to or has provisioned
-	Clusters []Cluster `json:"clusters"`
-
-	// AuthMechanism is the strategy used for either connecting to or provisioning
-	// the cluster. Supported mechanisms are: basic,x509,bearerToken,oidc,gcp-sa,aws-sa
-	AuthMechanism string `json:"auth_mechanism"`
-
-	// These fields are used by all auth mechanisms
-	LocationOfOrigin  string
-	Impersonate       string `json:"act-as,omitempty"`
-	ImpersonateGroups string `json:"act-as-groups,omitempty"`
-
-	// ------------------------------------------------------------------
-	// All fields below this line are encrypted before storage, so type is
-	// byte.
-	// ------------------------------------------------------------------
-
-	// Certificate data is used by x509 auth mechanisms over TLS
-	ClientCertificateData []byte `json:"client-certificate-data,omitempty"`
-	ClientKeyData         []byte `json:"client-key-data,omitempty"`
-
-	// Token is used for bearer-token auth mechanisms
-	Token []byte `json:"token,omitempty"`
-
-	// Username/Password for basic authentication to a cluster
-	Username []byte `json:"username,omitempty"`
-	Password []byte `json:"password,omitempty"`
-
-	// TokenCache is a cache for bearer tokens with an expiry time
-	// Used by GCP and AWS mechanisms
-	TokenCache TokenCache `json:"token_cache"`
-
-	// KeyData for a service account for GCP connectors
-	GCPKeyData []byte `json:"gcp_key_data"`
-
-	// AWS data
-	AWSAccessKeyID     []byte `json:"aws_access_key_id"`
-	AWSSecretAccessKey []byte `json:"aws_secret_access_key"`
-	AWSClusterID       []byte `json:"aws_cluster_id"`
-
-	// OIDC-related fields
-	OIDCIssuerURL                []byte `json:"idp-issuer-url"`
-	OIDCClientID                 []byte `json:"client-id"`
-	OIDCClientSecret             []byte `json:"client-secret"`
-	OIDCCertificateAuthorityData []byte `json:"idp-certificate-authority-data"`
-	OIDCIDToken                  []byte `json:"id-token"`
-	OIDCRefreshToken             []byte `json:"refresh-token"`
-
-	// The raw kubeconfig, used by local auth mechanisms
-	Kubeconfig []byte `json:"kubeconfig"`
-}
-
-// ServiceAccountExternal is an external ServiceAccount to be shared over REST
-type ServiceAccountExternal struct {
-	ID            uint              `json:"id"`
-	ProjectID     uint              `json:"project_id"`
-	Kind          string            `json:"kind"`
-	Clusters      []ClusterExternal `json:"clusters"`
-	AuthMechanism string            `json:"auth_mechanism"`
-}
-
-// Externalize generates an external ServiceAccount to be shared over REST
-func (s *ServiceAccount) Externalize() *ServiceAccountExternal {
-	clusters := make([]ClusterExternal, 0)
-
-	for _, cluster := range s.Clusters {
-		clusters = append(clusters, *cluster.Externalize())
-	}
-
-	return &ServiceAccountExternal{
-		ID:            s.ID,
-		ProjectID:     s.ProjectID,
-		Kind:          s.Kind,
-		Clusters:      clusters,
-		AuthMechanism: s.AuthMechanism,
-	}
-}

+ 19 - 0
internal/repository/cluster.go

@@ -0,0 +1,19 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// ClusterRepository represents the set of queries on the
+// Cluster model
+type ClusterRepository interface {
+	CreateClusterCandidate(cc *models.ClusterCandidate) (*models.ClusterCandidate, error)
+	ReadClusterCandidate(id uint) (*models.ClusterCandidate, error)
+	ListClusterCandidatesByProjectID(projectID uint) ([]*models.ClusterCandidate, error)
+	UpdateClusterCandidateCreatedClusterID(id uint, createdClusterID uint) (*models.ClusterCandidate, error)
+	CreateCluster(sa *models.Cluster) (*models.Cluster, error)
+	ReadCluster(id uint) (*models.Cluster, error)
+	ListClustersByProjectID(projectID uint) ([]*models.Cluster, error)
+	UpdateClusterTokenCache(tokenCache *ints.TokenCache) (*models.Cluster, error)
+}

+ 11 - 0
internal/repository/gitrepo.go

@@ -0,0 +1,11 @@
+package repository
+
+import "github.com/porter-dev/porter/internal/models"
+
+// GitRepoRepository represents the set of queries on the
+// GitRepo model
+type GitRepoRepository interface {
+	CreateGitRepo(gr *models.GitRepo) (*models.GitRepo, error)
+	ReadGitRepo(id uint) (*models.GitRepo, error)
+	ListGitReposByProjectID(projectID uint) ([]*models.GitRepo, error)
+}

+ 909 - 0
internal/repository/gorm/auth.go

@@ -0,0 +1,909 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// KubeIntegrationRepository uses gorm.DB for querying the database
+type KubeIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewKubeIntegrationRepository returns a KubeIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewKubeIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.KubeIntegrationRepository {
+	return &KubeIntegrationRepository{db, key}
+}
+
+// CreateKubeIntegration creates a new kube auth mechanism
+func (repo *KubeIntegrationRepository) CreateKubeIntegration(
+	am *ints.KubeIntegration,
+) (*ints.KubeIntegration, error) {
+	err := repo.EncryptKubeIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("KubeIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadKubeIntegration finds a kube auth mechanism by id
+func (repo *KubeIntegrationRepository) ReadKubeIntegration(
+	id uint,
+) (*ints.KubeIntegration, error) {
+	ki := &ints.KubeIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&ki).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptKubeIntegrationData(ki, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return ki, nil
+}
+
+// ListKubeIntegrationsByProjectID finds all kube auth mechanisms
+// for a given project id
+func (repo *KubeIntegrationRepository) ListKubeIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.KubeIntegration, error) {
+	kis := []*ints.KubeIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&kis).Error; err != nil {
+		return nil, err
+	}
+
+	for _, ki := range kis {
+		repo.DecryptKubeIntegrationData(ki, repo.key)
+	}
+
+	return kis, nil
+}
+
+// EncryptKubeIntegrationData will encrypt the kube integration data before
+// writing to the DB
+func (repo *KubeIntegrationRepository) EncryptKubeIntegrationData(
+	ki *ints.KubeIntegration,
+	key *[32]byte,
+) error {
+	if len(ki.ClientCertificateData) > 0 {
+		cipherData, err := repository.Encrypt(ki.ClientCertificateData, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.ClientCertificateData = cipherData
+	}
+
+	if len(ki.ClientKeyData) > 0 {
+		cipherData, err := repository.Encrypt(ki.ClientKeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.ClientKeyData = cipherData
+	}
+
+	if len(ki.Token) > 0 {
+		cipherData, err := repository.Encrypt(ki.Token, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Token = cipherData
+	}
+
+	if len(ki.Username) > 0 {
+		cipherData, err := repository.Encrypt(ki.Username, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Username = cipherData
+	}
+
+	if len(ki.Password) > 0 {
+		cipherData, err := repository.Encrypt(ki.Password, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Password = cipherData
+	}
+
+	if len(ki.Kubeconfig) > 0 {
+		cipherData, err := repository.Encrypt(ki.Kubeconfig, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Kubeconfig = cipherData
+	}
+
+	return nil
+}
+
+// DecryptKubeIntegrationData will decrypt the kube integration data before
+// returning it from the DB
+func (repo *KubeIntegrationRepository) DecryptKubeIntegrationData(
+	ki *ints.KubeIntegration,
+	key *[32]byte,
+) error {
+	if len(ki.ClientCertificateData) > 0 {
+		plaintext, err := repository.Decrypt(ki.ClientCertificateData, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.ClientCertificateData = plaintext
+	}
+
+	if len(ki.ClientKeyData) > 0 {
+		plaintext, err := repository.Decrypt(ki.ClientKeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.ClientKeyData = plaintext
+	}
+
+	if len(ki.Token) > 0 {
+		plaintext, err := repository.Decrypt(ki.Token, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Token = plaintext
+	}
+
+	if len(ki.Username) > 0 {
+		plaintext, err := repository.Decrypt(ki.Username, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Username = plaintext
+	}
+
+	if len(ki.Password) > 0 {
+		plaintext, err := repository.Decrypt(ki.Password, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Password = plaintext
+	}
+
+	if len(ki.Kubeconfig) > 0 {
+		plaintext, err := repository.Decrypt(ki.Kubeconfig, key)
+
+		if err != nil {
+			return err
+		}
+
+		ki.Kubeconfig = plaintext
+	}
+
+	return nil
+}
+
+// OIDCIntegrationRepository uses gorm.DB for querying the database
+type OIDCIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewOIDCIntegrationRepository returns a OIDCIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewOIDCIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.OIDCIntegrationRepository {
+	return &OIDCIntegrationRepository{db, key}
+}
+
+// CreateOIDCIntegration creates a new oidc auth mechanism
+func (repo *OIDCIntegrationRepository) CreateOIDCIntegration(
+	am *ints.OIDCIntegration,
+) (*ints.OIDCIntegration, error) {
+	err := repo.EncryptOIDCIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("OIDCIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadOIDCIntegration finds a oidc auth mechanism by id
+func (repo *OIDCIntegrationRepository) ReadOIDCIntegration(
+	id uint,
+) (*ints.OIDCIntegration, error) {
+	oidc := &ints.OIDCIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&oidc).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptOIDCIntegrationData(oidc, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return oidc, nil
+}
+
+// ListOIDCIntegrationsByProjectID finds all oidc auth mechanisms
+// for a given project id
+func (repo *OIDCIntegrationRepository) ListOIDCIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.OIDCIntegration, error) {
+	oidcs := []*ints.OIDCIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&oidcs).Error; err != nil {
+		return nil, err
+	}
+
+	for _, oidc := range oidcs {
+		repo.DecryptOIDCIntegrationData(oidc, repo.key)
+	}
+
+	return oidcs, nil
+}
+
+// EncryptOIDCIntegrationData will encrypt the oidc integration data before
+// writing to the DB
+func (repo *OIDCIntegrationRepository) EncryptOIDCIntegrationData(
+	oidc *ints.OIDCIntegration,
+	key *[32]byte,
+) error {
+	if len(oidc.IssuerURL) > 0 {
+		cipherData, err := repository.Encrypt(oidc.IssuerURL, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.IssuerURL = cipherData
+	}
+
+	if len(oidc.ClientID) > 0 {
+		cipherData, err := repository.Encrypt(oidc.ClientID, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.ClientID = cipherData
+	}
+
+	if len(oidc.ClientSecret) > 0 {
+		cipherData, err := repository.Encrypt(oidc.ClientSecret, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.ClientSecret = cipherData
+	}
+
+	if len(oidc.CertificateAuthorityData) > 0 {
+		cipherData, err := repository.Encrypt(oidc.CertificateAuthorityData, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.CertificateAuthorityData = cipherData
+	}
+
+	if len(oidc.IDToken) > 0 {
+		cipherData, err := repository.Encrypt(oidc.IDToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.IDToken = cipherData
+	}
+
+	if len(oidc.RefreshToken) > 0 {
+		cipherData, err := repository.Encrypt(oidc.RefreshToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.RefreshToken = cipherData
+	}
+
+	return nil
+}
+
+// DecryptOIDCIntegrationData will decrypt the kube integration data before
+// returning it from the DB
+func (repo *OIDCIntegrationRepository) DecryptOIDCIntegrationData(
+	oidc *ints.OIDCIntegration,
+	key *[32]byte,
+) error {
+	if len(oidc.IssuerURL) > 0 {
+		plaintext, err := repository.Decrypt(oidc.IssuerURL, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.IssuerURL = plaintext
+	}
+
+	if len(oidc.ClientID) > 0 {
+		plaintext, err := repository.Decrypt(oidc.ClientID, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.ClientID = plaintext
+	}
+
+	if len(oidc.ClientSecret) > 0 {
+		plaintext, err := repository.Decrypt(oidc.ClientSecret, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.ClientSecret = plaintext
+	}
+
+	if len(oidc.CertificateAuthorityData) > 0 {
+		plaintext, err := repository.Decrypt(oidc.CertificateAuthorityData, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.CertificateAuthorityData = plaintext
+	}
+
+	if len(oidc.IDToken) > 0 {
+		plaintext, err := repository.Decrypt(oidc.IDToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.IDToken = plaintext
+	}
+
+	if len(oidc.RefreshToken) > 0 {
+		plaintext, err := repository.Decrypt(oidc.RefreshToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oidc.RefreshToken = plaintext
+	}
+
+	return nil
+}
+
+// OAuthIntegrationRepository uses gorm.DB for querying the database
+type OAuthIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewOAuthIntegrationRepository returns a OAuthIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewOAuthIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.OAuthIntegrationRepository {
+	return &OAuthIntegrationRepository{db, key}
+}
+
+// CreateOAuthIntegration creates a new oauth auth mechanism
+func (repo *OAuthIntegrationRepository) CreateOAuthIntegration(
+	am *ints.OAuthIntegration,
+) (*ints.OAuthIntegration, error) {
+	err := repo.EncryptOAuthIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("OAuthIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadOAuthIntegration finds a oauth auth mechanism by id
+func (repo *OAuthIntegrationRepository) ReadOAuthIntegration(
+	id uint,
+) (*ints.OAuthIntegration, error) {
+	oauth := &ints.OAuthIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&oauth).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptOAuthIntegrationData(oauth, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return oauth, nil
+}
+
+// ListOAuthIntegrationsByProjectID finds all oauth auth mechanisms
+// for a given project id
+func (repo *OAuthIntegrationRepository) ListOAuthIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.OAuthIntegration, error) {
+	oauths := []*ints.OAuthIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&oauths).Error; err != nil {
+		return nil, err
+	}
+
+	for _, oauth := range oauths {
+		repo.DecryptOAuthIntegrationData(oauth, repo.key)
+	}
+
+	return oauths, nil
+}
+
+// EncryptOAuthIntegrationData will encrypt the oauth integration data before
+// writing to the DB
+func (repo *OAuthIntegrationRepository) EncryptOAuthIntegrationData(
+	oauth *ints.OAuthIntegration,
+	key *[32]byte,
+) error {
+	if len(oauth.ClientID) > 0 {
+		cipherData, err := repository.Encrypt(oauth.ClientID, key)
+
+		if err != nil {
+			return err
+		}
+
+		oauth.ClientID = cipherData
+	}
+
+	if len(oauth.AccessToken) > 0 {
+		cipherData, err := repository.Encrypt(oauth.AccessToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oauth.AccessToken = cipherData
+	}
+
+	if len(oauth.RefreshToken) > 0 {
+		cipherData, err := repository.Encrypt(oauth.RefreshToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oauth.RefreshToken = cipherData
+	}
+
+	return nil
+}
+
+// DecryptOAuthIntegrationData will decrypt the oauth integration data before
+// returning it from the DB
+func (repo *OAuthIntegrationRepository) DecryptOAuthIntegrationData(
+	oauth *ints.OAuthIntegration,
+	key *[32]byte,
+) error {
+	if len(oauth.ClientID) > 0 {
+		plaintext, err := repository.Decrypt(oauth.ClientID, key)
+
+		if err != nil {
+			return err
+		}
+
+		oauth.ClientID = plaintext
+	}
+
+	if len(oauth.AccessToken) > 0 {
+		plaintext, err := repository.Decrypt(oauth.AccessToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oauth.AccessToken = plaintext
+	}
+
+	if len(oauth.RefreshToken) > 0 {
+		plaintext, err := repository.Decrypt(oauth.RefreshToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		oauth.RefreshToken = plaintext
+	}
+
+	return nil
+}
+
+// GCPIntegrationRepository uses gorm.DB for querying the database
+type GCPIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewGCPIntegrationRepository returns a GCPIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewGCPIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.GCPIntegrationRepository {
+	return &GCPIntegrationRepository{db, key}
+}
+
+// CreateGCPIntegration creates a new gcp auth mechanism
+func (repo *GCPIntegrationRepository) CreateGCPIntegration(
+	am *ints.GCPIntegration,
+) (*ints.GCPIntegration, error) {
+	err := repo.EncryptGCPIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("GCPIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadGCPIntegration finds a gcp auth mechanism by id
+func (repo *GCPIntegrationRepository) ReadGCPIntegration(
+	id uint,
+) (*ints.GCPIntegration, error) {
+	gcp := &ints.GCPIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&gcp).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptGCPIntegrationData(gcp, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return gcp, nil
+}
+
+// ListGCPIntegrationsByProjectID finds all gcp auth mechanisms
+// for a given project id
+func (repo *GCPIntegrationRepository) ListGCPIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.GCPIntegration, error) {
+	gcps := []*ints.GCPIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&gcps).Error; err != nil {
+		return nil, err
+	}
+
+	for _, gcp := range gcps {
+		repo.DecryptGCPIntegrationData(gcp, repo.key)
+	}
+
+	return gcps, nil
+}
+
+// EncryptGCPIntegrationData will encrypt the gcp integration data before
+// writing to the DB
+func (repo *GCPIntegrationRepository) EncryptGCPIntegrationData(
+	gcp *ints.GCPIntegration,
+	key *[32]byte,
+) error {
+	if len(gcp.GCPKeyData) > 0 {
+		cipherData, err := repository.Encrypt(gcp.GCPKeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		gcp.GCPKeyData = cipherData
+	}
+
+	return nil
+}
+
+// DecryptGCPIntegrationData will decrypt the gcp integration data before
+// returning it from the DB
+func (repo *GCPIntegrationRepository) DecryptGCPIntegrationData(
+	gcp *ints.GCPIntegration,
+	key *[32]byte,
+) error {
+	if len(gcp.GCPKeyData) > 0 {
+		plaintext, err := repository.Decrypt(gcp.GCPKeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		gcp.GCPKeyData = plaintext
+	}
+
+	return nil
+}
+
+// AWSIntegrationRepository uses gorm.DB for querying the database
+type AWSIntegrationRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewAWSIntegrationRepository returns a AWSIntegrationRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewAWSIntegrationRepository(
+	db *gorm.DB,
+	key *[32]byte,
+) repository.AWSIntegrationRepository {
+	return &AWSIntegrationRepository{db, key}
+}
+
+// CreateAWSIntegration creates a new aws auth mechanism
+func (repo *AWSIntegrationRepository) CreateAWSIntegration(
+	am *ints.AWSIntegration,
+) (*ints.AWSIntegration, error) {
+	err := repo.EncryptAWSIntegrationData(am, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", am.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("AWSIntegrations")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(am); err != nil {
+		return nil, err
+	}
+
+	return am, nil
+}
+
+// ReadAWSIntegration finds a aws auth mechanism by id
+func (repo *AWSIntegrationRepository) ReadAWSIntegration(
+	id uint,
+) (*ints.AWSIntegration, error) {
+	aws := &ints.AWSIntegration{}
+
+	if err := repo.db.Where("id = ?", id).First(&aws).Error; err != nil {
+		return nil, err
+	}
+
+	err := repo.DecryptAWSIntegrationData(aws, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return aws, nil
+}
+
+// ListAWSIntegrationsByProjectID finds all aws auth mechanisms
+// for a given project id
+func (repo *AWSIntegrationRepository) ListAWSIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.AWSIntegration, error) {
+	awss := []*ints.AWSIntegration{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&awss).Error; err != nil {
+		return nil, err
+	}
+
+	for _, aws := range awss {
+		repo.DecryptAWSIntegrationData(aws, repo.key)
+	}
+
+	return awss, nil
+}
+
+// EncryptAWSIntegrationData will encrypt the aws integration data before
+// writing to the DB
+func (repo *AWSIntegrationRepository) EncryptAWSIntegrationData(
+	aws *ints.AWSIntegration,
+	key *[32]byte,
+) error {
+	if len(aws.AWSClusterID) > 0 {
+		cipherData, err := repository.Encrypt(aws.AWSClusterID, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSClusterID = cipherData
+	}
+
+	if len(aws.AWSAccessKeyID) > 0 {
+		cipherData, err := repository.Encrypt(aws.AWSAccessKeyID, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSAccessKeyID = cipherData
+	}
+
+	if len(aws.AWSSecretAccessKey) > 0 {
+		cipherData, err := repository.Encrypt(aws.AWSSecretAccessKey, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSSecretAccessKey = cipherData
+	}
+
+	if len(aws.AWSSessionToken) > 0 {
+		cipherData, err := repository.Encrypt(aws.AWSSessionToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSSessionToken = cipherData
+	}
+
+	return nil
+}
+
+// DecryptAWSIntegrationData will decrypt the aws integration data before
+// returning it from the DB
+func (repo *AWSIntegrationRepository) DecryptAWSIntegrationData(
+	aws *ints.AWSIntegration,
+	key *[32]byte,
+) error {
+	if len(aws.AWSClusterID) > 0 {
+		plaintext, err := repository.Decrypt(aws.AWSClusterID, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSClusterID = plaintext
+	}
+
+	if len(aws.AWSAccessKeyID) > 0 {
+		plaintext, err := repository.Decrypt(aws.AWSAccessKeyID, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSAccessKeyID = plaintext
+	}
+
+	if len(aws.AWSSecretAccessKey) > 0 {
+		plaintext, err := repository.Decrypt(aws.AWSSecretAccessKey, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSSecretAccessKey = plaintext
+	}
+
+	if len(aws.AWSSessionToken) > 0 {
+		plaintext, err := repository.Decrypt(aws.AWSSessionToken, key)
+
+		if err != nil {
+			return err
+		}
+
+		aws.AWSSessionToken = plaintext
+	}
+
+	return nil
+}

+ 461 - 0
internal/repository/gorm/auth_test.go

@@ -0,0 +1,461 @@
+package gorm_test
+
+import (
+	"testing"
+
+	"github.com/go-test/deep"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	orm "gorm.io/gorm"
+)
+
+func TestCreateKubeIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_ki.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	ki := &ints.KubeIntegration{
+		Mechanism:  ints.KubeLocal,
+		ProjectID:  tester.initProjects[0].ID,
+		UserID:     tester.initUsers[0].ID,
+		Kubeconfig: []byte("current-context: testing\n"),
+	}
+
+	expKI := *ki
+
+	ki, err := tester.repo.KubeIntegration.CreateKubeIntegration(ki)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	ki, err = tester.repo.KubeIntegration.ReadKubeIntegration(ki.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if ki.Model.ID != 1 {
+		t.Errorf("incorrect kube integration ID: expected %d, got %d\n", 1, ki.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	ki.Model = orm.Model{}
+
+	if diff := deep.Equal(expKI, *ki); diff != nil {
+		t.Errorf("incorrect kube integration")
+		t.Error(diff)
+	}
+}
+
+func TestListKubeIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_kis.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initKubeIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	kis, err := tester.repo.KubeIntegration.ListKubeIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(kis) != 1 {
+		t.Fatalf("length of kube integrations incorrect: expected %d, got %d\n", 1, len(kis))
+	}
+
+	// make sure data is correct
+	expKI := ints.KubeIntegration{
+		Mechanism:  ints.KubeLocal,
+		ProjectID:  tester.initProjects[0].ID,
+		UserID:     tester.initUsers[0].ID,
+		Kubeconfig: []byte("current-context: testing\n"),
+	}
+
+	ki := kis[0]
+
+	// reset fields for reflect.DeepEqual
+	ki.Model = orm.Model{}
+
+	if diff := deep.Equal(expKI, *ki); diff != nil {
+		t.Errorf("incorrect kube integration")
+		t.Error(diff)
+	}
+}
+
+func TestCreateOIDCIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_oidc.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	oidc := &ints.OIDCIntegration{
+		Client:       ints.OIDCKube,
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		IssuerURL:    []byte("https://oidc.example.com"),
+		ClientID:     []byte("exampleclientid"),
+		ClientSecret: []byte("exampleclientsecret"),
+		IDToken:      []byte("idtoken"),
+		RefreshToken: []byte("refreshtoken"),
+	}
+
+	expOIDC := *oidc
+
+	oidc, err := tester.repo.OIDCIntegration.CreateOIDCIntegration(oidc)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	oidc, err = tester.repo.OIDCIntegration.ReadOIDCIntegration(oidc.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if oidc.Model.ID != 1 {
+		t.Errorf("incorrect oidc integration ID: expected %d, got %d\n", 1, oidc.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	oidc.Model = orm.Model{}
+
+	if diff := deep.Equal(expOIDC, *oidc); diff != nil {
+		t.Errorf("incorrect oidc integration")
+		t.Error(diff)
+	}
+}
+
+func TestListOIDCIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_oidcs.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initOIDCIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	oidcs, err := tester.repo.OIDCIntegration.ListOIDCIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(oidcs) != 1 {
+		t.Fatalf("length of oidc integrations incorrect: expected %d, got %d\n", 1, len(oidcs))
+	}
+
+	// make sure data is correct
+	expOIDC := ints.OIDCIntegration{
+		Client:       ints.OIDCKube,
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		IssuerURL:    []byte("https://oidc.example.com"),
+		ClientID:     []byte("exampleclientid"),
+		ClientSecret: []byte("exampleclientsecret"),
+		IDToken:      []byte("idtoken"),
+		RefreshToken: []byte("refreshtoken"),
+	}
+
+	oidc := oidcs[0]
+
+	// reset fields for reflect.DeepEqual
+	oidc.Model = orm.Model{}
+
+	if diff := deep.Equal(expOIDC, *oidc); diff != nil {
+		t.Errorf("incorrect oidc integration")
+		t.Error(diff)
+	}
+}
+
+func TestCreateOAuthIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_oauth.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	oauth := &ints.OAuthIntegration{
+		Client:       ints.OAuthGithub,
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		ClientID:     []byte("exampleclientid"),
+		AccessToken:  []byte("idtoken"),
+		RefreshToken: []byte("refreshtoken"),
+	}
+
+	expOAuth := *oauth
+
+	oauth, err := tester.repo.OAuthIntegration.CreateOAuthIntegration(oauth)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	oauth, err = tester.repo.OAuthIntegration.ReadOAuthIntegration(oauth.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if oauth.Model.ID != 1 {
+		t.Errorf("incorrect oauth integration ID: expected %d, got %d\n", 1, oauth.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	oauth.Model = orm.Model{}
+
+	if diff := deep.Equal(expOAuth, *oauth); diff != nil {
+		t.Errorf("incorrect oauth integration")
+		t.Error(diff)
+	}
+}
+
+func TestListOAuthIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_oauths.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initOAuthIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	oauths, err := tester.repo.OAuthIntegration.ListOAuthIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(oauths) != 1 {
+		t.Fatalf("length of oauth integrations incorrect: expected %d, got %d\n", 1, len(oauths))
+	}
+
+	// make sure data is correct
+	expOAuth := ints.OAuthIntegration{
+		Client:       ints.OAuthGithub,
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		ClientID:     []byte("exampleclientid"),
+		AccessToken:  []byte("idtoken"),
+		RefreshToken: []byte("refreshtoken"),
+	}
+
+	oauth := oauths[0]
+
+	// reset fields for reflect.DeepEqual
+	oauth.Model = orm.Model{}
+
+	if diff := deep.Equal(expOAuth, *oauth); diff != nil {
+		t.Errorf("incorrect oauth integration")
+		t.Error(diff)
+	}
+}
+
+func TestCreateGCPIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_gcp.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	gcp := &ints.GCPIntegration{
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		GCPProjectID: "test-proj-123456",
+		GCPUserEmail: "test@test.it",
+		GCPKeyData:   []byte("{\"test\":\"key\"}"),
+	}
+
+	expGCP := *gcp
+
+	gcp, err := tester.repo.GCPIntegration.CreateGCPIntegration(gcp)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	gcp, err = tester.repo.GCPIntegration.ReadGCPIntegration(gcp.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if gcp.Model.ID != 1 {
+		t.Errorf("incorrect gcp integration ID: expected %d, got %d\n", 1, gcp.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	gcp.Model = orm.Model{}
+
+	if diff := deep.Equal(expGCP, *gcp); diff != nil {
+		t.Errorf("incorrect gcp integration")
+		t.Error(diff)
+	}
+}
+
+func TestListGCPIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_gcps.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initGCPIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	gcps, err := tester.repo.GCPIntegration.ListGCPIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(gcps) != 1 {
+		t.Fatalf("length of gcp integrations incorrect: expected %d, got %d\n", 1, len(gcps))
+	}
+
+	// make sure data is correct
+	expGCP := ints.GCPIntegration{
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		GCPProjectID: "test-proj-123456",
+		GCPUserEmail: "test@test.it",
+		GCPKeyData:   []byte("{\"test\":\"key\"}"),
+	}
+
+	gcp := gcps[0]
+
+	// reset fields for reflect.DeepEqual
+	gcp.Model = orm.Model{}
+
+	if diff := deep.Equal(expGCP, *gcp); diff != nil {
+		t.Errorf("incorrect gcp integration")
+		t.Error(diff)
+	}
+}
+
+func TestCreateAWSIntegration(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_create_aws.db",
+	}
+
+	setupTestEnv(tester, t)
+	initUser(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	aws := &ints.AWSIntegration{
+		ProjectID:          tester.initProjects[0].ID,
+		UserID:             tester.initUsers[0].ID,
+		AWSEntityID:        "entity",
+		AWSCallerID:        "caller",
+		AWSClusterID:       []byte("example-cluster-0"),
+		AWSAccessKeyID:     []byte("accesskey"),
+		AWSSecretAccessKey: []byte("secret"),
+		AWSSessionToken:    []byte("optional"),
+	}
+
+	expAWS := *aws
+
+	aws, err := tester.repo.AWSIntegration.CreateAWSIntegration(aws)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	aws, err = tester.repo.AWSIntegration.ReadAWSIntegration(aws.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if aws.Model.ID != 1 {
+		t.Errorf("incorrect aws integration ID: expected %d, got %d\n", 1, aws.Model.ID)
+	}
+
+	// reset fields for deep.Equal
+	aws.Model = orm.Model{}
+
+	if diff := deep.Equal(expAWS, *aws); diff != nil {
+		t.Errorf("incorrect aws integration")
+		t.Error(diff)
+	}
+}
+
+func TestListAWSIntegrationsByProjectID(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_list_awss.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	initAWSIntegration(tester, t)
+	defer cleanup(tester, t)
+
+	awss, err := tester.repo.AWSIntegration.ListAWSIntegrationsByProjectID(
+		tester.initProjects[0].Model.ID,
+	)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	if len(awss) != 1 {
+		t.Fatalf("length of aws integrations incorrect: expected %d, got %d\n", 1, len(awss))
+	}
+
+	// make sure data is correct
+	expAWS := ints.AWSIntegration{
+		ProjectID:          tester.initProjects[0].ID,
+		UserID:             tester.initUsers[0].ID,
+		AWSEntityID:        "entity",
+		AWSCallerID:        "caller",
+		AWSClusterID:       []byte("example-cluster-0"),
+		AWSAccessKeyID:     []byte("accesskey"),
+		AWSSecretAccessKey: []byte("secret"),
+		AWSSessionToken:    []byte("optional"),
+	}
+
+	aws := awss[0]
+
+	// reset fields for reflect.DeepEqual
+	aws.Model = orm.Model{}
+
+	if diff := deep.Equal(expAWS, *aws); diff != nil {
+		t.Errorf("incorrect aws integration")
+		t.Error(diff)
+	}
+}

+ 64 - 0
internal/repository/gorm/gitrepo.go

@@ -0,0 +1,64 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// GitRepoRepository uses gorm.DB for querying the database
+type GitRepoRepository struct {
+	db  *gorm.DB
+	key *[32]byte
+}
+
+// NewGitRepoRepository returns a GitRepoRepository which uses
+// gorm.DB for querying the database. It accepts an encryption key to encrypt
+// sensitive data
+func NewGitRepoRepository(db *gorm.DB, key *[32]byte) repository.GitRepoRepository {
+	return &GitRepoRepository{db, key}
+}
+
+// CreateGitRepo creates a new repo client and appends it to the in-memory list
+func (repo *GitRepoRepository) CreateGitRepo(gr *models.GitRepo) (*models.GitRepo, error) {
+	project := &models.Project{}
+
+	if err := repo.db.Where("id = ?", gr.ProjectID).First(&project).Error; err != nil {
+		return nil, err
+	}
+
+	assoc := repo.db.Model(&project).Association("GitRepos")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(gr); err != nil {
+		return nil, err
+	}
+
+	return gr, nil
+}
+
+// ReadGitRepo returns a repo client by id
+func (repo *GitRepoRepository) ReadGitRepo(id uint) (*models.GitRepo, error) {
+	gr := &models.GitRepo{}
+
+	// preload Clusters association
+	if err := repo.db.Where("id = ?", id).First(&gr).Error; err != nil {
+		return nil, err
+	}
+
+	return gr, nil
+}
+
+// ListGitReposByProjectID returns a list of repo clients that match a project id
+func (repo *GitRepoRepository) ListGitReposByProjectID(projectID uint) ([]*models.GitRepo, error) {
+	grs := []*models.GitRepo{}
+
+	if err := repo.db.Where("project_id = ?", projectID).Find(&grs).Error; err != nil {
+		return nil, err
+	}
+
+	return grs, nil
+}

+ 104 - 0
internal/repository/gorm/gitrepo_test.go

@@ -0,0 +1,104 @@
+package gorm_test
+
+// func TestCreateGitRepo(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_create_gr.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initUser(tester, t)
+// 	initProject(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	repoClient := &models.GitRepo{
+// 		ProjectID:    tester.initProjects[0].ID,
+// 		UserID:       tester.initUsers[0].ID,
+// 		RepoUserID:   1,
+// 		Kind:         models.GitRepoGithub,
+// 		AccessToken:  []byte("accesstoken1234"),
+// 		RefreshToken: []byte("refreshtoken1234"),
+// 	}
+
+// 	repoClient, err := tester.repo.GitRepo.CreateGitRepo(repoClient)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	repoClient, err = tester.repo.GitRepo.ReadGitRepo(repoClient.Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure id is 1
+// 	if repoClient.Model.ID != 1 {
+// 		t.Errorf("incorrect repo client ID: expected %d, got %d\n", 1, repoClient.Model.ID)
+// 	}
+
+// 	// make sure data is correct
+// 	expGitRepo := &models.GitRepo{
+// 		ProjectID:    tester.initProjects[0].ID,
+// 		UserID:       tester.initUsers[0].ID,
+// 		RepoUserID:   1,
+// 		Kind:         models.GitRepoGithub,
+// 		AccessToken:  []byte("accesstoken1234"),
+// 		RefreshToken: []byte("refreshtoken1234"),
+// 	}
+
+// 	copyGitRepo := repoClient
+
+// 	// reset fields for reflect.DeepEqual
+// 	copyGitRepo.Model = orm.Model{}
+
+// 	if diff := deep.Equal(copyGitRepo, expGitRepo); diff != nil {
+// 		t.Errorf("incorrect repo client")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestListGitReposByProjectID(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_list_grs.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initUser(tester, t)
+// 	initProject(tester, t)
+// 	initServiceAccount(tester, t)
+// 	initGitRepo(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	grs, err := tester.repo.GitRepo.ListGitReposByProjectID(
+// 		tester.initProjects[0].Model.ID,
+// 	)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	if len(grs) != 1 {
+// 		t.Fatalf("length of grs incorrect: expected %d, got %d\n", 1, len(grs))
+// 	}
+
+// 	// make sure data is correct
+// 	// make sure data is correct
+// 	expGitRepo := &models.GitRepo{
+// 		ProjectID:    tester.initProjects[0].ID,
+// 		UserID:       tester.initUsers[0].ID,
+// 		RepoUserID:   1,
+// 		Kind:         models.GitRepoGithub,
+// 		AccessToken:  []byte("accesstoken1234"),
+// 		RefreshToken: []byte("refreshtoken1234"),
+// 	}
+
+// 	copyGitRepo := grs[0]
+
+// 	// reset fields for reflect.DeepEqual
+// 	copyGitRepo.Model = orm.Model{}
+
+// 	if diff := deep.Equal(copyGitRepo, expGitRepo); diff != nil {
+// 		t.Errorf("incorrect repo client")
+// 		t.Error(diff)
+// 	}
+// }

+ 204 - 54
internal/repository/gorm/helpers_test.go

@@ -7,19 +7,23 @@ import (
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/config"
 	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
 	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 )
 
 type tester struct {
-	repo             *repository.Repository
-	key              *[32]byte
-	dbFileName       string
-	initUsers        []*models.User
-	initProjects     []*models.Project
-	initSACandidates []*models.ServiceAccountCandidate
-	initSAs          []*models.ServiceAccount
-	initRCs          []*models.RepoClient
+	repo         *repository.Repository
+	key          *[32]byte
+	dbFileName   string
+	initUsers    []*models.User
+	initProjects []*models.Project
+	initGRs      []*models.GitRepo
+	initKIs      []*ints.KubeIntegration
+	initOIDCs    []*ints.OIDCIntegration
+	initOAuths   []*ints.OAuthIntegration
+	initGCPs     []*ints.GCPIntegration
+	initAWSs     []*ints.AWSIntegration
 }
 
 func setupTestEnv(tester *tester, t *testing.T) {
@@ -38,14 +42,14 @@ func setupTestEnv(tester *tester, t *testing.T) {
 	err = db.AutoMigrate(
 		&models.Project{},
 		&models.Role{},
-		&models.ServiceAccount{},
-		&models.ServiceAccountAction{},
-		&models.ServiceAccountCandidate{},
-		&models.Cluster{},
-		&models.TokenCache{},
 		&models.User{},
 		&models.Session{},
-		&models.RepoClient{},
+		&models.GitRepo{},
+		&ints.KubeIntegration{},
+		&ints.OIDCIntegration{},
+		&ints.OAuthIntegration{},
+		&ints.GCPIntegration{},
+		&ints.AWSIntegration{},
 	)
 
 	if err != nil {
@@ -119,77 +123,223 @@ func initProjectRole(tester *tester, t *testing.T) {
 	}
 }
 
-func initServiceAccountCandidate(tester *tester, t *testing.T) {
+func initKubeIntegration(tester *tester, t *testing.T) {
 	t.Helper()
 
-	saCandidate := &models.ServiceAccountCandidate{
-		ProjectID:       1,
-		Kind:            "connector",
-		ClusterName:     "cluster-test",
-		ClusterEndpoint: "https://localhost",
-		AuthMechanism:   models.X509,
-		Kubeconfig:      []byte("current-context: testing\n"),
-		Actions: []models.ServiceAccountAction{
-			models.ServiceAccountAction{
-				Name:     models.TokenDataAction,
-				Resolved: false,
-			},
-		},
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
 	}
 
-	saCandidate, err := tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	ki := &ints.KubeIntegration{
+		Mechanism:  ints.KubeLocal,
+		ProjectID:  tester.initProjects[0].ID,
+		UserID:     tester.initUsers[0].ID,
+		Kubeconfig: []byte("current-context: testing\n"),
+	}
+
+	ki, err := tester.repo.KubeIntegration.CreateKubeIntegration(ki)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}
 
-	tester.initSACandidates = append(tester.initSACandidates, saCandidate)
+	tester.initKIs = append(tester.initKIs, ki)
 }
 
-func initServiceAccount(tester *tester, t *testing.T) {
+func initOIDCIntegration(tester *tester, t *testing.T) {
 	t.Helper()
 
-	sa := &models.ServiceAccount{
-		ProjectID:             1,
-		Kind:                  "connector",
-		AuthMechanism:         models.X509,
-		ClientCertificateData: []byte("-----BEGIN"),
-		ClientKeyData:         []byte("-----BEGIN"),
-		Clusters: []models.Cluster{
-			models.Cluster{
-				Name:                     "cluster-test",
-				Server:                   "https://localhost",
-				CertificateAuthorityData: []byte("-----BEGIN"),
-			},
-		},
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	oidc := &ints.OIDCIntegration{
+		Client:       ints.OIDCKube,
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		IssuerURL:    []byte("https://oidc.example.com"),
+		ClientID:     []byte("exampleclientid"),
+		ClientSecret: []byte("exampleclientsecret"),
+		IDToken:      []byte("idtoken"),
+		RefreshToken: []byte("refreshtoken"),
 	}
 
-	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
+	oidc, err := tester.repo.OIDCIntegration.CreateOIDCIntegration(oidc)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}
 
-	tester.initSAs = append(tester.initSAs, sa)
+	tester.initOIDCs = append(tester.initOIDCs, oidc)
 }
 
-func initRepoClient(tester *tester, t *testing.T) {
+func initOAuthIntegration(tester *tester, t *testing.T) {
 	t.Helper()
 
-	rc := &models.RepoClient{
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	oauth := &ints.OAuthIntegration{
+		Client:       ints.OAuthGithub,
 		ProjectID:    tester.initProjects[0].ID,
 		UserID:       tester.initUsers[0].ID,
-		RepoUserID:   1,
-		Kind:         models.RepoClientGithub,
-		AccessToken:  []byte("accesstoken1234"),
-		RefreshToken: []byte("refreshtoken1234"),
+		ClientID:     []byte("exampleclientid"),
+		AccessToken:  []byte("idtoken"),
+		RefreshToken: []byte("refreshtoken"),
 	}
 
-	rc, err := tester.repo.RepoClient.CreateRepoClient(rc)
+	oauth, err := tester.repo.OAuthIntegration.CreateOAuthIntegration(oauth)
 
 	if err != nil {
 		t.Fatalf("%v\n", err)
 	}
 
-	tester.initRCs = append(tester.initRCs, rc)
+	tester.initOAuths = append(tester.initOAuths, oauth)
+}
+
+func initGCPIntegration(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	gcp := &ints.GCPIntegration{
+		ProjectID:    tester.initProjects[0].ID,
+		UserID:       tester.initUsers[0].ID,
+		GCPProjectID: "test-proj-123456",
+		GCPUserEmail: "test@test.it",
+		GCPKeyData:   []byte("{\"test\":\"key\"}"),
+	}
+
+	gcp, err := tester.repo.GCPIntegration.CreateGCPIntegration(gcp)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initGCPs = append(tester.initGCPs, gcp)
+}
+
+func initAWSIntegration(tester *tester, t *testing.T) {
+	t.Helper()
+
+	if len(tester.initProjects) == 0 {
+		initProject(tester, t)
+	}
+
+	if len(tester.initUsers) == 0 {
+		initUser(tester, t)
+	}
+
+	aws := &ints.AWSIntegration{
+		ProjectID:          tester.initProjects[0].ID,
+		UserID:             tester.initUsers[0].ID,
+		AWSEntityID:        "entity",
+		AWSCallerID:        "caller",
+		AWSClusterID:       []byte("example-cluster-0"),
+		AWSAccessKeyID:     []byte("accesskey"),
+		AWSSecretAccessKey: []byte("secret"),
+		AWSSessionToken:    []byte("optional"),
+	}
+
+	aws, err := tester.repo.AWSIntegration.CreateAWSIntegration(aws)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	tester.initAWSs = append(tester.initAWSs, aws)
+}
+
+// func initServiceAccountCandidate(tester *tester, t *testing.T) {
+// 	t.Helper()
+
+// 	saCandidate := &models.ServiceAccountCandidate{
+// 		ProjectID:       1,
+// 		Kind:            "connector",
+// 		ClusterName:     "cluster-test",
+// 		ClusterEndpoint: "https://localhost",
+// 		Integration:   models.X509,
+// 		Kubeconfig:      []byte("current-context: testing\n"),
+// 		Actions: []models.ServiceAccountAction{
+// 			models.ServiceAccountAction{
+// 				Name:     models.TokenDataAction,
+// 				Resolved: false,
+// 			},
+// 		},
+// 	}
+
+// 	saCandidate, err := tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	tester.initSACandidates = append(tester.initSACandidates, saCandidate)
+// }
+
+// func initServiceAccount(tester *tester, t *testing.T) {
+// 	t.Helper()
+
+// 	sa := &models.ServiceAccount{
+// 		ProjectID:             1,
+// 		Kind:                  "connector",
+// 		Integration:         models.X509,
+// 		ClientCertificateData: []byte("-----BEGIN"),
+// 		ClientKeyData:         []byte("-----BEGIN"),
+// 		Clusters: []models.Cluster{
+// 			models.Cluster{
+// 				Name:                     "cluster-test",
+// 				Server:                   "https://localhost",
+// 				CertificateAuthorityData: []byte("-----BEGIN"),
+// 			},
+// 		},
+// 	}
+
+// 	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	tester.initSAs = append(tester.initSAs, sa)
+// }
+
+func initGitRepo(tester *tester, t *testing.T) {
+	t.Helper()
+
+	// rc := &models.GitRepo{
+	// 	ProjectID:    tester.initProjects[0].ID,
+	// 	UserID:       tester.initUsers[0].ID,
+	// 	RepoUserID:   1,
+	// 	Kind:         models.RepoClientGithub,
+	// 	AccessToken:  []byte("accesstoken1234"),
+	// 	RefreshToken: []byte("refreshtoken1234"),
+	// }
+
+	// rc, err := tester.repo.RepoClient.CreateRepoClient(rc)
+
+	// if err != nil {
+	// 	t.Fatalf("%v\n", err)
+	// }
+
+	// tester.initRCs = append(tester.initRCs, rc)
 }

+ 0 - 134
internal/repository/gorm/repoclient.go

@@ -1,134 +0,0 @@
-package gorm
-
-import (
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-// RepoClientRepository uses gorm.DB for querying the database
-type RepoClientRepository struct {
-	db  *gorm.DB
-	key *[32]byte
-}
-
-// NewRepoClientRepository returns a RepoClientRepository which uses
-// gorm.DB for querying the database. It accepts an encryption key to encrypt
-// sensitive data
-func NewRepoClientRepository(db *gorm.DB, key *[32]byte) repository.RepoClientRepository {
-	return &RepoClientRepository{db, key}
-}
-
-// CreateRepoClient creates a new repo client and appends it to the in-memory list
-func (repo *RepoClientRepository) CreateRepoClient(rc *models.RepoClient) (*models.RepoClient, error) {
-	err := repo.EncryptRepoClientData(rc, repo.key)
-
-	if err != nil {
-		return nil, err
-	}
-
-	project := &models.Project{}
-
-	if err := repo.db.Where("id = ?", rc.ProjectID).First(&project).Error; err != nil {
-		return nil, err
-	}
-
-	assoc := repo.db.Model(&project).Association("RepoClients")
-
-	if assoc.Error != nil {
-		return nil, assoc.Error
-	}
-
-	if err := assoc.Append(rc); err != nil {
-		return nil, err
-	}
-
-	return rc, nil
-}
-
-// ReadRepoClient returns a repo client by id
-func (repo *RepoClientRepository) ReadRepoClient(id uint) (*models.RepoClient, error) {
-	rc := &models.RepoClient{}
-
-	// preload Clusters association
-	if err := repo.db.Where("id = ?", id).First(&rc).Error; err != nil {
-		return nil, err
-	}
-
-	repo.DecryptRepoClientData(rc, repo.key)
-
-	return rc, nil
-}
-
-// ListRepoClientsByProjectID returns a list of repo clients that match a project id
-func (repo *RepoClientRepository) ListRepoClientsByProjectID(projectID uint) ([]*models.RepoClient, error) {
-	rcs := []*models.RepoClient{}
-
-	if err := repo.db.Where("project_id = ?", projectID).Find(&rcs).Error; err != nil {
-		return nil, err
-	}
-
-	for _, rc := range rcs {
-		repo.DecryptRepoClientData(rc, repo.key)
-	}
-
-	return rcs, nil
-}
-
-// EncryptRepoClientData will encrypt the repo client tokens before writing
-// to the DB
-func (repo *RepoClientRepository) EncryptRepoClientData(
-	rc *models.RepoClient,
-	key *[32]byte,
-) error {
-	if len(rc.AccessToken) > 0 {
-		cipherData, err := repository.Encrypt(rc.AccessToken, key)
-
-		if err != nil {
-			return err
-		}
-
-		rc.AccessToken = cipherData
-	}
-
-	if len(rc.RefreshToken) > 0 {
-		cipherData, err := repository.Encrypt(rc.RefreshToken, key)
-
-		if err != nil {
-			return err
-		}
-
-		rc.RefreshToken = cipherData
-	}
-
-	return nil
-}
-
-// DecryptRepoClientData will decrypt the repo client tokens before
-// returning it from the DB
-func (repo *RepoClientRepository) DecryptRepoClientData(
-	rc *models.RepoClient,
-	key *[32]byte,
-) error {
-	if len(rc.AccessToken) > 0 {
-		plaintext, err := repository.Decrypt(rc.AccessToken, key)
-
-		if err != nil {
-			return err
-		}
-
-		rc.AccessToken = plaintext
-	}
-
-	if len(rc.RefreshToken) > 0 {
-		plaintext, err := repository.Decrypt(rc.RefreshToken, key)
-
-		if err != nil {
-			return err
-		}
-
-		rc.RefreshToken = plaintext
-	}
-
-	return nil
-}

+ 0 - 112
internal/repository/gorm/repoclient_test.go

@@ -1,112 +0,0 @@
-package gorm_test
-
-import (
-	"testing"
-
-	"github.com/go-test/deep"
-	"github.com/porter-dev/porter/internal/models"
-	orm "gorm.io/gorm"
-)
-
-func TestCreateRepoClient(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_create_rc.db",
-	}
-
-	setupTestEnv(tester, t)
-	initUser(tester, t)
-	initProject(tester, t)
-	defer cleanup(tester, t)
-
-	repoClient := &models.RepoClient{
-		ProjectID:    tester.initProjects[0].ID,
-		UserID:       tester.initUsers[0].ID,
-		RepoUserID:   1,
-		Kind:         models.RepoClientGithub,
-		AccessToken:  []byte("accesstoken1234"),
-		RefreshToken: []byte("refreshtoken1234"),
-	}
-
-	repoClient, err := tester.repo.RepoClient.CreateRepoClient(repoClient)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	repoClient, err = tester.repo.RepoClient.ReadRepoClient(repoClient.Model.ID)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure id is 1
-	if repoClient.Model.ID != 1 {
-		t.Errorf("incorrect repo client ID: expected %d, got %d\n", 1, repoClient.Model.ID)
-	}
-
-	// make sure data is correct
-	expRepoClient := &models.RepoClient{
-		ProjectID:    tester.initProjects[0].ID,
-		UserID:       tester.initUsers[0].ID,
-		RepoUserID:   1,
-		Kind:         models.RepoClientGithub,
-		AccessToken:  []byte("accesstoken1234"),
-		RefreshToken: []byte("refreshtoken1234"),
-	}
-
-	copyRepoClient := repoClient
-
-	// reset fields for reflect.DeepEqual
-	copyRepoClient.Model = orm.Model{}
-
-	if diff := deep.Equal(copyRepoClient, expRepoClient); diff != nil {
-		t.Errorf("incorrect repo client")
-		t.Error(diff)
-	}
-}
-
-func TestListRepoClientsByProjectID(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_list_rcs.db",
-	}
-
-	setupTestEnv(tester, t)
-	initUser(tester, t)
-	initProject(tester, t)
-	initServiceAccount(tester, t)
-	initRepoClient(tester, t)
-	defer cleanup(tester, t)
-
-	rcs, err := tester.repo.RepoClient.ListRepoClientsByProjectID(
-		tester.initProjects[0].Model.ID,
-	)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	if len(rcs) != 1 {
-		t.Fatalf("length of rcs incorrect: expected %d, got %d\n", 1, len(rcs))
-	}
-
-	// make sure data is correct
-	// make sure data is correct
-	expRepoClient := &models.RepoClient{
-		ProjectID:    tester.initProjects[0].ID,
-		UserID:       tester.initUsers[0].ID,
-		RepoUserID:   1,
-		Kind:         models.RepoClientGithub,
-		AccessToken:  []byte("accesstoken1234"),
-		RefreshToken: []byte("refreshtoken1234"),
-	}
-
-	copyRepoClient := rcs[0]
-
-	// reset fields for reflect.DeepEqual
-	copyRepoClient.Model = orm.Model{}
-
-	if diff := deep.Equal(copyRepoClient, expRepoClient); diff != nil {
-		t.Errorf("incorrect repo client")
-		t.Error(diff)
-	}
-}

+ 9 - 5
internal/repository/gorm/repository.go

@@ -9,10 +9,14 @@ import (
 // gorm.DB for querying the database
 func NewRepository(db *gorm.DB, key *[32]byte) *repository.Repository {
 	return &repository.Repository{
-		User:           NewUserRepository(db),
-		Session:        NewSessionRepository(db),
-		Project:        NewProjectRepository(db),
-		ServiceAccount: NewServiceAccountRepository(db, key),
-		RepoClient:     NewRepoClientRepository(db, key),
+		User:             NewUserRepository(db),
+		Session:          NewSessionRepository(db),
+		Project:          NewProjectRepository(db),
+		GitRepo:          NewGitRepoRepository(db, key),
+		KubeIntegration:  NewKubeIntegrationRepository(db, key),
+		OIDCIntegration:  NewOIDCIntegrationRepository(db, key),
+		OAuthIntegration: NewOAuthIntegrationRepository(db, key),
+		GCPIntegration:   NewGCPIntegrationRepository(db, key),
+		AWSIntegration:   NewAWSIntegrationRepository(db, key),
 	}
 }

+ 466 - 466
internal/repository/gorm/serviceaccount.go

@@ -1,609 +1,609 @@
 package gorm
 
-import (
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-// ServiceAccountRepository uses gorm.DB for querying the database
-type ServiceAccountRepository struct {
-	db  *gorm.DB
-	key *[32]byte
-}
-
-// NewServiceAccountRepository returns a ServiceAccountRepository which uses
-// gorm.DB for querying the database. It accepts an encryption key to encrypt
-// sensitive data
-func NewServiceAccountRepository(db *gorm.DB, key *[32]byte) repository.ServiceAccountRepository {
-	return &ServiceAccountRepository{db, key}
-}
-
-// CreateServiceAccountCandidate creates a new service account candidate
-func (repo *ServiceAccountRepository) CreateServiceAccountCandidate(
-	saCandidate *models.ServiceAccountCandidate,
-) (*models.ServiceAccountCandidate, error) {
-	err := repo.EncryptServiceAccountCandidateData(saCandidate, repo.key)
-
-	if err != nil {
-		return nil, err
-	}
-
-	project := &models.Project{}
+// import (
+// 	"github.com/porter-dev/porter/internal/models"
+// 	"github.com/porter-dev/porter/internal/repository"
+// 	"gorm.io/gorm"
+// )
+
+// // ClusterRepository uses gorm.DB for querying the database
+// type ClusterRepository struct {
+// 	db  *gorm.DB
+// 	key *[32]byte
+// }
+
+// // NewClusterRepository returns a ClusterRepository which uses
+// // gorm.DB for querying the database. It accepts an encryption key to encrypt
+// // sensitive data
+// func NewClusterRepository(db *gorm.DB, key *[32]byte) repository.ClusterRepository {
+// 	return &ClusterRepository{db, key}
+// }
+
+// // CreateClusterCandidate creates a new service account candidate
+// func (repo *ClusterRepository) CreateClusterCandidate(
+// 	saCandidate *models.ClusterCandidate,
+// ) (*models.ClusterCandidate, error) {
+// 	err := repo.EncryptClusterCandidateData(saCandidate, repo.key)
+
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	project := &models.Project{}
 
-	if err := repo.db.Where("id = ?", saCandidate.ProjectID).First(&project).Error; err != nil {
-		return nil, err
-	}
+// 	if err := repo.db.Where("id = ?", saCandidate.ProjectID).First(&project).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	assoc := repo.db.Model(&project).Association("ServiceAccountCandidates")
-
-	if assoc.Error != nil {
-		return nil, assoc.Error
-	}
-
-	if err := assoc.Append(saCandidate); err != nil {
-		return nil, err
-	}
-
-	return saCandidate, nil
-}
-
-// ReadServiceAccountCandidate finds a service account candidate by id
-func (repo *ServiceAccountRepository) ReadServiceAccountCandidate(
-	id uint,
-) (*models.ServiceAccountCandidate, error) {
-	saCandidate := &models.ServiceAccountCandidate{}
-
-	if err := repo.db.Preload("Actions").Where("id = ?", id).First(&saCandidate).Error; err != nil {
-		return nil, err
-	}
+// 	assoc := repo.db.Model(&project).Association("ClusterCandidates")
+
+// 	if assoc.Error != nil {
+// 		return nil, assoc.Error
+// 	}
+
+// 	if err := assoc.Append(saCandidate); err != nil {
+// 		return nil, err
+// 	}
+
+// 	return saCandidate, nil
+// }
+
+// // ReadClusterCandidate finds a service account candidate by id
+// func (repo *ClusterRepository) ReadClusterCandidate(
+// 	id uint,
+// ) (*models.ClusterCandidate, error) {
+// 	saCandidate := &models.ClusterCandidate{}
+
+// 	if err := repo.db.Preload("Actions").Where("id = ?", id).First(&saCandidate).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	repo.DecryptServiceAccountCandidateData(saCandidate, repo.key)
-
-	return saCandidate, nil
-}
-
-// ListServiceAccountCandidatesByProjectID finds all service account candidates
-// for a given project id
-func (repo *ServiceAccountRepository) ListServiceAccountCandidatesByProjectID(
-	projectID uint,
-) ([]*models.ServiceAccountCandidate, error) {
-	saCandidates := []*models.ServiceAccountCandidate{}
-
-	if err := repo.db.Preload("Actions").Where("project_id = ?", projectID).Find(&saCandidates).Error; err != nil {
-		return nil, err
-	}
-
-	for _, saCandidate := range saCandidates {
-		repo.DecryptServiceAccountCandidateData(saCandidate, repo.key)
-	}
-
-	return saCandidates, nil
-}
-
-// UpdateServiceAccountCandidateCreatedSAID updates the CreatedServiceAccountID for
-// a candidate, after the candidate has been resolved.
-func (repo *ServiceAccountRepository) UpdateServiceAccountCandidateCreatedSAID(
-	id uint,
-	createdSAID uint,
-) (*models.ServiceAccountCandidate, error) {
-	saCandidate := &models.ServiceAccountCandidate{}
-
-	if err := repo.db.Where("id = ?", id).First(&saCandidate).Error; err != nil {
-		return nil, err
-	}
-
-	saCandidate.CreatedServiceAccountID = createdSAID
-
-	if err := repo.db.Save(saCandidate).Error; err != nil {
-		return nil, err
-	}
+// 	repo.DecryptClusterCandidateData(saCandidate, repo.key)
+
+// 	return saCandidate, nil
+// }
+
+// // ListClusterCandidatesByProjectID finds all service account candidates
+// // for a given project id
+// func (repo *ClusterRepository) ListClusterCandidatesByProjectID(
+// 	projectID uint,
+// ) ([]*models.ClusterCandidate, error) {
+// 	saCandidates := []*models.ClusterCandidate{}
+
+// 	if err := repo.db.Preload("Actions").Where("project_id = ?", projectID).Find(&saCandidates).Error; err != nil {
+// 		return nil, err
+// 	}
+
+// 	for _, saCandidate := range saCandidates {
+// 		repo.DecryptClusterCandidateData(saCandidate, repo.key)
+// 	}
+
+// 	return saCandidates, nil
+// }
+
+// // UpdateClusterCandidateCreatedSAID updates the CreatedClusterID for
+// // a candidate, after the candidate has been resolved.
+// func (repo *ClusterRepository) UpdateClusterCandidateCreatedSAID(
+// 	id uint,
+// 	createdSAID uint,
+// ) (*models.ClusterCandidate, error) {
+// 	saCandidate := &models.ClusterCandidate{}
+
+// 	if err := repo.db.Where("id = ?", id).First(&saCandidate).Error; err != nil {
+// 		return nil, err
+// 	}
+
+// 	saCandidate.CreatedClusterID = createdSAID
+
+// 	if err := repo.db.Save(saCandidate).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	return saCandidate, nil
-}
-
-// CreateServiceAccount creates a new servicea account
-func (repo *ServiceAccountRepository) CreateServiceAccount(
-	sa *models.ServiceAccount,
-) (*models.ServiceAccount, error) {
-	err := repo.EncryptServiceAccountData(sa, repo.key)
+// 	return saCandidate, nil
+// }
+
+// // CreateCluster creates a new servicea account
+// func (repo *ClusterRepository) CreateCluster(
+// 	sa *models.Cluster,
+// ) (*models.Cluster, error) {
+// 	err := repo.EncryptClusterData(sa, repo.key)
 
-	if err != nil {
-		return nil, err
-	}
+// 	if err != nil {
+// 		return nil, err
+// 	}
 
-	project := &models.Project{}
-
-	if err := repo.db.Where("id = ?", sa.ProjectID).First(&project).Error; err != nil {
-		return nil, err
-	}
+// 	project := &models.Project{}
+
+// 	if err := repo.db.Where("id = ?", sa.ProjectID).First(&project).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	assoc := repo.db.Model(&project).Association("ServiceAccounts")
+// 	assoc := repo.db.Model(&project).Association("Clusters")
 
-	if assoc.Error != nil {
-		return nil, assoc.Error
-	}
+// 	if assoc.Error != nil {
+// 		return nil, assoc.Error
+// 	}
 
-	if err := assoc.Append(sa); err != nil {
-		return nil, err
-	}
+// 	if err := assoc.Append(sa); err != nil {
+// 		return nil, err
+// 	}
 
-	// create a token cache by default
-	assoc = repo.db.Model(sa).Association("TokenCache")
+// 	// create a token cache by default
+// 	assoc = repo.db.Model(sa).Association("TokenCache")
 
-	if assoc.Error != nil {
-		return nil, assoc.Error
-	}
+// 	if assoc.Error != nil {
+// 		return nil, assoc.Error
+// 	}
 
-	if err := assoc.Append(&sa.TokenCache); err != nil {
-		return nil, err
-	}
+// 	if err := assoc.Append(&sa.TokenCache); err != nil {
+// 		return nil, err
+// 	}
 
-	return sa, nil
-}
+// 	return sa, nil
+// }
 
-// ReadServiceAccount finds a service account by id
-func (repo *ServiceAccountRepository) ReadServiceAccount(
-	id uint,
-) (*models.ServiceAccount, error) {
-	sa := &models.ServiceAccount{}
+// // ReadCluster finds a service account by id
+// func (repo *ClusterRepository) ReadCluster(
+// 	id uint,
+// ) (*models.Cluster, error) {
+// 	sa := &models.Cluster{}
 
-	// preload Clusters association
-	if err := repo.db.Preload("Clusters").Preload("TokenCache").Where("id = ?", id).First(&sa).Error; err != nil {
-		return nil, err
-	}
+// 	// preload Clusters association
+// 	if err := repo.db.Preload("Clusters").Preload("TokenCache").Where("id = ?", id).First(&sa).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	repo.DecryptServiceAccountData(sa, repo.key)
+// 	repo.DecryptClusterData(sa, repo.key)
 
-	return sa, nil
-}
+// 	return sa, nil
+// }
 
-// ListServiceAccountsByProjectID finds all service accounts
-// for a given project id
-func (repo *ServiceAccountRepository) ListServiceAccountsByProjectID(
-	projectID uint,
-) ([]*models.ServiceAccount, error) {
-	sas := []*models.ServiceAccount{}
+// // ListClustersByProjectID finds all service accounts
+// // for a given project id
+// func (repo *ClusterRepository) ListClustersByProjectID(
+// 	projectID uint,
+// ) ([]*models.Cluster, error) {
+// 	sas := []*models.Cluster{}
 
-	if err := repo.db.Preload("Clusters").Where("project_id = ?", projectID).Find(&sas).Error; err != nil {
-		return nil, err
-	}
+// 	if err := repo.db.Preload("Clusters").Where("project_id = ?", projectID).Find(&sas).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	for _, sa := range sas {
-		repo.DecryptServiceAccountData(sa, repo.key)
-	}
+// 	for _, sa := range sas {
+// 		repo.DecryptClusterData(sa, repo.key)
+// 	}
 
-	return sas, nil
-}
+// 	return sas, nil
+// }
 
-// UpdateServiceAccountTokenCache updates the token cache for a service account
-func (repo *ServiceAccountRepository) UpdateServiceAccountTokenCache(
-	tokenCache *models.TokenCache,
-) (*models.ServiceAccount, error) {
-	if tok := tokenCache.Token; len(tok) > 0 {
-		cipherData, err := repository.Encrypt(tok, repo.key)
+// // UpdateClusterTokenCache updates the token cache for a service account
+// func (repo *ClusterRepository) UpdateClusterTokenCache(
+// 	tokenCache *models.TokenCache,
+// ) (*models.Cluster, error) {
+// 	if tok := tokenCache.Token; len(tok) > 0 {
+// 		cipherData, err := repository.Encrypt(tok, repo.key)
 
-		if err != nil {
-			return nil, err
-		}
+// 		if err != nil {
+// 			return nil, err
+// 		}
 
-		tokenCache.Token = cipherData
-	}
+// 		tokenCache.Token = cipherData
+// 	}
 
-	sa := &models.ServiceAccount{}
+// 	sa := &models.Cluster{}
 
-	if err := repo.db.Where("id = ?", tokenCache.ServiceAccountID).First(&sa).Error; err != nil {
-		return nil, err
-	}
+// 	if err := repo.db.Where("id = ?", tokenCache.ClusterID).First(&sa).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	sa.TokenCache.Token = tokenCache.Token
-	sa.TokenCache.Expiry = tokenCache.Expiry
+// 	sa.TokenCache.Token = tokenCache.Token
+// 	sa.TokenCache.Expiry = tokenCache.Expiry
 
-	if err := repo.db.Save(sa).Error; err != nil {
-		return nil, err
-	}
+// 	if err := repo.db.Save(sa).Error; err != nil {
+// 		return nil, err
+// 	}
 
-	return sa, nil
-}
+// 	return sa, nil
+// }
 
-// EncryptServiceAccountData will encrypt the user's service account data before writing
-// to the DB
-func (repo *ServiceAccountRepository) EncryptServiceAccountData(
-	sa *models.ServiceAccount,
-	key *[32]byte,
-) error {
-	if len(sa.ClientCertificateData) > 0 {
-		cipherData, err := repository.Encrypt(sa.ClientCertificateData, key)
+// // EncryptClusterData will encrypt the user's service account data before writing
+// // to the DB
+// func (repo *ClusterRepository) EncryptClusterData(
+// 	sa *models.Cluster,
+// 	key *[32]byte,
+// ) error {
+// 	if len(sa.ClientCertificateData) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.ClientCertificateData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.ClientCertificateData = cipherData
-	}
+// 		sa.ClientCertificateData = cipherData
+// 	}
 
-	if len(sa.ClientKeyData) > 0 {
-		cipherData, err := repository.Encrypt(sa.ClientKeyData, key)
+// 	if len(sa.ClientKeyData) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.ClientKeyData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.ClientKeyData = cipherData
-	}
+// 		sa.ClientKeyData = cipherData
+// 	}
 
-	if len(sa.Token) > 0 {
-		cipherData, err := repository.Encrypt(sa.Token, key)
+// 	if len(sa.Token) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.Token, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.Token = cipherData
-	}
+// 		sa.Token = cipherData
+// 	}
 
-	if len(sa.Username) > 0 {
-		cipherData, err := repository.Encrypt(sa.Username, key)
+// 	if len(sa.Username) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.Username, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.Username = cipherData
-	}
+// 		sa.Username = cipherData
+// 	}
 
-	if len(sa.Password) > 0 {
-		cipherData, err := repository.Encrypt(sa.Password, key)
+// 	if len(sa.Password) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.Password, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.Password = cipherData
-	}
+// 		sa.Password = cipherData
+// 	}
 
-	if len(sa.GCPKeyData) > 0 {
-		cipherData, err := repository.Encrypt(sa.GCPKeyData, key)
+// 	if len(sa.GCPKeyData) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.GCPKeyData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.GCPKeyData = cipherData
-	}
+// 		sa.GCPKeyData = cipherData
+// 	}
 
-	if tok := sa.TokenCache.Token; len(tok) > 0 {
-		cipherData, err := repository.Encrypt(tok, key)
+// 	if tok := sa.TokenCache.Token; len(tok) > 0 {
+// 		cipherData, err := repository.Encrypt(tok, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.TokenCache.Token = cipherData
-	}
+// 		sa.TokenCache.Token = cipherData
+// 	}
 
-	if len(sa.AWSAccessKeyID) > 0 {
-		cipherData, err := repository.Encrypt(sa.AWSAccessKeyID, key)
+// 	if len(sa.AWSAccessKeyID) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.AWSAccessKeyID, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.AWSAccessKeyID = cipherData
-	}
+// 		sa.AWSAccessKeyID = cipherData
+// 	}
 
-	if len(sa.AWSSecretAccessKey) > 0 {
-		cipherData, err := repository.Encrypt(sa.AWSSecretAccessKey, key)
+// 	if len(sa.AWSSecretAccessKey) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.AWSSecretAccessKey, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.AWSSecretAccessKey = cipherData
-	}
+// 		sa.AWSSecretAccessKey = cipherData
+// 	}
 
-	if len(sa.AWSClusterID) > 0 {
-		cipherData, err := repository.Encrypt(sa.AWSClusterID, key)
+// 	if len(sa.AWSClusterID) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.AWSClusterID, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.AWSClusterID = cipherData
-	}
+// 		sa.AWSClusterID = cipherData
+// 	}
 
-	if len(sa.OIDCIssuerURL) > 0 {
-		cipherData, err := repository.Encrypt(sa.OIDCIssuerURL, key)
+// 	if len(sa.OIDCIssuerURL) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.OIDCIssuerURL, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCIssuerURL = cipherData
-	}
+// 		sa.OIDCIssuerURL = cipherData
+// 	}
 
-	if len(sa.OIDCClientID) > 0 {
-		cipherData, err := repository.Encrypt(sa.OIDCClientID, key)
+// 	if len(sa.OIDCClientID) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.OIDCClientID, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCClientID = cipherData
-	}
+// 		sa.OIDCClientID = cipherData
+// 	}
 
-	if len(sa.OIDCClientSecret) > 0 {
-		cipherData, err := repository.Encrypt(sa.OIDCClientSecret, key)
+// 	if len(sa.OIDCClientSecret) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.OIDCClientSecret, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCClientSecret = cipherData
-	}
+// 		sa.OIDCClientSecret = cipherData
+// 	}
 
-	if len(sa.OIDCCertificateAuthorityData) > 0 {
-		cipherData, err := repository.Encrypt(sa.OIDCCertificateAuthorityData, key)
+// 	if len(sa.OIDCCertificateAuthorityData) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.OIDCCertificateAuthorityData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCCertificateAuthorityData = cipherData
-	}
+// 		sa.OIDCCertificateAuthorityData = cipherData
+// 	}
 
-	if len(sa.OIDCIDToken) > 0 {
-		cipherData, err := repository.Encrypt(sa.OIDCIDToken, key)
+// 	if len(sa.OIDCIDToken) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.OIDCIDToken, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCIDToken = cipherData
-	}
+// 		sa.OIDCIDToken = cipherData
+// 	}
 
-	if len(sa.OIDCRefreshToken) > 0 {
-		cipherData, err := repository.Encrypt(sa.OIDCRefreshToken, key)
+// 	if len(sa.OIDCRefreshToken) > 0 {
+// 		cipherData, err := repository.Encrypt(sa.OIDCRefreshToken, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCRefreshToken = cipherData
-	}
+// 		sa.OIDCRefreshToken = cipherData
+// 	}
 
-	for i, cluster := range sa.Clusters {
-		if len(cluster.CertificateAuthorityData) > 0 {
-			cipherData, err := repository.Encrypt(cluster.CertificateAuthorityData, key)
+// 	for i, cluster := range sa.Clusters {
+// 		if len(cluster.CertificateAuthorityData) > 0 {
+// 			cipherData, err := repository.Encrypt(cluster.CertificateAuthorityData, key)
 
-			if err != nil {
-				return err
-			}
+// 			if err != nil {
+// 				return err
+// 			}
 
-			cluster.CertificateAuthorityData = cipherData
-			sa.Clusters[i] = cluster
-		}
-	}
+// 			cluster.CertificateAuthorityData = cipherData
+// 			sa.Clusters[i] = cluster
+// 		}
+// 	}
 
-	return nil
-}
+// 	return nil
+// }
 
-// EncryptServiceAccountCandidateData will encrypt the service account candidate data before
-// writing to the DB
-func (repo *ServiceAccountRepository) EncryptServiceAccountCandidateData(
-	saCandidate *models.ServiceAccountCandidate,
-	key *[32]byte,
-) error {
-	if len(saCandidate.Kubeconfig) > 0 {
-		cipherData, err := repository.Encrypt(saCandidate.Kubeconfig, key)
+// // EncryptClusterCandidateData will encrypt the service account candidate data before
+// // writing to the DB
+// func (repo *ClusterRepository) EncryptClusterCandidateData(
+// 	saCandidate *models.ClusterCandidate,
+// 	key *[32]byte,
+// ) error {
+// 	if len(saCandidate.Kubeconfig) > 0 {
+// 		cipherData, err := repository.Encrypt(saCandidate.Kubeconfig, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		saCandidate.Kubeconfig = cipherData
-	}
+// 		saCandidate.Kubeconfig = cipherData
+// 	}
 
-	return nil
-}
+// 	return nil
+// }
 
-// DecryptServiceAccountData will decrypt the user's service account data before
-// returning it from the DB
-func (repo *ServiceAccountRepository) DecryptServiceAccountData(
-	sa *models.ServiceAccount,
-	key *[32]byte,
-) error {
-	if len(sa.ClientCertificateData) > 0 {
-		plaintext, err := repository.Decrypt(sa.ClientCertificateData, key)
+// // DecryptClusterData will decrypt the user's service account data before
+// // returning it from the DB
+// func (repo *ClusterRepository) DecryptClusterData(
+// 	sa *models.Cluster,
+// 	key *[32]byte,
+// ) error {
+// 	if len(sa.ClientCertificateData) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.ClientCertificateData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.ClientCertificateData = plaintext
-	}
+// 		sa.ClientCertificateData = plaintext
+// 	}
 
-	if len(sa.ClientKeyData) > 0 {
-		plaintext, err := repository.Decrypt(sa.ClientKeyData, key)
+// 	if len(sa.ClientKeyData) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.ClientKeyData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.ClientKeyData = plaintext
-	}
+// 		sa.ClientKeyData = plaintext
+// 	}
 
-	if len(sa.Token) > 0 {
-		plaintext, err := repository.Decrypt(sa.Token, key)
+// 	if len(sa.Token) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.Token, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.Token = plaintext
-	}
+// 		sa.Token = plaintext
+// 	}
 
-	if len(sa.Username) > 0 {
-		plaintext, err := repository.Decrypt(sa.Username, key)
+// 	if len(sa.Username) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.Username, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.Username = plaintext
-	}
+// 		sa.Username = plaintext
+// 	}
 
-	if len(sa.Password) > 0 {
-		plaintext, err := repository.Decrypt(sa.Password, key)
+// 	if len(sa.Password) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.Password, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.Password = plaintext
-	}
+// 		sa.Password = plaintext
+// 	}
 
-	if len(sa.GCPKeyData) > 0 {
-		plaintext, err := repository.Decrypt(sa.GCPKeyData, key)
+// 	if len(sa.GCPKeyData) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.GCPKeyData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.GCPKeyData = plaintext
-	}
+// 		sa.GCPKeyData = plaintext
+// 	}
 
-	if tok := sa.TokenCache.Token; len(tok) > 0 {
-		plaintext, err := repository.Decrypt(tok, key)
+// 	if tok := sa.TokenCache.Token; len(tok) > 0 {
+// 		plaintext, err := repository.Decrypt(tok, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.TokenCache.Token = plaintext
-	}
+// 		sa.TokenCache.Token = plaintext
+// 	}
 
-	if len(sa.AWSAccessKeyID) > 0 {
-		plaintext, err := repository.Decrypt(sa.AWSAccessKeyID, key)
+// 	if len(sa.AWSAccessKeyID) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.AWSAccessKeyID, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.AWSAccessKeyID = plaintext
-	}
+// 		sa.AWSAccessKeyID = plaintext
+// 	}
 
-	if len(sa.AWSSecretAccessKey) > 0 {
-		plaintext, err := repository.Decrypt(sa.AWSSecretAccessKey, key)
+// 	if len(sa.AWSSecretAccessKey) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.AWSSecretAccessKey, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.AWSSecretAccessKey = plaintext
-	}
+// 		sa.AWSSecretAccessKey = plaintext
+// 	}
 
-	if len(sa.AWSClusterID) > 0 {
-		plaintext, err := repository.Decrypt(sa.AWSClusterID, key)
+// 	if len(sa.AWSClusterID) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.AWSClusterID, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.AWSClusterID = plaintext
-	}
+// 		sa.AWSClusterID = plaintext
+// 	}
 
-	if len(sa.OIDCIssuerURL) > 0 {
-		plaintext, err := repository.Decrypt(sa.OIDCIssuerURL, key)
+// 	if len(sa.OIDCIssuerURL) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.OIDCIssuerURL, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCIssuerURL = plaintext
-	}
+// 		sa.OIDCIssuerURL = plaintext
+// 	}
 
-	if len(sa.OIDCClientID) > 0 {
-		plaintext, err := repository.Decrypt(sa.OIDCClientID, key)
+// 	if len(sa.OIDCClientID) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.OIDCClientID, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCClientID = plaintext
-	}
+// 		sa.OIDCClientID = plaintext
+// 	}
 
-	if len(sa.OIDCClientSecret) > 0 {
-		plaintext, err := repository.Decrypt(sa.OIDCClientSecret, key)
+// 	if len(sa.OIDCClientSecret) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.OIDCClientSecret, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCClientSecret = plaintext
-	}
+// 		sa.OIDCClientSecret = plaintext
+// 	}
 
-	if len(sa.OIDCCertificateAuthorityData) > 0 {
-		plaintext, err := repository.Decrypt(sa.OIDCCertificateAuthorityData, key)
+// 	if len(sa.OIDCCertificateAuthorityData) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.OIDCCertificateAuthorityData, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCCertificateAuthorityData = plaintext
-	}
+// 		sa.OIDCCertificateAuthorityData = plaintext
+// 	}
 
-	if len(sa.OIDCIDToken) > 0 {
-		plaintext, err := repository.Decrypt(sa.OIDCIDToken, key)
+// 	if len(sa.OIDCIDToken) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.OIDCIDToken, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCIDToken = plaintext
-	}
+// 		sa.OIDCIDToken = plaintext
+// 	}
 
-	if len(sa.OIDCRefreshToken) > 0 {
-		plaintext, err := repository.Decrypt(sa.OIDCRefreshToken, key)
+// 	if len(sa.OIDCRefreshToken) > 0 {
+// 		plaintext, err := repository.Decrypt(sa.OIDCRefreshToken, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		sa.OIDCRefreshToken = plaintext
-	}
+// 		sa.OIDCRefreshToken = plaintext
+// 	}
 
-	for i, cluster := range sa.Clusters {
-		if len(cluster.CertificateAuthorityData) > 0 {
-			plaintext, err := repository.Decrypt(cluster.CertificateAuthorityData, key)
+// 	for i, cluster := range sa.Clusters {
+// 		if len(cluster.CertificateAuthorityData) > 0 {
+// 			plaintext, err := repository.Decrypt(cluster.CertificateAuthorityData, key)
 
-			if err != nil {
-				return err
-			}
+// 			if err != nil {
+// 				return err
+// 			}
 
-			cluster.CertificateAuthorityData = plaintext
-			sa.Clusters[i] = cluster
-		}
-	}
+// 			cluster.CertificateAuthorityData = plaintext
+// 			sa.Clusters[i] = cluster
+// 		}
+// 	}
 
-	return nil
-}
+// 	return nil
+// }
 
-// DecryptServiceAccountCandidateData will decrypt the service account candidate data before
-// returning it from the DB
-func (repo *ServiceAccountRepository) DecryptServiceAccountCandidateData(
-	saCandidate *models.ServiceAccountCandidate,
-	key *[32]byte,
-) error {
-	if len(saCandidate.Kubeconfig) > 0 {
-		plaintext, err := repository.Decrypt(saCandidate.Kubeconfig, key)
+// // DecryptClusterCandidateData will decrypt the service account candidate data before
+// // returning it from the DB
+// func (repo *ClusterRepository) DecryptClusterCandidateData(
+// 	saCandidate *models.ClusterCandidate,
+// 	key *[32]byte,
+// ) error {
+// 	if len(saCandidate.Kubeconfig) > 0 {
+// 		plaintext, err := repository.Decrypt(saCandidate.Kubeconfig, key)
 
-		if err != nil {
-			return err
-		}
+// 		if err != nil {
+// 			return err
+// 		}
 
-		saCandidate.Kubeconfig = plaintext
-	}
+// 		saCandidate.Kubeconfig = plaintext
+// 	}
 
-	return nil
-}
+// 	return nil
+// }

+ 416 - 416
internal/repository/gorm/serviceaccount_test.go

@@ -1,418 +1,418 @@
 package gorm_test
 
-import (
-	"testing"
-	"time"
-
-	"github.com/go-test/deep"
-	"github.com/porter-dev/porter/internal/models"
-	orm "gorm.io/gorm"
-)
-
-func TestCreateServiceAccountCandidate(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_create_sa_candidate.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	defer cleanup(tester, t)
-
-	saCandidate := &models.ServiceAccountCandidate{
-		ProjectID:       1,
-		Kind:            "connector",
-		ClusterName:     "cluster-test",
-		ClusterEndpoint: "https://localhost",
-		AuthMechanism:   models.X509,
-		Kubeconfig:      []byte("current-context: testing\n"),
-	}
-
-	saCandidate, err := tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	saCandidate, err = tester.repo.ServiceAccount.ReadServiceAccountCandidate(saCandidate.Model.ID)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure id is 1
-	if saCandidate.Model.ID != 1 {
-		t.Errorf("incorrect service accound candidate ID: expected %d, got %d\n", 1, saCandidate.Model.ID)
-	}
-
-	// make sure data is correct
-	expSACandidate := &models.ServiceAccountCandidate{
-		ProjectID:       1,
-		Kind:            "connector",
-		ClusterName:     "cluster-test",
-		ClusterEndpoint: "https://localhost",
-		AuthMechanism:   models.X509,
-		Kubeconfig:      []byte("current-context: testing\n"),
-		Actions:         []models.ServiceAccountAction{},
-	}
-
-	copySACandidate := saCandidate
-
-	// reset fields for reflect.DeepEqual
-	copySACandidate.Model = orm.Model{}
-
-	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
-		t.Errorf("incorrect sa candidate")
-		t.Error(diff)
-	}
-}
-
-func TestCreateServiceAccountCandidateWithAction(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_create_sa_candidate_w_action.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	initServiceAccountCandidate(tester, t)
-	defer cleanup(tester, t)
-
-	saCandidate := tester.initSACandidates[0]
-
-	saCandidate, err := tester.repo.ServiceAccount.ReadServiceAccountCandidate(saCandidate.Model.ID)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure IDs are correct
-	if saCandidate.Model.ID != 1 {
-		t.Errorf("incorrect service account candidate ID: expected %d, got %d\n", 1, saCandidate.Model.ID)
-	}
-
-	if len(saCandidate.Actions) != 1 {
-		t.Errorf("incorrect actions length: expected %d, got %d\n", 1, len(saCandidate.Actions))
-	}
-
-	if saCandidate.Actions[0].Model.ID != 1 {
-		t.Errorf("incorrect actions ID: expected %d, got %d\n", 1, saCandidate.Actions[0].Model.ID)
-	}
-
-	// make sure data is correct
-	expSACandidate := &models.ServiceAccountCandidate{
-		ProjectID:       1,
-		Kind:            "connector",
-		ClusterName:     "cluster-test",
-		ClusterEndpoint: "https://localhost",
-		AuthMechanism:   models.X509,
-		Kubeconfig:      []byte("current-context: testing\n"),
-		Actions: []models.ServiceAccountAction{
-			models.ServiceAccountAction{
-				ServiceAccountCandidateID: 1,
-				Name:                      models.TokenDataAction,
-				Resolved:                  false,
-			},
-		},
-	}
-
-	copySACandidate := saCandidate
-
-	// reset fields for reflect.DeepEqual
-	copySACandidate.Model = orm.Model{}
-
-	copySACandidate.Actions[0].Model = orm.Model{}
-
-	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
-		t.Errorf("incorrect sa candidate")
-		t.Error(diff)
-	}
-}
-
-func TestListServiceAccountCandidatesByProjectID(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_list_sa_candidates.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	initServiceAccountCandidate(tester, t)
-	defer cleanup(tester, t)
-
-	saCandidates, err := tester.repo.ServiceAccount.ListServiceAccountCandidatesByProjectID(
-		tester.initProjects[0].Model.ID,
-	)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	if len(saCandidates) != 1 {
-		t.Fatalf("length of sa candidates incorrect: expected %d, got %d\n", 1, len(saCandidates))
-	}
-
-	// make sure data is correct
-	expSACandidate := &models.ServiceAccountCandidate{
-		ProjectID:       1,
-		Kind:            "connector",
-		ClusterName:     "cluster-test",
-		ClusterEndpoint: "https://localhost",
-		AuthMechanism:   models.X509,
-		Kubeconfig:      []byte("current-context: testing\n"),
-		Actions: []models.ServiceAccountAction{
-			models.ServiceAccountAction{
-				ServiceAccountCandidateID: 1,
-				Name:                      models.TokenDataAction,
-				Resolved:                  false,
-			},
-		},
-	}
-
-	copySACandidate := saCandidates[0]
-
-	// reset fields for reflect.DeepEqual
-	copySACandidate.Model = orm.Model{}
-	copySACandidate.Actions[0].Model = orm.Model{}
-
-	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
-		t.Errorf("incorrect sa candidate")
-		t.Error(diff)
-	}
-}
-
-func TestCreateServiceAccount(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_create_sa.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	defer cleanup(tester, t)
-
-	sa := &models.ServiceAccount{
-		ProjectID:             1,
-		Kind:                  "connector",
-		AuthMechanism:         models.X509,
-		ClientCertificateData: []byte("-----BEGIN"),
-		ClientKeyData:         []byte("-----BEGIN"),
-	}
-
-	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure id is 1
-	if sa.Model.ID != 1 {
-		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
-	}
-
-	// make sure data is correct
-	expSA := &models.ServiceAccount{
-		ProjectID:             1,
-		Kind:                  "connector",
-		AuthMechanism:         models.X509,
-		ClientCertificateData: []byte("-----BEGIN"),
-		ClientKeyData:         []byte("-----BEGIN"),
-		Clusters:              []models.Cluster{},
-	}
-
-	copySA := sa
-
-	// reset fields for reflect.DeepEqual
-	copySA.Model = orm.Model{}
-
-	if diff := deep.Equal(copySA, expSA); diff != nil {
-		t.Errorf("incorrect service account")
-		t.Error(diff)
-	}
-}
-
-func TestCreateServiceAccountWithCluster(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_create_sa_w_cluster.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	initServiceAccount(tester, t)
-	defer cleanup(tester, t)
-
-	sa := tester.initSAs[0]
-
-	sa, err := tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure id is 1
-	if sa.Model.ID != 1 {
-		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
-	}
-
-	if len(sa.Clusters) != 1 {
-		t.Errorf("incorrect clusters length: expected %d, got %d\n", 1, len(sa.Clusters))
-	}
-
-	if sa.Clusters[0].Model.ID != 1 {
-		t.Errorf("incorrect clusters ID: expected %d, got %d\n", 1, sa.Clusters[0].Model.ID)
-	}
-
-	// make sure data is correct
-	expSA := &models.ServiceAccount{
-		ProjectID:             1,
-		Kind:                  "connector",
-		AuthMechanism:         models.X509,
-		ClientCertificateData: []byte("-----BEGIN"),
-		ClientKeyData:         []byte("-----BEGIN"),
-		Clusters: []models.Cluster{
-			models.Cluster{
-				ServiceAccountID:         1,
-				Name:                     "cluster-test",
-				Server:                   "https://localhost",
-				CertificateAuthorityData: []byte("-----BEGIN"),
-			},
-		},
-	}
-
-	copySA := sa
-
-	// reset fields for reflect.DeepEqual
-	copySA.Model = orm.Model{}
-	copySA.Clusters[0].Model = orm.Model{}
-
-	if diff := deep.Equal(copySA, expSA); diff != nil {
-		t.Errorf("incorrect service account")
-		t.Error(diff)
-	}
-}
-
-func TestListServiceAccountsByProjectID(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_list_sas.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	initServiceAccount(tester, t)
-	defer cleanup(tester, t)
-
-	sas, err := tester.repo.ServiceAccount.ListServiceAccountsByProjectID(
-		tester.initProjects[0].Model.ID,
-	)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	if len(sas) != 1 {
-		t.Fatalf("length of sas incorrect: expected %d, got %d\n", 1, len(sas))
-	}
-
-	// make sure data is correct
-	expSA := &models.ServiceAccount{
-		ProjectID:             1,
-		Kind:                  "connector",
-		AuthMechanism:         models.X509,
-		ClientCertificateData: []byte("-----BEGIN"),
-		ClientKeyData:         []byte("-----BEGIN"),
-		Clusters: []models.Cluster{
-			models.Cluster{
-				ServiceAccountID:         1,
-				Name:                     "cluster-test",
-				Server:                   "https://localhost",
-				CertificateAuthorityData: []byte("-----BEGIN"),
-			},
-		},
-	}
-
-	copySA := sas[0]
-
-	// reset fields for reflect.DeepEqual
-	copySA.Model = orm.Model{}
-	copySA.Clusters[0].Model = orm.Model{}
-
-	if diff := deep.Equal(copySA, expSA); diff != nil {
-		t.Errorf("incorrect service account")
-		t.Error(diff)
-	}
-}
-
-func TestUpdateServiceAccountToken(t *testing.T) {
-	tester := &tester{
-		dbFileName: "./porter_test_update_sa_token.db",
-	}
-
-	setupTestEnv(tester, t)
-	initProject(tester, t)
-	defer cleanup(tester, t)
-
-	sa := &models.ServiceAccount{
-		ProjectID:     1,
-		Kind:          "connector",
-		AuthMechanism: models.GCP,
-		GCPKeyData:    []byte(`{"key":"data"}`),
-		TokenCache: models.TokenCache{
-			Token:  []byte("token-1"),
-			Expiry: time.Now().Add(-1 * time.Hour),
-		},
-	}
-
-	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
-
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure service account id of token is 1
-	if sa.TokenCache.ServiceAccountID != 1 {
-		t.Fatalf("incorrect service account ID in token cache: expected %d, got %d\n", 1, sa.TokenCache.ServiceAccountID)
-	}
-
-	// make sure old token is expired
-	if isExpired := sa.TokenCache.IsExpired(); !isExpired {
-		t.Fatalf("token was not expired\n")
-	}
-
-	sa.TokenCache.Token = []byte("token-2")
-	sa.TokenCache.Expiry = time.Now().Add(24 * time.Hour)
-
-	sa, err = tester.repo.ServiceAccount.UpdateServiceAccountTokenCache(&sa.TokenCache)
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
-	if err != nil {
-		t.Fatalf("%v\n", err)
-	}
-
-	// make sure id is 1
-	if sa.Model.ID != 1 {
-		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
-	}
-
-	// make sure new token is correct and not expired
-	if sa.TokenCache.ServiceAccountID != 1 {
-		t.Fatalf("incorrect service account ID in token cache: expected %d, got %d\n", 1, sa.TokenCache.ServiceAccountID)
-	}
-
-	if isExpired := sa.TokenCache.IsExpired(); isExpired {
-		t.Fatalf("token was expired\n")
-	}
-
-	if string(sa.TokenCache.Token) != "token-2" {
-		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-2", sa.TokenCache.Token)
-	}
-}
+// import (
+// 	"testing"
+// 	"time"
+
+// 	"github.com/go-test/deep"
+// 	"github.com/porter-dev/porter/internal/models"
+// 	orm "gorm.io/gorm"
+// )
+
+// func TestCreateServiceAccountCandidate(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_create_sa_candidate.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	saCandidate := &models.ServiceAccountCandidate{
+// 		ProjectID:       1,
+// 		Kind:            "connector",
+// 		ClusterName:     "cluster-test",
+// 		ClusterEndpoint: "https://localhost",
+// 		Integration:   models.X509,
+// 		Kubeconfig:      []byte("current-context: testing\n"),
+// 	}
+
+// 	saCandidate, err := tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	saCandidate, err = tester.repo.ServiceAccount.ReadServiceAccountCandidate(saCandidate.Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure id is 1
+// 	if saCandidate.Model.ID != 1 {
+// 		t.Errorf("incorrect service accound candidate ID: expected %d, got %d\n", 1, saCandidate.Model.ID)
+// 	}
+
+// 	// make sure data is correct
+// 	expSACandidate := &models.ServiceAccountCandidate{
+// 		ProjectID:       1,
+// 		Kind:            "connector",
+// 		ClusterName:     "cluster-test",
+// 		ClusterEndpoint: "https://localhost",
+// 		Integration:   models.X509,
+// 		Kubeconfig:      []byte("current-context: testing\n"),
+// 		Actions:         []models.ServiceAccountAction{},
+// 	}
+
+// 	copySACandidate := saCandidate
+
+// 	// reset fields for reflect.DeepEqual
+// 	copySACandidate.Model = orm.Model{}
+
+// 	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
+// 		t.Errorf("incorrect sa candidate")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestCreateServiceAccountCandidateWithAction(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_create_sa_candidate_w_action.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	initServiceAccountCandidate(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	saCandidate := tester.initSACandidates[0]
+
+// 	saCandidate, err := tester.repo.ServiceAccount.ReadServiceAccountCandidate(saCandidate.Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure IDs are correct
+// 	if saCandidate.Model.ID != 1 {
+// 		t.Errorf("incorrect service account candidate ID: expected %d, got %d\n", 1, saCandidate.Model.ID)
+// 	}
+
+// 	if len(saCandidate.Actions) != 1 {
+// 		t.Errorf("incorrect actions length: expected %d, got %d\n", 1, len(saCandidate.Actions))
+// 	}
+
+// 	if saCandidate.Actions[0].Model.ID != 1 {
+// 		t.Errorf("incorrect actions ID: expected %d, got %d\n", 1, saCandidate.Actions[0].Model.ID)
+// 	}
+
+// 	// make sure data is correct
+// 	expSACandidate := &models.ServiceAccountCandidate{
+// 		ProjectID:       1,
+// 		Kind:            "connector",
+// 		ClusterName:     "cluster-test",
+// 		ClusterEndpoint: "https://localhost",
+// 		Integration:   models.X509,
+// 		Kubeconfig:      []byte("current-context: testing\n"),
+// 		Actions: []models.ServiceAccountAction{
+// 			models.ServiceAccountAction{
+// 				ServiceAccountCandidateID: 1,
+// 				Name:                      models.TokenDataAction,
+// 				Resolved:                  false,
+// 			},
+// 		},
+// 	}
+
+// 	copySACandidate := saCandidate
+
+// 	// reset fields for reflect.DeepEqual
+// 	copySACandidate.Model = orm.Model{}
+
+// 	copySACandidate.Actions[0].Model = orm.Model{}
+
+// 	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
+// 		t.Errorf("incorrect sa candidate")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestListServiceAccountCandidatesByProjectID(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_list_sa_candidates.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	initServiceAccountCandidate(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	saCandidates, err := tester.repo.ServiceAccount.ListServiceAccountCandidatesByProjectID(
+// 		tester.initProjects[0].Model.ID,
+// 	)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	if len(saCandidates) != 1 {
+// 		t.Fatalf("length of sa candidates incorrect: expected %d, got %d\n", 1, len(saCandidates))
+// 	}
+
+// 	// make sure data is correct
+// 	expSACandidate := &models.ServiceAccountCandidate{
+// 		ProjectID:       1,
+// 		Kind:            "connector",
+// 		ClusterName:     "cluster-test",
+// 		ClusterEndpoint: "https://localhost",
+// 		Integration:   models.X509,
+// 		Kubeconfig:      []byte("current-context: testing\n"),
+// 		Actions: []models.ServiceAccountAction{
+// 			models.ServiceAccountAction{
+// 				ServiceAccountCandidateID: 1,
+// 				Name:                      models.TokenDataAction,
+// 				Resolved:                  false,
+// 			},
+// 		},
+// 	}
+
+// 	copySACandidate := saCandidates[0]
+
+// 	// reset fields for reflect.DeepEqual
+// 	copySACandidate.Model = orm.Model{}
+// 	copySACandidate.Actions[0].Model = orm.Model{}
+
+// 	if diff := deep.Equal(copySACandidate, expSACandidate); diff != nil {
+// 		t.Errorf("incorrect sa candidate")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestCreateServiceAccount(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_create_sa.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	sa := &models.ServiceAccount{
+// 		ProjectID:             1,
+// 		Kind:                  "connector",
+// 		Integration:         models.X509,
+// 		ClientCertificateData: []byte("-----BEGIN"),
+// 		ClientKeyData:         []byte("-----BEGIN"),
+// 	}
+
+// 	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure id is 1
+// 	if sa.Model.ID != 1 {
+// 		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
+// 	}
+
+// 	// make sure data is correct
+// 	expSA := &models.ServiceAccount{
+// 		ProjectID:             1,
+// 		Kind:                  "connector",
+// 		Integration:         models.X509,
+// 		ClientCertificateData: []byte("-----BEGIN"),
+// 		ClientKeyData:         []byte("-----BEGIN"),
+// 		Clusters:              []models.Cluster{},
+// 	}
+
+// 	copySA := sa
+
+// 	// reset fields for reflect.DeepEqual
+// 	copySA.Model = orm.Model{}
+
+// 	if diff := deep.Equal(copySA, expSA); diff != nil {
+// 		t.Errorf("incorrect service account")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestCreateServiceAccountWithCluster(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_create_sa_w_cluster.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	initServiceAccount(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	sa := tester.initSAs[0]
+
+// 	sa, err := tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure id is 1
+// 	if sa.Model.ID != 1 {
+// 		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
+// 	}
+
+// 	if len(sa.Clusters) != 1 {
+// 		t.Errorf("incorrect clusters length: expected %d, got %d\n", 1, len(sa.Clusters))
+// 	}
+
+// 	if sa.Clusters[0].Model.ID != 1 {
+// 		t.Errorf("incorrect clusters ID: expected %d, got %d\n", 1, sa.Clusters[0].Model.ID)
+// 	}
+
+// 	// make sure data is correct
+// 	expSA := &models.ServiceAccount{
+// 		ProjectID:             1,
+// 		Kind:                  "connector",
+// 		Integration:         models.X509,
+// 		ClientCertificateData: []byte("-----BEGIN"),
+// 		ClientKeyData:         []byte("-----BEGIN"),
+// 		Clusters: []models.Cluster{
+// 			models.Cluster{
+// 				ServiceAccountID:         1,
+// 				Name:                     "cluster-test",
+// 				Server:                   "https://localhost",
+// 				CertificateAuthorityData: []byte("-----BEGIN"),
+// 			},
+// 		},
+// 	}
+
+// 	copySA := sa
+
+// 	// reset fields for reflect.DeepEqual
+// 	copySA.Model = orm.Model{}
+// 	copySA.Clusters[0].Model = orm.Model{}
+
+// 	if diff := deep.Equal(copySA, expSA); diff != nil {
+// 		t.Errorf("incorrect service account")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestListServiceAccountsByProjectID(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_list_sas.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	initServiceAccount(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	sas, err := tester.repo.ServiceAccount.ListServiceAccountsByProjectID(
+// 		tester.initProjects[0].Model.ID,
+// 	)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	if len(sas) != 1 {
+// 		t.Fatalf("length of sas incorrect: expected %d, got %d\n", 1, len(sas))
+// 	}
+
+// 	// make sure data is correct
+// 	expSA := &models.ServiceAccount{
+// 		ProjectID:             1,
+// 		Kind:                  "connector",
+// 		Integration:         models.X509,
+// 		ClientCertificateData: []byte("-----BEGIN"),
+// 		ClientKeyData:         []byte("-----BEGIN"),
+// 		Clusters: []models.Cluster{
+// 			models.Cluster{
+// 				ServiceAccountID:         1,
+// 				Name:                     "cluster-test",
+// 				Server:                   "https://localhost",
+// 				CertificateAuthorityData: []byte("-----BEGIN"),
+// 			},
+// 		},
+// 	}
+
+// 	copySA := sas[0]
+
+// 	// reset fields for reflect.DeepEqual
+// 	copySA.Model = orm.Model{}
+// 	copySA.Clusters[0].Model = orm.Model{}
+
+// 	if diff := deep.Equal(copySA, expSA); diff != nil {
+// 		t.Errorf("incorrect service account")
+// 		t.Error(diff)
+// 	}
+// }
+
+// func TestUpdateServiceAccountToken(t *testing.T) {
+// 	tester := &tester{
+// 		dbFileName: "./porter_test_update_sa_token.db",
+// 	}
+
+// 	setupTestEnv(tester, t)
+// 	initProject(tester, t)
+// 	defer cleanup(tester, t)
+
+// 	sa := &models.ServiceAccount{
+// 		ProjectID:     1,
+// 		Kind:          "connector",
+// 		Integration: models.GCP,
+// 		GCPKeyData:    []byte(`{"key":"data"}`),
+// 		TokenCache: models.TokenCache{
+// 			Token:  []byte("token-1"),
+// 			Expiry: time.Now().Add(-1 * time.Hour),
+// 		},
+// 	}
+
+// 	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
+
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure service account id of token is 1
+// 	if sa.TokenCache.ServiceAccountID != 1 {
+// 		t.Fatalf("incorrect service account ID in token cache: expected %d, got %d\n", 1, sa.TokenCache.ServiceAccountID)
+// 	}
+
+// 	// make sure old token is expired
+// 	if isExpired := sa.TokenCache.IsExpired(); !isExpired {
+// 		t.Fatalf("token was not expired\n")
+// 	}
+
+// 	sa.TokenCache.Token = []byte("token-2")
+// 	sa.TokenCache.Expiry = time.Now().Add(24 * time.Hour)
+
+// 	sa, err = tester.repo.ServiceAccount.UpdateServiceAccountTokenCache(&sa.TokenCache)
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+// 	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
+// 	if err != nil {
+// 		t.Fatalf("%v\n", err)
+// 	}
+
+// 	// make sure id is 1
+// 	if sa.Model.ID != 1 {
+// 		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
+// 	}
+
+// 	// make sure new token is correct and not expired
+// 	if sa.TokenCache.ServiceAccountID != 1 {
+// 		t.Fatalf("incorrect service account ID in token cache: expected %d, got %d\n", 1, sa.TokenCache.ServiceAccountID)
+// 	}
+
+// 	if isExpired := sa.TokenCache.IsExpired(); isExpired {
+// 		t.Fatalf("token was expired\n")
+// 	}
+
+// 	if string(sa.TokenCache.Token) != "token-2" {
+// 		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-2", sa.TokenCache.Token)
+// 	}
+// }

+ 45 - 0
internal/repository/integrations.go

@@ -0,0 +1,45 @@
+package repository
+
+import (
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// KubeIntegrationRepository represents the set of queries on the OIDC auth
+// mechanism
+type KubeIntegrationRepository interface {
+	CreateKubeIntegration(am *ints.KubeIntegration) (*ints.KubeIntegration, error)
+	ReadKubeIntegration(id uint) (*ints.KubeIntegration, error)
+	ListKubeIntegrationsByProjectID(projectID uint) ([]*ints.KubeIntegration, error)
+}
+
+// OIDCIntegrationRepository represents the set of queries on the OIDC auth
+// mechanism
+type OIDCIntegrationRepository interface {
+	CreateOIDCIntegration(am *ints.OIDCIntegration) (*ints.OIDCIntegration, error)
+	ReadOIDCIntegration(id uint) (*ints.OIDCIntegration, error)
+	ListOIDCIntegrationsByProjectID(projectID uint) ([]*ints.OIDCIntegration, error)
+}
+
+// OAuthIntegrationRepository represents the set of queries on the oauth
+// mechanism
+type OAuthIntegrationRepository interface {
+	CreateOAuthIntegration(am *ints.OAuthIntegration) (*ints.OAuthIntegration, error)
+	ReadOAuthIntegration(id uint) (*ints.OAuthIntegration, error)
+	ListOAuthIntegrationsByProjectID(projectID uint) ([]*ints.OAuthIntegration, error)
+}
+
+// AWSIntegrationRepository represents the set of queries on the AWS auth
+// mechanism
+type AWSIntegrationRepository interface {
+	CreateAWSIntegration(am *ints.AWSIntegration) (*ints.AWSIntegration, error)
+	ReadAWSIntegration(id uint) (*ints.AWSIntegration, error)
+	ListAWSIntegrationsByProjectID(projectID uint) ([]*ints.AWSIntegration, error)
+}
+
+// GCPIntegrationRepository represents the set of queries on the GCP auth
+// mechanism
+type GCPIntegrationRepository interface {
+	CreateGCPIntegration(am *ints.GCPIntegration) (*ints.GCPIntegration, error)
+	ReadGCPIntegration(id uint) (*ints.GCPIntegration, error)
+	ListGCPIntegrationsByProjectID(projectID uint) ([]*ints.GCPIntegration, error)
+}

+ 0 - 11
internal/repository/repoclient.go

@@ -1,11 +0,0 @@
-package repository
-
-import "github.com/porter-dev/porter/internal/models"
-
-// RepoClientRepository represents the set of queries on the
-// RepoClient model
-type RepoClientRepository interface {
-	CreateRepoClient(rc *models.RepoClient) (*models.RepoClient, error)
-	ReadRepoClient(id uint) (*models.RepoClient, error)
-	ListRepoClientsByProjectID(projectID uint) ([]*models.RepoClient, error)
-}

+ 10 - 5
internal/repository/repository.go

@@ -2,9 +2,14 @@ package repository
 
 // Repository collects the repositories for each model
 type Repository struct {
-	User           UserRepository
-	Project        ProjectRepository
-	Session        SessionRepository
-	ServiceAccount ServiceAccountRepository
-	RepoClient     RepoClientRepository
+	User             UserRepository
+	Project          ProjectRepository
+	Session          SessionRepository
+	GitRepo          GitRepoRepository
+	Cluster          ClusterRepository
+	KubeIntegration  KubeIntegrationRepository
+	OIDCIntegration  OIDCIntegrationRepository
+	OAuthIntegration OAuthIntegrationRepository
+	GCPIntegration   GCPIntegrationRepository
+	AWSIntegration   AWSIntegrationRepository
 }

+ 0 - 18
internal/repository/serviceaccount.go

@@ -1,18 +0,0 @@
-package repository
-
-import (
-	"github.com/porter-dev/porter/internal/models"
-)
-
-// ServiceAccountRepository represents the set of queries on the
-// ServiceAccount model
-type ServiceAccountRepository interface {
-	CreateServiceAccountCandidate(saCandidate *models.ServiceAccountCandidate) (*models.ServiceAccountCandidate, error)
-	ReadServiceAccountCandidate(id uint) (*models.ServiceAccountCandidate, error)
-	ListServiceAccountCandidatesByProjectID(projectID uint) ([]*models.ServiceAccountCandidate, error)
-	UpdateServiceAccountCandidateCreatedSAID(id uint, createdSAID uint) (*models.ServiceAccountCandidate, error)
-	CreateServiceAccount(sa *models.ServiceAccount) (*models.ServiceAccount, error)
-	ReadServiceAccount(id uint) (*models.ServiceAccount, error)
-	ListServiceAccountsByProjectID(projectID uint) ([]*models.ServiceAccount, error)
-	UpdateServiceAccountTokenCache(tokenCache *models.TokenCache) (*models.ServiceAccount, error)
-}

+ 330 - 0
internal/repository/test/auth.go

@@ -0,0 +1,330 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// KubeIntegrationRepository implements repository.KubeIntegrationRepository
+type KubeIntegrationRepository struct {
+	canQuery         bool
+	kubeIntegrations []*ints.KubeIntegration
+}
+
+// NewKubeIntegrationRepository will return errors if canQuery is false
+func NewKubeIntegrationRepository(canQuery bool) repository.KubeIntegrationRepository {
+	return &KubeIntegrationRepository{
+		canQuery,
+		[]*ints.KubeIntegration{},
+	}
+}
+
+// CreateKubeIntegration creates a new kube auth mechanism
+func (repo *KubeIntegrationRepository) CreateKubeIntegration(
+	am *ints.KubeIntegration,
+) (*ints.KubeIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.kubeIntegrations = append(repo.kubeIntegrations, am)
+	am.ID = uint(len(repo.kubeIntegrations))
+
+	return am, nil
+}
+
+// ReadKubeIntegration finds a kube auth mechanism by id
+func (repo *KubeIntegrationRepository) ReadKubeIntegration(
+	id uint,
+) (*ints.KubeIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.kubeIntegrations) || repo.kubeIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.kubeIntegrations[index], nil
+}
+
+// ListKubeIntegrationsByProjectID finds all kube auth mechanisms
+// for a given project id
+func (repo *KubeIntegrationRepository) ListKubeIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.KubeIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.KubeIntegration, 0)
+
+	for _, kubeAM := range repo.kubeIntegrations {
+		if kubeAM.ProjectID == projectID {
+			res = append(res, kubeAM)
+		}
+	}
+
+	return res, nil
+}
+
+// OIDCIntegrationRepository implements repository.OIDCIntegrationRepository
+type OIDCIntegrationRepository struct {
+	canQuery         bool
+	oidcIntegrations []*ints.OIDCIntegration
+}
+
+// NewOIDCIntegrationRepository will return errors if canQuery is false
+func NewOIDCIntegrationRepository(canQuery bool) repository.OIDCIntegrationRepository {
+	return &OIDCIntegrationRepository{
+		canQuery,
+		[]*ints.OIDCIntegration{},
+	}
+}
+
+// CreateOIDCIntegration creates a new oidc auth mechanism
+func (repo *OIDCIntegrationRepository) CreateOIDCIntegration(
+	am *ints.OIDCIntegration,
+) (*ints.OIDCIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.oidcIntegrations = append(repo.oidcIntegrations, am)
+	am.ID = uint(len(repo.oidcIntegrations))
+
+	return am, nil
+}
+
+// ReadOIDCIntegration finds a oidc auth mechanism by id
+func (repo *OIDCIntegrationRepository) ReadOIDCIntegration(
+	id uint,
+) (*ints.OIDCIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.oidcIntegrations) || repo.oidcIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.oidcIntegrations[index], nil
+}
+
+// ListOIDCIntegrationsByProjectID finds all oidc auth mechanisms
+// for a given project id
+func (repo *OIDCIntegrationRepository) ListOIDCIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.OIDCIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.OIDCIntegration, 0)
+
+	for _, oidcAM := range repo.oidcIntegrations {
+		if oidcAM.ProjectID == projectID {
+			res = append(res, oidcAM)
+		}
+	}
+
+	return res, nil
+}
+
+// OIntegrationRepository implements repository.OIntegrationRepository
+type OIntegrationRepository struct {
+	canQuery      bool
+	oIntegrations []*ints.OIntegration
+}
+
+// NewOIntegrationRepository will return errors if canQuery is false
+func NewOIntegrationRepository(canQuery bool) repository.OIntegrationRepository {
+	return &OIntegrationRepository{
+		canQuery,
+		[]*ints.OIntegration{},
+	}
+}
+
+// CreateOIntegration creates a new o auth mechanism
+func (repo *OIntegrationRepository) CreateOIntegration(
+	am *ints.OIntegration,
+) (*ints.OIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.oIntegrations = append(repo.oIntegrations, am)
+	am.ID = uint(len(repo.oIntegrations))
+
+	return am, nil
+}
+
+// ReadOIntegration finds a o auth mechanism by id
+func (repo *OIntegrationRepository) ReadOIntegration(
+	id uint,
+) (*ints.OIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.oIntegrations) || repo.oIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.oIntegrations[index], nil
+}
+
+// ListOIntegrationsByProjectID finds all o auth mechanisms
+// for a given project id
+func (repo *OIntegrationRepository) ListOIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.OIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.OIntegration, 0)
+
+	for _, oAM := range repo.oIntegrations {
+		if oAM.ProjectID == projectID {
+			res = append(res, oAM)
+		}
+	}
+
+	return res, nil
+}
+
+// AWSIntegrationRepository implements repository.AWSIntegrationRepository
+type AWSIntegrationRepository struct {
+	canQuery        bool
+	awsIntegrations []*ints.AWSIntegration
+}
+
+// NewAWSIntegrationRepository will return errors if canQuery is false
+func NewAWSIntegrationRepository(canQuery bool) repository.AWSIntegrationRepository {
+	return &AWSIntegrationRepository{
+		canQuery,
+		[]*ints.AWSIntegration{},
+	}
+}
+
+// CreateAWSIntegration creates a new aws auth mechanism
+func (repo *AWSIntegrationRepository) CreateAWSIntegration(
+	am *ints.AWSIntegration,
+) (*ints.AWSIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.awsIntegrations = append(repo.awsIntegrations, am)
+	am.ID = uint(len(repo.awsIntegrations))
+
+	return am, nil
+}
+
+// ReadAWSIntegration finds a aws auth mechanism by id
+func (repo *AWSIntegrationRepository) ReadAWSIntegration(
+	id uint,
+) (*ints.AWSIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.awsIntegrations) || repo.awsIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.awsIntegrations[index], nil
+}
+
+// ListAWSIntegrationsByProjectID finds all aws auth mechanisms
+// for a given project id
+func (repo *AWSIntegrationRepository) ListAWSIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.AWSIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.AWSIntegration, 0)
+
+	for _, awsAM := range repo.awsIntegrations {
+		if awsAM.ProjectID == projectID {
+			res = append(res, awsAM)
+		}
+	}
+
+	return res, nil
+}
+
+// GCPIntegrationRepository implements repository.GCPIntegrationRepository
+type GCPIntegrationRepository struct {
+	canQuery        bool
+	gcpIntegrations []*ints.GCPIntegration
+}
+
+// NewGCPIntegrationRepository will return errors if canQuery is false
+func NewGCPIntegrationRepository(canQuery bool) repository.GCPIntegrationRepository {
+	return &GCPIntegrationRepository{
+		canQuery,
+		[]*ints.GCPIntegration{},
+	}
+}
+
+// CreateGCPIntegration creates a new gcp auth mechanism
+func (repo *GCPIntegrationRepository) CreateGCPIntegration(
+	am *ints.GCPIntegration,
+) (*ints.GCPIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.gcpIntegrations = append(repo.gcpIntegrations, am)
+	am.ID = uint(len(repo.gcpIntegrations))
+
+	return am, nil
+}
+
+// ReadGCPIntegration finds a gcp auth mechanism by id
+func (repo *GCPIntegrationRepository) ReadGCPIntegration(
+	id uint,
+) (*ints.GCPIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.gcpIntegrations) || repo.gcpIntegrations[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.gcpIntegrations[index], nil
+}
+
+// ListGCPIntegrationsByProjectID finds all gcp auth mechanisms
+// for a given project id
+func (repo *GCPIntegrationRepository) ListGCPIntegrationsByProjectID(
+	projectID uint,
+) ([]*ints.GCPIntegration, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*ints.GCPIntegration, 0)
+
+	for _, gcpAM := range repo.gcpIntegrations {
+		if gcpAM.ProjectID == projectID {
+			res = append(res, gcpAM)
+		}
+	}
+
+	return res, nil
+}

+ 160 - 0
internal/repository/test/cluster.go

@@ -0,0 +1,160 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+)
+
+// ClusterRepository implements repository.ClusterRepository
+type ClusterRepository struct {
+	canQuery          bool
+	clusterCandidates []*models.ClusterCandidate
+	clusters          []*models.Cluster
+}
+
+// NewClusterRepository will return errors if canQuery is false
+func NewClusterRepository(canQuery bool) repository.ClusterRepository {
+	return &ClusterRepository{
+		canQuery,
+		[]*models.ClusterCandidate{},
+		[]*models.Cluster{},
+	}
+}
+
+// CreateClusterCandidate creates a new cluster candidate
+func (repo *ClusterRepository) CreateClusterCandidate(
+	cc *models.ClusterCandidate,
+) (*models.ClusterCandidate, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.clusterCandidates = append(repo.clusterCandidates, cc)
+	cc.ID = uint(len(repo.clusterCandidates))
+
+	return cc, nil
+}
+
+// ReadClusterCandidate finds a service account candidate by id
+func (repo *ClusterRepository) ReadClusterCandidate(id uint) (*models.ClusterCandidate, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.clusterCandidates) || repo.clusterCandidates[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.clusterCandidates[index], nil
+}
+
+// ListClusterCandidatesByProjectID finds all service account candidates
+// for a given project id
+func (repo *ClusterRepository) ListClusterCandidatesByProjectID(
+	projectID uint,
+) ([]*models.ClusterCandidate, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*models.ClusterCandidate, 0)
+
+	for _, saCandidate := range repo.clusterCandidates {
+		if saCandidate.ProjectID == projectID {
+			res = append(res, saCandidate)
+		}
+	}
+
+	return res, nil
+}
+
+// UpdateClusterCandidateCreatedClusterID updates the CreatedClusterID for
+// a candidate, after the candidate has been resolved.
+func (repo *ClusterRepository) UpdateClusterCandidateCreatedClusterID(
+	id uint,
+	createdClusterID uint,
+) (*models.ClusterCandidate, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	index := int(id - 1)
+	repo.clusterCandidates[index].CreatedClusterID = createdClusterID
+
+	return repo.clusterCandidates[index], nil
+}
+
+// CreateCluster creates a new servicea account
+func (repo *ClusterRepository) CreateCluster(
+	cluster *models.Cluster,
+) (*models.Cluster, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	if cluster == nil {
+		return nil, nil
+	}
+
+	repo.clusters = append(repo.clusters, cluster)
+	cluster.ID = uint(len(repo.clusters))
+
+	return cluster, nil
+}
+
+// ReadCluster finds a service account by id
+func (repo *ClusterRepository) ReadCluster(
+	id uint,
+) (*models.Cluster, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.clusters) || repo.clusters[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.clusters[index], nil
+}
+
+// ListClustersByProjectID finds all service accounts
+// for a given project id
+func (repo *ClusterRepository) ListClustersByProjectID(
+	projectID uint,
+) ([]*models.Cluster, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*models.Cluster, 0)
+
+	for _, sa := range repo.clusters {
+		if sa.ProjectID == projectID {
+			res = append(res, sa)
+		}
+	}
+
+	return res, nil
+}
+
+// UpdateClusterTokenCache updates the token cache for a cluster
+func (repo *ClusterRepository) UpdateClusterTokenCache(
+	tokenCache *ints.TokenCache,
+) (*models.Cluster, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	index := int(tokenCache.ClusterID - 1)
+	repo.clusters[index].TokenCache.Token = tokenCache.Token
+	repo.clusters[index].TokenCache.Expiry = tokenCache.Expiry
+
+	return repo.clusters[index], nil
+}

+ 67 - 0
internal/repository/test/gitrepo.go

@@ -0,0 +1,67 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+
+	"gorm.io/gorm"
+)
+
+// GitRepoRepository implements repository.GitRepoRepository
+type GitRepoRepository struct {
+	canQuery bool
+	gitRepos []*models.GitRepo
+}
+
+// NewGitRepoRepository will return errors if canQuery is false
+func NewGitRepoRepository(canQuery bool) repository.GitRepoRepository {
+	return &GitRepoRepository{
+		canQuery,
+		[]*models.GitRepo{},
+	}
+}
+
+// CreateGitRepo creates a new repo client and appends it to the in-memory list
+func (repo *GitRepoRepository) CreateGitRepo(gr *models.GitRepo) (*models.GitRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.gitRepos = append(repo.gitRepos, gr)
+	gr.ID = uint(len(repo.gitRepos))
+
+	return gr, nil
+}
+
+// ReadGitRepo returns a repo client by id
+func (repo *GitRepoRepository) ReadGitRepo(id uint) (*models.GitRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.gitRepos) || repo.gitRepos[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.gitRepos[index], nil
+}
+
+// ListGitReposByProjectID returns a list of repo clients that match a project id
+func (repo *GitRepoRepository) ListGitReposByProjectID(projectID uint) ([]*models.GitRepo, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*models.GitRepo, 0)
+
+	for _, gr := range repo.gitRepos {
+		if gr.ProjectID == projectID {
+			res = append(res, gr)
+		}
+	}
+
+	return res, nil
+}

+ 0 - 66
internal/repository/test/repoclient.go

@@ -1,66 +0,0 @@
-package test
-
-import (
-	"errors"
-
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-// RepoClientRepository implements repository.RepoClientRepository
-type RepoClientRepository struct {
-	canQuery    bool
-	repoClients []*models.RepoClient
-}
-
-// NewRepoClientRepository will return errors if canQuery is false
-func NewRepoClientRepository(canQuery bool) repository.RepoClientRepository {
-	return &RepoClientRepository{
-		canQuery,
-		[]*models.RepoClient{},
-	}
-}
-
-// CreateRepoClient creates a new repo client and appends it to the in-memory list
-func (repo *RepoClientRepository) CreateRepoClient(rc *models.RepoClient) (*models.RepoClient, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot write database")
-	}
-
-	repo.repoClients = append(repo.repoClients, rc)
-	rc.ID = uint(len(repo.repoClients))
-
-	return rc, nil
-}
-
-// ReadRepoClient returns a repo client by id
-func (repo *RepoClientRepository) ReadRepoClient(id uint) (*models.RepoClient, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot read from database")
-	}
-
-	if int(id-1) >= len(repo.repoClients) || repo.repoClients[id-1] == nil {
-		return nil, gorm.ErrRecordNotFound
-	}
-
-	index := int(id - 1)
-	return repo.repoClients[index], nil
-}
-
-// ListRepoClientsByProjectID returns a list of repo clients that match a project id
-func (repo *RepoClientRepository) ListRepoClientsByProjectID(projectID uint) ([]*models.RepoClient, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot read from database")
-	}
-
-	res := make([]*models.RepoClient, 0)
-
-	for _, rc := range repo.repoClients {
-		if rc.ProjectID == projectID {
-			res = append(res, rc)
-		}
-	}
-
-	return res, nil
-}

+ 5 - 4
internal/repository/test/repository.go

@@ -8,9 +8,10 @@ import (
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool) *repository.Repository {
 	return &repository.Repository{
-		User:           NewUserRepository(canQuery),
-		Session:        NewSessionRepository(canQuery),
-		Project:        NewProjectRepository(canQuery),
-		ServiceAccount: NewServiceAccountRepository(canQuery),
+		User:    NewUserRepository(canQuery),
+		Session: NewSessionRepository(canQuery),
+		Project: NewProjectRepository(canQuery),
+		Cluster: NewClusterRepository(canQuery),
+		GitRepo: NewGitRepoRepository(canQuery),
 	}
 }

+ 0 - 185
internal/repository/test/serviceaccount.go

@@ -1,185 +0,0 @@
-package test
-
-import (
-	"errors"
-
-	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
-	"gorm.io/gorm"
-)
-
-// ServiceAccountRepository implements repository.ServiceAccountRepository
-type ServiceAccountRepository struct {
-	canQuery                 bool
-	serviceAccountCandidates []*models.ServiceAccountCandidate
-	serviceAccounts          []*models.ServiceAccount
-	clusters                 []*models.Cluster
-}
-
-// NewServiceAccountRepository will return errors if canQuery is false
-func NewServiceAccountRepository(canQuery bool) repository.ServiceAccountRepository {
-	return &ServiceAccountRepository{
-		canQuery,
-		[]*models.ServiceAccountCandidate{},
-		[]*models.ServiceAccount{},
-		[]*models.Cluster{},
-	}
-}
-
-// CreateServiceAccountCandidate creates a new service account candidate
-func (repo *ServiceAccountRepository) CreateServiceAccountCandidate(
-	saCandidate *models.ServiceAccountCandidate,
-) (*models.ServiceAccountCandidate, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot write database")
-	}
-
-	repo.serviceAccountCandidates = append(repo.serviceAccountCandidates, saCandidate)
-	saCandidate.ID = uint(len(repo.serviceAccountCandidates))
-
-	return saCandidate, nil
-}
-
-// ReadServiceAccountCandidate finds a service account candidate by id
-func (repo *ServiceAccountRepository) ReadServiceAccountCandidate(
-	id uint,
-) (*models.ServiceAccountCandidate, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot read from database")
-	}
-
-	if int(id-1) >= len(repo.serviceAccountCandidates) || repo.serviceAccountCandidates[id-1] == nil {
-		return nil, gorm.ErrRecordNotFound
-	}
-
-	index := int(id - 1)
-	return repo.serviceAccountCandidates[index], nil
-}
-
-// ListServiceAccountCandidatesByProjectID finds all service account candidates
-// for a given project id
-func (repo *ServiceAccountRepository) ListServiceAccountCandidatesByProjectID(
-	projectID uint,
-) ([]*models.ServiceAccountCandidate, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot read from database")
-	}
-
-	res := make([]*models.ServiceAccountCandidate, 0)
-
-	for _, saCandidate := range repo.serviceAccountCandidates {
-		if saCandidate.ProjectID == projectID {
-			res = append(res, saCandidate)
-		}
-	}
-
-	return res, nil
-}
-
-// UpdateServiceAccountCandidateCreatedSAID updates the CreatedServiceAccountID for
-// a candidate, after the candidate has been resolved.
-func (repo *ServiceAccountRepository) UpdateServiceAccountCandidateCreatedSAID(
-	id uint,
-	createdSAID uint,
-) (*models.ServiceAccountCandidate, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot write database")
-	}
-
-	index := int(id - 1)
-	repo.serviceAccountCandidates[index].CreatedServiceAccountID = createdSAID
-
-	return repo.serviceAccountCandidates[index], nil
-}
-
-// CreateServiceAccount creates a new servicea account
-func (repo *ServiceAccountRepository) CreateServiceAccount(
-	sa *models.ServiceAccount,
-) (*models.ServiceAccount, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot write database")
-	}
-
-	if sa == nil {
-		return nil, nil
-	}
-
-	repo.serviceAccounts = append(repo.serviceAccounts, sa)
-	sa.ID = uint(len(repo.serviceAccounts))
-
-	for i, cluster := range sa.Clusters {
-		(&cluster).ServiceAccountID = sa.ID
-		clusterP, _ := repo.createCluster(&cluster)
-		sa.Clusters[i] = *clusterP
-	}
-
-	return sa, nil
-}
-
-// ReadServiceAccount finds a service account by id
-func (repo *ServiceAccountRepository) ReadServiceAccount(
-	id uint,
-) (*models.ServiceAccount, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot read from database")
-	}
-
-	if int(id-1) >= len(repo.serviceAccounts) || repo.serviceAccounts[id-1] == nil {
-		return nil, gorm.ErrRecordNotFound
-	}
-
-	index := int(id - 1)
-	return repo.serviceAccounts[index], nil
-}
-
-// ListServiceAccountsByProjectID finds all service accounts
-// for a given project id
-func (repo *ServiceAccountRepository) ListServiceAccountsByProjectID(
-	projectID uint,
-) ([]*models.ServiceAccount, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot read from database")
-	}
-
-	res := make([]*models.ServiceAccount, 0)
-
-	for _, sa := range repo.serviceAccounts {
-		if sa.ProjectID == projectID {
-			res = append(res, sa)
-		}
-	}
-
-	return res, nil
-}
-
-func (repo *ServiceAccountRepository) createCluster(
-	cluster *models.Cluster,
-) (*models.Cluster, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot write database")
-	}
-
-	if cluster == nil {
-		return nil, nil
-	}
-
-	repo.clusters = append(repo.clusters, cluster)
-	cluster.ID = uint(len(repo.clusters))
-
-	return cluster, nil
-}
-
-// UpdateServiceAccountTokenCache updates the token cache for a service account
-func (repo *ServiceAccountRepository) UpdateServiceAccountTokenCache(
-	tokenCache *models.TokenCache,
-) (*models.ServiceAccount, error) {
-	if !repo.canQuery {
-		return nil, errors.New("Cannot write database")
-	}
-
-	index := int(tokenCache.ServiceAccountID - 1)
-	repo.serviceAccounts[index].TokenCache.Token = tokenCache.Token
-	repo.serviceAccounts[index].TokenCache.Expiry = tokenCache.Expiry
-
-	return repo.serviceAccounts[index], nil
-}

+ 6 - 6
server/api/project_handler_test.go

@@ -129,7 +129,7 @@ var readProjectSATest = []*projTest{
 		endpoint:  "/api/projects/1/serviceAccounts/1",
 		body:      ``,
 		expStatus: http.StatusOK,
-		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}],"auth_mechanism":"oidc"}`,
+		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}],"integration":"oidc"}`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSABodyValidator,
@@ -176,7 +176,7 @@ var createProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates",
 		body:      `{"kubeconfig":"` + OIDCAuthWithDataForJSON + `"}`,
 		expStatus: http.StatusCreated,
-		expBody:   `[{"id":1,"actions":[],"created_sa_id":1,"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","auth_mechanism":"oidc"}]`,
+		expBody:   `[{"id":1,"actions":[],"created_sa_id":1,"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","integration":"oidc"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSACandidateBodyValidator,
@@ -202,7 +202,7 @@ var createProjectSACandidatesTests = []*projTest{
 					t.Errorf("service account ID of joined cluster is not 1")
 				}
 
-				if sa.AuthMechanism != models.OIDC {
+				if sa.Integration != models.OIDC {
 					t.Errorf("service account auth mechanism is not %s\n", models.OIDC)
 				}
 
@@ -231,7 +231,7 @@ var createProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates",
 		body:      `{"kubeconfig":"` + OIDCAuthWithoutDataForJSON + `"}`,
 		expStatus: http.StatusCreated,
-		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","auth_mechanism":"oidc"}]`,
+		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","integration":"oidc"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSACandidateBodyValidator,
@@ -255,7 +255,7 @@ var listProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates",
 		body:      ``,
 		expStatus: http.StatusOK,
-		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","auth_mechanism":"oidc"}]`,
+		expBody:   `[{"id":1,"actions":[{"name":"upload-oidc-idp-issuer-ca-data","filename":"/fake/path/to/ca.pem","docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"project_id":1,"kind":"connector","context_name":"context-test","cluster_name":"cluster-test","cluster_endpoint":"https://10.10.10.10","integration":"oidc"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSACandidateBodyValidator,
@@ -279,7 +279,7 @@ var resolveProjectSACandidatesTests = []*projTest{
 		endpoint:  "/api/projects/1/candidates/1/resolve",
 		body:      `[{"name": "upload-oidc-idp-issuer-ca-data", "oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}]`,
 		expStatus: http.StatusCreated,
-		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}],"auth_mechanism":"oidc"}`,
+		expBody:   `{"id":1,"project_id":1,"kind":"connector","clusters":[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}],"integration":"oidc"}`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
 			projectSABodyValidator,