diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fff4d69..d6a9ac7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Display one external source per line when doing showing a security group - chore: update crypto version #770 - chores: add git version/commit in user agent header #769 +- Prompting for validation before deleting deployments and models (dedicated-inference) +- Ability to delete multiple deployments and models at once (dedicated-inference) ## 1.88.0 diff --git a/cmd/aiservices/deployment/deployment_actions_test.go b/cmd/aiservices/deployment/deployment_actions_test.go index 9861eb10..5337b98d 100644 --- a/cmd/aiservices/deployment/deployment_actions_test.go +++ b/cmd/aiservices/deployment/deployment_actions_test.go @@ -103,10 +103,16 @@ func TestDeploymentDeleteScaleRevealLogs(t *testing.T) { now := time.Now() ts.deployments = []v3.ListDeploymentsResponseEntry{{ID: v3.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), Name: "alpha", CreatedAT: now, UpdatedAT: now}} // delete by name - del := &DeploymentDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), Deployment: "alpha"} + del := &DeploymentDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), Deployments: []string{"alpha"}, Force: true} if err := del.CmdRun(nil, nil); err != nil { t.Fatalf("delete: %v", err) } + // delete multiple (add another deployment first) + ts.deployments = append(ts.deployments, v3.ListDeploymentsResponseEntry{ID: v3.UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), Name: "beta", CreatedAT: now, UpdatedAT: now}) + delMultiple := &DeploymentDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), Deployments: []string{"alpha", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"}, Force: true} + if err := delMultiple.CmdRun(nil, nil); err != nil { + t.Fatalf("delete multiple: %v", err) + } // scale by id sc := &DeploymentScaleCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), Deployment: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", Size: 3} if err := sc.CmdRun(nil, nil); err != nil { diff --git a/cmd/aiservices/deployment/deployment_delete.go b/cmd/aiservices/deployment/deployment_delete.go index 9cfa3c4d..2c397bda 100644 --- a/cmd/aiservices/deployment/deployment_delete.go +++ b/cmd/aiservices/deployment/deployment_delete.go @@ -1,7 +1,6 @@ package deployment import ( - "context" "fmt" "os" @@ -17,8 +16,9 @@ type DeploymentDeleteCmd struct { _ bool `cli-cmd:"delete"` - Deployment string `cli-arg:"#" cli-usage:"ID or NAME"` - Zone v3.ZoneName `cli-short:"z" cli-usage:"zone"` + Deployments []string `cli-arg:"#" cli-usage:"NAME|ID..."` + Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` + Zone v3.ZoneName `cli-short:"z" cli-usage:"zone"` } func (c *DeploymentDeleteCmd) CmdAliases() []string { return exocmd.GDeleteAlias } @@ -37,24 +37,51 @@ func (c *DeploymentDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { return err } - // Resolve deployment ID using the SDK helper + // Resolve deployment IDs using the SDK helper list, err := client.ListDeployments(ctx) if err != nil { return err } - entry, err := list.FindListDeploymentsResponseEntry(c.Deployment) - if err != nil { - return err + + deploymentsToDelete := []v3.UUID{} + for _, deploymentStr := range c.Deployments { + entry, err := list.FindListDeploymentsResponseEntry(deploymentStr) + if err != nil { + if !c.Force { + return err + } + fmt.Fprintf(os.Stderr, "warning: %s not found.\n", deploymentStr) + continue + } + + if !c.Force { + if !utils.AskQuestion(ctx, fmt.Sprintf("Are you sure you want to delete deployment %q?", deploymentStr)) { + return nil + } + } + + deploymentsToDelete = append(deploymentsToDelete, entry.ID) } - id := entry.ID - if err := utils.RunAsync(ctx, client, fmt.Sprintf("Deleting deployment %s...", c.Deployment), func(ctx context.Context, c *v3.Client) (*v3.Operation, error) { - return c.DeleteDeployment(ctx, id) - }); err != nil { + var fns []func() error + for _, id := range deploymentsToDelete { + fns = append(fns, func() error { + op, err := client.DeleteDeployment(ctx, id) + if err != nil { + return err + } + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + return err + }) + } + + err = utils.DecorateAsyncOperations("Deleting deployment(s)...", fns...) + if err != nil { return err } + if !globalstate.Quiet { - fmt.Fprintln(os.Stdout, "Deployment deleted.") + fmt.Fprintln(os.Stdout, "Deployment(s) deleted.") } return nil } diff --git a/cmd/aiservices/model/model_delete.go b/cmd/aiservices/model/model_delete.go index 4d28307f..328bc7b0 100644 --- a/cmd/aiservices/model/model_delete.go +++ b/cmd/aiservices/model/model_delete.go @@ -1,7 +1,6 @@ package model import ( - "context" "fmt" "os" @@ -17,8 +16,9 @@ type ModelDeleteCmd struct { _ bool `cli-cmd:"delete"` - ID string `cli-arg:"#" cli-usage:"MODEL-ID (UUID)"` - Zone v3.ZoneName `cli-short:"z" cli-usage:"zone"` + IDs []string `cli-arg:"#" cli-usage:"MODEL-ID (UUID)..."` + Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` + Zone v3.ZoneName `cli-short:"z" cli-usage:"zone"` } func (c *ModelDeleteCmd) CmdAliases() []string { return exocmd.GDeleteAlias } @@ -35,18 +35,45 @@ func (c *ModelDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { return err } - id, err := v3.ParseUUID(c.ID) - if err != nil { - return fmt.Errorf("invalid model ID: %w", err) + modelsToDelete := []v3.UUID{} + for _, idStr := range c.IDs { + id, err := v3.ParseUUID(idStr) + if err != nil { + if !c.Force { + return fmt.Errorf("invalid model ID %q: %w", idStr, err) + } + fmt.Fprintf(os.Stderr, "warning: invalid model ID %q: %v\n", idStr, err) + continue + } + + if !c.Force { + if !utils.AskQuestion(ctx, fmt.Sprintf("Are you sure you want to delete model %q?", idStr)) { + return nil + } + } + + modelsToDelete = append(modelsToDelete, id) } - if err := utils.RunAsync(ctx, client, fmt.Sprintf("Deleting model %s...", c.ID), func(ctx context.Context, c *v3.Client) (*v3.Operation, error) { - return c.DeleteModel(ctx, id) - }); err != nil { + var fns []func() error + for _, id := range modelsToDelete { + fns = append(fns, func() error { + op, err := client.DeleteModel(ctx, id) + if err != nil { + return err + } + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + return err + }) + } + + err = utils.DecorateAsyncOperations("Deleting model(s)...", fns...) + if err != nil { return err } + if !globalstate.Quiet { - fmt.Fprintln(os.Stdout, "Model deleted.") + fmt.Fprintln(os.Stdout, "Model(s) deleted.") } return nil } diff --git a/cmd/aiservices/model/model_delete_test.go b/cmd/aiservices/model/model_delete_test.go index 92127766..1a9e6b03 100644 --- a/cmd/aiservices/model/model_delete_test.go +++ b/cmd/aiservices/model/model_delete_test.go @@ -44,14 +44,24 @@ func TestModelDeleteInvalidUUIDAndSuccess(t *testing.T) { } globalstate.EgoscaleV3Client = client.WithEndpoint(v3.Endpoint(srv.URL)) - // invalid UUID - cmd := &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "not-a-uuid"} + // invalid UUID without force + cmd := &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), IDs: []string{"not-a-uuid"}, Force: false} if err := cmd.CmdRun(nil, nil); err == nil || !regexp.MustCompile(`invalid model ID`).MatchString(err.Error()) { t.Fatalf("expected invalid uuid error, got %v", err) } + // invalid UUID with force (should skip with warning, no error) + cmd = &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), IDs: []string{"not-a-uuid"}, Force: true} + if err := cmd.CmdRun(nil, nil); err != nil { + t.Fatalf("expected no error with force flag, got %v", err) + } // success - cmd = &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "33333333-3333-3333-3333-333333333333"} + cmd = &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), IDs: []string{"33333333-3333-3333-3333-333333333333"}, Force: true} if err := cmd.CmdRun(nil, nil); err != nil { t.Fatalf("model delete: %v", err) } + // multiple models + cmd = &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), IDs: []string{"33333333-3333-3333-3333-333333333333", "44444444-4444-4444-4444-444444444444"}, Force: true} + if err := cmd.CmdRun(nil, nil); err != nil { + t.Fatalf("model delete multiple: %v", err) + } }