internals/skills/cloud-init-renderer/SKILL.md
Pure renderer from VmSpec + VmCloudInit to NoCloud seed ISO (user-data + meta-data + network-config). Covers composeUsers adopt-merge, SMBIOS vs cloud_init additive channels, xorriso ISO emission, and charly_install.strategy state machine. Source: charly/cloud_init_render.go, charly/cloud_init_iso.go, charly/charly_install.go. MUST be invoked before editing cloud-init emission paths.
npx skillsauth add overthinkos/overthink-plugins cloud-init-rendererInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Host-side renderer producing NoCloud seed ISOs for cloud_image VMs (and bootc VMs that include the cloud-init layer). Pure transformation — given VmSpec + VmCloudInit, produces three files (user-data, meta-data, network-config) and packages them into a FAT-labeled cidata ISO via xorriso.
Lives host-side, in the charly binary. The guest-side /charly-distros:cloud-init layer is complementary: it puts the cloud-init package into the bootc guest OS so that guest reads the seed ISO. The two sides cooperate across the host/guest boundary.
| File | Contents |
|---|---|
| charly/cloud_init_render.go | RenderCloudInit, ResolveKeyInjectionChannels, composeUsers, composePackages, composeRunCmd |
| charly/cloud_init_iso.go | WriteSeedISO via xorriso; genisoimage + mkisofs fallbacks |
| charly/charly_install.go | EnsureCharlyInGuest state machine (auto/scp/url/skip strategies) |
| charly/spec/cue_types_gen.go (generated) | VmCloudInit, VmCloudInitUser, VmCloudInitFile, VmCloudInitNetwork, VmCloudInitMirrors, VmCharlyInstall |
func RenderCloudInit(spec *VmSpec, rt CloudInitRuntimeParams) (userData, metaData, networkConfig string, err error)
Returns three strings — the three files NoCloud expects on the seed ISO. The caller (BuildCloudImage or the vm deploy preflight) then calls WriteSeedISO to pack them.
Egress validation gate. Before returning, each rendered document is validated
against a CUE schema — user-data against the vendored Canonical cloud-config
schema (#CloudConfig), meta-data against #CloudInitMeta, network-config against
#NetworkConfigV2 (via ValidateEgress). A malformed render fails here, so a
cloud-init that its own schema would reject never reaches the seed ISO. The gate
is owned by /charly-internals:egress.
Single most important function in this renderer. Produces the users: list in user-data.
When spec.Source.BaseUser is non-empty (cloud_image adopt pattern):
users:
- default # cloud-init sentinel: preserve distro's default account
- name: <base_user>
ssh_authorized_keys:
- <pubkey>
No useradd, no sudoers write, no shell change — cloud-init interprets "name: X" on an existing account as "append ssh_authorized_keys". The parity is exact with the container-side base_user: + user_policy: adopt pattern (/charly-image:image "user_policy").
When BaseUser is empty and VmSSH.User is non-empty (create pattern):
users:
- default
- name: <ssh_user>
sudo: ALL=(ALL) NOPASSWD:ALL
groups: [wheel, sudo]
shell: /bin/bash
lock_passwd: true
ssh_authorized_keys:
- <pubkey>
Full account provisioning. Used by bootc VMs where no base_user: applies.
When a user already appears in spec.CloudInit.Users: the renderer appends the ssh pubkey to that existing entry instead of emitting a new one. Lets authors declare a user with specific sudo/groups/shell fields and still get the pubkey injected.
Applies the per-source-kind auto-defaults documented in /charly-internals:vm-spec:
func ResolveKeyInjectionChannels(spec *VmSpec) (smbios bool, cloudInit bool) {
if spec.SSH != nil && spec.SSH.KeyInjection != nil {
// explicit overrides
return spec.SSH.KeyInjection.SMBIOS == "enabled" (with "auto" = source-kind default),
spec.SSH.KeyInjection.CloudInit == "enabled" (with "auto" = source-kind default)
}
// per-source-kind auto-defaults
switch spec.Source.Kind {
case "cloud_image":
return true, true // belt + suspenders
case "bootc":
return true, false // cloud_init channel only activates with cloud-init layer
}
}
Both channels are additive — when both are on, systemd-ssh-generator (SMBIOS path) and cloud-init (user-data path) both inject the key. Dedup happens in the guest's authorized_keys. There's no correctness issue with duplicate keys; the dual-injection pattern is the safe default.
The renderer prepends defaults to user-declared lists:
composePackages: prepends {openssh, curl, tar} (deduplicated against user's Packages). Guarantees the guest has SSH server + download tools + tar for later layer application.composeRunCmd: prepends {systemctl enable --now sshd}. Distro-specific user runcmd: entries can assume sshd is running.User-supplied fields extend defaults; they don't replace them. Prevents the common footgun where an author puts packages: [nginx] and accidentally breaks SSH because they overrode the default list.
func WriteSeedISO(userData, metaData, networkConfig string, outputPath string) error
Writes a FAT-labeled cidata ISO. Tool preference order:
xorriso (preferred — modern, scriptable).genisoimage (legacy but widely available).mkisofs (oldest fallback).Clean error when none are present, with distro-appropriate install recipe (dnf install xorriso, pacman -S libisoburn, apt-get install xorriso).
The ISO is mounted by QEMU as a CD-ROM; cloud-init's NoCloud datasource reads /dev/sr0 at first boot.
Runs post-boot inside the vm deploy preflight (the vmSubstrateLifecycle hook's PrepareVenue, charly/vm_deploy_lifecycle.go) after cloud-init completes, BEFORE the plugin walks the plans. Dispatches on spec.CloudInit.CharlyInstall.Strategy:
| Strategy | Action |
|---|---|
| auto / scp | scp $(os.Executable()) guest:/usr/local/bin/charly; chmod +x |
| url | ssh-execute curl -L <url> -o /usr/local/bin/charly && sha256sum -c (verified against VmCharlyInstall.Checksum) |
| skip | ssh 'which charly' — fails if missing, returns early if present |
Idempotent. If charly is already present at the target version, the function returns without re-scp'ing. See /charly-internals:vm-deploy-target for how this plugs into the overall deploy flow.
VmCloudInitNetwork.Ethernets passes through to cloud-init's network-config v2 as-is. When unset, the renderer emits an empty network-config (cloud-init defaults to DHCP on every virtio-net interface). Good default; override only for static-IP deployments.
Explicitly supported — not either/or. VmKeyInjection.SMBIOS: enabled + VmKeyInjection.CloudInit: enabled simultaneously is the default for cloud_image VMs. Rationale: belt-and-suspenders (SMBIOS via systemd-ssh-generator v250+, cloud-init via user-data — both paths exist on modern Linux). No duplication cost.
/charly-internals:vm-spec — VmCloudInit, VmSSH.KeyInjection, VmCharlyInstall types/charly-internals:libvirt-renderer — SMBIOS-channel emission (domain XML side)/charly-internals:vm-deploy-target — EnsureCharlyInGuest caller; SSH/cloud-init readiness waits/charly-vm:vm — command-family; cloud-init flow/charly-vm:vms-catalog — YAML-authoring reference/charly-vm:arch — charly_install.strategy: auto worked example; adopt-user pattern/charly-distros:cloud-init — guest-side pairing: the cloud-init package installed inside bootc images reads the seed ISO this renderer producestools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).