2
0
Эх сурвалжийг харах

delete management cluster datastores from the frontend (#4423)

Feroze Mohideen 2 жил өмнө
parent
commit
bdc702dc6c

+ 33 - 70
api/server/handlers/datastore/delete.go

@@ -1,7 +1,6 @@
 package datastore
 
 import (
-	"context"
 	"net/http"
 
 	"connectrpc.com/connect"
@@ -9,7 +8,6 @@ import (
 	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"
-	"github.com/porter-dev/porter/api/server/handlers/release"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -57,83 +55,48 @@ func (h *DeleteDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	if datastoreRecord == nil || datastoreRecord.ID == uuid.Nil {
-		err = telemetry.Error(ctx, span, nil, "datastore record does not exist")
-		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
-		return
-	}
+	if !datastoreRecord.OnManagementCluster {
+		if datastoreRecord == nil || datastoreRecord.ID == uuid.Nil {
+			err = telemetry.Error(ctx, span, nil, "datastore record does not exist")
+			h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
+			return
+		}
 
-	_, err = h.Repo().Datastore().UpdateStatus(ctx, datastoreRecord, models.DatastoreStatus_AwaitingDeletion)
-	if err != nil {
-		err = telemetry.Error(ctx, span, err, "error updating datastore status")
-		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		_, err = h.Repo().Datastore().UpdateStatus(ctx, datastoreRecord, models.DatastoreStatus_AwaitingDeletion)
+		if err != nil {
+			err = telemetry.Error(ctx, span, err, "error updating datastore status")
+			h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+
+		updateReq := connect.NewRequest(&porterv1.UpdateDatastoreRequest{
+			ProjectId:   int64(project.ID),
+			DatastoreId: datastoreRecord.ID.String(),
+		})
+
+		_, 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
+		}
+
+		w.WriteHeader(http.StatusAccepted)
 		return
 	}
 
-	updateReq := connect.NewRequest(&porterv1.UpdateDatastoreRequest{
-		ProjectId:   int64(project.ID),
-		DatastoreId: datastoreRecord.ID.String(),
+	req := connect.NewRequest(&porterv1.PatchCloudContractRequest{
+		ProjectId:    int64(project.ID),
+		Operation:    porterv1.EnumPatchCloudContractOperation_ENUM_PATCH_CLOUD_CONTRACT_OPERATION_DELETE,
+		ResourceType: porterv1.EnumPatchCloudContractType_ENUM_PATCH_CLOUD_CONTRACT_TYPE_DATASTORE,
+		ResourceId:   datastoreRecord.ID.String(),
 	})
-
-	_, err = h.Config().ClusterControlPlaneClient.UpdateDatastore(ctx, updateReq)
+	_, err = h.Config().ClusterControlPlaneClient.PatchCloudContract(ctx, req)
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error calling ccp update datastore")
+		err = telemetry.Error(ctx, span, err, "error patching cloud contract")
 		h.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	// if the release was deleted by helm without error, mark it as accepted
 	w.WriteHeader(http.StatusAccepted)
 }
-
-// UninstallDatastoreInput is the input type for UninstallDatastore
-type UninstallDatastoreInput struct {
-	ProjectID                         uint
-	Name                              string
-	CloudProvider                     string
-	CloudProviderCredentialIdentifier string
-	Request                           *http.Request
-}
-
-// UninstallDatastore uninstalls a datastore from a cluster
-func (h *DeleteDatastoreHandler) UninstallDatastore(ctx context.Context, inp UninstallDatastoreInput) error {
-	ctx, span := telemetry.NewSpan(ctx, "uninstall-datastore")
-	defer span.End()
-
-	telemetry.WithAttributes(span,
-		telemetry.AttributeKV{Key: "project-id", Value: inp.ProjectID},
-		telemetry.AttributeKV{Key: "name", Value: inp.Name},
-		telemetry.AttributeKV{Key: "cloud-provider", Value: inp.CloudProvider},
-		telemetry.AttributeKV{Key: "cloud-provider-credential-identifier", Value: inp.CloudProviderCredentialIdentifier},
-	)
-
-	var datastoreCluster *models.Cluster
-	clusters, err := h.Repo().Cluster().ListClustersByProjectID(inp.ProjectID)
-	if err != nil {
-		return telemetry.Error(ctx, span, err, "unable to get project clusters")
-	}
-
-	for _, cluster := range clusters {
-		if cluster.CloudProvider == inp.CloudProvider && cluster.CloudProviderCredentialIdentifier == inp.CloudProviderCredentialIdentifier {
-			datastoreCluster = cluster
-		}
-	}
-
-	if datastoreCluster == nil {
-		return telemetry.Error(ctx, span, nil, "unable to find datastore cluster")
-	}
-
-	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "cluster-id", Value: datastoreCluster.ID})
-
-	helmAgent, err := h.GetHelmAgent(ctx, inp.Request, datastoreCluster, release.Namespace_ACKSystem)
-	if err != nil {
-		return telemetry.Error(ctx, span, err, "unable to get helm client for cluster")
-	}
-
-	_, err = helmAgent.UninstallChart(ctx, inp.Name)
-	if err != nil {
-		return telemetry.Error(ctx, span, err, "unable to uninstall chart")
-	}
-
-	return nil
-}

+ 75 - 78
api/server/handlers/datastore/get.go

@@ -2,13 +2,11 @@ package datastore
 
 import (
 	"context"
-	"encoding/base64"
 	"net/http"
 
 	"connectrpc.com/connect"
 	"github.com/aws/aws-sdk-go/aws/arn"
 	"github.com/google/uuid"
-	"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"
@@ -101,63 +99,6 @@ func (c *GetDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	req := connect.NewRequest(&porterv1.ReadCloudContractRequest{
-		ProjectId: int64(project.ID),
-	})
-	ccpResp, err := c.Config().ClusterControlPlaneClient.ReadCloudContract(ctx, req)
-	if err != nil {
-		err = telemetry.Error(ctx, span, err, "error getting cloud contract")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-	if ccpResp.Msg == nil {
-		err = telemetry.Error(ctx, span, nil, "cloud contract not found")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
-		return
-	}
-
-	cloudContract := ccpResp.Msg.CloudContract
-	if cloudContract == nil {
-		err = telemetry.Error(ctx, span, nil, "cloud contract is empty")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
-		return
-	}
-
-	datastores := cloudContract.Datastores
-	if datastores == nil {
-		err = telemetry.Error(ctx, span, nil, "datastores is empty")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
-		return
-	}
-
-	var matchingDatastore *porterv1.ManagedDatastore
-	for _, ds := range datastores {
-		if ds.Id == datastoreRecord.ID.String() {
-			matchingDatastore = ds
-			break
-		}
-	}
-	if matchingDatastore == nil {
-		err = telemetry.Error(ctx, span, nil, "datastore not found")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
-		return
-	}
-	connectedClusterIds := make([]uint, 0)
-	if matchingDatastore.ConnectedClusters != nil {
-		for _, cc := range matchingDatastore.ConnectedClusters.ConnectedClusterIds {
-			connectedClusterIds = append(connectedClusterIds, uint(cc))
-		}
-	}
-
-	encoded, err := helpers.MarshalContractObject(ctx, matchingDatastore)
-	if err != nil {
-		err = telemetry.Error(ctx, span, err, "error marshaling datastore")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-		return
-	}
-
-	b64 := base64.StdEncoding.EncodeToString(encoded)
-
 	ds := datastore.Datastore{
 		Name:                              datastoreRecord.Name,
 		Type:                              datastoreRecord.Type,
@@ -166,27 +107,12 @@ func (c *GetDatastoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		Status:                            string(datastoreRecord.Status),
 		CloudProvider:                     SupportedDatastoreCloudProvider_AWS,
 		CloudProviderCredentialIdentifier: datastoreRecord.CloudProviderCredentialIdentifier,
-		ConnectedClusterIds:               connectedClusterIds,
 		OnManagementCluster:               true,
-		B64Proto:                          b64,
 	}
 
-	message := porterv1.DatastoreCredentialRequest{
-		ProjectId:   int64(project.ID),
-		DatastoreId: datastoreRecord.ID.String(),
-	}
-	credentialReq := connect.NewRequest(&message)
-	credentialCcpResp, err := c.Config().ClusterControlPlaneClient.DatastoreCredential(ctx, credentialReq)
-	if err == nil && credentialCcpResp != nil && credentialCcpResp.Msg != nil {
-		// the credential may not exist because the datastore is not yet ready
-		ds.Credential = datastore.Credential{
-			Host:         credentialCcpResp.Msg.Credential.Host,
-			Port:         int(credentialCcpResp.Msg.Credential.Port),
-			Username:     credentialCcpResp.Msg.Credential.Username,
-			Password:     credentialCcpResp.Msg.Credential.Password,
-			DatabaseName: credentialCcpResp.Msg.Credential.DatabaseName,
-		}
-	}
+	// this is done for backwards compatibility; eventually we will just return proto
+	ds.ConnectedClusterIds = c.connectedClusters(ctx, project, datastoreRecord.ID)
+	ds.Credential = c.credential(ctx, project, datastoreRecord.ID)
 
 	resp.Datastore = ds
 
@@ -213,7 +139,7 @@ func (c *GetDatastoreHandler) LEGACY_handleGetDatastore(ctx context.Context, pro
 		DatastoreRepository: c.Repo().Datastore(),
 	})
 	if err != nil {
-		return ds, err
+		return ds, telemetry.Error(ctx, span, err, "error listing datastores")
 	}
 
 	if len(datastores) != 1 {
@@ -245,3 +171,74 @@ func (c *GetDatastoreHandler) LEGACY_handleGetDatastore(ctx context.Context, pro
 
 	return ds, nil
 }
+
+func (c *GetDatastoreHandler) connectedClusters(ctx context.Context, project *models.Project, datastoreID uuid.UUID) []uint {
+	ctx, span := telemetry.NewSpan(ctx, "hydrate-connected-clusters")
+	defer span.End()
+
+	connectedClusterIds := make([]uint, 0)
+
+	req := connect.NewRequest(&porterv1.ReadCloudContractRequest{
+		ProjectId: int64(project.ID),
+	})
+	ccpResp, err := c.Config().ClusterControlPlaneClient.ReadCloudContract(ctx, req)
+	if err != nil {
+		return connectedClusterIds
+	}
+	if ccpResp.Msg == nil {
+		return connectedClusterIds
+	}
+
+	cloudContract := ccpResp.Msg.CloudContract
+	if cloudContract == nil {
+		return connectedClusterIds
+	}
+
+	datastores := cloudContract.Datastores
+	if datastores == nil {
+		return connectedClusterIds
+	}
+
+	var matchingDatastore *porterv1.ManagedDatastore
+	for _, ds := range datastores {
+		if ds.Id == datastoreID.String() {
+			matchingDatastore = ds
+			break
+		}
+	}
+
+	if matchingDatastore != nil && matchingDatastore.ConnectedClusters != nil {
+		for _, cc := range matchingDatastore.ConnectedClusters.ConnectedClusterIds {
+			connectedClusterIds = append(connectedClusterIds, uint(cc))
+		}
+	}
+
+	return connectedClusterIds
+}
+
+func (c *GetDatastoreHandler) credential(ctx context.Context, project *models.Project, datastoreID uuid.UUID) datastore.Credential {
+	ctx, span := telemetry.NewSpan(ctx, "hydrate-credential")
+	defer span.End()
+
+	message := porterv1.DatastoreCredentialRequest{
+		ProjectId:   int64(project.ID),
+		DatastoreId: datastoreID.String(),
+	}
+	req := connect.NewRequest(&message)
+	ccpResp, err := c.Config().ClusterControlPlaneClient.DatastoreCredential(ctx, req)
+	if err != nil {
+		return datastore.Credential{}
+	}
+
+	if ccpResp == nil || ccpResp.Msg == nil {
+		return datastore.Credential{}
+	}
+
+	return datastore.Credential{
+		Host:         ccpResp.Msg.Credential.Host,
+		Port:         int(ccpResp.Msg.Credential.Port),
+		Username:     ccpResp.Msg.Credential.Username,
+		Password:     ccpResp.Msg.Credential.Password,
+		DatabaseName: ccpResp.Msg.Credential.DatabaseName,
+	}
+}

+ 29 - 21
dashboard/src/main/home/env-dashboard/tabs/SettingsTab.tsx

@@ -2,29 +2,29 @@ import React, { useContext, useState } from "react";
 import { useHistory } from "react-router";
 import styled from "styled-components";
 
-import api from "shared/api";
-import { Context } from "shared/Context";
-
-import loading from "assets/loading.gif";
-
 import Button from "components/porter/Button";
-import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
 import Container from "components/porter/Container";
-import Image from "components/porter/Image";
 import Error from "components/porter/Error";
-import {envGroupPath} from "shared/util";
+import Image from "components/porter/Image";
+import Spacer from "components/porter/Spacer";
+import Text from "components/porter/Text";
+
+import api from "shared/api";
+import { Context } from "shared/Context";
+import { envGroupPath } from "shared/util";
+import loading from "assets/loading.gif";
 
 type Props = {
   envGroup: {
     name: string;
     type: string;
     linked_applications: string[];
-  }
+  };
 };
 
 const SettingsTab: React.FC<Props> = ({ envGroup }) => {
-  const { currentProject, currentCluster, setCurrentOverlay } = useContext(Context);
+  const { currentProject, currentCluster, setCurrentOverlay } =
+    useContext(Context);
   const history = useHistory();
 
   const [isDeleting, setIsDeleting] = useState<boolean>(false);
@@ -36,20 +36,21 @@ const SettingsTab: React.FC<Props> = ({ envGroup }) => {
         "<token>",
         {
           name: envGroup.name,
-          type: envGroup.type
+          type: envGroup.type,
         },
         {
           id: currentProject?.id ?? -1,
-          cluster_id: currentCluster?.id ?? -1
-        },
+          cluster_id: currentCluster?.id ?? -1,
+        }
       );
-    } catch (error) {
-    }
+    } catch (error) {}
   };
-  
+
   const handleDeletionSubmit = async (): Promise<void> => {
     if (envGroup?.linked_applications) {
-      setButtonStatus(<Error message="Remove this env group from all synced applications to delete." />);
+      setButtonStatus(
+        <Error message="Remove this env group from all synced applications to delete." />
+      );
       setCurrentOverlay && setCurrentOverlay(null);
       return;
     }
@@ -91,7 +92,9 @@ const SettingsTab: React.FC<Props> = ({ envGroup }) => {
             <Text size={16}>Deleting {envGroup.name}</Text>
           </Container>
           <Spacer y={0.5} />
-          <Text color="helper">Please wait while we delete this datastore.</Text>
+          <Text color="helper">
+            Please wait while we delete this datastore.
+          </Text>
         </>
       )}
       {!isDeleting && (
@@ -99,13 +102,18 @@ const SettingsTab: React.FC<Props> = ({ envGroup }) => {
           <Text size={16}>Delete {envGroup.name}</Text>
           <Spacer y={1} />
           <Text color="helper">
-            Delete this environment group including all secrets and environment-specific configuration.
+            Delete this environment group including all secrets and
+            environment-specific configuration.
           </Text>
           <Spacer y={1.2} />
           <Button
             color="#b91133"
             onClick={handleDeletionClick}
             status={buttonStatus}
+            disabled={envGroup.type === "datastore"}
+            disabledTooltipMessage={
+              "This environment group is managed by a datastore. You cannot delete it."
+            }
           >
             Delete {envGroup.name}
           </Button>
@@ -128,4 +136,4 @@ const StyledTemplateComponent = styled.div`
       opacity: 1;
     }
   }
-`;
+`;

+ 0 - 2
internal/datastore/datastore.go

@@ -26,8 +26,6 @@ type Datastore struct {
 	ConnectedClusterIds []uint `json:"connected_cluster_ids,omitempty"`
 	// OnManagementCluster is a flag indicating whether the datastore is on the management cluster
 	OnManagementCluster bool `json:"on_management_cluster"`
-	// B64Proto is the base64 encoded datastore proto. Note that this is only populated for datastores created with the new cloud contract flow
-	B64Proto string `json:"b64_proto"`
 }
 
 // Credential has all information about connecting to a datastore