Преглед на файлове

use new datastore create flow if beta_features_enabled is on (#4415)

Feroze Mohideen преди 2 години
родител
ревизия
25a1f592a9

+ 190 - 12
api/server/handlers/datastore/update.go

@@ -2,11 +2,14 @@ package datastore
 
 import (
 	"context"
+	"encoding/base64"
+	"encoding/json"
 	"errors"
 	"net/http"
 	"strings"
 
 	"connectrpc.com/connect"
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -19,8 +22,10 @@ import (
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/telemetry"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
 )
 
 // UpdateDatastoreHandler is a struct for updating datastores.
@@ -51,6 +56,9 @@ type UpdateDatastoreRequest struct {
 	Values map[string]interface{} `json:"values"`
 }
 
+// UpdateDatastoreResponse is the expected format of the response body
+type UpdateDatastoreResponse struct{}
+
 // ServeHTTP updates a datastore using the decoded values
 func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-update-datastore")
@@ -65,14 +73,143 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
+	betaFeaturesEnabled := project.GetFeatureFlag(models.BetaFeaturesEnabled, h.Config().LaunchDarklyClient)
 
 	telemetry.WithAttributes(span,
 		telemetry.AttributeKV{Key: "name", Value: request.Name},
 		telemetry.AttributeKV{Key: "type", Value: request.Type},
 		telemetry.AttributeKV{Key: "engine", Value: request.Engine},
+		telemetry.AttributeKV{Key: "beta-features-enabled", Value: betaFeaturesEnabled},
 	)
 
-	// TODO: replace this with ccp call
+	if !betaFeaturesEnabled {
+		err := h.legacy_DatastoreCreateFlow(ctx, request, project, cluster, r)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error creating datastore")
+			h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		h.WriteResult(w, r, UpdateDatastoreResponse{})
+		return
+	}
+
+	region, err := h.getClusterRegion(ctx, project.ID, cluster.ID)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error getting cluster region")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	// assume we are creating for now; will add update support later
+	datastoreProto := &porterv1.ManagedDatastore{
+		CloudProvider:                     porterv1.EnumCloudProvider_ENUM_CLOUD_PROVIDER_AWS,
+		CloudProviderCredentialIdentifier: cluster.CloudProviderCredentialIdentifier,
+		Region:                            region,
+		ConnectedClusters: &porterv1.ConnectedClusters{
+			ConnectedClusterIds: []int64{int64(cluster.ID)},
+		},
+	}
+	marshaledValues, err := json.Marshal(request.Values)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error marshaling values")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	var datastoreValues struct {
+		Config struct {
+			Name               string `json:"name"`
+			DatabaseName       string `json:"databaseName"`
+			MasterUsername     string `json:"masterUsername"`
+			MasterUserPassword string `json:"masterUserPassword"`
+			AllocatedStorage   int64  `json:"allocatedStorage"`
+			InstanceClass      string `json:"instanceClass"`
+			EngineVersion      string `json:"engineVersion"`
+		} `json:"config"`
+	}
+	err = json.Unmarshal(marshaledValues, &datastoreValues)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error unmarshaling rds postgres values")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if datastoreValues.Config.Name == "" {
+		err = telemetry.Error(ctx, span, nil, "datastore name is required")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	datastoreProto.Name = datastoreValues.Config.Name
+
+	switch request.Type {
+	case "RDS":
+		var engine porterv1.EnumAwsRdsEngine
+		switch request.Engine {
+		case "POSTGRES":
+			engine = porterv1.EnumAwsRdsEngine_ENUM_AWS_RDS_ENGINE_POSTGRESQL
+		case "AURORA-POSTGRES":
+			engine = porterv1.EnumAwsRdsEngine_ENUM_AWS_RDS_ENGINE_AURORA_POSTGRESQL
+		default:
+			err = telemetry.Error(ctx, span, nil, "invalid rds engine")
+			h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+			return
+		}
+		datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_AWS_RDS
+		datastoreProto.KindValues = &porterv1.ManagedDatastore_AwsRdsKind{
+			AwsRdsKind: &porterv1.AwsRds{
+				DatabaseName:              pointer.String(datastoreValues.Config.DatabaseName),
+				MasterUsername:            pointer.String(datastoreValues.Config.MasterUsername),
+				MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
+				AllocatedStorageGigabytes: pointer.Int64(datastoreValues.Config.AllocatedStorage),
+				InstanceClass:             pointer.String(datastoreValues.Config.InstanceClass),
+				Engine:                    engine,
+				EngineVersion:             pointer.String(datastoreValues.Config.EngineVersion),
+			},
+		}
+	case "ELASTICACHE":
+		datastoreProto.Kind = porterv1.EnumDatastoreKind_ENUM_DATASTORE_KIND_AWS_ELASTICACHE
+		datastoreProto.KindValues = &porterv1.ManagedDatastore_AwsElasticacheKind{
+			AwsElasticacheKind: &porterv1.AwsElasticache{
+				Engine:                    porterv1.EnumAwsElasticacheEngine_ENUM_AWS_ELASTICACHE_ENGINE_REDIS,
+				InstanceClass:             pointer.String(datastoreValues.Config.InstanceClass),
+				MasterUserPasswordLiteral: pointer.String(datastoreValues.Config.MasterUserPassword),
+				EngineVersion:             pointer.String(datastoreValues.Config.EngineVersion),
+			},
+		}
+	default:
+		err = telemetry.Error(ctx, span, nil, "invalid datastore type")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	req := connect.NewRequest(&porterv1.PatchCloudContractRequest{
+		ProjectId:    int64(project.ID),
+		Operation:    porterv1.EnumPatchCloudContractOperation_ENUM_PATCH_CLOUD_CONTRACT_OPERATION_UPDATE,
+		ResourceType: porterv1.EnumPatchCloudContractType_ENUM_PATCH_CLOUD_CONTRACT_TYPE_DATASTORE,
+		ResourceValues: &porterv1.PatchCloudContractRequest_Datastore{
+			Datastore: datastoreProto,
+		},
+	})
+	_, err = h.Config().ClusterControlPlaneClient.PatchCloudContract(ctx, req)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "error patching cloud contract")
+		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	h.WriteResult(w, r, UpdateDatastoreResponse{})
+}
+
+func (h *UpdateDatastoreHandler) legacy_DatastoreCreateFlow(
+	ctx context.Context,
+	request *UpdateDatastoreRequest,
+	project *models.Project,
+	cluster *models.Cluster,
+	r *http.Request,
+) error {
+	ctx, span := telemetry.NewSpan(ctx, "legacy-datastore-create")
+	defer span.End()
+
 	err := h.InstallDatastore(ctx, InstallDatastoreInput{
 		Name:    request.Name,
 		Type:    request.Type,
@@ -81,9 +218,7 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		Request: r,
 	})
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error installing datastore")
-		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
+		return telemetry.Error(ctx, span, err, "error installing datastore")
 	}
 
 	record, err := datastore.CreateOrGetRecord(ctx, datastore.CreateOrGetRecordInput{
@@ -96,9 +231,7 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		ClusterRepository:   h.Repo().Cluster(),
 	})
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error retrieving datastore record")
-		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
+		return telemetry.Error(ctx, span, err, "error retrieving datastore record")
 	}
 
 	updateReq := connect.NewRequest(&porterv1.UpdateDatastoreRequest{
@@ -108,13 +241,10 @@ func (h *UpdateDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	_, err = h.Config().ClusterControlPlaneClient.UpdateDatastore(ctx, updateReq)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error calling ccp update datastore")
-		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
+		return telemetry.Error(ctx, span, err, "error calling ccp update datastore")
 	}
 
-	// TODO: create an API-level representation of the db model rather than returning the model directly
-	h.WriteResult(w, r, record)
+	return nil
 }
 
 // InstallDatastoreInput is the input type for InstallDatastore
@@ -340,3 +470,51 @@ func templateNameFromDatastoreTypeAndEngine(databaseType string, databaseEngine
 		return "", errors.New("invalid database type")
 	}
 }
