Install the guest agent from a vendor-data script instead of editing the user's cloud-config
Merging the bootstrap into each Proxy's cloud-config was the wrong mechanism twice over. Detection only matched a literal #cloud-config first line, so a jinja template or a MIME archive still fell back to vendor-data, where cloud-init's default mergers drop it — the exact failure the merge existed to prevent. And the YAML round-trip re-typed unquoted scalars: version: 1.10 reached the guest as 1.1, comments were dropped and keys reordered. Neither is worth patching, because vendor-data delivered as a shell script bypasses the merger entirely: cloud-init hands vendor-data to its own handler chain, writes scripts to a separate directory and runs them from cc_scripts_vendor, after the user's packages and before the user's runcmd. The merge could not have been rescued anyway — merge_how in user-data governs only the first source, and merge_how in vendor-data is discarded before the document is stored. The Proxy's cloud-init now reaches the seed byte for byte in any format. installGuestAgent (default true) turns the script off; a configured vendorData travels beside it as a MIME multipart, declared text/plain so cloud-init re-sniffs it and this code never classifies user content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ Plan: [docs/plans/2026-09-10-2222-kvm-review-fixes.md](../plans/2026-09-10-2222-
|
||||
- [x] Step 1 — Unwedge deletion (finding 1)
|
||||
- [x] Step 2 — Validate names that become identifiers (finding 2)
|
||||
- [x] Step 3 — Read the SSH material lazily (finding 3)
|
||||
- [ ] Step 4 — Guest agent via vendor-data script (findings 4 and 5)
|
||||
- [x] Step 4 — Guest agent via vendor-data script (findings 4 and 5)
|
||||
- [ ] Step 5 — Validate, record, push
|
||||
|
||||
## Step 0 — plan committed
|
||||
@@ -98,6 +98,70 @@ a rotated Secret needs a manager restart. `TestNew_badKeyPathFailsFast` was
|
||||
inverted rather than deleted, so the old contract stays visible in the diff
|
||||
next to the new one.
|
||||
|
||||
## Step 4 — Guest agent via vendor-data script
|
||||
|
||||
The review filed two findings against `withGuestAgent` — detection too
|
||||
narrow, and YAML re-typing on the round-trip. Reading cloud-init 26.1's
|
||||
sources while planning turned both into one finding: the operator should
|
||||
not be editing the user's document at all.
|
||||
|
||||
Vendor-data gets its own handler chain (`stages.py:_default_vendordata_handlers`),
|
||||
with separate script and cloud-config paths. A vendor-data **script** is
|
||||
therefore written to the vendor script directory and run by
|
||||
`cc_scripts_vendor`, never touching the config merger that drops colliding
|
||||
keys. On the final-stage module list it lands after
|
||||
`package_update_upgrade_install` and before the user's `runcmd`, so the
|
||||
agent now comes up *earlier* than the old `runcmd` append did.
|
||||
|
||||
That also closed the door the previous approach had left ajar. The merge
|
||||
cannot be rescued from inside either document: `merge_how` in user-data
|
||||
governs only the first source, and `merge_how` in vendor-data is popped by
|
||||
`dict_extract_mergers` and discarded before the document is written. There
|
||||
was no fix that kept the merge.
|
||||
|
||||
`withGuestAgent`, `isCloudConfig` and `appendUnique` are gone, along with
|
||||
seed.go's `sigs.k8s.io/yaml` dependency. `installGuestAgent` (default true)
|
||||
turns the script off. When `vendorData` is also configured the two ship as
|
||||
a MIME multipart, with the user's document declared `text/plain` — a type
|
||||
cloud-init re-sniffs from the content's own header line, so this code never
|
||||
classifies what the user wrote.
|
||||
|
||||
Before deleting the old path, its damage was captured rather than taken on
|
||||
trust. Round-tripping a plausible cloud-config through it:
|
||||
|
||||
```sh
|
||||
# temporary test in the package, deleted afterwards
|
||||
go test -run TestZZProbeRetyping -v ./internal/provider/kvm/
|
||||
# in: version: 1.10
|
||||
# out: version: 1.1
|
||||
```
|
||||
|
||||
`1.10` arriving as `1.1` is worse than the reviewer's `passwd` example,
|
||||
since nothing about it looks wrong in the seed. Comments were dropped and
|
||||
keys reordered on the same pass.
|
||||
|
||||
The generated vendor-data was then checked against the cloud-init actually
|
||||
installed on this machine, rather than against the docs:
|
||||
|
||||
```sh
|
||||
python3 -c 'from cloudinit import user_data, handlers; ...'
|
||||
# vendor-script: multipart=False sniffed=text/x-shellscript
|
||||
# vendor-multipart: multipart=True part 1 text/plain -> text/cloud-config
|
||||
# part 2 text/x-shellscript
|
||||
```
|
||||
|
||||
and the script itself through `sh -n`.
|
||||
|
||||
Worth noting: the script has to choose a package manager where cloud-init's
|
||||
`packages:` key was distro-agnostic. It covers apt, dnf, yum, zypper and
|
||||
apk, and both systemd and OpenRC for the enable step, but an image outside
|
||||
that set now needs `installGuestAgent: false` and the agent baked in. The
|
||||
seed's user-data is byte-identical to the Proxy's `cloudInit` from here on,
|
||||
which is what the new `TestBuildSeedISO_userDataIsNeverRewritten` pins
|
||||
across seven formats. One consequence worth remembering: a user-data that
|
||||
sets `vendor_data: {enabled: false}` disables the agent, and cloud-init
|
||||
documents that as intended behaviour.
|
||||
|
||||
Worth noting: the review filed this as a KVM finding, but the fix is mostly
|
||||
in the controller, because KVM is the only provider that resolves a
|
||||
providerID segment against operator config at all. GCP and Kubernetes hand
|
||||
|
||||
Reference in New Issue
Block a user