Skip to content

Commit d6fe448

Browse files
authored
Feat/vm batch operation support (#29)
* feat(vm): add --all, --status, --with-volumes flags to shortcut commands * feat(vm): add batch validation, fetching, confirmation, and result display * feat(vm): implement batch execution for all actions including delete * feat(vm): --status filters interactive picker when --all is not set * feat(vm): add batch integration tests and help examples * feat(vm): interactive multi-select for shortcut commands When running shortcut commands (stop, start, hibernate, delete) without an instance ID, use MultiSelect picker instead of single Select. Users can filter by typing, select all with ctrl+a, and pick multiple instances to batch-operate on. Single selection continues the existing single-instance flow. Multiple selections route to the batch execution path. * fix(vm): remove hint text from multi-select prompt The hint bar already shows keyboard shortcuts, no need to duplicate them in the prompt text. * feat(volume): add delete shortcut with multi-select batch support * feat(vm): add stop command to help and --hostname glob filter for batch - Make `stop` a standalone shortcut command visible in `verda vm --help` (previously hidden as alias of `shutdown`) - Add `--hostname` flag to all shortcut commands accepting a glob pattern (e.g., "test-*") that implies batch mode without requiring `--all` - Hostname filter combines with `--status` as AND logic - Add filterByHostname helper using filepath.Match for glob matching * feat(vm): add --location filter to vm list Client-side filtering since the API doesn't support location as a query parameter. Case-insensitive match. * deps: upgrade verdagostack to v1.2.0 MultiSelect now supports text filtering and ctrl+a select all, enabling interactive batch operations in vm and volume commands. * fix(vm): use verda.ActionDelete constant instead of string literal Fixes goconst lint error for repeated "delete" string. * refactor(vm): use SDK action constants instead of string literals Replace hardcoded action strings with verda.ActionStart, verda.ActionShutdown, verda.ActionDelete, etc. throughout shortcuts, action map, and batch code.
1 parent c5f9cdd commit d6fe448

10 files changed

Lines changed: 1554 additions & 37 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/spf13/pflag v1.0.10
1010
github.com/spf13/viper v1.21.0
1111
github.com/verda-cloud/verdacloud-sdk-go v1.4.2
12-
github.com/verda-cloud/verdagostack v1.1.3
12+
github.com/verda-cloud/verdagostack v1.2.0
1313
go.yaml.in/yaml/v3 v3.0.4
1414
)
1515

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
104104
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
105105
github.com/verda-cloud/verdacloud-sdk-go v1.4.2 h1:oVb8fHVQOY+YPuuMYMee9gYCkPTwAw01LmkqxM21T/Y=
106106
github.com/verda-cloud/verdacloud-sdk-go v1.4.2/go.mod h1:pmlpiCL9fTSikZ3qWLJPpHOG0E8PKkQVUX5s4Z+SktY=
107-
github.com/verda-cloud/verdagostack v1.1.3 h1:aJy8ixF5KR3bDy5k5n1fpEHKgZxCIt/FAH+HAvNidSM=
108-
github.com/verda-cloud/verdagostack v1.1.3/go.mod h1:9uKLNxvH7JRkHj2sxZc4TI5O/JbbveCrLkOkekoY/sA=
107+
github.com/verda-cloud/verdagostack v1.2.0 h1:HdVFXWyfEMq17ldn3+FrePYYB5Jdquz9D/Y0V+4d2BE=
108+
github.com/verda-cloud/verdagostack v1.2.0/go.mod h1:h9XR9uCYBYauRyGF4NLlScD5bC2UEMxMkqg6fUVjyDo=
109109
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
110110
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
111111
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=

