This document describes how to install and operate the PowerDNS OCM solution in an environment that has no outbound internet access. All container images travel inside the OCM bundle; the target cluster never contacts an external registry.
| Tool | Minimum version | Purpose |
|---|---|---|
ocm CLI |
0.11.0 | Build, transfer, and inspect OCM component archives |
kubectl |
1.28 | Apply Kubernetes manifests |
kustomize (or kubectl apply -k) |
5.x | Render overlays |
| Writable OCI registry in the target environment | — | Serve images to cluster nodes |
The OCM CLI is available at https://ocm.software/docs/getting-started/.
Image-reference "localization" (rewriting a registry address to a private one) is handled by two separate, independent mechanisms in this repository. They are not layered on top of each other and neither depends on the other; which one applies depends on how you deploy:
| Mechanism | File | Consumed by | Covers |
|---|---|---|---|
| OCM-native declarative localization | ocm/localization-config.yaml |
OCM-aware deployers that instantiate the component version directly (e.g. the OCM Flux plugin, the OCM Add-on Manager, ocm CLI localization commands) — not used by the kubectl/kustomize procedure below |
Plain, YAML-path addressable image references inside deploy/base/**/*.yaml |
| Script-based localization (this guide) | hack/localize-images.sh |
Anyone deploying with plain kubectl apply -k (Steps 1–5 below) |
Every bundled image, including the multi-instance KRO ResourceGraphDefinition (deploy/kro/powerdns-instance-rgd.yaml), whose image references are literal strings that the declarative OCM mechanism cannot address |
Important: ocm/localization-config.yaml's kind: LocalizationConfig is a plain OCM CLI
configuration object (part of the Open Component Model specification, apiVersion: config.ocm.software/v1alpha1). It is not a Kubernetes object, is never applied to a
cluster with kubectl, and has no relation to this project's own Kubernetes CRDs (Zone,
ClusterZone, RRset, ClusterRRset — defined under deploy/base/crds/ and reconciled by the
PowerDNS Operator). The shared word "Localization" is coincidental terminology from two unrelated
domains (OCM image-reference rewriting vs. this project's DNS zone/record custom resources).
This guide documents the script-based path (hack/localize-images.sh), since it is the one
required for the plain kubectl/kustomize deployment procedure below and is the only one of the
two that also covers the multi-instance KRO manifests.
Run the following on a machine that can reach the upstream container registries:
make ocm-bundleThis executes two steps internally:
ocm add componentversions— buildsocm/ctf.tarfrom the component descriptor with external image references.ocm transfer commontransportarchive --copy-resources— copies every referenced image as a local blob intoocm/ctf-bundled.tar.
The resulting ocm/ctf-bundled.tar is a self-contained transport archive that embeds every bundled container image — the DNS components (dnsdist, pdns-recursor, pdns-auth, lightningstream), the operator, and the multi-instance images (Garage and its bootstrap helper) — plus the Kubernetes manifests.
Copy ocm/ctf-bundled.tar to the air-gapped environment using whatever transfer mechanism is available (USB, internal artifact store, secure file transfer):
scp ocm/ctf-bundled.tar user@jump-host:/staging/On the air-gapped machine (or a machine with access to both the bundle and the private registry), push the bundled images:
OCM_BIN=ocm
REGISTRY=harbor.example.com/powerdns-ocm # replace with your registry
${OCM_BIN} transfer commontransportarchive ocm/ctf-bundled.tar oci://${REGISTRY}This populates the registry with all component images under the specified prefix. Alternatively, use the make ocm-push target after setting the REGISTRY variable:
make ocm-push REGISTRY=harbor.example.com/powerdns-ocmTo verify the images are present in the registry after the push:
${OCM_BIN} get componentversion --repo oci://${REGISTRY} github.com/bwi/powerdns-ocmGenerate the air-gap Kustomize overlay with your registry address:
./hack/localize-images.sh --registry harbor.example.com/powerdns-ocmThe image set is derived from the OCM component descriptor, so this covers every bundled image (the DNS components, the operator, and the multi-instance Garage images). The command rewrites both deploy paths: it regenerates deploy/overlays/air-gap/kustomization.yaml for the single-instance base, and rewrites the multi-instance KRO ResourceGraphDefinition (deploy/kro/powerdns-instance-rgd.yaml) in place. It is idempotent — re-running it, or running it again with a different registry, always converges to the chosen registry. Inspect the result:
kubectl kustomize deploy/overlays/air-gap/
grep -nE '^[[:space:]]*image:' deploy/kro/powerdns-instance-rgd.yamlConfirm that every image: line refers exclusively to your private registry and contains no reference to docker.io, registry-1.docker.io, or ghcr.io.
For the single-instance base you may instead edit the newName values in deploy/overlays/air-gap/kustomization.yaml directly — but manual edits do not cover the multi-instance KRO path, so prefer the script if you deploy multi-instance.
kubectl apply -k deploy/overlays/air-gap/Kubernetes will pull all images from your private registry. No outbound internet connection is required after this point.
To confirm all pods are running:
kubectl -n dns get podsExpected output (all containers Running, READY 1/1):
NAME READY STATUS RESTARTS AGE
dnsdist-<hash> 1/1 Running 0 60s
pdns-auth-<hash> 2/2 Running 0 60s
pdns-recursor-<hash> 1/1 Running 0 60s
Confirm that no image pull from an external registry occurred during deployment. On each cluster node, inspect the container runtime log or use crictl / ctr:
# containerd (typical on Kubernetes nodes)
crictl images | grep -v harbor.example.comThe output should be empty (no images from external registries). Alternatively, examine pod events:
kubectl -n dns describe pods | grep -i "pulled\|pulling"All Pulled events should reference your private registry only.
The following table lists every image bundled in the OCM package and its expected reference after localization.
| Component | Upstream image | Localized as (example) |
|---|---|---|
| dnsdist | powerdns/dnsdist-19:1.9.16 |
<registry>/powerdns/dnsdist-19:1.9.16 |
| pdns-recursor | powerdns/pdns-recursor-52:5.2.11 |
<registry>/powerdns/pdns-recursor-52:5.2.11 |
| pdns-auth | powerdns/pdns-auth-49:4.9.16 |
<registry>/powerdns/pdns-auth-49:4.9.16 |
| lightningstream | powerdns/lightningstream:1.0.2 |
<registry>/powerdns/lightningstream:1.0.2 |
| garage (multi-instance only) | dxflrs/garage:v1.0.1 |
<registry>/dxflrs/garage:v1.0.1 |
<registry> is the value passed to --registry in Step 4. Garage is reserved in the OCM descriptor for multi-instance operation and is not deployed in the PoC; its localization entry is included for completeness.
See the Update and Upgrade Guide for the full versioning strategy, OCM update process, CRD migration concept, rollback, and validation steps.
Air-gap-specific reminders when upgrading:
- Rebuild and re-bundle on the internet-connected machine:
make ocm-bundle. - Transfer the new
ocm/ctf-bundled.tarand push the images to the private registry (Steps 2–3 above). - Re-run
hack/localize-images.sh --registry <registry>if any image tags changed. - Apply:
kubectl apply -k deploy/overlays/air-gap/; Kubernetes performs the rolling update automatically. - Verify no external registry pulls occur (Step 6 above).