+
+// getClusterRegion is a very hacky way of getting the region of the cluster; this will be replaced once we allow the user to specify region from the frontend
+func (h *UpdateDatastoreHandler) getClusterRegion(
+	ctx context.Context,
+	projectId uint,
+	clusterId uint,
+) (string, error) {
+	ctx, span := telemetry.NewSpan(ctx, "get-cluster-region")
+	defer span.End()
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "project-id", Value: projectId},
+		telemetry.AttributeKV{Key: "cluster-id", Value: clusterId},
+	)
+
+	var region string
+
+	var clusterContractRecord *models.APIContractRevision
+	clusterContractRevisions, err := h.Config().Repo.APIContractRevisioner().List(ctx, projectId, repository.WithClusterID(clusterId), repository.WithLatest(true))
+	if err != nil {
+		return region, telemetry.Error(ctx, span, err, "error getting latest cluster contract revisions")
+	}
+	if len(clusterContractRevisions) == 0 {
+		return region, telemetry.Error(ctx, span, nil, "no cluster contract revisions found")
+	}
+	clusterContractRecord = clusterContractRevisions[0]
+	var clusterContractProto porterv1.Contract
+	decoded, err := base64.StdEncoding.DecodeString(clusterContractRecord.Base64Contract)
+	if err != nil {
+		return region, telemetry.Error(ctx, span, err, "error decoding cluster contract")
+	}
+	err = helpers.UnmarshalContractObject(decoded, &clusterContractProto)
+	if err != nil {
+		return region, telemetry.Error(ctx, span, err, "error unmarshalling cluster contract")
+	}
+	clusterProto := clusterContractProto.Cluster
+	if clusterProto == nil {
+		return region, telemetry.Error(ctx, span, nil, "cluster contract proto is nil")
+	}
+	eksKindValues := clusterProto.GetEksKind()
+	if eksKindValues == nil {
+		return region, telemetry.Error(ctx, span, nil, "eks kind values are nil")
+	}
+	region = eksKindValues.Region
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "region", Value: region})
+
+	return region, nil
+}

