0019. ichor init: scope, the namespace decision, and the gaps it leaves open
Status: Accepted; superseded in part by ADR 0099 · Date: 2026-10-25 · Design section: Bootstrap (ichor init)
Decided: ichor init: VIP recommended not required, Ichor applied through the API server not inlined, topology presets carry count/schedulable
Context
docs/design/architecture.md's "Bootstrap" section gives ichor init six
steps and one example invocation:
ichor init --preset standard --cni cilium [--argocd --repo URL --path clusters/NAME] [--gitops-dir DIR]
This phase builds steps 1-5. Step 6 (hand-over) is excluded on purpose: it is a continuous reconciliation loop against Argo CD's own status (watch, poll, retry), not a one-shot action a CLI invocation can perform and then exit. It belongs to the controller, as a later unit's work.
Decision: VIP recommended, insecure apply's assumption stated
VIP is recommended, not required. buildInitPlan accepts any of the
CRD's four endpoint types (vip, loadBalancer, dns, node) and only
prints a warning -- never an error -- when the type isn't vip, carrying
the design document's own reason in the warning text (changing the
endpoint later means re-issuing every certificate in the cluster).
Insecure apply assumes a separate provisioning network, and says so.
ichor init --help and the interactive confirmation prompt both state
this explicitly, because maintenanceApplier.ApplyInsecure genuinely
cannot authenticate the node it's talking to -- that is what "insecure"
means here, per architecture.md's "Trust" section.
Decision: Ichor itself is applied through the API server, not inlined
The design's original step 3 said cluster.inlineManifests carries "Ichor
itself, the CNI (unless flannel), and, with --argocd, Argo CD plus a
root Application." Building it showed that claim does not hold once you
ask why each one needs to be there:
- The CNI genuinely has to be inlined: a node is not Ready and nothing can be scheduled before a CNI exists, and nothing else can put one there before the API server is even up.
- Ichor does not have that constraint. Design step 4 already gets a
working kubeconfig before step 5 creates any CR; by that point the API
server is reachable, and applying Ichor's own install manifest there
works exactly like any other
kubectl apply. It is also more debuggable: a failure applying through the API server comes back as an ordinary error, where a failure insidecluster.inlineManifestsis buried in Talos's own boot log.
So cluster.inlineManifests now carries only the CNI's bootstrap
manifest (cniBoot.InlineManifest, nil for flannel). Ichor's own
install manifest is applied separately, after step 4, by
applyIchorManifest, straight through the API server:
- The manifest is
make build-installer'sdist/install.yaml, embedded in theichorbinary at build time (internal/installmanifest), mirroringcmd/ichor-ui'suiembed/committed-placeholder pattern exactly (ADR 0017):make build-ichor-embedcopiesdist/install.yamlintointernal/installmanifest/install.yaml(gitignored) and builds with-tags installembed; a plaingo build ./..., with no priormakestep, embeds the committedplaceholder.yamlinstead, so a fresh checkout keeps compiling.applyIchorManifestrefuses outright if the embedded bytes are still the placeholder (checked by a sentinel string, not by trying to apply it and letting that fail some other way). - The embedded controller version can never drift from the CLI's own
version, because both come out of the same
go buildinvocation -- there is no separate fetch step, and no version flag for an operator to get wrong. The property this gives phase 10 (air-gap): the binary already carries everything it installs, nothing to mirror separately for this one component. - CRDs need a moment to become Established before resources that depend
on them can be created (kustomize's own ordering already puts the
Namespace and CRDs first in
dist/install.yaml, but "created" and "Established" are not the same moment).createWithRetrybounds this at 10 attempts, 500ms apart. (shortcut:a fixed budget, not a watch on the CRD'sEstablishedcondition; upgrade if a slower cluster ever needs longer than 5s here.)
Argo CD itself is never installed by init, at all, with or without
--argocd. Fetching and applying a third-party install manifest during
bootstrap means deciding which version, signed by whom, from where --
that is not this command's call to make, and architecture.md's "Ichor
doesn't install or operate CNI, storage or monitoring; it bootstraps CNI
and Argo CD once and hands them over" does not mean "fetches it from the
internet during a privileged bootstrap." --argocd prepares exactly two
things that are ready for use once Argo CD exists some other way: the
repository credentials Secret, and (with --gitops-dir) the root
Application. initArgoCD prints this plainly before doing either --
naming the Argo CD version range (v3.5.x+) this command's
Application/sync-option shapes were verified against -- specifically so
an operator who ran --argocd does not assume it means Argo CD is
running.
docs/design/architecture.md's step 3 and its "Hand-over" paragraph were
rewritten to match: the CNI is now the only component that follows the
inject-through-inlineManifests-then-remove path; Ichor and Argo CD
never did.
Decision: topology presets carry count and schedulable; a single --namespace
Four presets, not a single hardcoded one. presetTopology maps each
of single (1+0, schedulable), compact-ha (3+0, schedulable),
standard (3+n, tainted, recommended) and large (5+n, tainted) to the
control-plane count and schedulable value architecture.md's "Nodes"
section pairs it with. init derives both from --preset rather than
taking them as independent flags -- which is what makes "control planes
unschedulable with zero workers" (the combination the design explicitly
rejects) unreachable by construction: every schedulable=false row in
the table is one whose preset is sized for separate worker nodes added
later. TestPresetTopology_NeverRejectsSchedulableWithNoWorkers checks
the table itself stays that way; buildInitPlan's own rejection
(TestBuildInitPlan_RejectsUnschedulableWithNoWorkers, which inserts a
deliberately-wrong fifth entry for the duration of the test) is kept as
the real enforcement point for whatever a future preset might get wrong,
even though today's four rows never reach it.
One --namespace, not two. TalosClusterSpec.SecretsRef's doc
comment (ADR 0011) says the bundle Secret lives in the controller's own
namespace, which need not be the TalosCluster's -- RBAC cannot express
"the Secret this TalosCluster names, wherever it lives" below cluster
scope. Since applyIchorManifest now actually deploys the controller,
and config/manager's kustomize base hardcodes its namespace to
ichor-system (matching the RBAC markers' own literal), --namespace
defaults to ichor-system and buildInitPlan warns -- not rejects -- if
it is overridden, since a Secret or CR created elsewhere would be
invisible to the controller this same run just deployed. This remains a
simplification specific to the self-managed default (one controller, one
namespace, created by this same command); hub mode would need the
controller's namespace and the TalosCluster's as two separate flags.
Decision: disk roles, including the three that are now representable
Architecture.md's "Nodes" section names five roles: system (the install
target), ephemeral, storage, userVolume and unused. At the time
this phase started, only system had anywhere to go -- machine.install
-- so that is what shipped first: a required --install-disk flag,
folded into the same Config-kind patch as the inline manifests
(buildInlineManifestsPatch), with real values for
machine.type/machine.certSANs (the node's own role and address), not
placeholders, since this document is authored directly rather than
reconstructed from a diff.
The other three roles became representable once another unit extended
the mirror allow-list with VolumeConfig, UserVolumeConfig and
RawVolumeConfig (and pkg/render.DocumentFromPatch was extended here,
mechanically, to actually convert the three new TalosConfigPatchDocument
kinds -- the allow-list alone did not make them renderable). init now
exposes one optional CEL-selector flag per role
(--ephemeral-disk-selector, --storage-disk-selector,
--user-volume-disk-selector, each paired with an optional *-volume-name
for the latter two), building a VolumeConfig/RawVolumeConfig/
UserVolumeConfig document per selector given, appended to the same
bootstrap patch:
- Selectors, not device paths. Architecture.md is explicit that disks
are identified by WWID, serial or by-id path, not
/dev/sdX, so these flags take the same CEL expressionDiskSelector.Matchitself takes (e.g.disk.wwid == "...").initdoes not translate a device path into a selector on the operator's behalf -- it has no way to query the node's disk inventory yet, so it would be guessing at field names instead of using what the operator already knows about their own hardware. ephemeral's name is fixed (constants.EphemeralPartitionLabel,"EPHEMERAL"): Talos validates a systemVolumeConfig's name against its own known set, confirmed by readingvolume_config.go'svalidateVolumeConstraints, not assumed.storage/userVolumeneed a size hint. Talos's ownProvisioningSpec.Validaterejects aRawVolumeConfig/UserVolumeConfigwith neitherminSizenormaxSizeset ("min size or max size is required") -- confirmed by running it and reading the error, not by memory. Each getsminSize: "0"(a real, non-empty size, not an unset field --ByteSize.IsZero()checks for a nil/absent value, and"0"parses to a present zero) and nomaxSize, so it grows to fill its own disk.- No sharing, so no negative
maxSize. Architecture.md's note that a negativemaxSizemeans "space to leave free" (available - value), not "space to consume," only matters once two roles share one disk (EPHEMERAL reserving space with a negativemaxSize, a user volume taking what's left). Every role flag here implies its own dedicated disk -- the architecture doc's own "recommended layout," not the discouraged shared one -- so there is nothing here that needs a negative value, and the shared-disk case is not built. - No disk encryption flag.
EncryptionKey.KeyStaticwas dropped from the mirror type as secret material (same day, by the unit extending the allow-list); the remaining key kinds (KMS, TPM, node ID) are each their own integration, out of scope here.
Decision: a known, pre-existing test collision in a file this unit does not own
Verifying this work (make build test lint) surfaced one internal/controller
test failure: discovery_test.go's uuidRejected and
taloscluster_controller_test.go's uuidClusterReconcile are both the
literal "70707070-7070-7070-7070-707070707070", written independently by
different units sharing the same envtest API server within one Ginkgo
run -- whichever spec runs first creates a TalosMachine with that name,
and the other's own Create then fails with AlreadyExists. Confirmed
reproducible (same failing spec across two runs with different random
seeds, ruling out an ordering-dependent flake) and confirmed unrelated to
this unit's own diff (neither file is touched by any commit in this ADR's
history). internal/controller is out of this unit's scope to edit, so
this is recorded here rather than fixed: make test is green for every
package this ADR's work touches (internal/cli, pkg/cni, pkg/gitops,
pkg/render, internal/installmanifest), and fails only on this one,
pre-existing, out-of-scope collision.
Consequences
-
ichor init --argocd --gitops-dir DIRwithout Argo CD already installed on the target cluster produces a correct, ready-to-use GitOps skeleton and (if--repo-username/--repo-passwordwere given) a repository credentials Secret, but does not itself bring Argo CD up -- and says so. An operator installs Argo CD by some other means before its root Application can do anything. -
The CNI's own Argo CD Application lands in the GitOps skeleton's
apps/cni/application.yamlbecausepkg/cni.Preset.ArgoCDApplicationreturnspkg/gitops's own typed*gitops.Application(ADR 0013's and ADR 0015's updates) -- no conversion needed between the two packages. -
Hub mode, if built later, will need
ichor init(or whatever supersedes it) to take the controller's namespace and theTalosCluster's as two separate flags; today's single--namespaceis correct only because the self-managed default makes them the same namespace. -
Per-disk role selection today is one CEL selector per role, each implying a dedicated disk. The shared-disk layout (
systemDiskLayout: shared, EPHEMERAL reserving negativemaxSizeso a user volume gets what's left) is not built; architecture.md calls it "discouraged" for exactly the I/O-contention reason that makes it a reasonable thing to leave for whoever actually needs it. -
two UUID literals colliding across internal/controller/discovery_test.go and taloscluster_controller_test.gois a known, reported, unfixed bug outside this unit's scope; whoever next touches either file should pick a different literal for one of them. -
--discover-addressand--discover-cidr(repeatable) fillspec.discoverywith the same IPv4 and /24-or-smaller limits the CRD enforces, so nodes booted into maintenance mode after init are found without a hand patch (ADR 0011); unset leaves discovery off.