bundle.yaml reference
bundle.yaml is the single source of truth for what goes into a bundle.
Human-edited. The builder refuses to proceed without it.
Minimal example
schema_version: 1
bundle_version: "2026.04.1"
ubuntu:
suites: [noble]
architectures: [amd64]
debs:
- name: ansible
- name: git
- name: make
rke2:
version: "v1.33.1+rke2r1"
variants: [canal]
image_mode: all-in-one
helm:
version: "v3.17.3"
aether_ops:
version: "v0.1.43"
source: ./artifacts/aether-ops_0.1.43_linux_amd64.tar.gz
templates_dir: ./templates
Field reference
Top-level
| Field | Required | Description |
|---|---|---|
schema_version | yes | Spec schema version. Must be 1 for 0.1.x. |
bundle_version | yes | Calver string (e.g. 2026.04.1). Written into the manifest. |
ubuntu | yes | Ubuntu suite/arch targets for .deb resolution. |
debs | no | Top-level .deb packages to vendor. Transitive deps resolved automatically. |
rke2 | no | RKE2 version and image variants to fetch. Required for full single-node bundles. |
helm | no | Helm version to fetch. Required when the target role needs Helm. |
aether_ops | no | aether-ops source (local file, URL, or git ref). Required for management bundles. |
onramp | no | aether-onramp git repo to bundle. Cloned at build time. |
helm_charts | no | Helm chart repositories to bundle. |
images | no | Container image pre-pull behavior. |
templates_dir | yes | Path to templates. |
ubuntu
ubuntu:
suites: [noble] # Ubuntu codenames: jammy, noble, etc.
architectures: [amd64] # x86-64 only
# mirror: https://archive.ubuntu.com/ubuntu # override for internal mirror
Resolution happens per (suite × arch) pair. Multiple suites multiply the
bundle size.
debs
debs:
- name: ansible
- name: git
- name: make
- name: curl
- name: jq
- name: ssh
- name: sshpass
- name: iptables
- name: iptables-persistent
- name: python3-kubernetes
Each entry is name: (and optionally version: if you need to constrain
it). The builder resolves the transitive closure of dependencies from
Ubuntu's Packages.gz indexes (main + universe), downloads each, and
verifies SHA256 against the index.
You do not list dependencies yourself — only top-level packages.
rke2
rke2:
version: "v1.33.1+rke2r1"
variants: [canal]
image_mode: all-in-one
# source: https://github.com/rancher/rke2/releases/download
| Field | Description |
|---|---|
version | RKE2 release tag, including the +rke2rN suffix. |
variants | CNI variants to bundle. canal is the default used in 0.1.x. |
image_mode | all-in-one (single image tarball, includes canal) or core+variant (core + per-variant tarballs). Default all-in-one. |
source | Optional override of the GitHub releases base URL (for internal mirrors). |
helm
helm:
version: "v3.17.3"
Fetched from https://get.helm.sh. SHA256 verified against the published
checksum file.
aether_ops
Three mutually exclusive modes:
Mode 1 — local pre-built binary (what the in-tree spec uses):
aether_ops:
version: "v0.1.43"
source: ./artifacts/aether-ops_0.1.43_linux_amd64.tar.gz
Mode 2 — GitHub release (CI default; the release workflow rewrites
source: out of the spec to hit GitHub instead):
aether_ops:
version: "v0.1.43"
# no source: — builder fetches from aether-gui/aether-ops GitHub releases
Mode 3 — build from source:
aether_ops:
version: "v0.1.43-dev"
ref: "main"
repo: aether-gui/aether-ops
# frontend_ref: "" # optional frontend submodule override
Optional onramp user settings (applied by the launcher, not the builder):
aether_ops:
version: "v0.1.43"
onramp_user: aether # default: aether
# onramp_password: ... # do NOT commit; see precedence below
onramp_user is the OS account Ansible SSHes into to run aether-onramp
playbooks. It is distinct from the daemon's service account
aether-ops (which has no login shell and no password). Default:
aether.
The daemon account is also granted passwordless sudo via
/etc/sudoers.d/aether-ops (mode 0440, validated with visudo -c)
so the daemon's component probes can shell out via sudo -n to
kubectl, helm, docker, and similar tools. The grant is currently
NOPASSWD: ALL; a follow-up will tighten it to the specific binaries
probes invoke. The dropin is not operator-configurable — its user name
mirrors the aether-ops daemon-account constant the rest of the stack
already assumes.
onramp_password is the password the installer sets on that account.
It is resolved at install time in this order of precedence:
--onramp-password <value>on theinstallCLI — highest.AETHER_ONRAMP_PASSWORDenvironment variable.aether_ops.onramp_passwordin the bundle spec — this field. Honoured when set, but should only be used in gitignored / scratch specs.- A random 24-character password generated by the installer and logged
to stderr with an
IMPORTANT: record this passwordbanner — lowest.
Do not commit an onramp_password to a tracked spec. It ends up in
manifest.json inside the bundle, where anyone who can read the tarball
can read the password.
onramp
onramp:
repo: https://github.com/opennetworkinglab/aether-onramp.git
ref: main
recurse_submodules: true
# Optional: override files in the cloned tree before hashing.
patches:
- target: ocudu/roles/uEsimulator/templates/ue_zmq.conf
source: ./patches/onramp/ocudu/uEsimulator/ue_zmq.conf
- target: ocudu/roles/gNB/templates/gnb_zmq.yaml
content: |
# tiny inline override
zmq_port: 5555
The aether-onramp Ansible toolchain is cloned at build time, its resolved
commit SHA is pinned in the manifest, and the launcher extracts it to
/var/lib/aether-ops/aether-onramp on install.
patches: (optional) overrides files inside the cloned tree before the
manifest is hashed, so the bundle ships the operator's edits as canonical
content. Each entry sets exactly one of:
source:— path on the build host, resolved relative to the spec file.content:— inline literal, useful for one-line value swaps.
target: is rooted at the cloned onramp tree (i.e. relative to
/var/lib/aether-ops/aether-onramp/ on the target host), uses forward
slashes, and must not contain .. segments. The target file must already
exist in the upstream tree — patches do not implicitly add files in v1.
file_mode: (optional) overrides the existing mode; otherwise the
upstream file's mode is preserved.
Patches run after the built-in offline-mode adaptations (e.g. flipping
airgapped.enabled to true) so user overrides always win. The same
schema is also accepted by the patch-bundle tool
for ad-hoc edits to an already-built bundle.
helm_charts
helm_charts:
- name: sdcore-helm-charts
repo: https://github.com/omec-project/sdcore-helm-charts.git
ref: main
Each entry is cloned to /var/lib/aether-ops/helm-charts/<name> on the
target host.
images
images:
auto_extract: true
# list: [] # when auto_extract is false — must be complete
# extra: [] # standalone images unioned with auto-extracted set
exclude:
- quay.io/stackanetes/kubernetes-entrypoint:v0.3.1
Container images are pre-pulled and staged for RKE2's airgap image loader.
auto_extract: true— the builder scans the cloned Helm charts for image references and unions them withextra.auto_extract: false— onlylistis used, which must be the complete set of images needed (no scanning).exclude— images to skip. Legacy Docker v1 manifests (which go-containerregistry cannot pull) are a common entry here.
templates_dir
templates_dir: ./templates
Path to the directory containing systemd units, sshd_config.d/ drop-ins,
sudoers.d/ drop-ins, and RKE2 config templates. This field is required.
The spec in main
The canonical spec at specs/bundle.yaml is the reference. Read it to see
current conventions and inline comments explaining each choice.