ソースを参照

Merge branch 'master' of github.com:porter-dev/porter into inference-fe

Feroze Mohideen 2 年 前
コミット
70abad07fa

+ 13 - 0
api/client/deployment_target.go

@@ -44,3 +44,16 @@ func (c *Client) ListDeploymentTargets(
 
 	return resp, err
 }
+
+// DeleteDeploymentTarget deletes a deployment target in a project
+func (c *Client) DeleteDeploymentTarget(
+	ctx context.Context,
+	projectId uint,
+	deploymentTargetName string,
+) error {
+	return c.deleteRequest(
+		fmt.Sprintf("/projects/%d/targets/%s", projectId, deploymentTargetName),
+		nil,
+		nil,
+	)
+}

+ 9 - 18
api/server/handlers/deployment_target/delete.go

@@ -10,13 +10,12 @@ import (
 	"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"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 
-// DeleteDeploymentTargetHandler is the handler for DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id}
+// DeleteDeploymentTargetHandler is the handler for DELETE /api/projects/{project_id}/targets/{deployment_target_identifier}
 type DeleteDeploymentTargetHandler struct {
 	handlers.PorterHandlerReadWriter
 	authz.KubernetesAgentGetter
@@ -34,28 +33,20 @@ func NewDeleteDeploymentTargetHandler(
 	}
 }
 
-// ServeHTTP deletes the deployment target from the cluster
+// ServeHTTP deletes the deployment target from the project
 func (c *DeleteDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx, span := telemetry.NewSpan(r.Context(), "server-delete-deployment-target-by-id")
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-delete-deployment-target")
 	defer span.End()
 
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
-
-	deploymentTargetID, reqErr := requestutils.GetURLParamString(r, types.URLParamDeploymentTargetID)
-	if reqErr != nil {
-		err := telemetry.Error(ctx, span, reqErr, "error parsing deployment target id")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
-	if deploymentTargetID == "" {
-		err := telemetry.Error(ctx, span, nil, "deployment target id cannot be empty")
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-		return
-	}
+	deploymentTarget, _ := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget)
 
 	deleteReq := connect.NewRequest(&porterv1.DeleteDeploymentTargetRequest{
-		ProjectId:          int64(project.ID),
-		DeploymentTargetId: deploymentTargetID,
+		ProjectId: int64(project.ID),
+		DeploymentTargetIdentifier: &porterv1.DeploymentTargetIdentifier{
+			Id:   deploymentTarget.ID.String(),
+			Name: deploymentTarget.Name,
+		},
 	})
 
 	_, err := c.Config().ClusterControlPlaneClient.DeleteDeploymentTarget(ctx, deleteReq)

+ 29 - 0
api/server/router/deployment_target.go

@@ -89,6 +89,35 @@ func getDeploymentTargetRoutes(
 		Router:   r,
 	})
 
+	// DELETE /api/projects/{project_id}/targets/{deployment_target_identifier} -> deployment_target.DeleteDeploymentTargetHandler
+	deleteDeploymentTargetEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath,
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.DeploymentTargetScope,
+			},
+		},
+	)
+
+	deleteDeploymentTargetHandler := deployment_target.NewDeleteDeploymentTargetHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteDeploymentTargetEndpoint,
+		Handler:  deleteDeploymentTargetHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/targets/{deployment_target_identifier}/apps/{porter_app_name}/cloudsql -> porter_app.GetCloudSqlSecretHandler
 	getCloudSqlSecretEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 0 - 29
api/server/router/deployment_target_legacy.go

@@ -119,35 +119,6 @@ func getLegacyDeploymentTargetRoutes(
 		Router:   r,
 	})
 
