Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

colonel_byte.zarf

An Ansible collection that initialises an air-gapped Kubernetes cluster with zarf and deploys zarf packages onto it, from a playbook.

It is a working proof of ZEP-0072, which proposes native Ansible module support for zarf: the zarf binary dispatches on argv[0], so a zarf_init symlink to /usr/bin/zarf is an Ansible module, with a thin Python action plugin alongside it. Everything the proposal describes is implemented here – one binary dispatching on argv[0] with a symlink per module file, the WANT_JSON arguments file, the stdout guard, exit 0 with failure reported inside the JSON, the heartbeat status file and its polling monitor. The modules go further and link zarf in: the binary builds zarf’s own command tree and executes its command in its own process, so it enters the same code path an operator’s command line enters rather than shelling out to it. It needs no zarf on the node. choice-zarf-ansible-module records how that works and what it buys.

The proof began in colonel-byte/cargoship, beside the colonel_byte.cargoship collection whose shape it copies.

Modules

ModuleWhat it doesHow it is built
zarf_initzarf init against the cluster in a kubeconfigModule file, the one binary
zarf_package_deployzarf package deployModule file, the one binary
zarf_package_pullzarf package pull, to stage a package on disk for the airlockModule file, the one binary
zarf_package_removezarf package remove, by deployed name or by package sourceModule file, the one binary
zarf_package_mirror_resourceszarf package mirror-resources, to seed a registry and git server before a connected deployModule file, the one binary
zarf_update_credszarf tools update-creds <service>, to rotate what an initialised cluster holdsModule file, the one binary
zarf_kubectlzarf tools kubectl with the arguments given, and its output backModule file, the one binary
zarf_cluster_infoReports the cluster’s Kubernetes version, CRDs, StorageClasses and DeploymentsAction plugin
zarf_package_documentationExtracts the files a package carries in its documentation: mappingAction plugin
zarf_package_infoLists the packages deployed to a clusterAction plugin
zarf_package_inspectReads and verifies a package without deploying itAction plugin
zarf_state_infoReads the zarf-state Secret, withholding its eight credentials by defaultAction plugin

The first two converge a cluster and are the proof. zarf_package_remove is the other direction, which ZEP-0072 names and a playbook that only ever adds does not have; zarf_package_pull stages the package a deploy reads, which on an air-gapped node arrives as a file rather than from a registry the cluster can reach; zarf_package_mirror_resources pushes a package’s images and repositories into a registry and git server without deploying, which is what a connected: true deploy needs to have happened first; zarf_update_creds rotates the credentials an initialised cluster holds, and is the write half of zarf_state_info; zarf_kubectl is a passthrough to the kubectl zarf already vendors, which saves a management node a kubectl of its own. All seven are the same binary from cmd/zarfmod, invoked under seven names – argv[0] is what selects the module, which is the proposal’s own design, so each module file is a symlink.

zarf package create is deliberately not among them: it builds an artifact from a definition in a repository, which belongs in a pipeline rather than in a convergence run.

The other five read a cluster, are not part of the proof, and are plain action plugins with no binary behind them – choice-zarf-info-modules records why, and why zarf_state_info redacts.

There are five roles: packages, package_inspect and state wrap a read-only module apiece, deploy_packages manages multi-package stack deployments with drift detection, and prerequisites refuses to deploy a package onto a cluster that cannot run it.

Package prerequisites

A package can state what it needs of a cluster in a machine-readable document carried inside the package itself, and a deployment can refuse to proceed when the cluster does not satisfy it. That is the second thing in this repository: a colonel-byte.dev/v1alpha1 PackagePrerequisites format, a generated JSON Schema, and the modules and role that read one.

It exists for a failure that reports itself as a success. A workload package delivered as a HelmRelease needs a running helm-controller to do anything at all; deploy it onto a cluster without one and zarf applies the manifests, the API server accepts them, zarf package deploy reports a successful deployment, and nothing is ever reconciled.

- name: Refuse to deploy onto a cluster that cannot run this package
  ansible.builtin.include_role:
    name: colonel_byte.zarf.prerequisites
  vars:
    zarf_package: /srv/staging/zarf-package-operator-external-secrets-amd64-2.12.0.tar.zst
    zarf_public_key: /etc/zarf/colonel-byte-zarf-packages.pub
    zarf_kubeconfig: /etc/rancher/rke2/rke2.yaml

- name: Report what could not be checked
  ansible.builtin.debug:
    msg: "{{ zarf_prerequisites_skipped }}"
  when: zarf_prerequisites_skipped | length > 0

The cluster is read through zarf tools kubectl, so this needs no kubernetes.core – the management node is typically air-gapped, and a second Galaxy collection to stage onto it is a cost every consumer would pay. The guide is the format and what reads it; examples/packages holds three reference packages that declare one: a GitOps engine from each of Flux and Argo CD, and a workload that requires one.

Installing

