Kaynağa Gözat

integration handlers

Alexander Belanger 5 yıl önce
ebeveyn
işleme
0049bcfceb

+ 2 - 4
internal/models/cluster.go

@@ -154,7 +154,7 @@ type ClusterCandidateExternal struct {
 
 	// 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"`
+	AWSClusterIDGuess string `json:"aws_cluster_id_guess"`
 }
 
 // Externalize generates an external ClusterCandidateExternal to be shared over REST
@@ -173,7 +173,7 @@ func (cc *ClusterCandidate) Externalize() *ClusterCandidateExternal {
 		Server:            cc.Server,
 		ContextName:       cc.ContextName,
 		Resolvers:         resolvers,
-		AWSClusterIDGuess: cc.AWSClusterIDGuess,
+		AWSClusterIDGuess: string(cc.AWSClusterIDGuess),
 	}
 }
 
@@ -243,8 +243,6 @@ var ClusterResolverInfos = map[ClusterResolverName]ClusterResolverInfo{
 // 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"`

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

@@ -8,10 +8,15 @@ 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),
-		Cluster: NewClusterRepository(canQuery),
-		GitRepo: NewGitRepoRepository(canQuery),
+		User:             NewUserRepository(canQuery),
+		Session:          NewSessionRepository(canQuery),
+		Project:          NewProjectRepository(canQuery),
+		Cluster:          NewClusterRepository(canQuery),
+		GitRepo:          NewGitRepoRepository(canQuery),
+		KubeIntegration:  NewKubeIntegrationRepository(canQuery),
+		OIDCIntegration:  NewOIDCIntegrationRepository(canQuery),
+		OAuthIntegration: NewOAuthIntegrationRepository(canQuery),
+		GCPIntegration:   NewGCPIntegrationRepository(canQuery),
+		AWSIntegration:   NewAWSIntegrationRepository(canQuery),
 	}
 }

+ 6 - 19
server/api/k8s_handler.go

@@ -4,11 +4,9 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/url"
-	"time"
 
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/internal/kubernetes"
-	"github.com/porter-dev/porter/internal/models"
 
 	"github.com/gorilla/websocket"
 	"github.com/porter-dev/porter/internal/forms"
@@ -37,11 +35,11 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			UpdateTokenCache: app.updateTokenCache,
+			Repo: app.repo,
 		},
 	}
 
-	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.ServiceAccount)
+	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.Cluster)
 
 	// validate the form
 	if err := app.validator.Struct(form); err != nil {
@@ -71,17 +69,6 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (app *App) updateTokenCache(token string, expiry time.Time) error {
-	_, err := app.repo.ServiceAccount.UpdateServiceAccountTokenCache(
-		&models.TokenCache{
-			Token:  []byte(token),
-			Expiry: expiry,
-		},
-	)
-
-	return err
-}
-
 // HandleGetPodLogs returns real-time logs of the pod via websockets
 // TODO: Refactor repeated calls.
 func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
@@ -108,11 +95,11 @@ func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			UpdateTokenCache: app.updateTokenCache,
+			Repo: app.repo,
 		},
 	}
 
