From a4e8e51b5d63229bf04117a76adc890f5f1958a4 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Sat, 2 May 2026 22:18:26 -0700 Subject: [PATCH] feat(schemas): default Talos image build output and tighten dir constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shrink the minimum valid Talos image build config so authors can omit the entire `output:` block when the lab defaults are appropriate, and tighten `output.dir` to share the same #RelativePath constraint already applied to file inputs. - output.dir defaults to ".state/images" and is now #RelativePath, so absolute paths and `..` segments are rejected at validation time the same way userData.path already is. - output.bootArtifactName defaults to "talos-boot.img". - output.configArtifactName defaults to "talos-cidata.img". - output is now optional on #ImageBuild because every nested field has a default. The minimal example in README.md drops down to name + source.version + config.userData.path + config.metaData.localHostname. The generated Go types lose their NonEmptyString/ArtifactName constraints because CUE renders `*default | type` disjunctions as the underlying Go string. The cast removals in tools/labctl service.go follow from that — the validator still enforces the constraint at YAML parse time. Tests cover the new defaults via a minimal-config validator test and a new minimal-config txtar case asserting artifacts land at .state/images/talos-{boot,cidata}.img with no overrides. Co-Authored-By: Claude Opus 4.7 (1M context) --- schemas/lab/talos/README.md | 25 ++++++++++++------- schemas/lab/talos/cue_types_gen.go | 18 +++++++------ schemas/lab/talos/schema.cue | 20 +++++++++------ tools/labctl/cmd/labctl/main_test.go | 14 +++++++++++ .../testdata/script/talos_image_build.txtar | 5 ++++ .../adapters/talosconfig/validator_test.go | 19 ++++++++++++++ .../labctl/internal/app/talosimage/service.go | 6 ++--- 7 files changed, 80 insertions(+), 27 deletions(-) diff --git a/schemas/lab/talos/README.md b/schemas/lab/talos/README.md index bc9eaac..ef40167 100644 --- a/schemas/lab/talos/README.md +++ b/schemas/lab/talos/README.md @@ -9,6 +9,10 @@ The CUE schema is the source of truth; Go types are generated from `schema.cue`. ## Example +The minimum valid configuration relies on schema defaults for the Image +Factory URL, schematic ID, platform, architecture, artifact, output format, +output directory, and the boot/cidata artifact filenames: + ```cue package images @@ -16,18 +20,21 @@ import talos "github.com/gilmanlab/platform/schemas/lab/talos" build: talos.#ImageBuild & { name: "bootstrap-talos-controlplane" - source: { - version: "v1.13.0" - } + source: version: "v1.13.0" config: { userData: path: "controlplane.yaml" metaData: localHostname: "bootstrap-controlplane-1" } - output: { - dir: ".state/images/talos-bootstrap" - format: "img" - bootArtifactName: "talos-bootstrap-amd64.img" - configArtifactName: "talos-bootstrap-cidata.img" - } +} +``` + +Override any default by setting the field explicitly. For example, to land +artifacts in a different directory with bespoke filenames: + +```cue +build: output: { + dir: ".state/images/talos-bootstrap" + bootArtifactName: "talos-bootstrap-amd64.img" + configArtifactName: "talos-bootstrap-cidata.img" } ``` diff --git a/schemas/lab/talos/cue_types_gen.go b/schemas/lab/talos/cue_types_gen.go index 559679e..9d8c3ce 100644 --- a/schemas/lab/talos/cue_types_gen.go +++ b/schemas/lab/talos/cue_types_gen.go @@ -91,17 +91,20 @@ type MachineConfig struct { // ImageOutput describes the local artifacts produced by the build. type ImageOutput struct { - // Dir is the output directory for generated artifacts. - Dir NonEmptyString `json:"dir"` + // Dir is the output directory for generated artifacts. Must be a + // repo-relative path; defaults to ".state/images". + Dir string `json:"dir"` // Format is the local artifact format. Format OutputFormat `json:"format"` - // BootArtifactName is the Talos boot disk IMG filename. - BootArtifactName ArtifactName `json:"bootArtifactName"` + // BootArtifactName is the Talos boot disk IMG filename. Defaults to + // "talos-boot.img". + BootArtifactName string `json:"bootArtifactName"` - // ConfigArtifactName is the NoCloud cidata IMG filename. - ConfigArtifactName ArtifactName `json:"configArtifactName"` + // ConfigArtifactName is the NoCloud cidata IMG filename. Defaults to + // "talos-cidata.img". + ConfigArtifactName string `json:"configArtifactName"` } // ImageBuild is the top-level Talos image download and config packaging contract. @@ -115,6 +118,7 @@ type ImageBuild struct { // Config describes Talos machine configuration delivery. Config MachineConfig `json:"config"` - // Output describes the local artifacts produced by the build. + // Output describes the local artifacts produced by the build. Every + // field has a default, so authors can omit the block entirely. Output ImageOutput `json:"output"` } diff --git a/schemas/lab/talos/schema.cue b/schemas/lab/talos/schema.cue index d8f2bc5..45d8c30 100644 --- a/schemas/lab/talos/schema.cue +++ b/schemas/lab/talos/schema.cue @@ -97,14 +97,17 @@ package talos #ImageOutput: { @go(ImageOutput) - // Dir is the output directory for generated artifacts. - dir!: #NonEmptyString + // Dir is the output directory for generated artifacts. Must be a + // repo-relative path; defaults to ".state/images". + dir: *".state/images" | #RelativePath // Format is the local artifact format. format: #OutputFormat - // BootArtifactName is the Talos boot disk IMG filename. - bootArtifactName!: #ArtifactName @go(BootArtifactName) - // ConfigArtifactName is the NoCloud cidata IMG filename. - configArtifactName!: #ArtifactName @go(ConfigArtifactName) + // BootArtifactName is the Talos boot disk IMG filename. Defaults to + // "talos-boot.img". + bootArtifactName: *"talos-boot.img" | #ArtifactName @go(BootArtifactName) + // ConfigArtifactName is the NoCloud cidata IMG filename. Defaults to + // "talos-cidata.img". + configArtifactName: *"talos-cidata.img" | #ArtifactName @go(ConfigArtifactName) } // ImageBuild is the top-level Talos image download and config packaging contract. @@ -117,6 +120,7 @@ package talos source!: #ImageSource // Config describes Talos machine configuration delivery. config!: #MachineConfig - // Output describes the local artifacts produced by the build. - output!: #ImageOutput + // Output describes the local artifacts produced by the build. Every + // field has a default, so authors can omit the block entirely. + output: #ImageOutput } diff --git a/tools/labctl/cmd/labctl/main_test.go b/tools/labctl/cmd/labctl/main_test.go index 453b333..86c5545 100644 --- a/tools/labctl/cmd/labctl/main_test.go +++ b/tools/labctl/cmd/labctl/main_test.go @@ -93,6 +93,20 @@ output: return err } + minimalConfig := fmt.Appendf(nil, `name: talos-test +source: + factoryURL: %s + version: v1.13.0 +config: + userData: + path: controlplane.yaml + metaData: + localHostname: bootstrap-controlplane-1 +`, server.URL) + if err := os.WriteFile(filepath.Join(env.WorkDir, "talos-minimal.yaml"), minimalConfig, 0o600); err != nil { + return err + } + return nil } diff --git a/tools/labctl/cmd/labctl/testdata/script/talos_image_build.txtar b/tools/labctl/cmd/labctl/testdata/script/talos_image_build.txtar index 651a534..72c120c 100644 --- a/tools/labctl/cmd/labctl/testdata/script/talos_image_build.txtar +++ b/tools/labctl/cmd/labctl/testdata/script/talos_image_build.txtar @@ -43,6 +43,11 @@ stdout '"arch":"amd64"' stdout '"format":"img"' ! stderr . +exec labctl bootstrap talos image build talos-minimal.yaml +exists .state/images/talos-boot.img +exists .state/images/talos-cidata.img +! stderr . + -- invalid.yaml -- name: talos-test source: diff --git a/tools/labctl/internal/adapters/talosconfig/validator_test.go b/tools/labctl/internal/adapters/talosconfig/validator_test.go index 0bfa378..9d654d5 100644 --- a/tools/labctl/internal/adapters/talosconfig/validator_test.go +++ b/tools/labctl/internal/adapters/talosconfig/validator_test.go @@ -98,3 +98,22 @@ output: configArtifactName: talos-cidata.img ` } + +func TestValidateYAMLAppliesOutputDefaults(t *testing.T) { + minimal := `name: talos-test +source: + version: v1.13.0 +config: + userData: + path: controlplane.yaml + metaData: + localHostname: bootstrap-controlplane-1 +` + config, err := talosconfig.New().ValidateYAML("minimal.yaml", []byte(minimal)) + + require.NoError(t, err) + assert.Equal(t, ".state/images", config.Output.Dir) + assert.Equal(t, "talos-boot.img", config.Output.BootArtifactName) + assert.Equal(t, "talos-cidata.img", config.Output.ConfigArtifactName) + assert.Equal(t, "img", string(config.Output.Format)) +} diff --git a/tools/labctl/internal/app/talosimage/service.go b/tools/labctl/internal/app/talosimage/service.go index 28893c5..e94bf79 100644 --- a/tools/labctl/internal/app/talosimage/service.go +++ b/tools/labctl/internal/app/talosimage/service.go @@ -390,7 +390,7 @@ type buildPaths struct { } func resolvePaths(baseDir string, output schematalos.ImageOutput) (buildPaths, error) { - outputDir, err := resolvePath(baseDir, string(output.Dir)) + outputDir, err := resolvePath(baseDir, output.Dir) if err != nil { return buildPaths{}, err } @@ -398,8 +398,8 @@ func resolvePaths(baseDir string, output schematalos.ImageOutput) (buildPaths, e return buildPaths{ downloadsDir: filepath.Join(filepath.Dir(outputDir), "downloads", "talos"), outputDir: outputDir, - bootArtifactPath: filepath.Join(outputDir, string(output.BootArtifactName)), - configArtifactPath: filepath.Join(outputDir, string(output.ConfigArtifactName)), + bootArtifactPath: filepath.Join(outputDir, output.BootArtifactName), + configArtifactPath: filepath.Join(outputDir, output.ConfigArtifactName), }, nil }