internal/verda-cli/cmd/vm/action.go

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import (
1515

1616
// actionNameMap maps CLI flag values to action labels.
1717
var actionNameMap = map[string]string{
18-
"start": "Start",
19-
"shutdown": "Shutdown",
20-
"force_shutdown": "Force shutdown",
21-
"force-shutdown": "Force shutdown",
22-
"hibernate": "Hibernate",
23-
"delete": "Delete instance",
18+
verda.ActionStart: "Start",
19+
verda.ActionShutdown: "Shutdown",
20+
verda.ActionForceShutdown: "Force shutdown",
21+
"force-shutdown": "Force shutdown", // CLI alias (hyphenated)
22+
verda.ActionHibernate: "Hibernate",
23+
verda.ActionDelete: "Delete instance",
2424
}
2525

2626
// instanceAction defines a supported action with its display label and executor.
@@ -98,10 +98,14 @@ func availableActions(status string) []instanceAction {
9898
}
9999

100100
type actionOptions struct {
101-
InstanceID string
102-
Action string
103-
Yes bool
104-
Wait cmdutil.WaitOptions
101+
InstanceID string
102+
Action string
103+
Yes bool
104+
All bool
105+
Status string
106+
Hostname string
107+
WithVolumes bool
108+
Wait cmdutil.WaitOptions
105109
}
106110

107111
// NewCmdAction creates the vm action cobra command.
@@ -148,7 +152,7 @@ func runAction(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStream
148152
// In agent mode, --id and --action are required.
149153
if f.AgentMode() {
150154
var missing []string
151-
if opts.InstanceID == "" {
155+
if opts.InstanceID == "" && !opts.All && opts.Hostname == "" {
152156
missing = append(missing, "--id")
153157
}
154158
if opts.Action == "" {
@@ -159,6 +163,11 @@ func runAction(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStream
159163
}
160164
}
161165

166+
// Batch mode: --all or --hostname routes to batch execution.
167+
if opts.All || opts.Hostname != "" {
168+
return runBatchAction(cmd, f, ioStreams, opts)
169+
}
170+
162171
client, err := f.VerdaClient()
163172
if err != nil {
164173
return err
@@ -167,22 +176,16 @@ func runAction(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStream
167176
prompter := f.Prompter()
168177
ctx := cmd.Context()
169178

170-
// Select instance if not provided.
179+
// Select instance(s) if not provided.
171180
if opts.InstanceID == "" {
172-
// When action is pre-set (shortcut commands), only show instances
173-
// with statuses valid for that action.
174-
var statusFilter []string
175-
if opts.Action != "" {
176-
statusFilter = validFromForAction(opts.Action)
177-
}
178-
selected, err := selectInstance(ctx, f, ioStreams, client, statusFilter...)
179-
if err != nil {
180-
return err
181+
id, batchErr := resolveInstanceInteractive(cmd, f, ioStreams, client, opts)
182+
if batchErr != nil {
183+
return batchErr
181184
}
182-
if selected == "" {
183-
return nil
185+
if id == "" {
186+
return nil // canceled or routed to batch
184187
}
185-
opts.InstanceID = selected
188+
opts.InstanceID = id
186189
}
187190

188191
// Fetch instance details.
@@ -307,7 +310,7 @@ func runAction(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStream
307310
// runDeleteAgent handles delete in agent mode: requires --yes, deletes all volumes.
308311
func runDeleteAgent(ctx context.Context, f cmdutil.Factory, ioStreams cmdutil.IOStreams, client *verda.Client, inst *verda.Instance, yes bool) error {
309312
if !yes {
310-
return cmdutil.NewConfirmationRequiredError("delete")
313+
return cmdutil.NewConfirmationRequiredError(verda.ActionDelete)
311314
}
312315

313316
// In agent mode, delete the instance and all attached volumes.
@@ -327,14 +330,46 @@ func runDeleteAgent(ctx context.Context, f cmdutil.Factory, ioStreams cmdutil.IO
327330

328331
result := map[string]any{
329332
"id": inst.ID,
330-
"action": "delete",
333+
"action": verda.ActionDelete,
331334
"status": "completed",
332335
"volumes_deleted": len(volumeIDs),
333336
}
334337
_, _ = cmdutil.WriteStructured(ioStreams.Out, f.OutputFormat(), result)
335338
return nil
336339
}
337340

341+
// resolveInstanceInteractive handles interactive instance selection.
342+
// For shortcut commands (action pre-set), uses multi-select so users can pick
343+
// multiple instances. If multiple are selected, routes to batch and returns "".
344+
// Returns the selected instance ID, or "" if canceled/routed to batch.
345+
func resolveInstanceInteractive(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStreams, client *verda.Client, opts *actionOptions) (string, error) {
346+
ctx := cmd.Context()
347+
348+
var statusFilter []string
349+
if opts.Status != "" {
350+
statusFilter = []string{opts.Status}
351+
} else if opts.Action != "" {
352+
statusFilter = validFromForAction(opts.Action)
353+
}
354+
355+
// Shortcut commands use multi-select; generic "vm action" uses single-select.
356+
if opts.Action == "" {
357+
return selectInstance(ctx, f, ioStreams, client, statusFilter...)
358+
}
359+
360+
selected, err := selectInstances(ctx, f, ioStreams, client, statusFilter...)
361+
if err != nil {
362+
return "", err
363+
}
364+
if len(selected) == 0 {
365+
return "", nil
366+
}
367+
if len(selected) > 1 {
368+
return "", runBatchWithInstances(cmd, f, ioStreams, client, selected, opts)
369+
}
370+
return selected[0].ID, nil
371+
}
372+
338373
func selectInstance(ctx context.Context, f cmdutil.Factory, ioStreams cmdutil.IOStreams, client *verda.Client, statusFilter ...string) (string, error) {
339374
var sp interface{ Stop(string) }
340375
if status := f.Status(); status != nil {

0 commit comments

Comments
 (0)