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/cloud_init_types.go | VmCloudInit, VmCloudInitUser, VmCloudInitFile, VmCloudInitNetwork, VmCloudInitMirrors, VmCharlyInstall |
func RenderCloudInit(spec *VmSpec, vmName, sshPubKey string) (userData, metaData, networkConfig string, err error)
Returns three strings — the three files NoCloud expects on the seed ISO. The caller (BuildCloudImage or VmDeployTarget) then calls WriteSeedISO to pack them.
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 VmDeployTarget.Emit after cloud-init completes. 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
OpenCharly CLI (charly) binary installed into container/VM images for in-container use. Use when working with charly binary deployment inside containers, native D-Bus support, or the full charly toolchain (charly binary + virtualization + gocryptfs + socat).
development
Operator CachyOS workstation profile — a kind:local template + target:local deploy that installs the full dev stack (30 candies) onto a CachyOS host via ShellExecutor. Lives in the overthinkos/cachyos submodule. MUST be invoked before editing or applying the charly-cachyos workstation profile.
tools
Fedora box with the full charly toolchain using shared candies. Rootless-first — runs as uid=1000 with passwordless sudo (no root, no cap_add: ALL). Same candy list as charly-arch. Includes NVIDIA GPU runtime. MUST be invoked before building, deploying, configuring, or troubleshooting the charly-fedora box.
tools
Arch Linux box with the full charly toolchain. Rootless-first — runs as uid=1000 with passwordless sudo (no root, no cap_add: ALL). Composes /charly-coder:charly-mcp so the box is reachable as an MCP gateway on port 18765. NVIDIA GPU runtime composed in. MUST be invoked before building, deploying, configuring, or troubleshooting the charly-arch box.