-	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.ServiceAccount)
+	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.Cluster)
 
 	// validate the form
 	if err := app.validator.Struct(form); err != nil {
@@ -168,11 +155,11 @@ func (app *App) HandleListPods(w http.ResponseWriter, r *http.Request) {
 	// get the filter options
 	form := &forms.K8sForm{
 		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
-			UpdateTokenCache: app.updateTokenCache,
+			Repo: app.repo,
 		},
 	}
 
-	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.ServiceAccount)
+	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.Cluster)
 
 	// validate the form
 	if err := app.validator.Struct(form); err != nil {

+ 2 - 3
server/api/k8s_handler_test.go

@@ -80,8 +80,7 @@ var listNamespacesTests = []*k8sTest{
 		msg:    "List namespaces",
 		method: "GET",
 		endpoint: "/api/projects/1/k8s/namespaces?" + url.Values{
-			"service_account_id": []string{"1"},
-			"cluster_id":         []string{"1"},
+			"cluster_id": []string{"1"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusOK,
@@ -115,7 +114,7 @@ var defaultObjects = []runtime.Object{
 func initDefaultK8s(tester *tester) {
 	initUserDefault(tester)
 	initProject(tester)
-	initProjectSADefault(tester)
+	initProjectClusterDefault(tester)
 
 	agent := kubernetes.GetAgentTesting(defaultObjects...)
 

+ 180 - 180
server/api/oauth_github_handler.go

@@ -1,182 +1,182 @@
 package api
 
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"strconv"
-
-	"github.com/porter-dev/porter/internal/models"
-
-	"github.com/go-chi/chi"
-	"github.com/google/go-github/github"
-	"github.com/porter-dev/porter/internal/oauth"
-	"golang.org/x/oauth2"
-)
-
-// HandleGithubOAuthStartUser starts the oauth2 flow for a user login request.
-func (app *App) HandleGithubOAuthStartUser(w http.ResponseWriter, r *http.Request) {
-	state := oauth.CreateRandomState()
-
-	err := app.populateOAuthSession(w, r, state, false)
-
-	if err != nil {
-		app.handleErrorDataRead(err, w)
-		return
-	}
-
-	// specify access type offline to get a refresh token
-	url := app.GithubConfig.AuthCodeURL(state, oauth2.AccessTypeOnline)
-
-	http.Redirect(w, r, url, 302)
-}
-
-// HandleGithubOAuthStartProject starts the oauth2 flow for a project repo request.
-// In this handler, the project id gets written to the session (along with the oauth
-// state param), so that the correct project id can be identified in the callback.
-func (app *App) HandleGithubOAuthStartProject(w http.ResponseWriter, r *http.Request) {
-	state := oauth.CreateRandomState()
-
-	err := app.populateOAuthSession(w, r, state, true)
-
-	if err != nil {
-		app.handleErrorDataRead(err, w)
-		return
-	}
-
-	// specify access type offline to get a refresh token
-	url := app.GithubConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
-
-	http.Redirect(w, r, url, 302)
-}
-
-// HandleGithubOAuthCallback verifies the callback request by checking that the
-// state parameter has not been modified, and validates the token.
-// There is a difference between the oauth flow when logging a user in, and when
-// linking a repository.
-//
-// When logging a user in, the access token gets stored in the session, and no refresh
-// token is requested. We store the access token in the session because a user can be
-// logged in multiple times with a single access token.
-//
-// NOTE: this user flow will likely be augmented with Dex, or entirely replaced with Dex.
-//
-// However, when linking a repository, the access token and refresh token are requested when
-// the flow has started. A project also gets linked to the session. After callback, a new
-// github config gets stored for the project, and the user will then get redirected to
-// a URL that allows them to select their repositories they'd like to link. We require a refresh
-// token because we need permanent access to the linked repository.
-func (app *App) HandleGithubOAuthCallback(w http.ResponseWriter, r *http.Request) {
-	session, err := app.store.Get(r, app.cookieName)
-
-	if err != nil {
-		app.handleErrorDataRead(err, w)
-		return
-	}
-
-	if _, ok := session.Values["state"]; !ok {
-		app.sendExternalError(
-			err,
-			http.StatusForbidden,
-			HTTPError{
-				Code: http.StatusForbidden,
-				Errors: []string{
-					"Could not read cookie: are cookies enabled?",
-				},
-			},
-			w,
-		)
-
-		return
-	}
-
-	if r.URL.Query().Get("state") != session.Values["state"] {
-		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
-		return
-	}
-
-	token, err := app.GithubConfig.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
-
-	if err != nil {
-		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
-		return
-	}
-
-	if !token.Valid() {
-		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
-		return
-	}
-
-	userID, _ := session.Values["user_id"].(uint)
-	projID, _ := session.Values["project_id"].(uint)
-
-	app.updateProjectFromToken(projID, userID, token)
-
-	if session.Values["query_params"] != "" {
-		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
-	} else {
-		http.Redirect(w, r, "/dashboard", 302)
-	}
-}
-
-func (app *App) populateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error {
-	session, err := app.store.Get(r, app.cookieName)
-
-	if err != nil {
-		return err
-	}
-
-	// need state parameter to validate when redirected
-	session.Values["state"] = state
-
-	if isProject {
-		// read the project id and add it to the session
-		projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
-
-		if err != nil || projID == 0 {
-			return fmt.Errorf("could not read project id")
-		}
-
-		session.Values["project_id"] = projID
-		session.Values["query_params"] = r.URL.RawQuery
-	}
-
-	if err := session.Save(r, w); err != nil {
-		app.logger.Warn().Err(err)
-	}
-
-	return nil
-}
-
-func (app *App) upsertUserFromToken() error {
-	return fmt.Errorf("UNIMPLEMENTED")
-}
-
-// updates a project's repository clients with the token information.
-func (app *App) updateProjectFromToken(projectID uint, userID uint, tok *oauth2.Token) error {
-	// get the list of repositories that this token has access to
-	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
-
-	user, _, err := client.Users.Get(context.Background(), "")
-
-	if err != nil {
-		return err
-	}
-
-	repoClient := &models.RepoClient{
-		ProjectID:    projectID,
-		UserID:       userID,
-		RepoUserID:   uint(user.GetID()),
-		Kind:         models.RepoClientGithub,
-		AccessToken:  []byte(tok.AccessToken),
-		RefreshToken: []byte(tok.RefreshToken),
-	}
-
-	repoClient, err = app.repo.RepoClient.CreateRepoClient(repoClient)
-
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
+// import (
+// 	"context"
+// 	"fmt"
+// 	"net/http"
+// 	"strconv"
+
+// 	"github.com/porter-dev/porter/internal/models"
+
+// 	"github.com/go-chi/chi"
+// 	"github.com/google/go-github/github"
+// 	"github.com/porter-dev/porter/internal/oauth"
+// 	"golang.org/x/oauth2"
+// )
+
+// // HandleGithubOAuthStartUser starts the oauth2 flow for a user login request.
+// func (app *App) HandleGithubOAuthStartUser(w http.ResponseWriter, r *http.Request) {
+// 	state := oauth.CreateRandomState()
+
+// 	err := app.populateOAuthSession(w, r, state, false)
+
+// 	if err != nil {
+// 		app.handleErrorDataRead(err, w)
+// 		return
+// 	}
+
+// 	// specify access type offline to get a refresh token
+// 	url := app.GithubConfig.AuthCodeURL(state, oauth2.AccessTypeOnline)
+
+// 	http.Redirect(w, r, url, 302)
+// }
+
+// // HandleGithubOAuthStartProject starts the oauth2 flow for a project repo request.
+// // In this handler, the project id gets written to the session (along with the oauth
+// // state param), so that the correct project id can be identified in the callback.
+// func (app *App) HandleGithubOAuthStartProject(w http.ResponseWriter, r *http.Request) {
+// 	state := oauth.CreateRandomState()
+
+// 	err := app.populateOAuthSession(w, r, state, true)
+
+// 	if err != nil {
+// 		app.handleErrorDataRead(err, w)
+// 		return
+// 	}
+
+// 	// specify access type offline to get a refresh token
+// 	url := app.GithubConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+// 	http.Redirect(w, r, url, 302)
+// }
+
+// // HandleGithubOAuthCallback verifies the callback request by checking that the
+// // state parameter has not been modified, and validates the token.
+// // There is a difference between the oauth flow when logging a user in, and when
+// // linking a repository.
+// //
+// // When logging a user in, the access token gets stored in the session, and no refresh
+// // token is requested. We store the access token in the session because a user can be
+// // logged in multiple times with a single access token.
+// //
+// // NOTE: this user flow will likely be augmented with Dex, or entirely replaced with Dex.
+// //
+// // However, when linking a repository, the access token and refresh token are requested when
+// // the flow has started. A project also gets linked to the session. After callback, a new
+// // github config gets stored for the project, and the user will then get redirected to
+// // a URL that allows them to select their repositories they'd like to link. We require a refresh
+// // token because we need permanent access to the linked repository.
+// func (app *App) HandleGithubOAuthCallback(w http.ResponseWriter, r *http.Request) {
+// 	session, err := app.store.Get(r, app.cookieName)
+
+// 	if err != nil {
+// 		app.handleErrorDataRead(err, w)
+// 		return
+// 	}
+
+// 	if _, ok := session.Values["state"]; !ok {
+// 		app.sendExternalError(
+// 			err,
+// 			http.StatusForbidden,
+// 			HTTPError{
+// 				Code: http.StatusForbidden,
+// 				Errors: []string{
+// 					"Could not read cookie: are cookies enabled?",
+// 				},
+// 			},
+// 			w,
+// 		)
+
+// 		return
+// 	}
+
+// 	if r.URL.Query().Get("state") != session.Values["state"] {
+// 		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+// 		return
+// 	}
+
+// 	token, err := app.GithubConfig.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
+
+// 	if err != nil {
+// 		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+// 		return
+// 	}
+
+// 	if !token.Valid() {
+// 		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+// 		return
+// 	}
+
+// 	userID, _ := session.Values["user_id"].(uint)
+// 	projID, _ := session.Values["project_id"].(uint)
+
+// 	app.updateProjectFromToken(projID, userID, token)
+
+// 	if session.Values["query_params"] != "" {
+// 		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+// 	} else {
+// 		http.Redirect(w, r, "/dashboard", 302)
+// 	}
+// }
+
+// func (app *App) populateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error {
+// 	session, err := app.store.Get(r, app.cookieName)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	// need state parameter to validate when redirected
+// 	session.Values["state"] = state
+
+// 	if isProject {
+// 		// read the project id and add it to the session
+// 		projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+// 		if err != nil || projID == 0 {
+// 			return fmt.Errorf("could not read project id")
+// 		}
+
+// 		session.Values["project_id"] = projID
+// 		session.Values["query_params"] = r.URL.RawQuery
+// 	}
+
+// 	if err := session.Save(r, w); err != nil {
+// 		app.logger.Warn().Err(err)
+// 	}
+
+// 	return nil
+// }
+
+// func (app *App) upsertUserFromToken() error {
+// 	return fmt.Errorf("UNIMPLEMENTED")
+// }
+
+// // updates a project's repository clients with the token information.
+// func (app *App) updateProjectFromToken(projectID uint, userID uint, tok *oauth2.Token) error {
+// 	// get the list of repositories that this token has access to
+// 	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
+
+// 	user, _, err := client.Users.Get(context.Background(), "")
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	repoClient := &models.RepoClient{
+// 		ProjectID:    projectID,
+// 		UserID:       userID,
+// 		RepoUserID:   uint(user.GetID()),
+// 		Kind:         models.RepoClientGithub,
+// 		AccessToken:  []byte(tok.AccessToken),
+// 		RefreshToken: []byte(tok.RefreshToken),
+// 	}
+
+// 	repoClient, err = app.repo.RepoClient.CreateRepoClient(repoClient)
+
+// 	if err != nil {
+// 		return err
+// 	}
+
+// 	return nil
+// }

+ 92 - 156
server/api/project_handler.go

@@ -110,82 +110,65 @@ func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// HandleReadProjectServiceAccount reads a service account by id
-func (app *App) HandleReadProjectServiceAccount(w http.ResponseWriter, r *http.Request) {
-	id, err := strconv.ParseUint(chi.URLParam(r, "service_account_id"), 0, 64)
+// HandleReadProjectCluster reads a cluster by id
+func (app *App) HandleReadProjectCluster(w http.ResponseWriter, r *http.Request) {
+	id, err := strconv.ParseUint(chi.URLParam(r, "cluster_id"), 0, 64)
 
 	if err != nil || id == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 
-	sa, err := app.repo.ServiceAccount.ReadServiceAccount(uint(id))
+	cluster, err := app.repo.Cluster.ReadCluster(uint(id))
 
 	if err != nil {
 		app.handleErrorRead(err, ErrProjectDataRead, w)
 		return
 	}
 
-	saExt := sa.Externalize()
+	clusterExt := cluster.Externalize()
 
 	w.WriteHeader(http.StatusOK)
 
-	if err := json.NewEncoder(w).Encode(saExt); err != nil {
+	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 }
 
-// HandleListProjectClusters returns a list of clusters that have linked ServiceAccounts.
-// If multiple service accounts exist for a cluster, the service account created later
-// will take precedence. This may be changed in a future release to return multiple
-// service accounts.
+// HandleListProjectClusters returns a list of clusters that have linked Integrations.
 func (app *App) HandleListProjectClusters(w http.ResponseWriter, r *http.Request) {
-	id, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
-	if err != nil || id == 0 {
+	if err != nil || projID == 0 {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 
-	sas, err := app.repo.ServiceAccount.ListServiceAccountsByProjectID(uint(id))
+	clusters, err := app.repo.Cluster.ListClustersByProjectID(uint(projID))
 
 	if err != nil {
 		app.handleErrorRead(err, ErrProjectDataRead, w)
 		return
 	}
 
-	clusters := make([]*models.ClusterExternal, 0)
+	extClusters := make([]*models.ClusterExternal, 0)
 
-	// clusterMapIndex used for checking if cluster has already been added
-	// maps from the cluster's endpoint to the index in the cluster array
-	clusterMapIndex := make(map[string]int)
-
-	for _, sa := range sas {
-		for _, cluster := range sa.Clusters {
-			if currIndex, ok := clusterMapIndex[cluster.Server]; ok {
-				if clusters[currIndex].ServiceAccountID <= cluster.ServiceAccountID {
-					clusters[currIndex] = cluster.Externalize()
-					continue
-				}
-			}
-
-			clusterMapIndex[cluster.Server] = len(clusters)
-			clusters = append(clusters, cluster.Externalize())
-		}
+	for _, cluster := range clusters {
+		extClusters = append(extClusters, cluster.Externalize())
 	}
 
 	w.WriteHeader(http.StatusOK)
 
-	if err := json.NewEncoder(w).Encode(clusters); err != nil {
+	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 }
 
-// HandleCreateProjectSACandidates handles the creation of ServiceAccountCandidates
-// using a kubeconfig and a project id
-func (app *App) HandleCreateProjectSACandidates(w http.ResponseWriter, r *http.Request) {
+// HandleCreateProjectClusterCandidates handles the creation of ClusterCandidates using
+// a kubeconfig and a project id
+func (app *App) HandleCreateProjectClusterCandidates(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 	if err != nil || projID == 0 {
@@ -193,7 +176,7 @@ func (app *App) HandleCreateProjectSACandidates(w http.ResponseWriter, r *http.R
 		return
 	}
 
-	form := &forms.CreateServiceAccountCandidatesForm{
+	form := &forms.CreateClusterCandidatesForm{
 		ProjectID: uint(projID),
 	}
 
@@ -209,81 +192,92 @@ func (app *App) HandleCreateProjectSACandidates(w http.ResponseWriter, r *http.R
 		return
 	}
 
-	// convert the form to a ServiceAccountCandidate
-	saCandidates, err := form.ToServiceAccountCandidates(app.isLocal)
+	// convert the form to a ClusterCandidate
+	ccs, err := form.ToClusterCandidates(app.isLocal)
 
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 
-	extSACandidates := make([]*models.ServiceAccountCandidateExternal, 0)
+	extClusters := make([]*models.ClusterCandidateExternal, 0)
 
-	for _, saCandidate := range saCandidates {
+	session, err := app.store.Get(r, app.cookieName)
+
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	userID, _ := session.Values["user_id"].(uint)
+
+	for _, cc := range ccs {
 		// handle write to the database
-		saCandidate, err = app.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+		cc, err = app.repo.Cluster.CreateClusterCandidate(cc)
 
 		if err != nil {
 			app.handleErrorDataWrite(err, w)
 			return
 		}
 
-		app.logger.Info().Msgf("New service account candidate created: %d", saCandidate.ID)
+		app.logger.Info().Msgf("New cluster candidate created: %d", cc.ID)
 
-		// if the SA candidate does not have any actions to perform, create the ServiceAccount
+		// if the ClusterCandidate does not have any actions to perform, create the Cluster
 		// automatically
-		if len(saCandidate.Actions) == 0 {
-			// we query the repo again to get the decrypted version of the SA candidate
-			saCandidate, err = app.repo.ServiceAccount.ReadServiceAccountCandidate(saCandidate.ID)
+		if len(cc.Resolvers) == 0 {
+			// we query the repo again to get the decrypted version of the cluster candidate
+			cc, err = app.repo.Cluster.ReadClusterCandidate(cc.ID)
 
 			if err != nil {
 				app.handleErrorDataRead(err, w)
 				return
 			}
 
-			saForm := &forms.ServiceAccountActionResolver{
-				ServiceAccountCandidateID: saCandidate.ID,
-				SACandidate:               saCandidate,
+			clusterForm := &forms.ResolveClusterForm{
+				Resolver:           &models.ClusterResolverAll{},
+				ClusterCandidateID: cc.ID,
+				ProjectID:          uint(projID),
+				UserID:             userID,
 			}
 
-			err := saForm.PopulateServiceAccount(app.repo.ServiceAccount)
+			err := clusterForm.ResolveIntegration(*app.repo)
 
 			if err != nil {
 				app.handleErrorDataWrite(err, w)
 				return
 			}
 
-			sa, err := app.repo.ServiceAccount.CreateServiceAccount(saForm.SA)
+			cluster, err := clusterForm.ResolveCluster(*app.repo)
 
 			if err != nil {
 				app.handleErrorDataWrite(err, w)
 				return
 			}
 
-			saCandidate, err = app.repo.ServiceAccount.UpdateServiceAccountCandidateCreatedSAID(saCandidate.ID, sa.ID)
+			cc, err = app.repo.Cluster.UpdateClusterCandidateCreatedClusterID(cc.ID, cluster.ID)
 
 			if err != nil {
 				app.handleErrorDataWrite(err, w)
 				return
 			}
 
-			app.logger.Info().Msgf("New service account created: %d", sa.ID)
+			app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
 		}
 
-		extSACandidates = append(extSACandidates, saCandidate.Externalize())
+		extClusters = append(extClusters, cc.Externalize())
 	}
 
 	w.WriteHeader(http.StatusCreated)
 
-	if err := json.NewEncoder(w).Encode(extSACandidates); err != nil {
+	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 }
 
-// HandleListProjectSACandidates returns a list of externalized ServiceAccountCandidate
-// ([]models.ServiceAccountCandidateExternal) based on a project ID
-func (app *App) HandleListProjectSACandidates(w http.ResponseWriter, r *http.Request) {
+// HandleListProjectClusterCandidates returns a list of externalized ClusterCandidates
+// ([]models.ClusterCandidateExternal) based on a project ID
+func (app *App) HandleListProjectClusterCandidates(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 	if err != nil || projID == 0 {
@@ -291,31 +285,31 @@ func (app *App) HandleListProjectSACandidates(w http.ResponseWriter, r *http.Req
 		return
 	}
 
-	saCandidates, err := app.repo.ServiceAccount.ListServiceAccountCandidatesByProjectID(uint(projID))
+	ccs, err := app.repo.Cluster.ListClusterCandidatesByProjectID(uint(projID))
 
 	if err != nil {
 		app.handleErrorRead(err, ErrProjectDataRead, w)
 		return
 	}
 
-	extSACandidates := make([]*models.ServiceAccountCandidateExternal, 0)
+	extCCs := make([]*models.ClusterCandidateExternal, 0)
 
-	for _, saCandidate := range saCandidates {
-		extSACandidates = append(extSACandidates, saCandidate.Externalize())
+	for _, cc := range ccs {
+		extCCs = append(extCCs, cc.Externalize())
 	}
 
 	w.WriteHeader(http.StatusOK)
 
-	if err := json.NewEncoder(w).Encode(extSACandidates); err != nil {
+	if err := json.NewEncoder(w).Encode(extCCs); err != nil {
 		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
 		return
 	}
 }
 
-// HandleResolveSACandidateActions accepts a list of action configurations for a
-// given ServiceAccountCandidate, which "resolves" that ServiceAccountCandidate
-// and creates a ServiceAccount for a specific project
-func (app *App) HandleResolveSACandidateActions(w http.ResponseWriter, r *http.Request) {
+// HandleResolveClusterCandidate accepts a list of resolving objects (ClusterResolver)
+// for a given ClusterCandidate, which "resolves" that ClusterCandidate and creates a
+// Cluster for a specific project
+func (app *App) HandleResolveClusterCandidate(w http.ResponseWriter, r *http.Request) {
 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
 
 	if err != nil || projID == 0 {
@@ -330,118 +324,60 @@ func (app *App) HandleResolveSACandidateActions(w http.ResponseWriter, r *http.R
 		return
 	}
 
-	// decode actions from request
-	actions := make([]*models.ServiceAccountAllActions, 0)
+	session, err := app.store.Get(r, app.cookieName)
 
-	if err := json.NewDecoder(r.Body).Decode(&actions); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 
-	var saResolverBase *forms.ServiceAccountActionResolver = &forms.ServiceAccountActionResolver{
-		ServiceAccountCandidateID: uint(candID),
-		SA:                        nil,
-		SACandidate:               nil,
-	}
-
-	// for each action, create the relevant form and populate the service account
-	// we'll chain the .PopulateServiceAccount functions
-	for _, action := range actions {
-		var err error
-		switch action.Name {
-		case models.ClusterCADataAction:
-			form := &forms.ClusterCADataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				ClusterCAData:                action.ClusterCAData,
-			}
-
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.ClusterLocalhostAction:
-			form := &forms.ClusterLocalhostAction{
-				ServiceAccountActionResolver: saResolverBase,
-				ClusterHostname:              action.ClusterHostname,
-			}
-
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.ClientCertDataAction:
-			form := &forms.ClientCertDataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				ClientCertData:               action.ClientCertData,
-			}
-
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.ClientKeyDataAction:
-			form := &forms.ClientKeyDataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				ClientKeyData:                action.ClientKeyData,
-			}
-
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.OIDCIssuerDataAction:
-			form := &forms.OIDCIssuerDataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				OIDCIssuerCAData:             action.OIDCIssuerCAData,
-			}
+	userID, _ := session.Values["user_id"].(uint)
 
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.TokenDataAction:
-			form := &forms.TokenDataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				TokenData:                    action.TokenData,
-			}
+	// decode actions from request
+	resolver := &models.ClusterResolverAll{}
 
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.GCPKeyDataAction:
-			form := &forms.GCPKeyDataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				GCPKeyData:                   action.GCPKeyData,
-			}
+	if err := json.NewDecoder(r.Body).Decode(resolver); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
 
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.AWSDataAction:
-			form := &forms.AWSDataAction{
-				ServiceAccountActionResolver: saResolverBase,
-				AWSAccessKeyID:               action.AWSAccessKeyID,
-				AWSSecretAccessKey:           action.AWSSecretAccessKey,
-				AWSClusterID:                 action.AWSClusterID,
-			}
+	clusterResolver := &forms.ResolveClusterForm{
+		Resolver:           resolver,
+		ClusterCandidateID: uint(candID),
+		ProjectID:          uint(projID),
+		UserID:             userID,
+	}
 
-			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		}
+	err = clusterResolver.ResolveIntegration(*app.repo)
 
-		if err != nil {
-			app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-			return
-		}
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
 	}
 
-	sa, err := app.repo.ServiceAccount.CreateServiceAccount(saResolverBase.SA)
+	cluster, err := clusterResolver.ResolveCluster(*app.repo)
 
 	if err != nil {
 		app.handleErrorDataWrite(err, w)
 		return
 	}
 
-	if sa != nil {
-		app.logger.Info().Msgf("New service account created: %d", sa.ID)
+	_, err = app.repo.Cluster.UpdateClusterCandidateCreatedClusterID(uint(candID), cluster.ID)
 
-		_, err := app.repo.ServiceAccount.UpdateServiceAccountCandidateCreatedSAID(uint(candID), sa.ID)
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
 
-		if err != nil {
-			app.handleErrorDataWrite(err, w)
-			return
-		}
+	app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
 
-		saExternal := sa.Externalize()
+	clusterExt := cluster.Externalize()
 
-		w.WriteHeader(http.StatusCreated)
+	w.WriteHeader(http.StatusCreated)
 
-		if err := json.NewEncoder(w).Encode(saExternal); err != nil {
-			app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-			return
-		}
-	} else {
-		w.WriteHeader(http.StatusNotModified)
+	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
 	}
 }
 

+ 96 - 99
server/api/project_handler_test.go

@@ -3,10 +3,13 @@ package api_test
 import (
 	"encoding/json"
 	"net/http"
-	"reflect"
 	"strings"
 	"testing"
 
+	"github.com/porter-dev/porter/internal/kubernetes/fixtures"
+	"github.com/porter-dev/porter/internal/models/integrations"
+	"gorm.io/gorm"
+
 	"github.com/go-test/deep"
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
@@ -117,28 +120,28 @@ func TestHandleReadProject(t *testing.T) {
 	testProjRequests(t, readProjectTests, true)
 }
 
-var readProjectSATest = []*projTest{
+var readProjectClusterTest = []*projTest{
 	&projTest{
 		initializers: []func(t *tester){
 			initUserDefault,
 			initProject,
-			initProjectSADefault,
+			initProjectClusterDefault,
 		},
-		msg:       "Read project service account",
+		msg:       "Read project cluster",
 		method:    "GET",
-		endpoint:  "/api/projects/1/serviceAccounts/1",
+		endpoint:  "/api/projects/1/clusters/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"}],"integration":"oidc"}`,
+		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10"}`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectSABodyValidator,
+			projectClusterBodyValidator,
 		},
 	},
 }
 
 func TestHandleReadProjectSA(t *testing.T) {
-	testProjRequests(t, readProjectSATest, true)
+	testProjRequests(t, readProjectClusterTest, true)
 }
 
 var listProjectClustersTest = []*projTest{
@@ -146,17 +149,17 @@ var listProjectClustersTest = []*projTest{
 		initializers: []func(t *tester){
 			initUserDefault,
 			initProject,
-			initProjectSADefault,
+			initProjectClusterDefault,
 		},
 		msg:       "List project clusters",
 		method:    "GET",
 		endpoint:  "/api/projects/1/clusters",
 		body:      ``,
 		expStatus: http.StatusOK,
-		expBody:   `[{"id":1,"service_account_id":1,"name":"cluster-test","server":"https://10.10.10.10"}]`,
+		expBody:   `[{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectClustersValidator,
+			projectClustersBodyValidator,
 		},
 	},
 }
@@ -165,58 +168,49 @@ func TestHandleListProjectClusters(t *testing.T) {
 	testProjRequests(t, listProjectClustersTest, true)
 }
 
-var createProjectSACandidatesTests = []*projTest{
+var createProjectClusterCandidatesTests = []*projTest{
 	&projTest{
 		initializers: []func(t *tester){
 			initUserDefault,
 			initProject,
 		},
-		msg:       "Create project SA candidate w/ no actions -- should create SA by default",
+		msg:       "Create project cluster candidate w/ no actions -- should create SA by default",
 		method:    "POST",
-		endpoint:  "/api/projects/1/candidates",
+		endpoint:  "/api/projects/1/clusters/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","integration":"oidc"}]`,
+		expBody:   `[{"id":1,"resolvers":[],"created_cluster_id":1,"project_id":1,"context_name":"context-test","name":"cluster-test","server":"https://10.10.10.10"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectSACandidateBodyValidator,
-			// check that ServiceAccount was created by default
+			projectClusterCandidateBodyValidator,
+			// check that Cluster was created by default
 			func(c *projTest, tester *tester, t *testing.T) {
-				serviceAccounts, err := tester.repo.ServiceAccount.ListServiceAccountsByProjectID(1)
+				clusters, err := tester.repo.Cluster.ListClustersByProjectID(1)
 
 				if err != nil {
 					t.Fatalf("%v\n", err)
 				}
 
-				if len(serviceAccounts) != 1 {
-					t.Fatal("Expected service account to be created by default, but does not exist\n")
-				}
-
-				sa := serviceAccounts[0]
-
-				if len(sa.Clusters) != 1 {
-					t.Fatalf("cluster not written\n")
-				}
-
-				if sa.Clusters[0].ServiceAccountID != 1 {
-					t.Errorf("service account ID of joined cluster is not 1")
+				if len(clusters) != 1 {
+					t.Fatal("Expected cluster to be created by default, but does not exist\n")
 				}
 
-				if sa.Integration != models.OIDC {
-					t.Errorf("service account auth mechanism is not %s\n", models.OIDC)
+				gotCluster := clusters[0]
+				gotCluster.Model = gorm.Model{}
+
+				expCluster := &models.Cluster{
+					AuthMechanism:            models.OIDC,
+					ProjectID:                1,
+					Name:                     "cluster-test",
+					Server:                   "https://10.10.10.10",
+					OIDCIntegrationID:        1,
+					TokenCache:               integrations.TokenCache{},
+					CertificateAuthorityData: []byte("-----BEGIN CER"),
 				}
 
-				if string(sa.OIDCCertificateAuthorityData) != "LS0tLS1CRUdJTiBDRVJ=" {
-					t.Errorf("service account key data and input do not match: expected %s, got %s\n",
-						string(sa.OIDCCertificateAuthorityData), "LS0tLS1CRUdJTiBDRVJ=")
-				}
-
-				if string(sa.OIDCClientID) != "porter-api" {
-					t.Errorf("service account oidc client id is not %s\n", "porter-api")
-				}
-
-				if string(sa.OIDCIDToken) != "token" {
-					t.Errorf("service account oidc id token is not %s\n", "token")
+				if diff := deep.Equal(gotCluster, expCluster); diff != nil {
+					t.Errorf("handler returned wrong body:\n")
+					t.Error(diff)
 				}
 			},
 		},
@@ -228,67 +222,67 @@ var createProjectSACandidatesTests = []*projTest{
 		},
 		msg:       "Create project SA candidate",
 		method:    "POST",
-		endpoint:  "/api/projects/1/candidates",
+		endpoint:  "/api/projects/1/clusters/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","integration":"oidc"}]`,
+		expBody:   `[{"id":1,"resolvers":[{"name":"upload-oidc-idp-issuer-ca-data","data":{"filename":"/fake/path/to/ca.pem"},"docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"created_cluster_id":0,"project_id":1,"context_name":"context-test","name":"cluster-test","server":"https://10.10.10.10"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectSACandidateBodyValidator,
+			projectClusterCandidateBodyValidator,
 		},
 	},
 }
 
-func TestHandleCreateProjectSACandidate(t *testing.T) {
-	testProjRequests(t, createProjectSACandidatesTests, true)
+func TestHandleCreateProjectClusterCandidate(t *testing.T) {
+	testProjRequests(t, createProjectClusterCandidatesTests, true)
 }
 
-var listProjectSACandidatesTests = []*projTest{
+var listProjectClusterCandidatesTests = []*projTest{
 	&projTest{
 		initializers: []func(t *tester){
 			initUserDefault,
 			initProject,
-			initProjectSACandidate,
+			initProjectClusterCandidate,
 		},
-		msg:       "List project SA candidates",
+		msg:       "List project cluster candidates",
 		method:    "GET",
-		endpoint:  "/api/projects/1/candidates",
+		endpoint:  "/api/projects/1/clusters/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","integration":"oidc"}]`,
+		expBody:   `[{"id":1,"resolvers":[{"name":"upload-oidc-idp-issuer-ca-data","data":{"filename":"/fake/path/to/ca.pem"},"docs":"https://github.com/porter-dev/porter","resolved":false,"fields":"oidc_idp_issuer_ca_data"}],"created_cluster_id":0,"project_id":1,"context_name":"context-test","name":"cluster-test","server":"https://10.10.10.10"}]`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectSACandidateBodyValidator,
+			projectClusterCandidateBodyValidator,
 		},
 	},
 }
 
-func TestHandleListProjectSACandidates(t *testing.T) {
-	testProjRequests(t, listProjectSACandidatesTests, true)
+func TestHandleListProjectClusterCandidates(t *testing.T) {
+	testProjRequests(t, listProjectClusterCandidatesTests, true)
 }
 
-var resolveProjectSACandidatesTests = []*projTest{
+var resolveProjectClusterCandidatesTests = []*projTest{
 	&projTest{
 		initializers: []func(t *tester){
 			initUserDefault,
 			initProject,
-			initProjectSACandidate,
+			initProjectClusterCandidate,
 		},
-		msg:       "Resolve project SA candidate",
+		msg:       "Resolve project cluster candidate",
 		method:    "POST",
-		endpoint:  "/api/projects/1/candidates/1/resolve",
-		body:      `[{"name": "upload-oidc-idp-issuer-ca-data", "oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}]`,
+		endpoint:  "/api/projects/1/clusters/candidates/1/resolve",
+		body:      `{"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"}],"integration":"oidc"}`,
+		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10"}`,
 		useCookie: true,
 		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectSABodyValidator,
+			projectClusterBodyValidator,
 		},
 	},
 }
 
-func TestHandleResolveProjectSACandidate(t *testing.T) {
-	testProjRequests(t, resolveProjectSACandidatesTests, true)
+func TestHandleResolveProjectClusterCandidate(t *testing.T) {
+	testProjRequests(t, resolveProjectClusterCandidatesTests, true)
 }
 
 var deleteProjectTests = []*projTest{
@@ -332,43 +326,46 @@ func initProject(tester *tester) {
 	})
 }
 
-func initProjectSACandidate(tester *tester) {
+func initProjectClusterCandidate(tester *tester) {
 	proj, _ := tester.repo.Project.ReadProject(1)
 
-	form := &forms.CreateServiceAccountCandidatesForm{
-		ProjectID:  uint(proj.ID),
-		Kubeconfig: OIDCAuthWithoutData,
+	form := &forms.CreateClusterCandidatesForm{
+		ProjectID:  proj.ID,
+		Kubeconfig: fixtures.OIDCAuthWithoutData,
 	}
 
 	// convert the form to a ServiceAccountCandidate
-	saCandidates, _ := form.ToServiceAccountCandidates(false)
+	ccs, _ := form.ToClusterCandidates(false)
 
-	for _, saCandidate := range saCandidates {
-		tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+	for _, cc := range ccs {
+		tester.repo.Cluster.CreateClusterCandidate(cc)
 	}
 }
 
-func initProjectSADefault(tester *tester) {
+func initProjectClusterDefault(tester *tester) {
 	proj, _ := tester.repo.Project.ReadProject(1)
 
-	form := &forms.CreateServiceAccountCandidatesForm{
-		ProjectID:  uint(proj.ID),
-		Kubeconfig: OIDCAuthWithData,
+	form := &forms.CreateClusterCandidatesForm{
+		ProjectID:  proj.ID,
+		Kubeconfig: fixtures.OIDCAuthWithData,
 	}
 
 	// convert the form to a ServiceAccountCandidate
-	saCandidates, _ := form.ToServiceAccountCandidates(false)
+	ccs, _ := form.ToClusterCandidates(false)
 
-	for _, saCandidate := range saCandidates {
-		tester.repo.ServiceAccount.CreateServiceAccountCandidate(saCandidate)
+	for _, cc := range ccs {
+		tester.repo.Cluster.CreateClusterCandidate(cc)
 	}
 
-	saForm := forms.ServiceAccountActionResolver{
-		ServiceAccountCandidateID: 1,
+	clusterForm := forms.ResolveClusterForm{
+		Resolver:           &models.ClusterResolverAll{},
+		ClusterCandidateID: 1,
+		ProjectID:          1,
+		UserID:             1,
 	}
 
-	saForm.PopulateServiceAccount(tester.repo.ServiceAccount)
-	tester.repo.ServiceAccount.CreateServiceAccount(saForm.SA)
+	clusterForm.ResolveIntegration(*tester.repo)
+	clusterForm.ResolveCluster(*tester.repo)
 }
 
 func projectBasicBodyValidator(c *projTest, tester *tester, t *testing.T) {
@@ -385,39 +382,39 @@ func projectModelBodyValidator(c *projTest, tester *tester, t *testing.T) {
 	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
 	json.Unmarshal([]byte(c.expBody), expBody)
 
-	if !reflect.DeepEqual(gotBody, expBody) {
-		t.Errorf("%s, handler returned wrong body: got %v want %v",
-			c.msg, gotBody, expBody)
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
 	}
 }
 
-func projectSACandidateBodyValidator(c *projTest, tester *tester, t *testing.T) {
-	gotBody := make([]*models.ServiceAccountCandidateExternal, 0)
-	expBody := make([]*models.ServiceAccountCandidateExternal, 0)
+func projectClusterCandidateBodyValidator(c *projTest, tester *tester, t *testing.T) {
+	gotBody := make([]*models.ClusterCandidateExternal, 0)
+	expBody := make([]*models.ClusterCandidateExternal, 0)
 
 	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
 	json.Unmarshal([]byte(c.expBody), &expBody)
 
-	if !reflect.DeepEqual(gotBody, expBody) {
-		t.Errorf("%s, handler returned wrong body: got %v want %v",
-			c.msg, gotBody, expBody)
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
 	}
 }
 
-func projectSABodyValidator(c *projTest, tester *tester, t *testing.T) {
-	gotBody := &models.ServiceAccountExternal{}
-	expBody := &models.ServiceAccountExternal{}
+func projectClusterBodyValidator(c *projTest, tester *tester, t *testing.T) {
+	gotBody := &models.ClusterExternal{}
+	expBody := &models.ClusterExternal{}
 
 	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
 	json.Unmarshal([]byte(c.expBody), expBody)
 
-	if !reflect.DeepEqual(gotBody, expBody) {
-		t.Errorf("%s, handler returned wrong body: got %v want %v",
-			c.msg, gotBody, expBody)
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
 	}
 }
 
-func projectClustersValidator(c *projTest, tester *tester, t *testing.T) {
+func projectClustersBodyValidator(c *projTest, tester *tester, t *testing.T) {
 	gotBody := make([]*models.ClusterExternal, 0)
 	expBody := make([]*models.ClusterExternal, 0)
 

+ 10 - 10
server/api/release_handler.go

@@ -27,7 +27,7 @@ func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 	form := &forms.ListReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				UpdateTokenCache: app.updateTokenCache,
+				Repo: app.repo,
 			},
 		},
 		ListFilter: &helm.ListFilter{},
@@ -67,7 +67,7 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				UpdateTokenCache: app.updateTokenCache,
+				Repo: app.repo,
 			},
 		},
 		Name:     name,
@@ -111,7 +111,7 @@ func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Reques
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				UpdateTokenCache: app.updateTokenCache,
+				Repo: app.repo,
 			},
 		},
 		Name:     name,
@@ -165,7 +165,7 @@ func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request)
 	form := &forms.ListReleaseHistoryForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				UpdateTokenCache: app.updateTokenCache,
+				Repo: app.repo,
 			},
 		},
 		Name: name,
@@ -214,7 +214,7 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				UpdateTokenCache: app.updateTokenCache,
+				Repo: app.repo,
 			},
 		},
 		Name: name,
@@ -222,7 +222,7 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 
 	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
 		vals,
-		app.repo.ServiceAccount,
+		app.repo.Cluster,
 	)
 
 	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
@@ -269,7 +269,7 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 	form := &forms.RollbackReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
 			Form: &helm.Form{
-				UpdateTokenCache: app.updateTokenCache,
+				Repo: app.repo,
 			},
 		},
 		Name: name,
@@ -277,7 +277,7 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 
 	form.ReleaseForm.PopulateHelmOptionsFromQueryParams(
 		vals,
-		app.repo.ServiceAccount,
+		app.repo.Cluster,
 	)
 
 	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
@@ -320,7 +320,7 @@ func (app *App) getAgentFromQueryParams(
 	r *http.Request,
 	form *forms.ReleaseForm,
 	// populate uses the query params to populate a form
-	populate ...func(vals url.Values, repo repository.ServiceAccountRepository) error,
+	populate ...func(vals url.Values, repo repository.ClusterRepository) error,
 ) (*helm.Agent, error) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 
@@ -330,7 +330,7 @@ func (app *App) getAgentFromQueryParams(
 	}
 
 	for _, f := range populate {
-		err := f(vals, app.repo.ServiceAccount)
+		err := f(vals, app.repo.Cluster)
 
 		if err != nil {
 			return nil, err

+ 36 - 70
server/api/release_handler_test.go

@@ -94,14 +94,13 @@ var listReleasesTests = []*releaseTest{
 		msg:    "List releases no namespace",
 		method: "GET",
 		endpoint: "/api/projects/1/releases?" + url.Values{
-			"namespace":          []string{""},
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
-			"storage":            []string{"memory"},
-			"limit":              []string{"20"},
-			"skip":               []string{"0"},
-			"byDate":             []string{"false"},
-			"statusFilter":       []string{"deployed"},
+			"namespace":    []string{""},
+			"cluster_id":   []string{"1"},
+			"storage":      []string{"memory"},
+			"limit":        []string{"20"},
+			"skip":         []string{"0"},
+			"byDate":       []string{"false"},
+			"statusFilter": []string{"deployed"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusOK,
@@ -119,14 +118,13 @@ var listReleasesTests = []*releaseTest{
 		method:    "GET",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases?" + url.Values{
-			"namespace":          []string{"default"},
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
-			"storage":            []string{"memory"},
-			"limit":              []string{"20"},
-			"skip":               []string{"0"},
-			"byDate":             []string{"false"},
-			"statusFilter":       []string{"deployed"},
+			"namespace":    []string{"default"},
+			"cluster_id":   []string{"1"},
+			"storage":      []string{"memory"},
+			"limit":        []string{"20"},
+			"skip":         []string{"0"},
+			"byDate":       []string{"false"},
+			"statusFilter": []string{"deployed"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusOK,
@@ -139,30 +137,6 @@ var listReleasesTests = []*releaseTest{
 			releaseReleaseArrBodyValidator,
 		},
 	},
-	&releaseTest{
-		initializers: []func(tester *tester){
-			initDefaultReleases,
-		},
-		msg:       "List releases missing required",
-		method:    "GET",
-		namespace: "default",
-		endpoint: "/api/projects/1/releases?" + url.Values{
-			"service_account_id": []string{"1"},
-			"namespace":          []string{"default"},
-			"storage":            []string{"memory"},
-			"limit":              []string{"20"},
-			"skip":               []string{"0"},
-			"byDate":             []string{"false"},
-			"statusFilter":       []string{"deployed"},
-		}.Encode(),
-		body:      "",
-		expStatus: http.StatusUnprocessableEntity,
-		expBody:   `{"code":601,"errors":["required validation failed"]}`,
-		useCookie: true,
-		validators: []func(c *releaseTest, tester *tester, t *testing.T){
-			releaseBasicBodyValidator,
-		},
-	},
 }
 
 func TestHandleListReleases(t *testing.T) {
@@ -178,10 +152,9 @@ var getReleaseTests = []*releaseTest{
 		method:    "GET",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases/airwatch/1?" + url.Values{
-			"namespace":          []string{""},
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
-			"storage":            []string{"memory"},
+			"namespace":  []string{""},
+			"cluster_id": []string{"1"},
+			"storage":    []string{"memory"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusOK,
@@ -199,10 +172,9 @@ var getReleaseTests = []*releaseTest{
 		method:    "GET",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases/airwatch/5?" + url.Values{
-			"namespace":          []string{""},
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
-			"storage":            []string{"memory"},
+			"namespace":  []string{""},
+			"cluster_id": []string{"1"},
+			"storage":    []string{"memory"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusNotFound,
@@ -227,10 +199,9 @@ var listReleaseHistoryTests = []*releaseTest{
 		method:    "GET",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases/wordpress/history?" + url.Values{
-			"namespace":          []string{""},
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
-			"storage":            []string{"memory"},
+			"namespace":  []string{""},
+			"cluster_id": []string{"1"},
+			"storage":    []string{"memory"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusOK,
@@ -248,10 +219,9 @@ var listReleaseHistoryTests = []*releaseTest{
 		method:    "GET",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases/asldfkja/history?" + url.Values{
-			"namespace":          []string{""},
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
-			"storage":            []string{"memory"},
+			"namespace":  []string{""},
+			"cluster_id": []string{"1"},
+			"storage":    []string{"memory"},
 		}.Encode(),
 		body:      "",
 		expStatus: http.StatusNotFound,
@@ -276,8 +246,7 @@ var upgradeReleaseTests = []*releaseTest{
 		method:    "POST",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases/wordpress/upgrade?" + url.Values{
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
+			"cluster_id": []string{"1"},
 		}.Encode(),
 		body: `
 			{
@@ -294,10 +263,9 @@ var upgradeReleaseTests = []*releaseTest{
 				req, err := http.NewRequest(
 					"GET",
 					"/api/projects/1/releases/wordpress/3?"+url.Values{
-						"namespace":          []string{"default"},
-						"cluster_id":         []string{"1"},
-						"service_account_id": []string{"1"},
-						"storage":            []string{"memory"},
+						"namespace":  []string{"default"},
+						"cluster_id": []string{"1"},
+						"storage":    []string{"memory"},
 					}.Encode(),
 					strings.NewReader(""),
 				)
@@ -356,8 +324,7 @@ var rollbackReleaseTests = []*releaseTest{
 		method:    "POST",
 		namespace: "default",
 		endpoint: "/api/projects/1/releases/wordpress/rollback?" + url.Values{
-			"cluster_id":         []string{"1"},
-			"service_account_id": []string{"1"},
+			"cluster_id": []string{"1"},
 		}.Encode(),
 		body: `
 			{
@@ -374,10 +341,9 @@ var rollbackReleaseTests = []*releaseTest{
 				req, err := http.NewRequest(
 					"GET",
 					"/api/projects/1/releases/wordpress/3?"+url.Values{
-						"namespace":          []string{"default"},
-						"cluster_id":         []string{"1"},
-						"service_account_id": []string{"1"},
-						"storage":            []string{"memory"},
+						"namespace":  []string{"default"},
+						"cluster_id": []string{"1"},
+						"storage":    []string{"memory"},
 					}.Encode(),
 					strings.NewReader(""),
 				)
@@ -425,7 +391,7 @@ func TestRollbackRelease(t *testing.T) {
 func initDefaultReleases(tester *tester) {
 	initUserDefault(tester)
 	initProject(tester)
-	initProjectSADefault(tester)
+	initProjectClusterDefault(tester)
 
 	agent := tester.app.TestAgents.HelmAgent
 
@@ -439,7 +405,7 @@ func initDefaultReleases(tester *tester) {
 func initHistoryReleases(tester *tester) {
 	initUserDefault(tester)
 	initProject(tester)
-	initProjectSADefault(tester)
+	initProjectClusterDefault(tester)
 
 	agent := tester.app.TestAgents.HelmAgent
 

+ 173 - 173
server/api/repo_handler.go

@@ -1,175 +1,175 @@
 package api
 
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"net/url"
-	"strconv"
-
-	"golang.org/x/oauth2"
-
-	"github.com/go-chi/chi"
-	"github.com/google/go-github/v32/github"
-)
-
-// Repo represents a GitHub or Gitab repository
-type Repo struct {
-	FullName string
-	Kind     string
-}
-
-// DirectoryItem represents a file or subfolder in a repository
-type DirectoryItem struct {
-	Path string
-	Type string
-}
-
-// HandleListRepos retrieves a list of repo names
-func (app *App) HandleListRepos(w http.ResponseWriter, r *http.Request) {
-	tok, err := app.githubTokenFromRequest(r)
-
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	res := make([]Repo, 0)
-
-	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
-
-	// list all repositories for specified user
-	repos, _, err := client.Repositories.List(context.Background(), "", nil)
-
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	// TODO -- check if repo has already been appended -- there may be duplicates
-	for _, repo := range repos {
-		res = append(res, Repo{
-			FullName: repo.GetFullName(),
-			Kind:     "github",
-		})
-	}
-
-	json.NewEncoder(w).Encode(res)
-}
-
-// HandleGetBranches retrieves a list of branch names for a specified repo
-func (app *App) HandleGetBranches(w http.ResponseWriter, r *http.Request) {
-	tok, err := app.githubTokenFromRequest(r)
-
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	name := chi.URLParam(r, "name")
-
-	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
-
-	// List all branches for a specified repo
-	branches, _, err := client.Repositories.ListBranches(context.Background(), "", name, nil)
-	if err != nil {
-		fmt.Println(err)
-		return
-	}
-
-	res := []string{}
-	for _, b := range branches {
-		res = append(res, b.GetName())
-	}
-
-	json.NewEncoder(w).Encode(res)
-}
-
-// HandleGetBranchContents retrieves the contents of a specific branch and subdirectory
-func (app *App) HandleGetBranchContents(w http.ResponseWriter, r *http.Request) {
-	tok, err := app.githubTokenFromRequest(r)
-
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
-
-	queryParams, err := url.ParseQuery(r.URL.RawQuery)
-	if err != nil {
-		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
-		return
-	}
-
-	name := chi.URLParam(r, "name")
-	branch := chi.URLParam(r, "branch")
-
-	repoContentOptions := github.RepositoryContentGetOptions{}
-	repoContentOptions.Ref = branch
-	_, directoryContents, _, err := client.Repositories.GetContents(context.Background(), "", name, queryParams["dir"][0], &repoContentOptions)
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	res := []DirectoryItem{}
-	for i := range directoryContents {
-		d := DirectoryItem{}
-		d.Path = *directoryContents[i].Path
-		d.Type = *directoryContents[i].Type
-		res = append(res, d)
-	}
-
-	// Ret2: recursively traverse all dirs to create config bundle (case on type == dir)
-	// https://api.github.com/repos/porter-dev/porter/contents?ref=frontend-graph
-	// fmt.Println(res)
-	json.NewEncoder(w).Encode(res)
-}
-
-func (app *App) githubTokenFromRequest(
-	r *http.Request,
-) (*oauth2.Token, error) {
-	// read project id
-	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
-
-	if err != nil || projID == 0 {
-		return nil, fmt.Errorf("could not read project id")
-	}
-
-	// read user id
-	session, err := app.store.Get(r, app.cookieName)
-
-	if err != nil {
-		return nil, fmt.Errorf("could not read user id")
-	}
-
-	userID, ok := session.Values["user_id"].(uint)
-
-	if !ok {
-		return nil, fmt.Errorf("could not read user id")
-	}
-
-	// query for repo client
-	repoClients, err := app.repo.RepoClient.ListRepoClientsByProjectID(uint(projID))
-
-	if err != nil {
-		return nil, err
-	}
-
-	for _, rc := range repoClients {
-		// find the RepoClient that matches the user id in the request
-		if rc.UserID == userID {
-			// TODO -- refresh token is irrelevant at the moment, because the access token
-			// doesn't expire.
-			return &oauth2.Token{
-				AccessToken:  string(rc.AccessToken),
-				RefreshToken: string(rc.RefreshToken),
-				TokenType:    "Bearer",
-			}, nil
-		}
-	}
-
-	return nil, fmt.Errorf("could not find matching token")
-}
+// import (
+// 	"context"
+// 	"encoding/json"
+// 	"fmt"
+// 	"net/http"
+// 	"net/url"
+// 	"strconv"
+
+// 	"golang.org/x/oauth2"
+
+// 	"github.com/go-chi/chi"
+// 	"github.com/google/go-github/v32/github"
+// )
+
+// // Repo represents a GitHub or Gitab repository
+// type Repo struct {
+// 	FullName string
+// 	Kind     string
+// }
+
+// // DirectoryItem represents a file or subfolder in a repository
+// type DirectoryItem struct {
+// 	Path string
+// 	Type string
+// }
+
+// // HandleListRepos retrieves a list of repo names
+// func (app *App) HandleListRepos(w http.ResponseWriter, r *http.Request) {
+// 	tok, err := app.githubTokenFromRequest(r)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	res := make([]Repo, 0)
+
+// 	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
+
+// 	// list all repositories for specified user
+// 	repos, _, err := client.Repositories.List(context.Background(), "", nil)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	// TODO -- check if repo has already been appended -- there may be duplicates
+// 	for _, repo := range repos {
+// 		res = append(res, Repo{
+// 			FullName: repo.GetFullName(),
+// 			Kind:     "github",
+// 		})
+// 	}
+
+// 	json.NewEncoder(w).Encode(res)
+// }
+
+// // HandleGetBranches retrieves a list of branch names for a specified repo
+// func (app *App) HandleGetBranches(w http.ResponseWriter, r *http.Request) {
+// 	tok, err := app.githubTokenFromRequest(r)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	name := chi.URLParam(r, "name")
+
+// 	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
+
+// 	// List all branches for a specified repo
+// 	branches, _, err := client.Repositories.ListBranches(context.Background(), "", name, nil)
+// 	if err != nil {
+// 		fmt.Println(err)
+// 		return
+// 	}
+
+// 	res := []string{}
+// 	for _, b := range branches {
+// 		res = append(res, b.GetName())
+// 	}
+
+// 	json.NewEncoder(w).Encode(res)
+// }
+
+// // HandleGetBranchContents retrieves the contents of a specific branch and subdirectory
+// func (app *App) HandleGetBranchContents(w http.ResponseWriter, r *http.Request) {
+// 	tok, err := app.githubTokenFromRequest(r)
+
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
+
+// 	queryParams, err := url.ParseQuery(r.URL.RawQuery)
+// 	if err != nil {
+// 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+// 		return
+// 	}
+
+// 	name := chi.URLParam(r, "name")
+// 	branch := chi.URLParam(r, "branch")
+
+// 	repoContentOptions := github.RepositoryContentGetOptions{}
+// 	repoContentOptions.Ref = branch
+// 	_, directoryContents, _, err := client.Repositories.GetContents(context.Background(), "", name, queryParams["dir"][0], &repoContentOptions)
+// 	if err != nil {
+// 		app.handleErrorInternal(err, w)
+// 		return
+// 	}
+
+// 	res := []DirectoryItem{}
+// 	for i := range directoryContents {
+// 		d := DirectoryItem{}
+// 		d.Path = *directoryContents[i].Path
+// 		d.Type = *directoryContents[i].Type
+// 		res = append(res, d)
+// 	}
+
+// 	// Ret2: recursively traverse all dirs to create config bundle (case on type == dir)
+// 	// https://api.github.com/repos/porter-dev/porter/contents?ref=frontend-graph
+// 	// fmt.Println(res)
+// 	json.NewEncoder(w).Encode(res)
+// }
+
+// func (app *App) githubTokenFromRequest(
+// 	r *http.Request,
+// ) (*oauth2.Token, error) {
+// 	// read project id
+// 	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+// 	if err != nil || projID == 0 {
+// 		return nil, fmt.Errorf("could not read project id")
+// 	}
+
+// 	// read user id
+// 	session, err := app.store.Get(r, app.cookieName)
+
+// 	if err != nil {
+// 		return nil, fmt.Errorf("could not read user id")
+// 	}
+
+// 	userID, ok := session.Values["user_id"].(uint)
+
+// 	if !ok {
+// 		return nil, fmt.Errorf("could not read user id")
+// 	}
+
+// 	// query for repo client
+// 	gitRepos, err := app.repo.GitRepo.ListGitReposByProjectID(uint(projID))
+
+// 	if err != nil {
+// 		return nil, err
+// 	}
+
+// 	for _, rc := range repoClients {
+// 		// find the RepoClient that matches the user id in the request
+// 		if rc.UserID == userID {
+// 			// TODO -- refresh token is irrelevant at the moment, because the access token
+// 			// doesn't expire.
+// 			return &oauth2.Token{
+// 				AccessToken:  string(rc.AccessToken),
+// 				RefreshToken: string(rc.RefreshToken),
+// 				TokenType:    "Bearer",
+// 			}, nil
+// 		}
+// 	}
+
+// 	return nil, fmt.Errorf("could not find matching token")
+// }

+ 21 - 22
server/api/repo_handler_test.go

@@ -3,7 +3,6 @@ package api_test
 import (
 	"encoding/json"
 	"net/http"
-	"net/url"
 	"strings"
 	"testing"
 
@@ -68,27 +67,27 @@ func testReposRequests(t *testing.T, tests []*reposTest, canQuery bool) {
 
 // ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
 
-var listReposTests = []*reposTest{
-	&reposTest{
-		initializers: []func(tester *tester){
-			initDefaultRepos,
-		},
-		msg:       "List repos",
-		method:    "GET",
-		endpoint:  "/api/repos/github/porter/master/contents?dir=" + url.QueryEscape("./"),
-		body:      "",
-		expStatus: http.StatusOK,
-		expBody:   "unimplemented",
-		useCookie: true,
-		validators: []func(c *reposTest, tester *tester, t *testing.T){
-			reposListValidator,
-		},
-	},
-}
-
-func TestHandleListRepos(t *testing.T) {
-	testReposRequests(t, listReposTests, true)
-}
+// var listReposTests = []*reposTest{
+// 	&reposTest{
+// 		initializers: []func(tester *tester){
+// 			initDefaultRepos,
+// 		},
+// 		msg:       "List repos",
+// 		method:    "GET",
+// 		endpoint:  "/api/repos/github/porter/master/contents?dir=" + url.QueryEscape("./"),
+// 		body:      "",
+// 		expStatus: http.StatusOK,
+// 		expBody:   "unimplemented",
+// 		useCookie: true,
+// 		validators: []func(c *reposTest, tester *tester, t *testing.T){
+// 			reposListValidator,
+// 		},
+// 	},
+// }
+
+// func TestHandleListRepos(t *testing.T) {
+// 	testReposRequests(t, listReposTests, true)
+// }
 
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 

+ 21 - 21
server/api/template_handler_test.go

@@ -67,27 +67,27 @@ func testTemplatesRequests(t *testing.T, tests []*templatesTest, canQuery bool)
 
 // ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
 
-var listTemplatesTests = []*templatesTest{
-	&templatesTest{
-		initializers: []func(tester *tester){
-			initDefaultTemplates,
-		},
-		msg:       "List templates",
-		method:    "GET",
-		endpoint:  "/api/templates",
-		body:      "",
-		expStatus: http.StatusOK,
-		expBody:   "unimplemented",
-		useCookie: true,
-		validators: []func(c *templatesTest, tester *tester, t *testing.T){
-			templatesListValidator,
-		},
-	},
-}
-
-func TestHandleListTemplates(t *testing.T) {
-	testTemplatesRequests(t, listTemplatesTests, true)
-}
+// var listTemplatesTests = []*templatesTest{
+// 	&templatesTest{
+// 		initializers: []func(tester *tester){
+// 			initDefaultTemplates,
+// 		},
+// 		msg:       "List templates",
+// 		method:    "GET",
+// 		endpoint:  "/api/templates",
+// 		body:      "",
+// 		expStatus: http.StatusOK,
+// 		expBody:   "unimplemented",
+// 		useCookie: true,
+// 		validators: []func(c *templatesTest, tester *tester, t *testing.T){
+// 			templatesListValidator,
+// 		},
+// 	},
+// }
+
+// func TestHandleListTemplates(t *testing.T) {
+// 	testTemplatesRequests(t, listTemplatesTests, true)
+// }
 
 // ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
 

+ 0 - 13
server/api/user_handler_test.go

@@ -507,19 +507,6 @@ func userModelBodyValidator(c *userTest, tester *tester, t *testing.T) {
 	}
 }
 
-func userContextBodyValidator(c *userTest, tester *tester, t *testing.T) {
-	gotBody := &[]models.Context{}
-	expBody := &[]models.Context{}
-
-	json.Unmarshal(tester.rr.Body.Bytes(), gotBody)
-	json.Unmarshal([]byte(c.expBody), expBody)
-
-	if !reflect.DeepEqual(gotBody, expBody) {
-		t.Errorf("%s, handler returned wrong body: got %v want %v",
-			c.msg, gotBody, expBody)
-	}
-}
-
 func userProjectsListValidator(c *userTest, tester *tester, t *testing.T) {
 	gotBody := make([]*models.ProjectExternal, 0)
 	expBody := make([]*models.ProjectExternal, 0)

+ 21 - 21
server/router/middleware/auth.go

@@ -66,8 +66,8 @@ type bodyProjectID struct {
 	ProjectID uint64 `json:"project_id"`
 }
 
-type bodyServiceAccountID struct {
-	ServiceAccountID uint64 `json:"service_account_id"`
+type bodyClusterID struct {
+	ClusterID uint64 `json:"cluster_id"`
 }
 
 // DoesUserIDMatch checks the id URL parameter and verifies that it matches
@@ -156,16 +156,16 @@ func (auth *Auth) DoesUserHaveProjectAccess(
 	})
 }
 
-// DoesUserHaveServiceAccountAccess looks for a project_id parameter and a
-// service_account_id parameter, and verifies that the service account belongs
+// DoesUserHaveClusterAccess looks for a project_id parameter and a
+// cluster_id parameter, and verifies that the cluster belongs
 // to the project
-func (auth *Auth) DoesUserHaveServiceAccountAccess(
+func (auth *Auth) DoesUserHaveClusterAccess(
 	next http.Handler,
 	projLoc IDLocation,
-	saLoc IDLocation,
+	clusterLoc IDLocation,
 ) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		serviceAccountID, err := findServiceAccountIDInRequest(r, saLoc)
+		clusterID, err := findClusterIDInRequest(r, clusterLoc)
 
 		if err != nil {
 			http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
@@ -180,7 +180,7 @@ func (auth *Auth) DoesUserHaveServiceAccountAccess(
 		}
 
 		// get the service accounts belonging to the project
-		serviceAccounts, err := auth.repo.ServiceAccount.ListServiceAccountsByProjectID(uint(projID))
+		clusters, err := auth.repo.Cluster.ListClustersByProjectID(uint(projID))
 
 		if err != nil {
 			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -189,8 +189,8 @@ func (auth *Auth) DoesUserHaveServiceAccountAccess(
 
 		doesExist := false
 
-		for _, sa := range serviceAccounts {
-			if sa.ID == uint(serviceAccountID) {
+		for _, cluster := range clusters {
+			if cluster.ID == uint(clusterID) {
 				doesExist = true
 				break
 			}
@@ -323,18 +323,18 @@ func findProjIDInRequest(r *http.Request, projLoc IDLocation) (uint64, error) {
 	return projID, nil
 }
 
-func findServiceAccountIDInRequest(r *http.Request, saLoc IDLocation) (uint64, error) {
-	var saID uint64
+func findClusterIDInRequest(r *http.Request, clusterLoc IDLocation) (uint64, error) {
+	var clusterID uint64
 	var err error
 
-	if saLoc == URLParam {
-		saID, err = strconv.ParseUint(chi.URLParam(r, "service_account_id"), 0, 64)
+	if clusterLoc == URLParam {
+		clusterID, err = strconv.ParseUint(chi.URLParam(r, "cluster_id"), 0, 64)
 
 		if err != nil {
 			return 0, err
 		}
-	} else if saLoc == BodyParam {
-		form := &bodyServiceAccountID{}
+	} else if clusterLoc == BodyParam {
+		form := &bodyClusterID{}
 		body, err := ioutil.ReadAll(r.Body)
 
 		if err != nil {
@@ -347,7 +347,7 @@ func findServiceAccountIDInRequest(r *http.Request, saLoc IDLocation) (uint64, e
 			return 0, err
 		}
 
-		saID = form.ServiceAccountID
+		clusterID = form.ClusterID
 
 		// need to create a new stream for the body
 		r.Body = ioutil.NopCloser(bytes.NewReader(body))
@@ -358,12 +358,12 @@ func findServiceAccountIDInRequest(r *http.Request, saLoc IDLocation) (uint64, e
 			return 0, err
 		}
 
-		if saStrArr, ok := vals["service_account_id"]; ok && len(saStrArr) == 1 {
-			saID, err = strconv.ParseUint(saStrArr[0], 10, 64)
+		if clStrArr, ok := vals["cluster_id"]; ok && len(clStrArr) == 1 {
+			clusterID, err = strconv.ParseUint(clStrArr[0], 10, 64)
 		} else {
-			return 0, errors.New("service account id not found")
+			return 0, errors.New("cluster id not found")
 		}
 	}
 
-	return saID, nil
+	return clusterID, nil
 }

+ 62 - 62
server/router/router.go

@@ -41,21 +41,21 @@ func New(
 		r.Method("POST", "/logout", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleLogoutUser, l)))
 
 		// /api/oauth routes
-		r.Method(
-			"GET",
-			"/oauth/projects/{project_id}/github",
-			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleGithubOAuthStartProject, l),
-				mw.URLParam,
-				mw.WriteAccess,
-			),
-		)
-
-		r.Method(
-			"GET",
-			"/oauth/github/callback",
-			requestlog.NewHandler(a.HandleGithubOAuthCallback, l),
-		)
+		// r.Method(
+		// 	"GET",
+		// 	"/oauth/projects/{project_id}/github",
+		// 	auth.DoesUserHaveProjectAccess(
+		// 		requestlog.NewHandler(a.HandleGithubOAuthStartProject, l),
+		// 		mw.URLParam,
+		// 		mw.WriteAccess,
+		// 	),
+		// )
+
+		// r.Method(
+		// 	"GET",
+		// 	"/oauth/github/callback",
+		// 	requestlog.NewHandler(a.HandleGithubOAuthCallback, l),
+		// )
 
 		// /api/projects routes
 		r.Method(
@@ -70,10 +70,10 @@ func New(
 
 		r.Method(
 			"GET",
-			"/projects/{project_id}/serviceAccounts/{service_account_id}",
+			"/projects/{project_id}/clusters/{cluster_id}",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
-					requestlog.NewHandler(a.HandleReadProjectServiceAccount, l),
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleReadProjectCluster, l),
 					mw.URLParam,
 					mw.URLParam,
 				),
@@ -96,9 +96,9 @@ func New(
 
 		r.Method(
 			"POST",
-			"/projects/{project_id}/candidates",
+			"/projects/{project_id}/clusters/candidates",
 			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleCreateProjectSACandidates, l),
+				requestlog.NewHandler(a.HandleCreateProjectClusterCandidates, l),
 				mw.URLParam,
 				mw.WriteAccess,
 			),
@@ -106,9 +106,9 @@ func New(
 
 		r.Method(
 			"GET",
-			"/projects/{project_id}/candidates",
+			"/projects/{project_id}/clusters/candidates",
 			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleListProjectSACandidates, l),
+				requestlog.NewHandler(a.HandleListProjectClusterCandidates, l),
 				mw.URLParam,
 				mw.WriteAccess,
 			),
@@ -116,9 +116,9 @@ func New(
 
 		r.Method(
 			"POST",
-			"/projects/{project_id}/candidates/{candidate_id}/resolve",
+			"/projects/{project_id}/clusters/candidates/{candidate_id}/resolve",
 			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleResolveSACandidateActions, l),
+				requestlog.NewHandler(a.HandleResolveClusterCandidate, l),
 				mw.URLParam,
 				mw.WriteAccess,
 			),
@@ -139,7 +139,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/releases",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleListReleases, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -153,7 +153,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/releases/{name}/{revision}/components",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleGetReleaseComponents, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -167,7 +167,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/releases/{name}/history",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleListReleaseHistory, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -181,7 +181,7 @@ func New(
 			"POST",
 			"/projects/{project_id}/releases/{name}/upgrade",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleUpgradeRelease, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -195,7 +195,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/releases/{name}/{revision}",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleGetRelease, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -209,7 +209,7 @@ func New(
 			"POST",
 			"/projects/{project_id}/releases/{name}/rollback",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleRollbackRelease, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -220,35 +220,35 @@ func New(
 		)
 
 		// /api/projects/{project_id}/repos routes
-		r.Method(
-			"GET",
-			"/projects/{project_id}/repos",
-			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleListRepos, l),
-				mw.URLParam,
-				mw.ReadAccess,
-			),
-		)
-
-		r.Method(
-			"GET",
-			"/projects/{project_id}/repos/{kind}/{name}/branches",
-			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleGetBranches, l),
-				mw.URLParam,
-				mw.ReadAccess,
-			),
-		)
-
-		r.Method(
-			"GET",
-			"/projects/{project_id}/repos/{kind}/{name}/{branch}/contents",
-			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleGetBranchContents, l),
-				mw.URLParam,
-				mw.ReadAccess,
-			),
-		)
+		// r.Method(
+		// 	"GET",
+		// 	"/projects/{project_id}/repos",
+		// 	auth.DoesUserHaveProjectAccess(
+		// 		requestlog.NewHandler(a.HandleListRepos, l),
+		// 		mw.URLParam,
+		// 		mw.ReadAccess,
+		// 	),
+		// )
+
+		// r.Method(
+		// 	"GET",
+		// 	"/projects/{project_id}/repos/{kind}/{name}/branches",
+		// 	auth.DoesUserHaveProjectAccess(
+		// 		requestlog.NewHandler(a.HandleGetBranches, l),
+		// 		mw.URLParam,
+		// 		mw.ReadAccess,
+		// 	),
+		// )
+
+		// r.Method(
+		// 	"GET",
+		// 	"/projects/{project_id}/repos/{kind}/{name}/{branch}/contents",
+		// 	auth.DoesUserHaveProjectAccess(
+		// 		requestlog.NewHandler(a.HandleGetBranchContents, l),
+		// 		mw.URLParam,
+		// 		mw.ReadAccess,
+		// 	),
+		// )
 
 		// /api/templates routes
 		r.Method(
@@ -264,7 +264,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/k8s/namespaces",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleListNamespaces, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -278,7 +278,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/k8s/{namespace}/pod/{name}/logs",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleGetPodLogs, l),
 					mw.URLParam,
 					mw.QueryParam,
@@ -292,7 +292,7 @@ func New(
 			"GET",
 			"/projects/{project_id}/k8s/pods",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveServiceAccountAccess(
+				auth.DoesUserHaveClusterAccess(
 					requestlog.NewHandler(a.HandleListPods, l),
 					mw.URLParam,
 					mw.QueryParam,