Browse Source

e2e flow working, need to revamp new cluster creation for duplicates

Stefan McShane 3 years ago
parent
commit
007701441d

+ 63 - 19
api/server/handlers/project/create_cluster.go

@@ -1,6 +1,8 @@
 package project
 
 import (
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"net/http"
 
@@ -11,6 +13,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -29,20 +32,53 @@ func NewProvisionClusterHandler(
 }
 
 // ServeHTTP creates a CAPI cluster by adding the configuration to a NATS stream
+// This inserts a row into the cluster table in order to create an ID for this cluster, as well as stores the raw request JSON for updating later
 func (c *CreateClusterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	if !c.Config().DisableCAPIProvisioner {
-		// TODO: delete this block after April 2023. It is only required whilst people are not easily able to get NATS and Cluster Control Plane running on their local environment
-		w.WriteHeader(http.StatusCreated)
-		return
-	}
-
 	var capiClusterReq types.CAPIClusterRequest
 	ctx := r.Context()
 
 	if ok := c.DecodeAndValidate(w, r, &capiClusterReq); !ok {
 		return
 	}
-	c.Config().Repo.Cluster()
+
+	if capiClusterReq.ClusterID == 0 {
+		dbCluster := models.Cluster{
+			ProjectID:                         uint(capiClusterReq.ProjectID),
+			Status:                            types.UpdatingUnavailable,
+			ProvisionedBy:                     "CAPI",
+			CloudProvider:                     "AWS",
+			CloudProviderCredentialIdentifier: capiClusterReq.CloudProviderCredentialsID,
+			Name:                              capiClusterReq.ClusterSettings.ClusterName,
+		}
+		cl, err := c.Config().Repo.Cluster().CreateCluster(&dbCluster)
+		if err != nil {
+			e := fmt.Errorf("error creating new cluster: %w", err)
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			return
+		}
+		capiClusterReq.ClusterID = int64(cl.ID)
+	}
+
+	by, err := json.Marshal(capiClusterReq)
+	if err != nil {
+		e := fmt.Errorf("error marshalling capi config: %w", err)
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+		return
+	}
+	b64 := base64.StdEncoding.EncodeToString(by)
+
+	capiConfig := models.CAPIConfig{
+		ClusterID:         int(capiClusterReq.ClusterID),
+		ProjectID:         int(capiClusterReq.ProjectID),
+		Base64RequestJSON: string(b64),
+	}
+
+	_, err = c.Config().Repo.CAPIConfigRepository().Insert(ctx, capiConfig)
+	if err != nil {
+		e := fmt.Errorf("error creating new capi config: %w", err)
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+		return
+	}
 
 	capiCluster := porterv1.Kubernetes{
 		ProjectId: int32(capiClusterReq.ProjectID),
@@ -75,22 +111,30 @@ func (c *CreateClusterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		}
 	}
 
-	by, err := proto.Marshal(&capiCluster)
-	if err != nil {
-		e := fmt.Errorf("error marshalling proto: %w", err)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
-		return
-	}
+	// This gates the cluster actually being provisioned by CAPI
+	// This can be removed whenever we are able to run NATS and CCP locally, easier
+	if !c.Config().DisableCAPIProvisioner {
+		kubeBy, err := proto.Marshal(&capiCluster)
+		if err != nil {
+			e := fmt.Errorf("error marshalling proto: %w", err)
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			return
+		}
 
-	subject := "porter.system.infrastructure.update"
-	_, err = c.Config().NATS.JetStream.Publish(subject, by, nats.Context(ctx))
-	if err != nil {
-		e := fmt.Errorf("error publishing cluster for creation: %w", err)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
-		return
+		subject := "porter.system.infrastructure.update"
+		_, err = c.Config().NATS.JetStream.Publish(subject, kubeBy, nats.Context(ctx))
+		if err != nil {
+			e := fmt.Errorf("error publishing cluster for creation: %w", err)
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			return
+		}
 	}
 
 	w.WriteHeader(http.StatusCreated)
+	c.WriteResult(w, r, types.Cluster{
+		ID: uint(capiClusterReq.ClusterID),
+	})
+
 }
 
 var (

+ 21 - 2
api/server/handlers/project_integration/create_aws.go

@@ -1,8 +1,11 @@
 package project_integration
 
 import (
+	"fmt"
 	"net/http"
 
+	"github.com/bufbuild/connect-go"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
@@ -29,9 +32,9 @@ func NewCreateAWSHandler(
 func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	ctx := r.Context()
 
 	request := &types.CreateAWSRequest{}
-
 	if ok := p.DecodeAndValidate(w, r, request); !ok {
 		return
 	}
@@ -39,7 +42,6 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	aws := CreateAWSIntegration(request, project.ID, user.ID)
 
 	aws, err := p.Repo().AWSIntegration().CreateAWSIntegration(aws)
-
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
@@ -49,6 +51,23 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		AWSIntegration: aws.ToAWSIntegrationType(),
 	}
 
+	if !p.Config().DisableCAPIProvisioner {
+		credReq := porterv1.CreateAssumeRoleChainRequest{
+			ProjectId:       int64(project.ID),
+			SourceArn:       "arn:aws:iam::108458755588:role/CAPIManagement", // hard coded as this is the final hop for a CAPI cluster
+			TargetAccessId:  request.AWSAccessKeyID,
+			TargetSecretKey: request.AWSSecretAccessKey,
+		}
+		credResp, err := p.Config().ClusterControlPlaneClient.CreateAssumeRoleChain(ctx, connect.NewRequest(&credReq))
+		if err != nil {
+			e := fmt.Errorf("unable to create CAPI required credential: %w", err)
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			return
+		}
+		res.CloudProviderCredentialIdentifier = credResp.Msg.TargetArn
+		fmt.Println("stefan", credResp.Msg.ProjectId, credResp.Msg.TargetArn)
+	}
+
 	p.WriteResult(w, r, res)
 }
 

+ 1 - 0
api/types/project_integration.go

@@ -85,6 +85,7 @@ type CreateAWSRequest struct {
 
 type CreateAWSResponse struct {
 	*AWSIntegration
+	CloudProviderCredentialIdentifier string `json:"cloud_provider_credentials_id"`
 }
 
 type OverwriteAWSRequest struct {

+ 4 - 4
dashboard/src/components/CredentialsForm.tsx

@@ -82,7 +82,7 @@ const CredentialsForm: React.FC<Props> = ({
       )
       .then(({ data }) => {
         setCreateStatus("successful");
-        proceed(data.id);
+        proceed(data.cloud_provider_credentials_id);
       })
       .catch((err) => {
         console.error(err);
@@ -98,7 +98,7 @@ const CredentialsForm: React.FC<Props> = ({
             {
               awsCredentials.map((cred: AWSCredential, i: number) => {
                 return (
-                  <Credential 
+                  <Credential
                     key={cred.id}
                     isSelected={cred.id === selectedCredentials?.id}
                     onClick={() => {
@@ -143,7 +143,7 @@ const CredentialsForm: React.FC<Props> = ({
               </CloseButton>
             )
           }
-          <InputRow 
+          <InputRow
             type="string"
             value={awsAccessKeyID}
             setValue={(e: string) => setAWSAccessKeyID(e)}
@@ -151,7 +151,7 @@ const CredentialsForm: React.FC<Props> = ({
             placeholder="ex: AKIAIOSFODNN7EXAMPLE"
             isRequired
           />
-          <InputRow 
+          <InputRow
             type="password"
             value={awsSecretAccessKey}
             setValue={(e: string) => setAWSSecretAccessKey(e)}

+ 3 - 3
internal/models/aws_assume_role_chain.go

@@ -13,17 +13,17 @@ type AWSAssumeRoleChain struct {
 	ID uuid.UUID `gorm:"type:uuid;primaryKey"`
 
 	// SourceARN is ARN which will assume the target ARN
-	SourceARN string `json:"source_arn"`
+	SourceARN string `json:"source_arn" gorm:"UNIQUE_INDEX:aws_assume_role_chains_project_id_source_arn_target_arn_key"`
 
 	// TargetARN is ARN which will assume the target ARN
-	TargetARN string `json:"target_arn"`
+	TargetARN string `json:"target_arn" gorm:"UNIQUE_INDEX:aws_assume_role_chains_project_id_source_arn_target_arn_key"`
 
 	// ExternalID is ID which is required when assuming a role
 	ExternalID string `json:"external_id"`
 
 	// ProjectID is the ID of the project that the config belongs to.
 	// This should be a foreign key, but GORM doesnt play well with FKs.
-	ProjectID int
+	ProjectID int `json:"project_id" gorm:"UNIQUE_INDEX:aws_assume_role_chains_project_id_source_arn_target_arn_key"`
 }
 
 // TableName overrides the table name

+ 3 - 0
internal/models/capi_config.go

@@ -15,6 +15,9 @@ type CAPIConfig struct {
 	// Base64Config is the CAPI config for a cluster, encoded in base64
 	Base64Config string
 
+	// Base64RequestJSON is the JSON submitted by the frontend, encoded in base64
+	Base64RequestJSON string
+
 	// ClusterID is the ID of the cluster that the config created.
 	// This should be a foreign key, but GORM doesnt play well with FKs.
 	ClusterID int

+ 14 - 0
internal/repository/capi_config.go

@@ -0,0 +1,14 @@
+package repository
+
+import (
+	"context"
+
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// CAPIConfigRepository represents queries on the capi_configs table
+type CAPIConfigRepository interface {
+	Insert(ctx context.Context, conf models.CAPIConfig) (models.CAPIConfig, error)
+	// List returns a slice of CAPIConfig, sorted by created_at descending
+	List(ctx context.Context, projectID uint, clusterID uint) ([]models.CAPIConfig, error)
+}

+ 44 - 0
internal/repository/gorm/capi_config.go

@@ -0,0 +1,44 @@
+package gorm
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// CAPIConfigRepository uses gorm.DB for querying the database
+type CAPIConfigRepository struct {
+	db *gorm.DB
+}
+
+// NewCAPIConfigRepository creates a CAPIConfig connection
+func NewCAPIConfigRepository(db *gorm.DB) repository.CAPIConfigRepository {
+	return &CAPIConfigRepository{db}
+}
+
+// Insert creates a new record in the capi_configs table
+func (cr CAPIConfigRepository) Insert(ctx context.Context, conf models.CAPIConfig) (models.CAPIConfig, error) {
+	if conf.ID == uuid.Nil {
+		conf.ID = uuid.New()
+	}
+	tx := cr.db.Create(&conf)
+	if tx.Error != nil {
+		return conf, tx.Error
+	}
+	return conf, nil
+}
+
+// List returns a list of capi configs sorted by created date for a given project and cluster
+func (cr CAPIConfigRepository) List(ctx context.Context, projectID uint, clusterID uint) ([]models.CAPIConfig, error) {
+	var confs []models.CAPIConfig
+
+	tx := cr.db.Preload("capi_configs").Where("project_id = ? and cluster_id = ?", projectID, clusterID).Order("created_at").Find(&confs)
+	if tx.Error != nil {
+		return nil, tx.Error
+	}
+
+	return confs, nil
+}

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

@@ -49,6 +49,7 @@ type GormRepository struct {
 	tag                       repository.TagRepository
 	stack                     repository.StackRepository
 	monitor                   repository.MonitorTestResultRepository
+	capiConfig                repository.CAPIConfigRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -218,6 +219,9 @@ func (t *GormRepository) Stack() repository.StackRepository {
 func (t *GormRepository) MonitorTestResult() repository.MonitorTestResultRepository {
 	return t.monitor
 }
+func (t *GormRepository) CAPIConfigRepository() repository.CAPIConfigRepository {
+	return t.capiConfig
+}
 
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
@@ -265,5 +269,6 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		tag:                       NewTagRepository(db),
 		stack:                     NewStackRepository(db),
 		monitor:                   NewMonitorTestResultRepository(db),
+		capiConfig:                NewCAPIConfigRepository(db),
 	}
 }

+ 1 - 0
internal/repository/repository.go

@@ -43,4 +43,5 @@ type Repository interface {
 	Tag() TagRepository
 	Stack() StackRepository
 	MonitorTestResult() MonitorTestResultRepository
+	CAPIConfigRepository() CAPIConfigRepository
 }

+ 34 - 0
internal/repository/test/capi_config.go

@@ -0,0 +1,34 @@
+package test
+
+import (
+	"context"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// CAPIConfigRepository uses gorm.DB for querying the database
+type CAPIConfigRepository struct {
+	db *gorm.DB
+}
+
+// NewCAPIConfigRepository creates a CAPIConfig connection
+func NewCAPIConfigRepository(db *gorm.DB) repository.CAPIConfigRepository {
+	return &CAPIConfigRepository{db}
+}
+
+// Insert creates a new record in the capi_configs table
+func (cr CAPIConfigRepository) Insert(ctx context.Context, conf models.CAPIConfig) (models.CAPIConfig, error) {
+	panic("not implemented")
+	return conf, nil
+}
+
+// List returns a list of capi configs sorted by created date for a given project and cluster
+func (cr CAPIConfigRepository) List(ctx context.Context, projectID uint, clusterID uint) ([]models.CAPIConfig, error) {
+	var confs []models.CAPIConfig
+
+	panic("not implemented")
+
+	return confs, nil
+}

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

@@ -47,6 +47,7 @@ type TestRepository struct {
 	tag                       repository.TagRepository
 	stack                     repository.StackRepository
 	monitor                   repository.MonitorTestResultRepository
+	capiConfig                repository.CAPIConfigRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -216,6 +217,9 @@ func (t *TestRepository) Stack() repository.StackRepository {
 func (t *TestRepository) MonitorTestResult() repository.MonitorTestResultRepository {
 	return t.monitor
 }
+func (t *TestRepository) CAPIConfigRepository() repository.CAPIConfigRepository {
+	return t.capiConfig
+}
 
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
@@ -263,5 +267,6 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		tag:                       NewTagRepository(),
 		stack:                     NewStackRepository(),
 		monitor:                   NewMonitorTestResultRepository(canQuery),
+		capiConfig:                NewCAPIConfigRepository(canQuery),
 	}
 }