|
| 1 | +# Template Command Knowledge |
| 2 | + |
| 3 | +## Quick Reference |
| 4 | +- Parent: `verda template` (alias: `tmpl`) |
| 5 | +- Subcommands: `create`, `list` (alias `ls`), `show`, `delete` (alias `rm`) |
| 6 | +- Files: |
| 7 | + - `template.go` -- Parent command, registers subcommands |
| 8 | + - `create.go` -- Create command, name validation, runs VM wizard in template mode |
| 9 | + - `list.go` -- List command with `--type` filter and structured output |
| 10 | + - `show.go` -- Show command with field display and structured output |
| 11 | + - `delete.go` -- Delete command with confirmation prompt |
| 12 | + - `types.go` -- Re-exports types/functions from shared `internal/verda-cli/template/` |
| 13 | + |
| 14 | +## Domain-Specific Logic |
| 15 | + |
| 16 | +### Template YAML Format |
| 17 | +All fields except `resource` are optional. Stored at `~/.verda/templates/<resource>/<name>.yaml`. |
| 18 | + |
| 19 | +| Field | Type | Description | |
| 20 | +|-------|------|-------------| |
| 21 | +| `resource` | string | Resource type, currently only `"vm"` | |
| 22 | +| `billing_type` | string | `"on-demand"` or `"spot"` | |
| 23 | +| `contract` | string | `"PAY_AS_YOU_GO"`, `"SPOT"`, `"LONG_TERM"` | |
| 24 | +| `kind` | string | `"GPU"` or `"CPU"` | |
| 25 | +| `instance_type` | string | e.g. `"1V100.6V"` | |
| 26 | +| `location` | string | e.g. `"FIN-01"` | |
| 27 | +| `image` | string | OS image slug | |
| 28 | +| `os_volume_size` | int | GiB | |
| 29 | +| `storage` | []StorageSpec | Each has `type` and `size` | |
| 30 | +| `storage_skip` | bool | Skip storage step in wizard | |
| 31 | +| `ssh_keys` | []string | Key **names** (not IDs) | |
| 32 | +| `startup_script` | string | Script **name** (not ID) | |
| 33 | +| `startup_script_skip` | bool | Skip startup script step in wizard | |
| 34 | +| `hostname_pattern` | string | Pattern with `{random}` and `{location}` placeholders | |
| 35 | + |
| 36 | +### Name Validation and Auto-Reformatting |
| 37 | +- Valid names match `^[a-z0-9][a-z0-9-]*$` |
| 38 | +- `normalizeName()` auto-formats: lowercase, replace spaces/underscores with hyphens, strip invalid chars, collapse consecutive hyphens, trim leading/trailing hyphens |
| 39 | +- Create command re-prompts on invalid names and on name collisions with existing templates |
| 40 | + |
| 41 | +### Template Resolution (--from flag) |
| 42 | +- If ref contains `"/"` or ends with `".yaml"` -> treated as a file path |
| 43 | +- Otherwise -> resolved as `~/.verda/templates/vm/<ref>.yaml` |
| 44 | +- Empty ref (bare `--from`) -> interactive picker via `pickTemplate()` |
| 45 | +- Resolution logic lives in `internal/verda-cli/template/template.go` `Resolve()` |
| 46 | + |
| 47 | +### Skip Flags |
| 48 | +- `storage_skip: true` -> wizard skips additional storage step entirely |
| 49 | +- `startup_script_skip: true` -> wizard skips startup script step entirely |
| 50 | +- Captured when user selects "None (skip)" during template creation wizard |
| 51 | +- Maps to `opts.storageSkip` and `opts.startupScriptSkip` in `createOptions` |
| 52 | + |
| 53 | +### Hostname Pattern Expansion |
| 54 | +- `{random}` -> 3 petname words joined by hyphens (via `github.com/dustinkirkland/golang-petname`) |
| 55 | +- `{location}` -> lowercased location code (e.g. `"FIN-01"` -> `"fin-01"`) |
| 56 | +- Only expanded when `hostname_pattern` is set AND `opts.Hostname` is empty (no `--hostname` flag) |
| 57 | + |
| 58 | +### SSH Keys and Startup Scripts |
| 59 | +- Stored in template by **name**, not ID |
| 60 | +- Resolved to IDs at `vm create --from` time via `resolveSSHKeyNames()` and `resolveStartupScriptName()` |
| 61 | +- On API error or name not found, produces a warning and the wizard prompts later |
| 62 | +- Names are stored in `opts.sshKeyNames` / `opts.startupScriptName` for template-saving round-trip |
| 63 | + |
| 64 | +## Gotchas & Edge Cases |
| 65 | + |
| 66 | +- **Import cycle**: `cmd/template/` cannot import `cmd/vm/` for the Template type (circular dependency). Shared types live in `internal/verda-cli/template/`, re-exported by `cmd/template/types.go` via type aliases and `var` bindings. |
| 67 | +- **`billingTypeSet` / `locationSet` flags**: Needed because `IsSet` in the wizard can't distinguish `"on-demand"` (falsy `IsSpot=false`) from "unset". When a template sets billing type or location, these booleans are set to `true` so the wizard skips those steps. |
| 68 | +- **`NoOptDefVal` on `--from` flag**: Set to `" "` (space) so `--from` without a value is recognized as "flag changed but empty". When the user writes `verda vm create --from gpu-training`, cobra parses `gpu-training` as a positional arg; `RunE` recombines it into `opts.From`. |
| 69 | +- **Startup script "None (skip)" label**: The wizard presents "None (skip)" as a selectable option. Previously, this label text was captured as the script name. Fixed by checking `Value != ""` before storing the name. |
| 70 | +- **`ensurePricingCache`**: The confirm-deploy step calls this to fetch instance type and volume type pricing when the cache is empty. This happens when a template pre-filled earlier steps (skipping the steps that normally populate the cache). |
| 71 | +- **Only first storage entry applied**: `applyTemplate()` only reads `tmpl.Storage[0]` because the wizard's convenience fields (`StorageSize`/`StorageType`) support a single additional volume. |
| 72 | +- **AutoDescription**: `Template.AutoDescription()` joins non-empty `InstanceType`, `Image`, and `Location` with `", "` for the list view. |
| 73 | +- **Directory permissions**: Template directories created with `0700`, files with `0644`. |
| 74 | +- **Non-existent directory**: `List()` and `ListAll()` return `nil, nil` (not an error) when the templates directory doesn't exist yet. |
| 75 | + |
| 76 | +## Relationships |
| 77 | + |
| 78 | +- **`internal/verda-cli/template/`** -- Shared types (`Template`, `StorageSpec`, `Entry`) and I/O functions (`Save`, `Load`, `LoadFromPath`, `Resolve`, `List`, `ListAll`, `Delete`, `ValidateName`, `ExpandHostnamePattern`). Breaks the import cycle between `cmd/template/` and `cmd/vm/`. |
| 79 | +- **`cmd/vm/wizard.go`** -- `WizardMode` (Deploy vs Template), `RunTemplateWizard()` (runs wizard without hostname/description/confirm steps), `TemplateResult` struct, `ensurePricingCache()` |
| 80 | +- **`cmd/vm/template_apply.go`** -- `loadTemplateRef()`, `applyTemplate()`, `resolveTemplateNames()`, `resolveSSHKeyNames()`, `resolveStartupScriptName()`, `printTemplateSummary()`, `pickTemplate()` |
| 81 | +- **`cmd/vm/create.go`** -- `--from` flag definition, `resolveCreateInputs()` orchestrates template loading + wizard invocation, `createOptions` struct with template-related internal fields (`billingTypeSet`, `locationSet`, `storageSkip`, `startupScriptSkip`, `sshKeyNames`, `startupScriptName`) |
| 82 | +- **`cmdutil`** -- `Factory`, `IOStreams`, `LongDesc`, `Examples`, `DefaultSubCommandRun`, `WriteStructured` |
| 83 | +- **`clioptions`** -- `VerdaDir()` for resolving `~/.verda/` base path |
| 84 | +- **`petname`** -- `github.com/dustinkirkland/golang-petname` for `{random}` hostname expansion |
0 commit comments