Skip to content

fix(cloudinit): merge cloud-config documents before the seed is written - #97

Merged
NovusEdge merged 1 commit into
mainfrom
fix/cloudinit-merge-documents
Sep 6, 2026
Merged

fix(cloudinit): merge cloud-config documents before the seed is written#97
NovusEdge merged 1 commit into
mainfrom
fix/cloudinit-merge-documents

Conversation

@NovusEdge

@NovusEdge NovusEdge commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Closes #93. Unblocks #85 for AlmaLinux and Rocky.

stoat handed cloud-init a cloud-config-archive and let it merge the documents.
An archive is a top-level YAML list, and cloud-init 24.4 on AlmaLinux 9 and
Rocky 9 reads user-data with .get() before it checks the type:

File ".../cloudinit/cmd/main.py", line 359, in _should_wait_via_user_data
    if parsed_yaml.get("bootcmd"):
AttributeError: 'list' object has no attribute 'get'

#91 avoided the archive when the seed held one document, which covered a guest
with no recipes. A guest with a recipe still produced two or more documents and
still failed.

mergeDocs folds them into one mapping before the seed is written: a list
appends to the list already there, a mapping merges key by key, and anything
else takes the later value. Those are the rules the merge_how directive used
to ask cloud-init for. The seed no longer depends on the guest's cloud-init
version, and merge_how, withMergeHow, archiveDoc and buildArchive are
gone.

What the rendered seed looks like now

yaml.Marshal sorts keys, drops comments, and emits the sudo rule as a plain
scalar. The parsed values are unchanged. Ubuntu with one recipe:

#cloud-config
mounts:
    - - work
      - /mnt/work
      - 9p
      - trans=virtio,version=9p2000.L,rw,_netdev,nofail
      - "0"
      - "0"
packages:
    - git
ssh_pwauth: false
users:
    - name: stoat
      shell: /bin/bash
      ssh_authorized_keys:
        - ssh-ed25519 AAAA k
      sudo: ALL=(ALL) NOPASSWD:ALL

The package header says to re-verify the seed on hardware before changing its
shape, so the tests that pinned the file's bytes now pin the parsed values, and
this PR carries live evidence.

Tests

merge_test.go covers the merge rules directly: lists append in document
order, mappings recurse, a later scalar wins, write_files entries accumulate,
a document that is not a mapping fails, and the error names which document
failed.

The seed tests assert parsed values instead of rendered bytes.
TestSeedIsAlwaysAMapping runs six guests with and without a recipe.
TestSeedCarriesRecipesOnAlmaLinux is the case #93 is about.

Live evidence

Filled in below as the gates finish.

Summary by CodeRabbit

  • Bug Fixes
    • Cloud-init seeds now consistently use a single merged #cloud-config document.
    • Preserves account data, package lists, file definitions, commands, mounts, and recipe configuration when combining cloud-init fragments.
    • Resolves conflicts by retaining later values while preserving and combining compatible lists and nested settings.
    • Improves compatibility for AlmaLinux and Rocky Linux guests.

stoat handed cloud-init a cloud-config-archive and let it merge the
documents. An archive is a top-level YAML list, and cloud-init 24.4 on
AlmaLinux 9 and Rocky 9 reads user-data with .get() before it checks the
type, so init-local fails and cloud-init status reports error for the life
of the VM. #91 avoided the archive for a one-document seed, which left any
guest with a recipe still broken there.

mergeDocs folds the documents into one mapping: a list appends, a mapping
merges key by key, and anything else takes the later value. Those are the
rules the merge_how directive used to ask cloud-init for. The seed no longer
depends on the guest's cloud-init version.

The rendered YAML differs from the template: yaml.Marshal sorts keys, drops
comments, and emits the sudo rule as a plain scalar. The parsed values are
the same, and the tests now assert those.