-	// DELETE /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id} -> deployment_target.DeleteDeploymentTargetHandler
-	deleteDeploymentTargetEndpoint := factory.NewAPIEndpoint(
-		&types.APIRequestMetadata{
-			Verb:   types.APIVerbDelete,
-			Method: types.HTTPVerbDelete,
-			Path: &types.Path{
-				Parent:       basePath,
-				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamDeploymentTargetID),
-			},
-			Scopes: []types.PermissionScope{
-				types.UserScope,
-				types.ProjectScope,
-				types.ClusterScope,
-			},
-		},
-	)
-
-	deleteDeploymentTargetHandler := deployment_target.NewDeleteDeploymentTargetHandler(
-		config,
-		factory.GetDecoderValidator(),
-		factory.GetResultWriter(),
-	)
-
-	routes = append(routes, &router.Route{
-		Endpoint: deleteDeploymentTargetEndpoint,
-		Handler:  deleteDeploymentTargetHandler,
-		Router:   r,
-	})
-
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets/{deployment_target_id} -> deployment_target.GetDeploymentTargetHandler
 	getDeploymentTargetEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 70 - 0
cli/cmd/commands/target.go

@@ -1,10 +1,12 @@
 package commands
 
 import (
+	"bufio"
 	"context"
 	"fmt"
 	"os"
 	"sort"
+	"strings"
 	"text/tabwriter"
 
 	"github.com/fatih/color"
@@ -61,6 +63,23 @@ If the --preview flag is set, only deployment targets for preview environments w
 	listTargetCmd.Flags().BoolVar(&includePreviews, "preview", false, "List preview environments")
 	targetCmd.AddCommand(listTargetCmd)
 
+	deleteTargetCmd := &cobra.Command{
+		Use:   "delete",
+		Short: "Deletes a deployment target",
+		Long:  `Deletes a deployment target in the project. Currently, this command only supports the deletion of preview environments.`,
+		Run: func(cmd *cobra.Command, args []string) {
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteTarget)
+			if err != nil {
+				os.Exit(1)
+			}
+		},
+	}
+
+	deleteTargetCmd.Flags().StringVar(&targetName, "name", "", "Name of deployment target")
+	deleteTargetCmd.Flags().BoolP("force", "f", false, "Force deletion without confirmation")
+	deleteTargetCmd.MarkFlagRequired("name") // nolint:errcheck,gosec
+	targetCmd.AddCommand(deleteTargetCmd)
+
 	return targetCmd
 }
 
@@ -126,6 +145,57 @@ func listTargets(ctx context.Context, user *types.GetAuthenticatedUserResponse,
 	return nil
 }
 
