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.

Search the docs

Search pages and headings