diff --git a/api/client/deployment_target.go b/api/client/deployment_target.go index 20a6d7f885..f0c8f1684d 100644 --- a/api/client/deployment_target.go +++ b/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, + ) +} diff --git a/api/server/handlers/deployment_target/delete.go b/api/server/handlers/deployment_target/delete.go index ed7edcbf56..64bc9aa04f 100644 --- a/api/server/handlers/deployment_target/delete.go +++ b/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) diff --git a/api/server/router/deployment_target.go b/api/server/router/deployment_target.go index 7b88cf87ce..4aa594a830 100644 --- a/api/server/router/deployment_target.go +++ b/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{ diff --git a/api/server/router/deployment_target_legacy.go b/api/server/router/deployment_target_legacy.go index 231a4375a1..dd74e1c4c0 100644 --- a/api/server/router/deployment_target_legacy.go +++ b/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{ diff --git a/cli/cmd/commands/target.go b/cli/cmd/commands/target.go index 0fe78a347f..a5fc957a81 100644 --- a/cli/cmd/commands/target.go +++ b/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 "✓" diff --git a/dashboard/src/main/home/app-dashboard/apps/Apps.tsx b/dashboard/src/main/home/app-dashboard/apps/Apps.tsx index fb6335b05d..e4fe1bb348 100644 --- a/dashboard/src/main/home/app-dashboard/apps/Apps.tsx +++ b/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, } ); diff --git a/dashboard/src/shared/api.tsx b/dashboard/src/shared/api.tsx index 847ce52888..3899a51360 100644 --- a/dashboard/src/shared/api.tsx +++ b/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<