+func deleteTarget(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, cmd *cobra.Command, args []string) error {
+	name, err := cmd.Flags().GetString("name")
+	if err != nil {
+		return fmt.Errorf("error finding name flag: %w", err)
+	}
+	if name == "" {
+		return fmt.Errorf("name flag must be set")
+	}
+
+	force, err := cmd.Flags().GetBool("force")
+	if err != nil {
+		return fmt.Errorf("error finding force flag: %w", err)
+	}
+
+	var confirmed bool
+	if !force {
+		confirmed, err = confirmAction(fmt.Sprintf("Are you sure you want to delete target '%s'?", name))
+		if err != nil {
+			return fmt.Errorf("error confirming action: %w", err)
+		}
+	}
+	if !confirmed && !force {
+		color.New(color.FgYellow).Println("Deletion aborted") // nolint:errcheck,gosec
+		return nil
+	}
+
+	err = client.DeleteDeploymentTarget(ctx, cliConf.Project, name)
+	if err != nil {
+		return fmt.Errorf("error deleting target: %w", err)
+	}
+
+	color.New(color.FgGreen).Printf("Deleted target '%s'\n", name) // nolint:errcheck,gosec
+
+	return nil
+}
+
+func confirmAction(prompt string) (bool, error) {
+	reader := bufio.NewReader(os.Stdin)
+	fmt.Printf("%s [Y/n]: ", prompt)
+
+	response, err := reader.ReadString('\n')
+	if err != nil {
+		return false, fmt.Errorf("error reading input: %w", err)
+	}
+
+	response = strings.TrimSpace(response)
+	confirmed := strings.ToLower(response) == "y" || response == ""
+
+	return confirmed, nil
+}
+
 func checkmark(b bool) string {
 	if b {
 		return "✓"

+ 85 - 70
dashboard/src/lib/hooks/useCluster.ts

@@ -1,5 +1,9 @@
 import { useContext, useState } from "react";
-import { Contract, PreflightCheckRequest } from "@porter-dev/api-contracts";
+import {
+  Contract,
+  EnumCloudProvider,
+  PreflightCheckRequest,
+} from "@porter-dev/api-contracts";
 import { useQuery } from "@tanstack/react-query";
 import axios from "axios";
 import { match } from "ts-pattern";
@@ -334,6 +338,80 @@ export const useClusterState = ({
   };
 };
 
+export const preflightChecks = async (
+  contract: Contract,
+  projectId: number
+): Promise<UpdateClusterResponse["preflightChecks"]> => {
+  if (!contract.cluster) {
+    throw new Error("Cluster is missing");
+  }
+
+  let preflightCheckResp;
+  if (
+    contract.cluster.cloudProvider === EnumCloudProvider.AWS ||
+    contract.cluster.cloudProvider === EnumCloudProvider.AZURE
+  ) {
+    preflightCheckResp = await api.cloudContractPreflightCheck(
+      "<token>",
+      contract,
+      {
+        project_id: projectId,
+      }
+    );
+  } else {
+    preflightCheckResp = await api.legacyPreflightCheck(
+      "<token>",
+      new PreflightCheckRequest({
+        contract,
+      }),
+      {
+        id: projectId,
+      }
+    );
+  }
+
+  const parsed = await preflightCheckValidator.parseAsync(
+    preflightCheckResp.data
+  );
+
+  if (parsed.errors.length > 0) {
+    const cloudProviderSpecificChecks = match(contract.cluster.cloudProvider)
+      .with(EnumCloudProvider.AWS, () => CloudProviderAWS.preflightChecks)
+      .with(EnumCloudProvider.GCP, () => CloudProviderGCP.preflightChecks)
+      .with(EnumCloudProvider.AZURE, () => CloudProviderAzure.preflightChecks)
+      .otherwise(() => []);
+
+    return parsed.errors.map((e) => {
+      return match(
+        cloudProviderSpecificChecks.find(
+          (cloudProviderCheck) => e.name === cloudProviderCheck.name
+        )
+      )
+        .returnType<ClientPreflightCheck>()
+        .with(undefined, () => ({
+          title: "Unknown preflight check",
+          status: "failure" as const,
+          name: "UNKNOWN" as const,
+          error: {
+            detail:
+              "Your cloud provider returned an unknown error. Please reach out to Porter support.",
+            metadata: {},
+          },
+        }))
+        .otherwise((preflightCheckMatch) => ({
+          title: preflightCheckMatch.displayName,
+          status: "failure" as const,
+          name: preflightCheckMatch.name,
+          error: {
+            detail: e.error.message,
+            metadata: e.error.metadata,
+            resolution: preflightCheckMatch.resolution,
+          },
+        }));
+    });
+  }
+};
+
 type TUseUpdateCluster = {
   updateCluster: (
     clientContract: ClientClusterContract,
@@ -343,6 +421,7 @@ type TUseUpdateCluster = {
   isHandlingPreflightChecks: boolean;
   isCreatingContract: boolean;
 };
+
 export const useUpdateCluster = ({
   projectId,
 }: {
@@ -374,76 +453,12 @@ export const useUpdateCluster = ({
     if (!skipPreflightChecks) {
       setIsHandlingPreflightChecks(true);
       try {
-        let preflightCheckResp;
-        if (
-          clientContract.cluster.cloudProvider === "AWS" ||
-          clientContract.cluster.cloudProvider === "Azure"
-        ) {
-          preflightCheckResp = await api.cloudContractPreflightCheck(
-            "<token>",
-            newContract,
-            {
-              project_id: projectId,
-            }
-          );
-        } else {
-          preflightCheckResp = await api.legacyPreflightCheck(
-            "<token>",
-            new PreflightCheckRequest({
-              contract: newContract,
-            }),
-            {
-              id: projectId,
-            }
-          );
-        }
-
-        const parsed = await preflightCheckValidator.parseAsync(
-          preflightCheckResp.data
+        const preflightCheckResults = await preflightChecks(
+          newContract,
+          projectId
         );
-
-        if (parsed.errors.length > 0) {
-          const cloudProviderSpecificChecks = match(
-            clientContract.cluster.cloudProvider
-          )
-            .with("AWS", () => CloudProviderAWS.preflightChecks)
-            .with("GCP", () => CloudProviderGCP.preflightChecks)
-            .with("Azure", () => CloudProviderAzure.preflightChecks)
-            .otherwise(() => []);
-
-          const clientPreflightChecks: ClientPreflightCheck[] = parsed.errors
-            .map((e) => {
-              const preflightCheckMatch = cloudProviderSpecificChecks.find(
-                (cloudProviderCheck) => e.name === cloudProviderCheck.name
-              );
-              if (!preflightCheckMatch) {
-                return {
-                  title: "Unknown preflight check",
-                  status: "failure" as const,
-                  name: "UNKNOWN" as const,
-                  error: {
-                    detail:
-                      "Your cloud provider returned an unknown error. Please reach out to Porter support.",
-                    metadata: {},
-                  },
-                };
-              }
-              return {
-                title: preflightCheckMatch.displayName,
-                status: "failure" as const,
-                name: preflightCheckMatch.name,
-                error: {
-                  detail: e.error.message,
-                  metadata: e.error.metadata,
-                  resolution: preflightCheckMatch.resolution,
-                },
-              };
-            })
-            .filter(valueExists);
-
-          return {
-            preflightChecks: clientPreflightChecks,
-          };
+        if (preflightCheckResults) {
+          return { preflightChecks: preflightCheckResults };
         }
         // otherwise, continue to create the contract
       } catch (err) {

+ 0 - 1
dashboard/src/main/home/app-dashboard/apps/Apps.tsx

@@ -226,7 +226,6 @@ const Apps: React.FC = () => {
         {},
         {
           project_id: currentProject.id,
-          cluster_id: currentCluster.id,
           deployment_target_id: currentDeploymentTarget.id,
         }
       );

+ 5 - 5
dashboard/src/main/home/infrastructure-dashboard/modals/PreflightChecksModal.tsx

@@ -122,7 +122,7 @@ const PreflightChecksModal: React.FC<Props> = ({
   );
 
   const acceptChanges = async (): Promise<void> => {
-    void submitAndPatchCheckSuggestions({
+    await submitAndPatchCheckSuggestions({
       preflightChecks: checksWithSuggestions,
     });
     onClose();
@@ -140,8 +140,8 @@ const PreflightChecksModal: React.FC<Props> = ({
         </Text>
         <PorterOperatorComponent>
           <Button
-            onClick={async () => {
-              await submitSkippingPreflightChecks();
+            onClick={() => {
+              void submitSkippingPreflightChecks();
             }}
             color="red"
           >
@@ -162,8 +162,8 @@ const PreflightChecksModal: React.FC<Props> = ({
         <ButtonContainer>
           {allHaveSuggestions ? (
             <Button
-              onClick={async () => {
-                await acceptChanges();
+              onClick={() => {
+                void acceptChanges();
               }}
             >
               <Icon src={file_diff} height={"15px"} />

+ 2 - 3
dashboard/src/shared/api.tsx

@@ -997,11 +997,10 @@ const deleteDeploymentTarget = baseApi<
   {},
   {
     project_id: number;
-    cluster_id: number;
     deployment_target_id: string;
   }
->("DELETE", ({ project_id, cluster_id, deployment_target_id }) => {
-  return `/api/projects/${project_id}/clusters/${cluster_id}/deployment-targets/${deployment_target_id}`;
+>("DELETE", ({ project_id, deployment_target_id }) => {
+  return `/api/projects/${project_id}/targets/${deployment_target_id}`;
 });
 
 const getBranchHead = baseApi<