Skip to content

Commit 1f75209

Browse files
hi-leiclaude
andcommitted
fix: filter images by instance type and use image_type slug
- Wizard stepImage now uses GetImagesByInstanceType to show only compatible images for the selected instance type (e.g. no CUDA images for CPU instances) - Template edit image picker filters by instance type - resolveImageName filters by instance type and resolves to image_type slug instead of image ID (API expects slug) - Template edit kind change clears instance type and image when they no longer match - Added back option to instance type picker in template edit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c60fb8b commit 1f75209

3 files changed

Lines changed: 51 additions & 18 deletions

File tree

internal/verda-cli/cmd/template/edit.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ func buildFieldMenu(tmpl *Template) []editableField {
144144
return nil //nolint:nilerr // user canceled
145145
}
146146
t.Kind = choices[idx]
147+
// Clear instance type and image when kind changes — they
148+
// may not be compatible with the new kind (e.g. CUDA images
149+
// are invalid for CPU instances).
150+
if t.InstanceType != "" && !matchKind(t.InstanceType, t.Kind) {
151+
t.InstanceType = ""
152+
}
153+
t.Image = ""
147154
return nil
148155
},
149156
},
@@ -281,9 +288,10 @@ func editInstanceType(ctx context.Context, f cmdutil.Factory, t *Template) error
281288
return errors.New("no instance types available")
282289
}
283290

291+
choices = append(choices, "← Back")
284292
idx, selErr := f.Prompter().Select(ctx, "Instance type", choices)
285-
if selErr != nil {
286-
return nil //nolint:nilerr // user canceled
293+
if selErr != nil || idx == len(values) {
294+
return nil //nolint:nilerr // user canceled or back
287295
}
288296
t.InstanceType = values[idx]
289297
return nil
@@ -323,7 +331,11 @@ func editImage(ctx context.Context, f cmdutil.Factory, t *Template) error {
323331
if err != nil {
324332
return err
325333
}
334+
// Filter images by instance type when available.
326335
images, err := cmdutil.WithSpinner(ctx, f.Status(), "Loading images...", func() ([]verda.Image, error) {
336+
if t.InstanceType != "" {
337+
return client.Images.GetImagesByInstanceType(ctx, t.InstanceType)
338+
}
327339
return client.Images.Get(ctx)
328340
})
329341
if err != nil {

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,29 +174,44 @@ func resolveTemplateNames(ctx context.Context, ioStreams cmdutil.IOStreams, clie
174174
return warnings
175175
}
176176

177-
// resolveImageName resolves an image name to its ID via the API.
178-
// For backward compatibility, if the name matches an existing image ID directly,
179-
// it is used as-is (handles templates created before name-based storage).
177+
// resolveImageName resolves an image name to its image_type slug via the API.
178+
// The API expects image_type (e.g. "ubuntu-24.04-cuda-13.0-open-docker"), not
179+
// the image ID. Filters by instance type when available so incompatible images
180+
// (e.g. CUDA on CPU) are rejected early with a clear warning.
180181
func resolveImageName(ctx context.Context, client *verda.Client, name string, opts *createOptions) (warnings []string) {
181182
if name == "" {
182183
return nil
183184
}
184-
images, err := client.Images.Get(ctx)
185+
var images []verda.Image
186+
var err error
187+
if opts.InstanceType != "" {
188+
images, err = client.Images.GetImagesByInstanceType(ctx, opts.InstanceType)
189+
} else {
190+
images, err = client.Images.Get(ctx)
191+
}
185192
if err != nil {
186193
return []string{fmt.Sprintf("failed to resolve image name: %v", err)}
187194
}
188195
// Try matching by name first.
189196
for _, img := range images {
190197
if img.Name == name {
191-
opts.Image = img.ID
198+
opts.Image = img.ImageType
199+
opts.imageName = img.Name
200+
return nil
201+
}
202+
}
203+
// Match by image_type slug (templates may store this directly).
204+
for _, img := range images {
205+
if img.ImageType == name {
206+
opts.Image = img.ImageType
192207
opts.imageName = img.Name
193208
return nil
194209
}
195210
}
196-
// Backward compat: if the value matches an image ID, use it directly.
211+
// Backward compat: if the value matches an image ID, resolve to image_type.
197212
for _, img := range images {
198213
if img.ID == name {
199-
opts.Image = img.ID
214+
opts.Image = img.ImageType
200215
opts.imageName = img.Name
201216
return nil
202217
}

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,48 +379,54 @@ func stepLocation(getClient clientFunc, cache *apiCache, opts *createOptions, mo
379379
// --- Step 6: OS Image ---
380380

381381
func stepImage(getClient clientFunc, opts *createOptions) wizard.Step {
382-
var imagesByID map[string]string // ID → Name lookup, built by Loader
382+
var imageNames map[string]string // ImageType → Name lookup, built by Loader
383383
return wizard.Step{
384384
Name: "image",
385385
Description: "Operating system image",
386386
Prompt: wizard.SelectPrompt,
387387
Required: true,
388-
Loader: func(ctx context.Context, _ tui.Prompter, status tui.Status, _ *wizard.Store) ([]wizard.Choice, error) {
388+
DependsOn: []string{"instance-type"},
389+
Loader: func(ctx context.Context, _ tui.Prompter, status tui.Status, store *wizard.Store) ([]wizard.Choice, error) {
389390
client, err := getClient()
390391
if err != nil {
391392
return nil, err
392393
}
394+
// Filter images by instance type when available.
395+
instType, _ := store.Collected()["instance-type"].(string)
393396
images, err := cmdutil.WithSpinner(ctx, status, "Loading OS images...", func() ([]verda.Image, error) {
397+
if instType != "" {
398+
return client.Images.GetImagesByInstanceType(ctx, instType)
399+
}
394400
return client.Images.Get(ctx)
395401
})
396402
if err != nil {
397403
return nil, fmt.Errorf("fetching images: %w", err)
398404
}
399-
imagesByID = make(map[string]string, len(images))
405+
imageNames = make(map[string]string, len(images))
400406
var choices []wizard.Choice
401407
for _, img := range images {
402408
if img.IsCluster {
403409
continue
404410
}
405-
imagesByID[img.ID] = img.Name
411+
imageNames[img.ImageType] = img.Name
406412
desc := ""
407413
if len(img.Details) > 0 {
408414
desc = strings.Join(img.Details, ", ")
409415
}
410416
choices = append(choices, wizard.Choice{
411417
Label: img.Name,
412-
Value: img.ID,
418+
Value: img.ImageType,
413419
Description: desc,
414420
})
415421
}
416422
return choices, nil
417423
},
418424
Default: func(_ map[string]any) any { return opts.Image },
419425
Setter: func(v any) {
420-
id := v.(string)
421-
opts.Image = id
422-
if imagesByID != nil {
423-
opts.imageName = imagesByID[id]
426+
slug := v.(string)
427+
opts.Image = slug
428+
if imageNames != nil {
429+
opts.imageName = imageNames[slug]
424430
}
425431
},
426432
Resetter: func() { opts.Image = ""; opts.imageName = "" },

0 commit comments

Comments
 (0)