Przeglądaj źródła

create cluster without kubeconfig

Alexander Belanger 5 lat temu
rodzic
commit
f87a98257f

+ 55 - 0
internal/forms/cluster.go

@@ -3,7 +3,9 @@ package forms
 import (
 	"encoding/base64"
 	"errors"
+	"fmt"
 	"net/url"
+	"regexp"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
@@ -14,6 +16,59 @@ import (
 	ints "github.com/porter-dev/porter/internal/models/integrations"
 )
 
+// CreateClusterForm represents the accepted values for creating a
+// cluster through manual configuration (not through a kubeconfig)
+type CreateClusterForm struct {
+	Name      string `json:"name" form:"required"`
+	ProjectID uint   `json:"project_id" form:"required"`
+	Server    string `json:"server" form:"required"`
+
+	GCPIntegrationID uint `json:"gcp_integration_id"`
+	AWSIntegrationID uint `json:"aws_integration_id"`
+
+	CertificateAuthorityData string `json:"certificate_authority_data,omitempty"`
+}
+
+// ToCluster converts the form to a cluster
+func (ccf *CreateClusterForm) ToCluster() (*models.Cluster, error) {
+	var authMechanism models.ClusterAuth
+
+	if ccf.GCPIntegrationID != 0 {
+		authMechanism = models.GCP
+	} else if ccf.AWSIntegrationID != 0 {
+		authMechanism = models.AWS
+	} else {
+		return nil, fmt.Errorf("must include aws or gcp integration id")
+	}
+
+	cert := make([]byte, 0)
+
+	if ccf.CertificateAuthorityData != "" {
+		// determine if data is base64 decoded using regex
+		re := regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
+
+		// if it matches the base64 regex, decode it
+		if re.MatchString(ccf.CertificateAuthorityData) {
+			decoded, err := base64.StdEncoding.DecodeString(ccf.CertificateAuthorityData)
+
+			if err != nil {
+				return nil, err
+			}
+
+			cert = []byte(decoded)
+		}
+	}
+
+	return &models.Cluster{
+		AuthMechanism:            authMechanism,
+		Name:                     ccf.Name,
+		Server:                   ccf.Server,
+		GCPIntegrationID:         ccf.GCPIntegrationID,
+		AWSIntegrationID:         ccf.AWSIntegrationID,
+		CertificateAuthorityData: cert,
+	}, nil
+}
+
 // ResolveClusterForm will resolve a cluster candidate and create a new cluster
 type ResolveClusterForm struct {
 	Resolver *models.ClusterResolverAll `form:"required"`

+ 6 - 6
internal/models/cluster.go

@@ -13,12 +13,12 @@ type ClusterAuth string
 // The support cluster candidate auth mechanisms
 const (
 	X509   ClusterAuth = "x509"
-	Basic              = "basic"
-	Bearer             = "bearerToken"
-	OIDC               = "oidc"
-	GCP                = "gcp-sa"
-	AWS                = "aws-sa"
-	Local              = "local"
+	Basic  ClusterAuth = "basic"
+	Bearer ClusterAuth = "bearerToken"
+	OIDC   ClusterAuth = "oidc"
+	GCP    ClusterAuth = "gcp-sa"
+	AWS    ClusterAuth = "aws-sa"
+	Local  ClusterAuth = "local"
 )
 
 // Cluster is an integration that can connect to a Kubernetes cluster via

+ 335 - 0
server/api/cluster_handler.go

@@ -0,0 +1,335 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/forms"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// HandleCreateProjectCluster creates a new cluster
+func (app *App) HandleCreateProjectCluster(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateClusterForm{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a registry
+	cluster, err := form.ToCluster()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	cluster, err = app.repo.Cluster.CreateCluster(cluster)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
+
+	w.WriteHeader(http.StatusCreated)
+
+	clusterExt := cluster.Externalize()
+
+	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// 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
+	}
+
+	cluster, err := app.repo.Cluster.ReadCluster(uint(id))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	clusterExt := cluster.Externalize()
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// HandleListProjectClusters returns a list of clusters that have linked Integrations.
+func (app *App) HandleListProjectClusters(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	clusters, err := app.repo.Cluster.ListClustersByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	extClusters := make([]*models.ClusterExternal, 0)
+
+	for _, cluster := range clusters {
+		extClusters = append(extClusters, cluster.Externalize())
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// 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 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateClusterCandidatesForm{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to a ClusterCandidate
+	ccs, err := form.ToClusterCandidates(app.isLocal)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	extClusters := make([]*models.ClusterCandidateExternal, 0)
+
+	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
+		cc, err = app.repo.Cluster.CreateClusterCandidate(cc)
+
+		if err != nil {
+			app.handleErrorDataWrite(err, w)
+			return
+		}
+
+		app.logger.Info().Msgf("New cluster candidate created: %d", cc.ID)
+
+		// if the ClusterCandidate does not have any actions to perform, create the Cluster
+		// automatically
+		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
+			}
+
+			clusterForm := &forms.ResolveClusterForm{
+				Resolver:           &models.ClusterResolverAll{},
+				ClusterCandidateID: cc.ID,
+				ProjectID:          uint(projID),
+				UserID:             userID,
+			}
+
+			err := clusterForm.ResolveIntegration(*app.repo)
+
+			if err != nil {
+				app.handleErrorDataWrite(err, w)
+				return
+			}
+
+			cluster, err := clusterForm.ResolveCluster(*app.repo)
+
+			if err != nil {
+				app.handleErrorDataWrite(err, w)
+				return
+			}
+
+			cc, err = app.repo.Cluster.UpdateClusterCandidateCreatedClusterID(cc.ID, cluster.ID)
+
+			if err != nil {
+				app.handleErrorDataWrite(err, w)
+				return
+			}
+
+			app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
+		}
+
+		extClusters = append(extClusters, cc.Externalize())
+	}
+
+	w.WriteHeader(http.StatusCreated)
+
+	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// 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 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	ccs, err := app.repo.Cluster.ListClusterCandidatesByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	extCCs := make([]*models.ClusterCandidateExternal, 0)
+
+	for _, cc := range ccs {
+		extCCs = append(extCCs, cc.Externalize())
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(extCCs); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
+// 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 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	candID, err := strconv.ParseUint(chi.URLParam(r, "candidate_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	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)
+
+	// decode actions from request
+	resolver := &models.ClusterResolverAll{}
+
+	if err := json.NewDecoder(r.Body).Decode(resolver); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	clusterResolver := &forms.ResolveClusterForm{
+		Resolver:           resolver,
+		ClusterCandidateID: uint(candID),
+		ProjectID:          uint(projID),
+		UserID:             userID,
+	}
+
+	err = clusterResolver.ResolveIntegration(*app.repo)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	cluster, err := clusterResolver.ResolveCluster(*app.repo)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	_, err = app.repo.Cluster.UpdateClusterCandidateCreatedClusterID(uint(candID), cluster.ID)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
+
+	clusterExt := cluster.Externalize()
+
+	w.WriteHeader(http.StatusCreated)
+
+	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 403 - 0
server/api/cluster_handler_test.go

@@ -0,0 +1,403 @@
+package api_test
+
+import (
+	"encoding/json"
+	"net/http"
+	"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"
+)
+
+// ------------------------- TEST TYPES AND MAIN LOOP ------------------------- //
+
+type clusterTest struct {
+	initializers []func(t *tester)
+	msg          string
+	method       string
+	endpoint     string
+	body         string
+	expStatus    int
+	expBody      string
+	useCookie    bool
+	validators   []func(c *clusterTest, tester *tester, t *testing.T)
+}
+
+func testClusterRequests(t *testing.T, tests []*clusterTest, canQuery bool) {
+	for _, c := range tests {
+		// create a new tester
+		tester := newTester(canQuery)
+
+		// if there's an initializer, call it
+		for _, init := range c.initializers {
+			init(tester)
+		}
+
+		req, err := http.NewRequest(
+			c.method,
+			c.endpoint,
+			strings.NewReader(c.body),
+		)
+
+		tester.req = req
+
+		if c.useCookie {
+			req.AddCookie(tester.cookie)
+		}
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		tester.execute()
+		rr := tester.rr
+
+		// first, check that the status matches
+		if status := rr.Code; status != c.expStatus {
+			t.Errorf("%s, handler returned wrong status code: got %v want %v",
+				c.msg, status, c.expStatus)
+		}
+
+		// if there's a validator, call it
+		for _, validate := range c.validators {
+			validate(c, tester, t)
+		}
+	}
+}
+
+// ------------------------- TEST FIXTURES AND FUNCTIONS  ------------------------- //
+
+var createClusterTests = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:       "Create cluster",
+		method:    "POST",
+		endpoint:  "/api/projects/1/clusters",
+		body:      `{"name":"cluster-test","server":"https://10.10.10.10:6443","aws_integration_id":1}`,
+		expStatus: http.StatusCreated,
+		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10:6443","service":"eks"}`,
+		useCookie: true,
+		validators: []func(c *clusterTest, tester *tester, t *testing.T){
+			projectClusterBodyValidator,
+		},
+	},
+}
+
+func TestHandleCreateCluster(t *testing.T) {
+	testRegistryRequests(t, createRegistryTests, true)
+}
+
+var readProjectClusterTest = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initProjectClusterDefault,
+		},
+		msg:       "Read project cluster",
+		method:    "GET",
+		endpoint:  "/api/projects/1/clusters/1",
+		body:      ``,
+		expStatus: http.StatusOK,
+		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10","service":"kube"}`,
+		useCookie: true,
+		validators: []func(c *clusterTest, tester *tester, t *testing.T){
+			projectClusterBodyValidator,
+		},
+	},
+}
+
+func TestHandleReadProjectSA(t *testing.T) {
+	testClusterRequests(t, readProjectClusterTest, true)
+}
+
+var listProjectClustersTest = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initProjectClusterDefault,
+		},
+		msg:       "List project clusters",
+		method:    "GET",
+		endpoint:  "/api/projects/1/clusters",
+		body:      ``,
+		expStatus: http.StatusOK,
+		expBody:   `[{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10","service":"kube"}]`,
+		useCookie: true,
+		validators: []func(c *clusterTest, tester *tester, t *testing.T){
+			projectClustersBodyValidator,
+		},
+	},
+}
+
+func TestHandleListProjectClusters(t *testing.T) {
+	testClusterRequests(t, listProjectClustersTest, true)
+}
+
+var createProjectClusterCandidatesTests = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:       "Create project cluster candidate w/ no actions -- should create SA by default",
+		method:    "POST",
+		endpoint:  "/api/projects/1/clusters/candidates",
+		body:      `{"kubeconfig":"` + OIDCAuthWithDataForJSON + `"}`,
+		expStatus: http.StatusCreated,
+		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 *clusterTest, tester *tester, t *testing.T){
+			projectClusterCandidateBodyValidator,
+			// check that Cluster was created by default
+			func(c *clusterTest, tester *tester, t *testing.T) {
+				clusters, err := tester.repo.Cluster.ListClustersByProjectID(1)
+
+				if err != nil {
+					t.Fatalf("%v\n", err)
+				}
+
+				if len(clusters) != 1 {
+					t.Fatal("Expected cluster to be created by default, but does not exist\n")
+				}
+
+				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 diff := deep.Equal(gotCluster, expCluster); diff != nil {
+					t.Errorf("handler returned wrong body:\n")
+					t.Error(diff)
+				}
+			},
+		},
+	},
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+		},
+		msg:       "Create project SA candidate",
+		method:    "POST",
+		endpoint:  "/api/projects/1/clusters/candidates",
+		body:      `{"kubeconfig":"` + OIDCAuthWithoutDataForJSON + `"}`,
+		expStatus: http.StatusCreated,
+		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 *clusterTest, tester *tester, t *testing.T){
+			projectClusterCandidateBodyValidator,
+		},
+	},
+}
+
+func TestHandleCreateProjectClusterCandidate(t *testing.T) {
+	testClusterRequests(t, createProjectClusterCandidatesTests, true)
+}
+
+var listProjectClusterCandidatesTests = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initProjectClusterCandidate,
+		},
+		msg:       "List project cluster candidates",
+		method:    "GET",
+		endpoint:  "/api/projects/1/clusters/candidates",
+		body:      ``,
+		expStatus: http.StatusOK,
+		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 *clusterTest, tester *tester, t *testing.T){
+			projectClusterCandidateBodyValidator,
+		},
+	},
+}
+
+func TestHandleListProjectClusterCandidates(t *testing.T) {
+	testClusterRequests(t, listProjectClusterCandidatesTests, true)
+}
+
+var resolveProjectClusterCandidatesTests = []*clusterTest{
+	&clusterTest{
+		initializers: []func(t *tester){
+			initUserDefault,
+			initProject,
+			initProjectClusterCandidate,
+		},
+		msg:       "Resolve project cluster candidate",
+		method:    "POST",
+		endpoint:  "/api/projects/1/clusters/candidates/1/resolve",
+		body:      `{"oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}`,
+		expStatus: http.StatusCreated,
+		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10","service":"kube"}`,
+		useCookie: true,
+		validators: []func(c *clusterTest, tester *tester, t *testing.T){
+			projectClusterBodyValidator,
+		},
+	},
+}
+
+func TestHandleResolveProjectClusterCandidate(t *testing.T) {
+	testClusterRequests(t, resolveProjectClusterCandidatesTests, true)
+}
+
+// ------------------------- INITIALIZERS AND VALIDATORS ------------------------- //
+
+func initProjectClusterCandidate(tester *tester) {
+	proj, _ := tester.repo.Project.ReadProject(1)
+
+	form := &forms.CreateClusterCandidatesForm{
+		ProjectID:  proj.ID,
+		Kubeconfig: fixtures.OIDCAuthWithoutData,
+	}
+
+	// convert the form to a ServiceAccountCandidate
+	ccs, _ := form.ToClusterCandidates(false)
+
+	for _, cc := range ccs {
+		tester.repo.Cluster.CreateClusterCandidate(cc)
+	}
+}
+
+func initProjectClusterDefault(tester *tester) {
+	proj, _ := tester.repo.Project.ReadProject(1)
+
+	form := &forms.CreateClusterCandidatesForm{
+		ProjectID:  proj.ID,
+		Kubeconfig: fixtures.OIDCAuthWithData,
+	}
+
+	// convert the form to a ServiceAccountCandidate
+	ccs, _ := form.ToClusterCandidates(false)
+
+	for _, cc := range ccs {
+		tester.repo.Cluster.CreateClusterCandidate(cc)
+	}
+
+	clusterForm := forms.ResolveClusterForm{
+		Resolver:           &models.ClusterResolverAll{},
+		ClusterCandidateID: 1,
+		ProjectID:          1,
+		UserID:             1,
+	}
+
+	clusterForm.ResolveIntegration(*tester.repo)
+	clusterForm.ResolveCluster(*tester.repo)
+}
+
+func projectClusterCandidateBodyValidator(c *clusterTest, 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 diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
+}
+
+func projectClusterBodyValidator(c *clusterTest, 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 diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
+}
+
+func projectClustersBodyValidator(c *clusterTest, tester *tester, t *testing.T) {
+	gotBody := make([]*models.ClusterExternal, 0)
+	expBody := make([]*models.ClusterExternal, 0)
+
+	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
+	json.Unmarshal([]byte(c.expBody), &expBody)
+
+	if diff := deep.Equal(gotBody, expBody); diff != nil {
+		t.Errorf("handler returned wrong body:\n")
+		t.Error(diff)
+	}
+}
+
+const OIDCAuthWithDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://10.10.10.10\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://10.10.10.10\n        idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n      name: oidc`
+
+const OIDCAuthWithoutDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://10.10.10.10\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://10.10.10.10\n        idp-certificate-authority: /fake/path/to/ca.pem\n      name: oidc`
+
+const OIDCAuthWithoutData string = `
+apiVersion: v1
+clusters:
+- cluster:
+    server: https://10.10.10.10
+    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
+  name: cluster-test
+contexts:
+- context:
+    cluster: cluster-test
+    user: test-admin
+  name: context-test
+current-context: context-test
+kind: Config
+preferences: {}
+users:
+- name: test-admin
+  user:
+    auth-provider:
+      config:
+        client-id: porter-api
+        id-token: token
+        idp-issuer-url: https://10.10.10.10
+        idp-certificate-authority: /fake/path/to/ca.pem
+      name: oidc
+`
+
+const OIDCAuthWithData string = `
+apiVersion: v1
+clusters:
+- cluster:
+    server: https://10.10.10.10
+    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
+  name: cluster-test
+contexts:
+- context:
+    cluster: cluster-test
+    user: test-admin
+  name: context-test
+current-context: context-test
+kind: Config
+preferences: {}
+users:
+- name: test-admin
+  user:
+    auth-provider:
+      config:
+        client-id: porter-api
+        id-token: token
+        idp-issuer-url: https://10.10.10.10
+        idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
+      name: oidc
+`

