Files
home-network-k3s-impl/infra/devcontainer/Containerfile
T
jakman85andClaude Opus 5 8059297036
gate / conformance (pull_request) Has been skipped
gate / gate (pull_request) Successful in 1m53s
fix(devcontainer): bake wg + qrencode into the image instead of splitting the VPN procedure across host and container
Operator correction: "sop, wg, and any other tool requirement is supposed to be
baked into the devcontainer."

My previous commit discovered that `wg` existed only on the host, `sops` only in
the container, and `qrencode` on neither -- and fixed it the wrong way, by
splitting every step of the key procedure across the boundary and telling the
operator to `dnf install qrencode` on their workstation. That breaks CLAUDE.md §5
and the devcontainer README's "Don't install the toolchain on the host", produces
a procedure that cannot be reproduced in CI, and is miserable to follow.

wireguard-tools + qrencode are now distro packages in the Containerfile. Both are
in Fedora 44's repos (wireguard-tools 1.0.20260223 -- the same upstream version as
the image this component runs -- and qrencode 4.1.1), unpinned like every other
dnf package there: the pinned thing is the Fedora base, and neither is a gate tool
whose version changes what green means.

Three things got SIMPLER rather than merely relocated, which is the tell that the
rule was right:

  * Step 2 is one pipeline in one shell -- `sops set ... "$(wg genkey)"` writes the
    key straight into the already-encrypted document. No staged copy in .cache, no
    `*.key`-suffix warning, no sha256 comparison to prove the right bytes landed.
  * new-peer.sh derives the server public key from wg-server-key.sops.yaml itself
    (`sops -d --extract | wg pubkey`), so there is no local key copy to keep in
    step with the encrypted one, and no magic host path it must live at.
  * `.cache/` is gitignored at the repo root (mirroring infra/ansible/.gitignore),
    giving the container a persistent non-committable scratch dir. new-peer.sh
    writes profiles to .cache/wg-peers/ -- necessary because the repo bind-mount is
    the container's only persistent read-write mount, so a profile in /tmp there is
    gone before it can reach a desktop. Its blanket "refuse to write inside the
    checkout" guard is now a `git check-ignore` test of the real property.

Verified by execution, not reasoning: the image rebuilds; wg/qrencode/sops all
resolve in it; `sops -d | wg pubkey` runs in one shell; new-peer.sh refuses the
REPLACE-ME placeholder with an actionable message; and a full run with a throwaway
server key renders a correct profile at 0600 under a 0700 gitignored dir, prints
the QR, and refuses a non-ignored destination inside the repo.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-06 00:48:32 -05:00

98 lines
6.4 KiB
Docker