Signed-off-by: NovusEdge <novusedge0@gmail.com>
@NovusEdge NovusEdge added bug Something isn't working recipes bundled or index recipes guest guest OS support labels Sep 6, 2026
@NovusEdge NovusEdge self-assigned this Sep 6, 2026
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: c84791b1-4e7d-4ef4-84c8-946fd127e437

📥 Commits

Reviewing files that changed from the base of the PR and between 47ec90e and 5699d7c.

📒 Files selected for processing (5)
  • internal/cloudinit/cloudinit.go
  • internal/cloudinit/cloudinit_test.go
  • internal/cloudinit/merge.go
  • internal/cloudinit/merge_test.go
  • internal/cloudinit/scripts_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The change replaces cloud-config archives with one merged #cloud-config mapping. The new merge logic recursively merges mappings, appends lists, and applies later scalar values. Seed and integration tests validate accounts, recipes, packages, commands, and unknown keys.

Changes

Cloud-config merge

Layer / File(s) Summary
Merge engine and merge rules
internal/cloudinit/merge.go, internal/cloudinit/merge_test.go
mergeDocs parses cloud-config documents and emits one mapping. Nested mappings merge recursively, lists append, and later incompatible values replace earlier values. Tests cover ordering, errors, and write_files.
Seed integration and coverage
internal/cloudinit/cloudinit.go, internal/cloudinit/cloudinit_test.go, internal/cloudinit/scripts_test.go
userData now calls mergeDocs. Tests verify merged account data, recipe packages, runcmd, write_files, headers, and guest coverage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 5699d

Cloud-config seeds are now emitted as one mapping, avoiding the archive-list parsing failure while retaining account settings, packages, commands, and recipe content. The covered behavior is ready to merge.

Sequence Diagram(s)

sequenceDiagram
  participant SeedBuilder
  participant mergeDocs
  participant CloudInit
  SeedBuilder->>mergeDocs: pass base and recipe documents
  mergeDocs->>mergeDocs: merge mappings and append lists
  mergeDocs-->>SeedBuilder: return one `#cloud-config` mapping
  SeedBuilder->>CloudInit: provide merged user-data document
Loading

Poem

A rabbit blends the clouds with care
Lists hop onward, row by row
Maps unite without a snare
Recipes join the seed below
One clear config starts the show

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.52% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 29 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: merging cloud-config documents before writing the seed.
Linked Issues check ✅ Passed The changes satisfy issue #93 by merging cloud-config documents in Go into one mapping, recursively merging mappings, appending lists, applying later values for incompatible types, and removing archiv…
Out of Scope Changes check ✅ Passed All changes support the linked issue. The production changes, removed archive machinery, and updated tests are directly related to producing valid merged cloud-config seeds.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cloudinit-merge-documents

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@NovusEdge

Copy link
Copy Markdown
Owner Author

Live evidence

Merged-seed candidate 5699d7c, one attempt per row.

Gate Guest Recipes Result
guest-expansion/live/m1 AlmaLinux 9 none start, verify, stop passed
common-recipes/live/m1 Ubuntu 24.04 devtools, python-dev start, verify, stop passed
common-recipes/live/m1 Alpine devtools, python-dev start, verify, stop passed
common-recipes/live/m1 Debian 13 devtools, python-dev start, verify, stop passed

The three recipe rows are the multi-document path: the base account block, the guest's own extra packages, the 9p mounts document where the guest takes one, and two recipe fragments, all folded into one mapping. Each row checks that both recipes report healthy, that the declared outputs resolve in the guest, and that cloud-init finishes with no hard errors.

AlmaLinux with a recipe stays unproven on hardware, because no recipe declares AlmaLinux yet. That is #85. This change is what unblocks it.

@NovusEdge
NovusEdge merged commit 40d9b2d into main Sep 6, 2026
6 checks passed
@NovusEdge
NovusEdge deleted the fix/cloudinit-merge-documents branch September 6, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working guest guest OS support recipes bundled or index recipes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Merge cloud-config documents so recipes work on AlmaLinux and Rocky

1 participant