+ 0 - 271
server/api/project_handler.go

@@ -110,277 +110,6 @@ func (app *App) HandleReadProject(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// 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
-	}
-
-	cluster, err := app.repo.Cluster.ReadCluster(uint(id))
-
-	if err != nil {
-		app.handleErrorRead(err, ErrProjectDataRead, w)
-		return
-	}
-
-	clusterExt := cluster.Externalize()
-
-	w.WriteHeader(http.StatusOK)
-
-	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-}
-
-// HandleListProjectClusters returns a list of clusters that have linked Integrations.
-func (app *App) HandleListProjectClusters(w http.ResponseWriter, r *http.Request) {
-	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
-
-	if err != nil || projID == 0 {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	clusters, err := app.repo.Cluster.ListClustersByProjectID(uint(projID))
-
-	if err != nil {
-		app.handleErrorRead(err, ErrProjectDataRead, w)
-		return
-	}
-
-	extClusters := make([]*models.ClusterExternal, 0)
-
-	for _, cluster := range clusters {
-		extClusters = append(extClusters, cluster.Externalize())
-	}
-
-	w.WriteHeader(http.StatusOK)
-
-	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-}
-
-// 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 {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	form := &forms.CreateClusterCandidatesForm{
-		ProjectID: uint(projID),
-	}
-
-	// decode from JSON to form value
-	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	// validate the form
-	if err := app.validator.Struct(form); err != nil {
-		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
-		return
-	}
-
-	// convert the form to a ClusterCandidate
-	ccs, err := form.ToClusterCandidates(app.isLocal)
-
-	if err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	extClusters := make([]*models.ClusterCandidateExternal, 0)
-
-	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
-		cc, err = app.repo.Cluster.CreateClusterCandidate(cc)
-
-		if err != nil {
-			app.handleErrorDataWrite(err, w)
-			return
-		}
-
-		app.logger.Info().Msgf("New cluster candidate created: %d", cc.ID)
-
-		// if the ClusterCandidate does not have any actions to perform, create the Cluster
-		// automatically
-		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
-			}
-
-			clusterForm := &forms.ResolveClusterForm{
-				Resolver:           &models.ClusterResolverAll{},
-				ClusterCandidateID: cc.ID,
-				ProjectID:          uint(projID),
-				UserID:             userID,
-			}
-
-			err := clusterForm.ResolveIntegration(*app.repo)
-
-			if err != nil {
-				app.handleErrorDataWrite(err, w)
-				return
-			}
-
-			cluster, err := clusterForm.ResolveCluster(*app.repo)
-
-			if err != nil {
-				app.handleErrorDataWrite(err, w)
-				return
-			}
-
-			cc, err = app.repo.Cluster.UpdateClusterCandidateCreatedClusterID(cc.ID, cluster.ID)
-
-			if err != nil {
-				app.handleErrorDataWrite(err, w)
-				return
-			}
-
-			app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
-		}
-
-		extClusters = append(extClusters, cc.Externalize())
-	}
-
-	w.WriteHeader(http.StatusCreated)
-
-	if err := json.NewEncoder(w).Encode(extClusters); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-}
-
-// 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 {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	ccs, err := app.repo.Cluster.ListClusterCandidatesByProjectID(uint(projID))
-
-	if err != nil {
-		app.handleErrorRead(err, ErrProjectDataRead, w)
-		return
-	}
-
-	extCCs := make([]*models.ClusterCandidateExternal, 0)
-
-	for _, cc := range ccs {
-		extCCs = append(extCCs, cc.Externalize())
-	}
-
-	w.WriteHeader(http.StatusOK)
-
-	if err := json.NewEncoder(w).Encode(extCCs); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-}
-
-// 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 {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	candID, err := strconv.ParseUint(chi.URLParam(r, "candidate_id"), 0, 64)
-
-	if err != nil || projID == 0 {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	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)
-
-	// decode actions from request
-	resolver := &models.ClusterResolverAll{}
-
-	if err := json.NewDecoder(r.Body).Decode(resolver); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-
-	clusterResolver := &forms.ResolveClusterForm{
-		Resolver:           resolver,
-		ClusterCandidateID: uint(candID),
-		ProjectID:          uint(projID),
-		UserID:             userID,
-	}
-
-	err = clusterResolver.ResolveIntegration(*app.repo)
-
-	if err != nil {
-		app.handleErrorDataWrite(err, w)
-		return
-	}
-
-	cluster, err := clusterResolver.ResolveCluster(*app.repo)
-
-	if err != nil {
-		app.handleErrorDataWrite(err, w)
-		return
-	}
-
-	_, err = app.repo.Cluster.UpdateClusterCandidateCreatedClusterID(uint(candID), cluster.ID)
-
-	if err != nil {
-		app.handleErrorDataWrite(err, w)
-		return
-	}
-
-	app.logger.Info().Msgf("New cluster created: %d", cluster.ID)
-
-	clusterExt := cluster.Externalize()
-
-	w.WriteHeader(http.StatusCreated)
-
-	if err := json.NewEncoder(w).Encode(clusterExt); err != nil {
-		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
-		return
-	}
-}
-
 // HandleDeleteProject deletes a project from the db, reading from the project_id
 // in the URL param
 func (app *App) HandleDeleteProject(w http.ResponseWriter, r *http.Request) {

+ 0 - 309
server/api/project_handler_test.go

@@ -6,12 +6,7 @@ import (
 	"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"
 )
 
@@ -120,171 +115,6 @@ func TestHandleReadProject(t *testing.T) {
 	testProjRequests(t, readProjectTests, true)
 }
 
-var readProjectClusterTest = []*projTest{
-	&projTest{
-		initializers: []func(t *tester){
-			initUserDefault,
-			initProject,
-			initProjectClusterDefault,
-		},
-		msg:       "Read project cluster",
-		method:    "GET",
-		endpoint:  "/api/projects/1/clusters/1",
-		body:      ``,
-		expStatus: http.StatusOK,
-		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10","service":"kube"}`,
-		useCookie: true,
-		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectClusterBodyValidator,
-		},
-	},
-}
-
-func TestHandleReadProjectSA(t *testing.T) {
-	testProjRequests(t, readProjectClusterTest, true)
-}
-
-var listProjectClustersTest = []*projTest{
-	&projTest{
-		initializers: []func(t *tester){
-			initUserDefault,
-			initProject,
-			initProjectClusterDefault,
-		},
-		msg:       "List project clusters",
-		method:    "GET",
-		endpoint:  "/api/projects/1/clusters",
-		body:      ``,
-		expStatus: http.StatusOK,
-		expBody:   `[{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10","service":"kube"}]`,
-		useCookie: true,
-		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectClustersBodyValidator,
-		},
-	},
-}
-
-func TestHandleListProjectClusters(t *testing.T) {
-	testProjRequests(t, listProjectClustersTest, true)
-}
-
-var createProjectClusterCandidatesTests = []*projTest{
-	&projTest{
-		initializers: []func(t *tester){
-			initUserDefault,
-			initProject,
-		},
-		msg:       "Create project cluster candidate w/ no actions -- should create SA by default",
-		method:    "POST",
-		endpoint:  "/api/projects/1/clusters/candidates",
-		body:      `{"kubeconfig":"` + OIDCAuthWithDataForJSON + `"}`,
-		expStatus: http.StatusCreated,
-		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){
-			projectClusterCandidateBodyValidator,
-			// check that Cluster was created by default
-			func(c *projTest, tester *tester, t *testing.T) {
-				clusters, err := tester.repo.Cluster.ListClustersByProjectID(1)
-
-				if err != nil {
-					t.Fatalf("%v\n", err)
-				}
-
-				if len(clusters) != 1 {
-					t.Fatal("Expected cluster to be created by default, but does not exist\n")
-				}
-
-				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 diff := deep.Equal(gotCluster, expCluster); diff != nil {
-					t.Errorf("handler returned wrong body:\n")
-					t.Error(diff)
-				}
-			},
-		},
-	},
-	&projTest{
-		initializers: []func(t *tester){
-			initUserDefault,
-			initProject,
-		},
-		msg:       "Create project SA candidate",
-		method:    "POST",
-		endpoint:  "/api/projects/1/clusters/candidates",
-		body:      `{"kubeconfig":"` + OIDCAuthWithoutDataForJSON + `"}`,
-		expStatus: http.StatusCreated,
-		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){
-			projectClusterCandidateBodyValidator,
-		},
-	},
-}
-
-func TestHandleCreateProjectClusterCandidate(t *testing.T) {
-	testProjRequests(t, createProjectClusterCandidatesTests, true)
-}
-
-var listProjectClusterCandidatesTests = []*projTest{
-	&projTest{
-		initializers: []func(t *tester){
-			initUserDefault,
-			initProject,
-			initProjectClusterCandidate,
-		},
-		msg:       "List project cluster candidates",
-		method:    "GET",
-		endpoint:  "/api/projects/1/clusters/candidates",
-		body:      ``,
-		expStatus: http.StatusOK,
-		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){
-			projectClusterCandidateBodyValidator,
-		},
-	},
-}
-
-func TestHandleListProjectClusterCandidates(t *testing.T) {
-	testProjRequests(t, listProjectClusterCandidatesTests, true)
-}
-
-var resolveProjectClusterCandidatesTests = []*projTest{
-	&projTest{
-		initializers: []func(t *tester){
-			initUserDefault,
-			initProject,
-			initProjectClusterCandidate,
-		},
-		msg:       "Resolve project cluster candidate",
-		method:    "POST",
-		endpoint:  "/api/projects/1/clusters/candidates/1/resolve",
-		body:      `{"oidc_idp_issuer_ca_data": "LS0tLS1CRUdJTiBDRVJ="}`,
-		expStatus: http.StatusCreated,
-		expBody:   `{"id":1,"project_id":1,"name":"cluster-test","server":"https://10.10.10.10","service":"kube"}`,
-		useCookie: true,
-		validators: []func(c *projTest, tester *tester, t *testing.T){
-			projectClusterBodyValidator,
-		},
-	},
-}
-
-func TestHandleResolveProjectClusterCandidate(t *testing.T) {
-	testProjRequests(t, resolveProjectClusterCandidatesTests, true)
-}
-
 var deleteProjectTests = []*projTest{
 	&projTest{
 		initializers: []func(t *tester){
@@ -326,48 +156,6 @@ func initProject(tester *tester) {
 	})
 }
 
-func initProjectClusterCandidate(tester *tester) {
-	proj, _ := tester.repo.Project.ReadProject(1)
-
-	form := &forms.CreateClusterCandidatesForm{
-		ProjectID:  proj.ID,
-		Kubeconfig: fixtures.OIDCAuthWithoutData,
-	}
-
-	// convert the form to a ServiceAccountCandidate
-	ccs, _ := form.ToClusterCandidates(false)
-
-	for _, cc := range ccs {
-		tester.repo.Cluster.CreateClusterCandidate(cc)
-	}
-}
-
-func initProjectClusterDefault(tester *tester) {
-	proj, _ := tester.repo.Project.ReadProject(1)
-
-	form := &forms.CreateClusterCandidatesForm{
-		ProjectID:  proj.ID,
-		Kubeconfig: fixtures.OIDCAuthWithData,
-	}
-
-	// convert the form to a ServiceAccountCandidate
-	ccs, _ := form.ToClusterCandidates(false)
-
-	for _, cc := range ccs {
-		tester.repo.Cluster.CreateClusterCandidate(cc)
-	}
-
-	clusterForm := forms.ResolveClusterForm{
-		Resolver:           &models.ClusterResolverAll{},
-		ClusterCandidateID: 1,
-		ProjectID:          1,
-		UserID:             1,
-	}
-
-	clusterForm.ResolveIntegration(*tester.repo)
-	clusterForm.ResolveCluster(*tester.repo)
-}
-
 func projectBasicBodyValidator(c *projTest, tester *tester, t *testing.T) {
 	if body := tester.rr.Body.String(); strings.TrimSpace(body) != strings.TrimSpace(c.expBody) {
 		t.Errorf("%s, handler returned wrong body: got %v want %v",
@@ -387,100 +175,3 @@ func projectModelBodyValidator(c *projTest, tester *tester, t *testing.T) {
 		t.Error(diff)
 	}
 }
-
-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 diff := deep.Equal(gotBody, expBody); diff != nil {
-		t.Errorf("handler returned wrong body:\n")
-		t.Error(diff)
-	}
-}
-
-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 diff := deep.Equal(gotBody, expBody); diff != nil {
-		t.Errorf("handler returned wrong body:\n")
-		t.Error(diff)
-	}
-}
-
-func projectClustersBodyValidator(c *projTest, tester *tester, t *testing.T) {
-	gotBody := make([]*models.ClusterExternal, 0)
-	expBody := make([]*models.ClusterExternal, 0)
-
-	json.Unmarshal(tester.rr.Body.Bytes(), &gotBody)
-	json.Unmarshal([]byte(c.expBody), &expBody)
-
-	if diff := deep.Equal(gotBody, expBody); diff != nil {
-		t.Errorf("handler returned wrong body:\n")
-		t.Error(diff)
-	}
-}
-
-const OIDCAuthWithDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://10.10.10.10\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://10.10.10.10\n        idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n      name: oidc`
-
-const OIDCAuthWithoutDataForJSON string = `apiVersion: v1\nclusters:\n- cluster:\n    server: https://10.10.10.10\n    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=\n  name: cluster-test\ncontexts:\n- context:\n    cluster: cluster-test\n    user: test-admin\n  name: context-test\ncurrent-context: context-test\nkind: Config\npreferences: {}\nusers:\n- name: test-admin\n  user:\n    auth-provider:\n      config:\n        client-id: porter-api\n        id-token: token\n        idp-issuer-url: https://10.10.10.10\n        idp-certificate-authority: /fake/path/to/ca.pem\n      name: oidc`
-
-const OIDCAuthWithoutData string = `
-apiVersion: v1
-clusters:
-- cluster:
-    server: https://10.10.10.10
-    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
-  name: cluster-test
-contexts:
-- context:
-    cluster: cluster-test
-    user: test-admin
-  name: context-test
-current-context: context-test
-kind: Config
-preferences: {}
-users:
-- name: test-admin
-  user:
-    auth-provider:
-      config:
-        client-id: porter-api
-        id-token: token
-        idp-issuer-url: https://10.10.10.10
-        idp-certificate-authority: /fake/path/to/ca.pem
-      name: oidc
-`
-
-const OIDCAuthWithData string = `
-apiVersion: v1
-clusters:
-- cluster:
-    server: https://10.10.10.10
-    certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
-  name: cluster-test
-contexts:
-- context:
-    cluster: cluster-test
-    user: test-admin
-  name: context-test
-current-context: context-test
-kind: Config
-preferences: {}
-users:
-- name: test-admin
-  user:
-    auth-provider:
-      config:
-        client-id: porter-api
-        id-token: token
-        idp-issuer-url: https://10.10.10.10
-        idp-certificate-authority-data: LS0tLS1CRUdJTiBDRVJ=
-      name: oidc
-`

+ 103 - 37
server/router/router.go

@@ -32,15 +32,62 @@ func New(
 		r.Method("GET", "/readyz", http.HandlerFunc(a.HandleReady))
 
 		// /api/users routes
-		r.Method("GET", "/users/{user_id}", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleReadUser, l), mw.URLParam))
-		r.Method("GET", "/users/{user_id}/projects", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleListUserProjects, l), mw.URLParam))
-		r.Method("POST", "/users", requestlog.NewHandler(a.HandleCreateUser, l))
-		r.Method("DELETE", "/users/{user_id}", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleDeleteUser, l), mw.URLParam))
-		r.Method("POST", "/login", requestlog.NewHandler(a.HandleLoginUser, l))
-		r.Method("GET", "/auth/check", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleAuthCheck, l)))
-		r.Method("POST", "/logout", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleLogoutUser, l)))
+		r.Method(
+			"GET",
+			"/users/{user_id}",
+			auth.DoesUserIDMatch(
+				requestlog.NewHandler(a.HandleReadUser, l),
+				mw.URLParam,
+			),
+		)
 
