How it works
One page. End-to-end. From an operator editing bundle.yaml to RKE2 and
aether-ops running on an airgapped box.
The whole picture
flowchart LR
subgraph build["On the build machine"]
spec["bundle.yaml<br/><i>human-edited spec</i>"]
lock["bundle.lock.json<br/><i>committed, like go.sum</i>"]
builder["build-bundle<br/><i>Go tool</i>"]
bundle["bundle.tar.zst<br/><i>+ manifest.json inside</i>"]
launcher["aether-ops-bootstrap<br/><i>static binary</i>"]
end
subgraph transport["Sneakernet / artifact store"]
files[("launcher + bundle + .sha256")]
end
subgraph target["On the target host (airgapped)"]
preflight["Preflight checks"]
comps["Components run in order:<br/>debs → ssh → sudoers → service_account<br/>→ rke2 → helm → onramp → aether_ops"]
state["state.json written"]
handoff(["aether-ops reachable"])
end
spec --> builder
lock --> builder
builder --> bundle
bundle --> files
launcher --> files
files --> preflight
preflight --> comps
comps --> state
state --> handoff
What happens on the build machine
- You edit
bundle.yaml. One file describes everything the bundle contains — which Ubuntu suite and architecture, which top-level.debpackages, which RKE2 version and image variants, which Helm version, which aether-ops build. See bundle.yaml reference. - You run
make bundle. Internally this callscmd/build-bundle, which:- Resolves every requested
.deband its transitive dependencies by fetching Ubuntu'sPackages.gzindexes (main + universe). - Downloads each artifact and verifies its SHA256 against the canonical
source (Ubuntu Packages index, GitHub release checksums,
get.helm.sh). - Checks a
bundle.lock.jsonfor.debdrift — or writes one on first build. In 0.1.x, drift is logged as a warning and the lockfile is rewritten. - Generates
manifest.jsondescribing the bundle contents with component versions, file paths, and hashes. - Stages everything into a tree and packs it with
tar + zstd. - Emits
dist/bundle.tar.zstand a.sha256sidecar.
- Resolves every requested
- CI tags a release. On a
v*git tag, GoReleaser builds the static launcher binary,build-bundleis invoked in the CI runner, SBOMs and Grype vulnerability reports are generated, and everything is attached to the GitHub release. See release process.
What moves through the airgap
The operator (or an automated sneakernet pipeline) carries exactly three things to the target host:
aether-ops-bootstrap— the launcher binary.bundle.tar.zst— the offline payload.bundle.tar.zst.sha256— for an integrity check before installing.
No other installers, no dependencies, no "also install Docker first" — those decisions were all made on the build machine.
What happens on the target host
When the operator runs ./aether-ops-bootstrap install --bundle bundle.tar.zst:
- Preflight. The launcher checks Ubuntu version (22.04, 24.04, or 26.04 soon to be released), root privileges, systemd presence, architecture, disk space, and whether a prior install is already present.
- Bundle opened. The tarball's
manifest.jsonis read and itsschema_versioncompared to the launcher's. A mismatch aborts the run. - State loaded.
/var/lib/aether-ops-bootstrap/state.jsonis read if it exists. Empty state means "fresh install"; non-empty state means an upgrade, repair, or no-op. - Components run in order. For each component, the launcher computes a
Plan(current, desired). If current equals desired, the component is usually skipped. Otherwise the plan is applied. - Final state written. On success, the state file records the launcher version, bundle version, bundle hash, per-component versions, and an append-only history entry for the run.
- Handoff. The launcher exits cleanly. aether-ops is now reachable on the host; the operator can open the UI or hand it to the team.
On failure, the launcher collects a diagnostic bundle under /tmp containing
the partial log, the state file, and relevant systemd journals — so support
engineers can debug without a second trip.
Why this shape
A few load-bearing design decisions that show up later in the docs:
- Two artifacts, two version schemes. Launcher code changes slowly (semver) and has a stable interface. Bundle content changes every release (calver) because every upstream pin moves on its own cadence.
manifest.jsonas the contract. Both the builder and the launcher import the same Go types frominternal/bundle. Schema changes are a single PR that touches both sides.- Small set of shell-outs. Archive handling and orchestration are in Go,
while host-native tools such as
dpkg,systemctl,useradd,groupadd, andvisudoare invoked for the OS semantics they already own. - State as the source of truth for reconciliation.
upgradeandrepairare the same loop asinstall, just starting from a non-empty state.