# Reproducible dev toolbox for the home-network-k3s platform (Phase 1+).
# Bakes the NON-Python prereqs (OpenTofu, age, sops, flux) plus ansible + kube tooling
# AND the pinned conformance-harness toolchain (kyverno CLI, kyverno-json, chainsaw,
# yamllint, check-jsonschema, yq, promtool) into a single pinned baseline image, so `make -C ci gate`
# runs identically here and in CI. This is the Proxmox (bpg/proxmox) toolbox + conformance
# harness: OpenTofu drives the Proxmox-hosted k3s cluster via the bpg provider (dev.sh opens
# an SSH tunnel to the kube-API), NOT a local libvirt/KVM host. The libvirt-client/qemu-img
# packages are retained as harmless disk-image utilities. Fedora base is pinned for
# reproducibility.
ARG FEDORA_VERSION=44
FROM registry.fedoraproject.org/fedora:${FEDORA_VERSION}
ARG OPENTOFU_VERSION
ARG AGE_VERSION
ARG SOPS_VERSION
ARG FLUX_VERSION
ARG KUBECTL_VERSION
ARG HELM_VERSION
# Conformance test toolchain (pinned to match ci/versions.env).
ARG KYVERNO_VERSION
ARG KYVERNO_JSON_VERSION
ARG CHAINSAW_VERSION
ARG YAMLLINT_VERSION
ARG YQ_VERSION
ARG CHECK_JSONSCHEMA_VERSION
# promtool: the engine that evaluates the alert-rule UNIT TESTS (platform/**/*.promtool-test.yaml).
# Pinned to the Prometheus version prod actually runs — see versions.env for why that lockstep is
# the point, and for the live-cluster command that establishes it.
ARG PROMTOOL_VERSION
ARG TARGETARCH=amd64
# Distro packages: libvirt client libs (harmless disk-image utilities), ansible, ssh,
# disk-image + cloud-init seed tooling, python3-pip (for the pip-installed test tools),
# diffutils (provides `diff`, used by L1 golden render checks), and common utilities.
#
# wireguard-tools + qrencode serve the REMOTE-ACCESS VPN (platform/vpn-ingress):
# * wireguard-tools gives `wg genkey` / `wg pubkey`, needed to mint the server key and to derive
# every client profile's `[Peer] PublicKey` (infra/vpn-ingress/new-peer.sh).
# * qrencode renders a client profile as a QR code, which is how a PHONE gets enrolled.
# They are here rather than on the operator's host because this image is THE toolchain
# (CLAUDE.md §5; README "Don't install the toolchain on the host"). That rule was briefly broken
# by a runbook that told the operator to run `wg` on the host and `sops` in here, and then split
# every step across the boundary to work around it — a procedure nobody should have to follow, and
# one that cannot be reproduced in CI. Both tools in one image means `sops -d … | wg pubkey` is
# just a pipeline again.
# Unpinned on purpose, like every other dnf package here: the Fedora base is what is pinned
# (FEDORA_VERSION), and these are not gate tools whose version changes what green means.
RUN dnf -y install --setopt=install_weak_deps=False \
libvirt-client qemu-img cloud-utils genisoimage openssh-clients \
ansible-core python3-libvirt python3-pip diffutils \
wireguard-tools qrencode \
git make jq unzip tar gzip xz which ca-certificates curl \
&& dnf clean all
# Pinned Go binaries from official releases.
RUN set -eux; \
curl -fsSL "https://github.com/opentofu/opentofu/releases/download/v${OPENTOFU_VERSION}/tofu_${OPENTOFU_VERSION}_linux_${TARGETARCH}.zip" -o /tmp/tofu.zip; \
unzip -d /usr/local/bin /tmp/tofu.zip tofu; chmod +x /usr/local/bin/tofu; \
curl -fsSL "https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-linux-${TARGETARCH}.tar.gz" | tar -xz -C /tmp; \
install -m0755 /tmp/age/age /tmp/age/age-keygen /usr/local/bin/; \
curl -fsSL "https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.linux.${TARGETARCH}" -o /usr/local/bin/sops; chmod +x /usr/local/bin/sops; \
curl -fsSL "https://github.com/fluxcd/flux2/releases/download/v${FLUX_VERSION}/flux_${FLUX_VERSION}_linux_${TARGETARCH}.tar.gz" | tar -xz -C /usr/local/bin flux; \
curl -fsSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl; chmod +x /usr/local/bin/kubectl; \
curl -fsSL "https://get.helm.sh/helm-v${HELM_VERSION}-linux-${TARGETARCH}.tar.gz" | tar -xz -C /tmp; \
install -m0755 "/tmp/linux-${TARGETARCH}/helm" /usr/local/bin/helm; \
rm -rf /tmp/*
# Pinned conformance test toolchain (L0/L1 static gate + the L2/L3 Chainsaw runner).
# Arch strings differ per tool: yq/chainsaw/kyverno-json use ${TARGETARCH} (amd64), the
# kyverno CLI tarball uses x86_64 — map amd64 -> x86_64 for it only.
RUN set -eux; \
python3 -m pip install --no-cache-dir --break-system-packages \
"yamllint==${YAMLLINT_VERSION}" "check-jsonschema==${CHECK_JSONSCHEMA_VERSION}"; \
curl -fsSL "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_${TARGETARCH}" -o /usr/local/bin/yq; \
chmod +x /usr/local/bin/yq; \
curl -fsSL "https://github.com/kyverno/chainsaw/releases/download/v${CHAINSAW_VERSION}/chainsaw_linux_${TARGETARCH}.tar.gz" | tar -xz -C /usr/local/bin chainsaw; \
KYVERNO_CLI_ARCH="$([ "${TARGETARCH}" = amd64 ] && echo x86_64 || echo "${TARGETARCH}")"; \
curl -fsSL "https://github.com/kyverno/kyverno/releases/download/v${KYVERNO_VERSION}/kyverno-cli_v${KYVERNO_VERSION}_linux_${KYVERNO_CLI_ARCH}.tar.gz" | tar -xz -C /usr/local/bin kyverno; \
curl -fsSL "https://github.com/kyverno/kyverno-json/releases/download/v${KYVERNO_JSON_VERSION}/kyverno-json_linux_${TARGETARCH}.tar.gz" | tar -xz -C /usr/local/bin kyverno-json; \
PROM_TARBALL="prometheus-${PROMTOOL_VERSION}.linux-${TARGETARCH}"; \
curl -fsSL "https://github.com/prometheus/prometheus/releases/download/v${PROMTOOL_VERSION}/${PROM_TARBALL}.tar.gz" \
| tar -xz -C /usr/local/bin --strip-components=1 "${PROM_TARBALL}/promtool"; \
chmod +x /usr/local/bin/yq /usr/local/bin/chainsaw /usr/local/bin/kyverno /usr/local/bin/kyverno-json \
/usr/local/bin/promtool; \
rm -rf /tmp/*
# Record the baked toolchain for provenance.
RUN { echo "opentofu=${OPENTOFU_VERSION}"; echo "age=${AGE_VERSION}"; echo "sops=${SOPS_VERSION}"; \
echo "flux=${FLUX_VERSION}"; echo "kubectl=${KUBECTL_VERSION}"; echo "helm=${HELM_VERSION}"; \
echo "kyverno=${KYVERNO_VERSION}"; echo "kyverno-json=${KYVERNO_JSON_VERSION}"; \
echo "chainsaw=${CHAINSAW_VERSION}"; echo "yamllint=${YAMLLINT_VERSION}"; \
echo "yq=${YQ_VERSION}"; echo "check-jsonschema=${CHECK_JSONSCHEMA_VERSION}"; \
echo "promtool=${PROMTOOL_VERSION}"; \
} > /etc/dev-toolchain-versions
WORKDIR /work
CMD ["/bin/bash"]