Build the module wrapper, put it on the path of the node the play runs on, and link each module file name to it. Zarf is linked in, so it is a large binary and its build pulls zarf’s dependency tree from vendor/:

go run ./magefiles/core build:modules          # build/zarfmod, with the three symlinks beside it
sudo install -m 0755 build/zarfmod /usr/local/bin/
for module in zarf_init zarf_package_deploy zarf_package_pull zarf_package_remove \
              zarf_package_mirror_resources zarf_update_creds zarf_kubectl; do
  sudo ln -sf zarfmod "/usr/local/bin/$module"
done

Then make the collection visible to Ansible:

ln -s "$PWD/ansible/colonel_byte/zarf" ~/.ansible/collections/ansible_collections/colonel_byte/zarf

Zarf itself is a prerequisite of the read-only modules, which shell out to the installed zarf; the three module files need none, and nothing is installed on the cluster’s nodes. ghcr.io/colonel-byte/zarf-ansible-modules comes with all of it – ansible-core, the collection, the module wrapper with its names linked to it, and a zarf – for a management node that would rather not install any of them:

docker run --rm -v "$PWD:/workspace" ghcr.io/colonel-byte/zarf-ansible-modules:v0.88.0 \
  ansible-playbook -i management.ini playbooks/zarf-bundle-order.yml

Using it

- name: Initialise the cluster
  hosts: localhost
  connection: local
  tasks:
    - name: Run zarf init
      colonel_byte.zarf.zarf_init:
        init_package: /srv/staging/zarf-init-amd64-v0.85.0.tar.zst
        kubeconfig: /etc/rancher/rke2/rke2.yaml
        registry_mode: proxy
        timeout: 15m
      delegate_to: localhost
      run_once: true
      no_log: true

A play running with -v shows each component as zarf reaches it:

[zarf] component 1: zarf-injector [running]
[zarf] component 1: zarf-injector [done]
[zarf] component 2: zarf-seed-registry [running]

Reading a cluster back, with the credentials withheld:

    - name: Read the zarf state
      colonel_byte.zarf.zarf_state_info:
        kubeconfig: /etc/rancher/rke2/rke2.yaml
      register: zarf_state

More playbooks are in ansible/examples/playbooks/, and the guide at docs/guides/zarf-ansible-module.md walks the whole thing.

Why there are two converging modules

A cluster whose distribution ships no storage provider cannot be initialised on its own: zarf’s registry claims a PVC during the seed phase, and with no StorageClass the claim stays Pending until the Helm install times out. A storage provider package deployed with connected: true pushes no images, so it needs no registry, and so needs no initialised cluster – which makes it the one thing that can come first.

That alternation is the shape of the init-local-path uds-bundle, and the cluster suite walks it in both orders against two real k3d clusters.

Building and testing

Targets run through mage. go run ./magefiles/core <target> needs only the Go toolchain:

go run ./magefiles/core build:modules            # both module files into build/
go run ./magefiles/core test:unit                # everything but the e2e suites
go run ./magefiles/core test:endToEndNonCluster  # needs ansible-playbook; no cluster
go run ./magefiles/core test:endToEndCluster     # needs k3d, zarf, Docker and the network
go run ./magefiles/core generate:document        # regenerate docs/ansible, docs/golang, docs/index.md, docs/SUMMARY.md
go run ./magefiles/core generate:schema          # regenerate schema/ from the Go types in api/
go run ./magefiles/core examples:lint            # zarf dev lint over the reference packages

The unit tests are where the collection and the Go code are held against each other: TestActionPluginDocsMatchModuleParams holds each action plugin’s DOCUMENTATION block against the Go parameter struct, TestInfoPluginDocsMatchRenderedFlags does the same for the read-only modules against the flags their command_parts functions name, and TestEveryActionPluginIsCovered fails on a plugin that neither test looks at. TestInitArgsAreKnownFlags and TestDeployArgsAreKnownFlags parse every flag each module can render against zarf’s own init and package deploy commands, so pflag decides what exists – a renamed flag fails them, and nothing has to be refreshed by hand. On the prerequisites side, TestCommittedSchemasAreCurrent fails when a committed schema stops matching the Go type it is reflected from, and TestExampleDocumentsMatchTheTypes decodes each reference package’s document into those types with unknown fields rejected.

Known limits of the proof

  • Component progress is read back out of zarf’s log stream, so the modules render --log-format json by default and report a total component count of 0 unless components was named.
  • changedSignal is partial on every successful run. Zarf reports which components it deployed, not whether a component found the cluster already in the state it wanted, and the command tree hands back an error rather than a deployment result, so neither module can honestly report complete. This one needs the module to live inside zarf, beside the deployer.
  • Check mode reports the converging tasks as skipped: neither zarf init nor zarf package deploy has a dry run. The read-only modules answer in check mode, because reading a cluster changes nothing.

The rest collapse if dispatch moves inside zarf, which is the point of the proposal.

License

Apache 2.0. See LICENSE.