-		// /integrations routes
+		r.Method(
+			"GET",
+			"/users/{user_id}/projects",
+			auth.DoesUserIDMatch(
+				requestlog.NewHandler(a.HandleListUserProjects, l),
+				mw.URLParam,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/users",
+			requestlog.NewHandler(a.HandleCreateUser, l),
+		)
+
+		r.Method(
+			"DELETE",
+			"/users/{user_id}",
+			auth.DoesUserIDMatch(
+				requestlog.NewHandler(a.HandleDeleteUser, l),
+				mw.URLParam,
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/login",
+			requestlog.NewHandler(a.HandleLoginUser, l),
+		)
+
+		r.Method(
+			"GET",
+			"/auth/check",
+			auth.BasicAuthenticate(
+				requestlog.NewHandler(a.HandleAuthCheck, l),
+			),
+		)
+
+		r.Method(
+			"POST",
+			"/logout",
+			auth.BasicAuthenticate(
+				requestlog.NewHandler(a.HandleLogoutUser, l),
+			),
+		)
+
+		// /api/integrations routes
 		r.Method(
 			"GET",
 			"/integrations/cluster",
@@ -65,6 +112,15 @@ func New(
 			),
 		)
 
+		// /api/templates routes
+		r.Method(
+			"GET",
+			"/templates",
+			auth.BasicAuthenticate(
+				requestlog.NewHandler(a.HandleListTemplates, l),
+			),
+		)
+
 		// /api/oauth routes
 		// r.Method(
 		// 	"GET",
@@ -94,19 +150,24 @@ func New(
 		)
 
 		r.Method(
-			"GET",
-			"/projects/{project_id}/clusters/{cluster_id}",
+			"POST",
+			"/projects",
+			auth.BasicAuthenticate(
+				requestlog.NewHandler(a.HandleCreateProject, l),
+			),
+		)
+
+		r.Method(
+			"DELETE",
+			"/projects/{project_id}",
 			auth.DoesUserHaveProjectAccess(
-				auth.DoesUserHaveClusterAccess(
-					requestlog.NewHandler(a.HandleReadProjectCluster, l),
-					mw.URLParam,
-					mw.URLParam,
-				),
+				requestlog.NewHandler(a.HandleDeleteProject, l),
 				mw.URLParam,
-				mw.ReadAccess,
+				mw.WriteAccess,
 			),
 		)
 
+		// /api/projects/{project_id}/clusters routes
 		r.Method(
 			"GET",
 			"/projects/{project_id}/clusters",
@@ -117,8 +178,31 @@ func New(
 			),
 		)
 
-		r.Method("POST", "/projects", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleCreateProject, l)))
+		r.Method(
+			"POST",
+			"/projects/{project_id}/clusters",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleCreateProjectCluster, l),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		r.Method(
+			"GET",
+			"/projects/{project_id}/clusters/{cluster_id}",
+			auth.DoesUserHaveProjectAccess(
+				auth.DoesUserHaveClusterAccess(
+					requestlog.NewHandler(a.HandleReadProjectCluster, l),
+					mw.URLParam,
+					mw.URLParam,
+				),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
 
+		// /api/projects/{project_id}/clusters/candidates routes
 		r.Method(
 			"POST",
 			"/projects/{project_id}/clusters/candidates",
@@ -149,16 +233,6 @@ func New(
 			),
 		)
 
-		r.Method(
-			"DELETE",
-			"/projects/{project_id}",
-			auth.DoesUserHaveProjectAccess(
-				requestlog.NewHandler(a.HandleDeleteProject, l),
-				mw.URLParam,
-				mw.WriteAccess,
-			),
-		)
-
 		// /api/projects/{project_id}/integrations routes
 		r.Method(
 			"POST",
@@ -201,6 +275,7 @@ func New(
 			),
 		)
 
+		// /api/projects/{project_id}/registries/{registry_id}/repositories routes
 		r.Method(
 			"GET",
 			"/projects/{project_id}/registries/{registry_id}/repositories",
@@ -362,7 +437,7 @@ func New(
 		// 	),
 		// )
 
-		// /api/projects/{project_id}/images routes
+		// /api/projects/{project_id}/deploy route
 		r.Method(
 			"POST",
 			"/projects/{project_id}/deploy",
@@ -377,15 +452,6 @@ func New(
 			),
 		)
 
-		// /api/templates routes
-		r.Method(
-			"GET",
-			"/templates",
-			auth.BasicAuthenticate(
-				requestlog.NewHandler(a.HandleListTemplates, l),
-			),
-		)
-
 		// /api/projects/{project_id}/k8s routes
 		r.Method(
 			"GET",