Skip to content

Add CFEngine implementation (standalone + hub) - #54

Merged
darkn3rd merged 6 commits into
masterfrom
cfengine
Aug 27, 2026
Merged

Add CFEngine implementation (standalone + hub)#54
darkn3rd merged 6 commits into
masterfrom
cfengine

Conversation

@darkn3rd

Copy link
Copy Markdown
Owner

Summary

  • Adds a CFEngine implementation of the lessons provisioning area, parallel to the existing Chef/Ansible/Puppet/Salt ones, implemented once in configbox/cfengine/shared_policy/lessons/ (a delivery-agnostic bundle library reading a CFEngine augments variable, def.lessons) and consumed two ways:
    • standalone/ - masterless: cf-agent -Kf promises.cf applied directly, no policy hub involved at all.
    • hub/ - the guest bootstraps to itself (cf-agent --bootstrap 127.0.0.1) and runs as its own self-hosted policy hub over cf-serverd - the same self-targeting trick this repo already uses for Salt's roster/dynamic trees and Ansible's ansible_local, so no second machine is ever needed.
  • New generator scriptbox/scripts/generate_cfengine.rb, reading the same scriptbox/config/*.yml source of truth as every other tool, emitting a CFEngine augments def.json. Pre-aggregates every append-typed step's lines per destination file in Ruby (extract_appends!) before serializing, rather than one promise per originating step - see the "notable bugs found" section below for why that's load-bearing, not just tidier.
  • Vendors a real external CFEngine module (nickanderson/cfengine-apt) under .module-vendor/apt/, demonstrated via a new apt_pin step type that pins an already-installed apt package to an exact version/priority - additive to the existing go step's plain apt+PPA install, not a replacement, opt-in via --select cfengine_apt_pin.
  • cmpaths.toml, Rakefile (generate:cfengine), and .gitignore updated consistently with the existing Puppet/Salt conventions.

Notable bugs found and fixed along the way (all confirmed via live cf-agent runs, several only surfaced on a genuinely fresh VM - see test plan)

  • CFEngine classes are global/persistent for the entire agent run, not reset per bundle invocation - needed explicit scope => "bundle" on every per-step dispatch class, or the first step with e.g. an add_apt_repo left that class stuck true for every later step.
  • A data => variable re-derived once per value of an outer slist only actually resolves for that slist's first value, silently keeping stale data for every later iteration - fixed by passing a plain string path into the shared dispatcher bundle instead of an extracted container, so its own mergedata() lookup is genuinely local to each call.
  • A promise's identity is (bundle, source line, expanded promiser) - not the calling context: multiple steps appending different lines to the same destination file, all going through one shared dispatcher bundle, only actually applied the first step's content. Fixed by pre-merging all lines per destination file in the generator itself (one promise per real file, not per originating step).
  • A files: promise creating a directory needs a trailing /. on the promiser - without it, create => "true" on a missing path creates a plain file instead, invisible on a box where that directory already happened to exist from an earlier debugging run, but reproducible immediately from a truly fresh VM.
  • The hub tree's own /var/cfengine/inputs (what cf-agent actually reads) never gets refreshed from /var/cfengine/masterfiles (what a hub serves) after the very first bootstrap, since we don't pull in the standard Masterfiles Policy Framework's own update.cf bundle - fixed with an explicit sync in the hub bootstrap script on every provision.

Test plan

  • standalone/ - vagrant up and a subsequent vagrant provision both converge cleanly (55/56+ steps depending on tag selection); every MVP-scope step type (apt/apt_repository/add_apt_repo, apt_pin, sysctl, file, append, script) verified installing correctly via verify_commands.rb
  • hub/ - verified twice: a full vagrant destroy && vagrant up from a completely blank VM, and a vagrant provision re-run against an already-converged box - both clean, matching standalone's own results exactly
  • apt_pin - verified end-to-end with --select cfengine_apt_pin: the vendored module writes a correct /etc/apt/preferences.d/golang-go, and apt-cache policy confirms the pin is genuinely active (priority 1001 replacing the default 500), not just coincidentally matching the newest available version
  • rake test - all existing tests pass unchanged; rake generate:all confirmed to produce byte-identical output for Puppet/Chef/Ansible/Salt (no regressions)
  • One pre-existing, cross-tool data-model bug remains open, not introduced or fixed by this branch: a file-type step and multiple append-type steps both targeting .zshrc causes the file step to silently wipe whatever the append steps inserted (already flagged for Salt) - a redesign (treating the zsh baseline as its own append entry instead of a competing whole-file replace) is planned as a separate follow-up branch/PR

🤖 Generated with Claude Code

@darkn3rd
darkn3rd merged commit 18e28ff into master Aug 27, 2026
22 checks passed
@darkn3rd
darkn3rd deleted the cfengine branch August 27, 2026 04:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant