0117. ichor init reads a structured config file
Status: Accepted · Date: 2026-11-04 · Design section: Bootstrap · Touches: 0019, 0094, 0113
Decided: ichor init -f init.yaml reads the init settings from an InitConfig file (apiVersion: ichor.maesh.dev/v1alpha1) instead of a long flag line. The file is decoded strictly into one typed Go struct (unknown, duplicate and mistyped keys, and a wrong apiVersion or kind, are errors that name the file and the key) and every value is then set through the init flag it stands for, so the file adds no second code path: parsing, defaults and validation are the flags' own, and a file and the equivalent flags behave the same. A flag given on the command line overrides the file. The required flags (--cluster, --endpoint-address, --node-endpoint, --install-disk) are required unless the file gives them; the error names the flag and the file key. Relative paths in the file (ui.tls.certFile, ui.tls.keyFile) resolve against the file's directory. The file holds paths, never key material. --print-config-template prints the commented example, which a test parses.
Context
A real first install needs a dozen or more flags (endpoint, subnets, mirrors, disk selectors, discovery, UI URL, TLS, kube API). The line is long, hard to review and easy to get subtly wrong between runs. The user asked for a file for v0.1.0.
Decision
1. Format
apiVersion: ichor.maesh.dev/v1alpha1
kind: InitConfig
cluster:
name: demo # --cluster (required)
namespace: ichor-system # -n/--namespace
preset: single # --preset
endpoint:
type: node # --endpoint-type
address: 192.168.1.50 # --endpoint-address (required)
network:
podSubnets: # --pod-subnets
- 10.248.0.0/16
serviceSubnets: # --service-subnets
- 10.249.0.0/16
cni:
tunnel: geneve # --cni-tunnel
registryMirrors: # --registry-mirror HOST=URL, as a map
ghcr.io: http://192.168.1.10:5000
firstNode:
endpoint: 192.168.1.50:50000 # --node-endpoint (required)
installDisk: /dev/sda # --install-disk (required)
disks:
ephemeralSelector: 'disk.wwid == "..."' # --ephemeral-disk-selector
storage: # --storage-disk-selector, --storage-volume-name
selector: 'disk.wwid == "..."'
volumeName: storage
userVolume: # --user-volume-disk-selector, --user-volume-name
selector: 'disk.wwid == "..."'
volumeName: data
discovery:
addresses: # --discover-address
- 192.168.1.51
cidrs: # --discover-cidr
- 192.168.1.64/29
ui: # leave the whole block out for no sign-in (no --ui-url)
url: https://192.168.1.60 # --ui-url
adminUser: admin # --admin-user
tls: # optional; leave out for Ichor's own CA
certFile: ./ui.crt # --ui-tls-cert
keyFile: ./ui.key # --ui-tls-key
# secretName: ichor-ui-cert # --ui-tls-secret, instead of certFile and keyFile
kubeAPI:
serverURL: https://192.168.1.60:6443 # --kube-api-server-url
tlsServerName: 192.168.1.50 # --kube-api-tls-server-name
waitTimeout: 30m # --wait-timeout
Lists are written in block style (one - item per line) in everything Ichor shows; the parser accepts the inline form as well. The hidden and deprecated flags (--cni, --talos-version, --kubernetes-version, --version-contract) stay flag-only: the release set decides them (ADR 0113). An empty value in the file is the same as leaving the key out.
2. One code path
bindInitFlags registers the flags on any flag set; applyInitFile decodes the file and calls Set on the flag of each key it holds, skipping a flag whose Changed is already true. initOptions therefore needs no second constructor, and validateInitUIExtras, validateInitUsers and the plan validation see the same struct whichever way it was filled. ui.tls.secretName together with certFile fails in validateInitUIExtras, with the flag-form message.
3. Required settings
The four required flags were enforced by cobra before RunE, which cannot know about the file. They are now checked in RunE after the file is applied, with the same wording as cobra (required flag(s) "cluster" (file key cluster.name), ... not set). A setting is present if a flag set it or the file did. The check is not weaker: a value neither source gives is refused before anything happens. The hidden --cni flag was in the list by mistake although it has a default and cilium is the only CNI (ADR 0101); it is not required.
4. Paths and secrets
A relative certFile or keyFile resolves against the directory of the file, not the working directory, so a file and its certificate can be moved together. The file holds only paths: key bytes are read by the existing readUITLSFiles and never printed or put in an error.
5. Template
ichor init --print-config-template prints the example above with comments. The text lives in one constant, initConfigTemplate, and a test parses it strictly, so it cannot drift from the struct.
Rejected
- A second options type with its own validation: two paths that would drift.
- A raw-YAML escape hatch for machine configuration: the file sets the init flags only; machine configuration stays typed (
TalosConfigPatch). - Key material in the file: a file that gets committed or pasted must not be a secret.
- Version fields in the file: ADR 0113 and 0115 keep the release set out of the install's inputs.
Test
Table test: the full example gives the same initOptions as the equivalent flag line. A flag overrides the file. An unknown key, a duplicate key, a wrong type, apiVersion or kind fails naming the key and the file. A relative certFile resolves against the file's directory. A missing required setting names the flag and the key. An omitted ui block turns no users on. secretName with certFile fails the existing validation. The template parses and has no inline lists.