From ec3326578dd013b499d24f3bb195d90ee70f9dca Mon Sep 17 00:00:00 2001 From: Paul Habfast Date: Thu, 11 Dec 2025 08:43:54 +0100 Subject: [PATCH 1/3] [sc-157181] asking for confirmation when deleting a model or deployment --- cmd/aiservices/deployment/deployment_actions_test.go | 2 +- cmd/aiservices/deployment/deployment_delete.go | 7 +++++++ cmd/aiservices/model/model_delete.go | 11 +++++++++-- cmd/aiservices/model/model_delete_test.go | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/cmd/aiservices/deployment/deployment_actions_test.go b/cmd/aiservices/deployment/deployment_actions_test.go index 9861eb10..49047040 100644 --- a/cmd/aiservices/deployment/deployment_actions_test.go +++ b/cmd/aiservices/deployment/deployment_actions_test.go @@ -103,7 +103,7 @@ 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(), Deployment: "alpha", Force: true} if err := del.CmdRun(nil, nil); err != nil { t.Fatalf("delete: %v", err) } diff --git a/cmd/aiservices/deployment/deployment_delete.go b/cmd/aiservices/deployment/deployment_delete.go index 9cfa3c4d..3a08e361 100644 --- a/cmd/aiservices/deployment/deployment_delete.go +++ b/cmd/aiservices/deployment/deployment_delete.go @@ -18,6 +18,7 @@ type DeploymentDeleteCmd struct { _ bool `cli-cmd:"delete"` Deployment string `cli-arg:"#" cli-usage:"ID or NAME"` + Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` Zone v3.ZoneName `cli-short:"z" cli-usage:"zone"` } @@ -48,6 +49,12 @@ func (c *DeploymentDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { } id := entry.ID + if !c.Force { + if !utils.AskQuestion(ctx, fmt.Sprintf("Are you sure you want to delete deployment %q?", c.Deployment)) { + return nil + } + } + 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 { diff --git a/cmd/aiservices/model/model_delete.go b/cmd/aiservices/model/model_delete.go index 4d28307f..075a181c 100644 --- a/cmd/aiservices/model/model_delete.go +++ b/cmd/aiservices/model/model_delete.go @@ -17,8 +17,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"` + ID 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 } @@ -40,6 +41,12 @@ func (c *ModelDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { return fmt.Errorf("invalid model ID: %w", err) } + if !c.Force { + if !utils.AskQuestion(ctx, fmt.Sprintf("Are you sure you want to delete model %q?", c.ID)) { + return nil + } + } + 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 { diff --git a/cmd/aiservices/model/model_delete_test.go b/cmd/aiservices/model/model_delete_test.go index 92127766..2172c550 100644 --- a/cmd/aiservices/model/model_delete_test.go +++ b/cmd/aiservices/model/model_delete_test.go @@ -45,12 +45,12 @@ func TestModelDeleteInvalidUUIDAndSuccess(t *testing.T) { globalstate.EgoscaleV3Client = client.WithEndpoint(v3.Endpoint(srv.URL)) // invalid UUID - cmd := &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "not-a-uuid"} + cmd := &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "not-a-uuid", Force: true} 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) } // success - cmd = &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "33333333-3333-3333-3333-333333333333"} + cmd = &ModelDeleteCmd{CliCommandSettings: exocmd.DefaultCLICmdSettings(), ID: "33333333-3333-3333-3333-333333333333", Force: true} if err := cmd.CmdRun(nil, nil); err != nil { t.Fatalf("model delete: %v", err) } From 05c83e2b107ae11aaf7b114ed3b840d660952584 Mon Sep 17 00:00:00 2001 From: Paul Habfast Date: Thu, 11 Dec 2025 08:56:15 +0100 Subject: [PATCH 2/3] [sc-157181] ability to delete multiple deployments or models at once --- .../deployment/deployment_actions_test.go | 8 ++- .../deployment/deployment_delete.go | 54 +++++++++++++------ cmd/aiservices/model/model_delete.go | 46 +++++++++++----- cmd/aiservices/model/model_delete_test.go | 16 ++++-- 4 files changed, 90 insertions(+), 34 deletions(-) diff --git a/cmd/aiservices/deployment/deployment_actions_test.go b/cmd/aiservices/deployment/deployment_actions_test.go index 49047040..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", Force: true} + 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 3a08e361..081bb805 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,9 +16,9 @@ type DeploymentDeleteCmd struct { _ bool `cli-cmd:"delete"` - Deployment string `cli-arg:"#" cli-usage:"ID or NAME"` - Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` - 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 } @@ -38,30 +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 - } - id := entry.ID - if !c.Force { - if !utils.AskQuestion(ctx, fmt.Sprintf("Are you sure you want to delete deployment %q?", c.Deployment)) { - return nil + 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) + } + + 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 + }) } - 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 { + err = utils.DecorateAsyncOperations(fmt.Sprintf("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 075a181c..da470080 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,7 +16,7 @@ type ModelDeleteCmd struct { _ bool `cli-cmd:"delete"` - ID string `cli-arg:"#" cli-usage:"MODEL-ID (UUID)"` + 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"` } @@ -36,24 +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?", c.ID)) { - return nil + 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(fmt.Sprintf("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 2172c550..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", Force: true} + // 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", Force: true} + 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) + } } From 2ef508d970885ef9b5b0d91fc76da3292d2b04bf Mon Sep 17 00:00:00 2001 From: Paul Habfast Date: Thu, 11 Dec 2025 11:08:35 +0100 Subject: [PATCH 3/3] [sc-157181] changelog and linting --- CHANGELOG.md | 2 ++ cmd/aiservices/deployment/deployment_delete.go | 2 +- cmd/aiservices/model/model_delete.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) 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_delete.go b/cmd/aiservices/deployment/deployment_delete.go index 081bb805..2c397bda 100644 --- a/cmd/aiservices/deployment/deployment_delete.go +++ b/cmd/aiservices/deployment/deployment_delete.go @@ -75,7 +75,7 @@ func (c *DeploymentDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { }) } - err = utils.DecorateAsyncOperations(fmt.Sprintf("Deleting deployment(s)..."), fns...) + err = utils.DecorateAsyncOperations("Deleting deployment(s)...", fns...) if err != nil { return err } diff --git a/cmd/aiservices/model/model_delete.go b/cmd/aiservices/model/model_delete.go index da470080..328bc7b0 100644 --- a/cmd/aiservices/model/model_delete.go +++ b/cmd/aiservices/model/model_delete.go @@ -67,7 +67,7 @@ func (c *ModelDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { }) } - err = utils.DecorateAsyncOperations(fmt.Sprintf("Deleting model(s)..."), fns...) + err = utils.DecorateAsyncOperations("Deleting model(s)...", fns...) if err != nil { return err }