Skip to main content

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

FieldRequiredDescription
schema_versionyesSpec schema version. Must be 1 for 0.1.x.
bundle_versionyesCalver string (e.g. 2026.04.1). Written into the manifest.
ubuntuyesUbuntu suite/arch targets for .deb resolution.
debsnoTop-level .deb packages to vendor. Transitive deps resolved automatically.
rke2noRKE2 version and image variants to fetch. Required for full single-node bundles.
helmnoHelm version to fetch. Required when the target role needs Helm.
aether_opsnoaether-ops source (local file, URL, or git ref). Required for management bundles.
onrampnoaether-onramp git repo to bundle. Cloned at build time.
helm_chartsnoHelm chart repositories to bundle.
imagesnoContainer image pre-pull behavior.
templates_diryesPath 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
FieldDescription
versionRKE2 release tag, including the +rke2rN suffix.
variantsCNI variants to bundle. canal is the default used in 0.1.x.
image_modeall-in-one (single image tarball, includes canal) or core+variant (core + per-variant tarballs). Default all-in-one.
sourceOptional 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:

  1. --onramp-password <value> on the install CLI — highest.
  2. AETHER_ONRAMP_PASSWORD environment variable.
  3. aether_ops.onramp_password in the bundle spec — this field. Honoured when set, but should only be used in gitignored / scratch specs.
  4. A random 24-character password generated by the installer and logged to stderr with an IMPORTANT: record this password banner — 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 with extra.
  • auto_extract: false — only list is 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.