+ 1 - 0
dashboard/src/lib/databases/types.ts

@@ -241,6 +241,7 @@ const elasticacheRedisConfigValidator = z.object({
     .string()
     .nonempty("Master password is required")
     .default(""),
+  engineVersion: z.string().default("7.1"),
 });
 
 export const dbFormValidator = z.object({

+ 1 - 0
dashboard/src/lib/hooks/useDatabaseMethods.ts

@@ -76,6 +76,7 @@ const clientDbToCreateInput = (values: DbFormData): CreateDatastoreInput => {
             masterUsername: values.config.masterUsername,
             masterUserPassword: values.config.masterUserPassword,
             instanceClass: values.config.instanceClass,
+            engineVersion: values.config.engineVersion,
           },
         },
         type: "ELASTICACHE",

+ 2 - 2
dashboard/src/main/home/database-dashboard/DatabaseTabs.tsx

@@ -55,7 +55,7 @@ const DatabaseTabs: React.FC<DbTabProps> = ({ tabParam }) => {
   }
 
   return (
-    <>
+    <div>
       <TabSelector
         noBuffer
         options={tabs}
@@ -73,7 +73,7 @@ const DatabaseTabs: React.FC<DbTabProps> = ({ tabParam }) => {
         .with("connected-apps", () => <ConnectedAppsTab />)
         .otherwise(() => null)}
       <Spacer y={2} />
-    </>
+    </div>
   );
 };
 

+ 0 - 1
dashboard/src/main/home/infrastructure-dashboard/ClusterTabs.tsx

@@ -8,7 +8,6 @@ import { match } from "ts-pattern";
 
 import Banner from "components/porter/Banner";
 import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
 import TabSelector from "components/TabSelector";
 import { type ClientClusterContract } from "lib/clusters/types";