Phase 1 (author-only): Flux + SOPS bootstrap + interface wiring (T-1.4)

bootstrap/sops (age-keygen refusing in-repo, .sops.yaml, install-age-key.sh
glue installing the sops-age Secret) + bootstrap/flux (bootstrap.sh: flux
install -> apply -k clusters/dev; decryption/url patches). PM wiring: place
.sops.yaml at repo root (SOPS walks to root); inline the 3 flux patches into
clusters/dev (avoids kustomize ../ load-restriction). clusters/dev builds with
decryption enabled; full CI gate PASS. No key material; nothing executed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
jakman85andClaude Opus 4.8 committed 2026-08-16 01:21:01 -05:00
1 parent 0e901f135c
commit 84a2d762f0
12 files changed
+573 -4

No files matched your search

+91 -2
View File
@@ -1,3 +1,92 @@
# bootstrap/flux
# bootstrap/flux — Flux bootstrap → `clusters/dev` (Phase 1, T-1.4)
_Phase 1 (T-1.x) — to be authored on execution go. See infra/PHASE1-SCOPE.md._
Points Flux at the authored **`clusters/dev`** entrypoint and turns on SOPS decryption.
Spec: [ADR-0004 (Flux)](../../../home-network-k3s/decisions/adr-0004-gitops-flux.md),
[docs/05](../../../home-network-k3s/docs/05-gitops-and-delivery.md) · Requirements: **R-GITOPS-01**
(pull-based reconcile), **R-GITOPS-03** (drift correction), **R-SEC-01** (SOPS decryption).
> **AUTHOR-ONLY. Nothing in this directory has been executed.** No `flux`, `kubectl`, key-gen, or
> cluster contact was run. These are files + runbooks the PM/operator validates and runs.
## Files
| File | Purpose |
|---|---|
| `bootstrap.sh` | **The single bootstrap entrypoint** the Infra worker's final Ansible play calls. `flux install` → verify `sops-age` Secret → `kubectl apply -k clusters/dev`. |
| `patches/platform-sops-decryption.yaml` | Strategic-merge patch enabling `decryption.provider: sops` + `secretRef: sops-age` on the `platform` Kustomization (realises the base TODO). |
| `patches/apps-sops-decryption.yaml` | Same, for the `apps` Kustomization. |
| `patches/flux-system-source-url.yaml` | Overrides the `flux-system` GitRepository `url` placeholder with the real bootstrap source (template — fill at bootstrap). |
## Why `flux install` + apply, not `flux bootstrap git` (Phase 1)
The reconciliation graph is **already authored** in `clusters/base` (read-only to this worker):
`flux-system.yaml` is the `flux-system` **GitRepository**; `platform.yaml`/`apps.yaml` are the Flux
**Kustomizations**; `clusters/dev` is the overlay that patches `cluster-vars` (R-ENV-02).
`flux bootstrap git` **generates and owns its own** `gotk-sync` GitRepository + Kustomization named
`flux-system` and commits them into the repo — which would **duplicate/conflict** with the authored
base. It also requires a **writable remote git host**, which does not exist in Phase 1 (spec docs/05
"Source-of-truth", CONTRIBUTING.md: bring-up is from a local/out-of-band bootstrap source; Gitea
arrives in Phase 6). So Phase 1 uses `flux install` (controllers only) + `kubectl apply -k
clusters/dev` (materialise the authored graph). This **reconciles with** `clusters/base` instead of
regenerating it.
## Bootstrap order (what runs, in sequence)
```
tofu apply → cloud-init → Ansible (k3s + Cilium) [Infra worker, T-1.1..1.3]
│
▼ (final Ansible play calls the entrypoints below)
1. bootstrap/sops/age-keygen.sh # operator generates age key OUTSIDE repo
2. bootstrap/sops/age-key-secret.md # operator installs private key as sops-age Secret
3. bootstrap/flux/bootstrap.sh # flux install → verify sops-age → apply -k clusters/dev
│
▼
Flux reconciles: GitRepository(flux-system) → Kustomization(platform) → Kustomization(apps),
with SOPS decryption enabled (patches/) and cluster-vars overlay (R-ENV-02).
```
## Interface note — for the Infra worker (T-1.3 Ansible)
**Single entrypoint your final play calls:** `bootstrap/flux/bootstrap.sh` (run inside the
`infra/devcontainer` toolchain so pinned `flux`/`kubectl` are on PATH; `FLUX_VERSION`/`SOPS_VERSION`
live in `infra/devcontainer/versions.env`).
**Order your play must enforce** (the age key is a hard dependency of a working decrypt):
1. `bootstrap/sops/age-keygen.sh` (operator step — generates key outside repo).
2. Install the `sops-age` Secret per `bootstrap/sops/age-key-secret.md` (the `kubectl create secret`
template). `bootstrap.sh` **fails fast** if this Secret is absent.
3. `bootstrap/flux/bootstrap.sh` (this entrypoint).
`bootstrap.sh` is idempotent-friendly (`flux install` and `kubectl apply -k` re-run safely) and
never handles key material — it only *verifies* the Secret exists.
## Handoff — clusters owner / PM (files this worker may not edit)
`clusters/` is out of this worker's write scope, so three wirings are delivered as patches here and
must be applied into the `clusters/dev` overlay (`clusters/dev/kustomization.yaml` `patches:`):
```yaml
patches:
- path: ../../bootstrap/flux/patches/flux-system-source-url.yaml # real source URL
- path: ../../bootstrap/flux/patches/platform-sops-decryption.yaml # SOPS on platform
- path: ../../bootstrap/flux/patches/apps-sops-decryption.yaml # SOPS on apps
```
Alternatively the base TODOs in `clusters/base/{platform,apps}.yaml` and the `url` in
`clusters/base/flux-system.yaml` can be filled in directly (same effect) — the clusters owner's call.
Either way the `sops-age` `secretRef` name and `flux-system` source name are kept in lockstep with
the authored base.
## Phase-6 cutover (WS-GITOPS — NOT now)
Once Gitea is stable, migrate the source to Gitea (docs/05, CONTRIBUTING.md). At that point
`flux bootstrap git` becomes appropriate (there is a writable remote to own):
```bash
# ILLUSTRATIVE — Phase 6, not run in Phase 1.
flux bootstrap git \
--url=https://gitea.home.keithjackson.dev/<org>/home-network-k3s-impl.git \
--branch=main \
--path=clusters/dev
```
Keep an off-cluster mirror so a source-of-truth outlives the cluster (docs/05 "Circularity & DR").
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
#
# bootstrap.sh — the SINGLE Flux + SOPS bootstrap entrypoint for the dev cluster.
#
# Phase 1, T-1.4 · ADR-0004 (Flux) · ADR-0007 (SOPS+age) · R-GITOPS-01 · R-SEC-01.
# =============================================================================
# *** AUTHORED, NOT RUN. The Infra worker's FINAL Ansible play invokes this ***
# *** (interface contract, see ../README.md). Agents must not execute it. ***
# =============================================================================
#
# WHAT THIS DOES (Phase 1, bootstrap-source method — reconciles from the LOCAL repo, no remote
# git host exists yet per spec docs/05 + CONTRIBUTING.md):
#
# 1. `flux install` — deploy the Flux controllers ONLY (source, kustomize, helm controllers).
# We do NOT use `flux bootstrap git` in Phase 1 because that command commits
# and OWNS its own gotk-sync GitRepository+Kustomization named `flux-system`.
# Our entrypoints are ALREADY authored in clusters/base (flux-system.yaml =
# the GitRepository; platform.yaml/apps.yaml = the Kustomizations). Letting
# bootstrap regenerate them would DUPLICATE/conflict. `flux install` +
# applying the authored clusters/dev overlay is the reconciling path.
# 2. sops-age Secret — the operator installs the age PRIVATE key out-of-band BEFORE reconcile so
# decryption works on first pass (../sops/age-key-secret.md). This script
# only VERIFIES it exists; it never handles key material.
# 3. `kubectl apply -k clusters/dev` — materialises the authored reconciliation graph:
# GitRepository `flux-system` (source) -> Kustomization `platform` ->
# Kustomization `apps` (dependsOn platform), with cluster-vars overlay
# (R-ENV-02) and SOPS decryption enabled via the patches in ./patches/.
#
# PREREQUISITES the operator/PM resolves before this runs (see ../README.md "Handoff"):
# - clusters/base/flux-system.yaml url placeholder is overridden — wire ./patches/flux-system-
# source-url.yaml into clusters/dev/kustomization.yaml (or set the URL) so the source is real.
# - ./patches/platform-sops-decryption.yaml + apps-sops-decryption.yaml are wired into
# clusters/dev/kustomization.yaml `patches:` so decryption is enabled at reconcile.
# - .sops.yaml (from ../sops) is installed at the repo root with the real age recipient.
#
# PHASE 6 (WS-GITOPS, later — NOT Phase 1): once Gitea is up, cut the source over with
# `flux bootstrap git` against gitea.home.keithjackson.dev — see ../README.md "Phase-6 cutover".
#
set -euo pipefail
# --- config (env-overridable) ------------------------------------------------
FLUX_ENV_PATH="${FLUX_ENV_PATH:-clusters/dev}" # dev is the ONLY Phase-1 build target
FLUX_NAMESPACE="${FLUX_NAMESPACE:-flux-system}"
SOPS_SECRET_NAME="${SOPS_SECRET_NAME:-sops-age}"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel 2>/dev/null || cd "${script_dir}/../.." && pwd)"
echo "==> Flux + SOPS bootstrap (dev) — path=${FLUX_ENV_PATH}"
# --- 0. tooling present (from infra/devcontainer; versions pinned there) ------
for bin in flux kubectl; do
command -v "$bin" >/dev/null 2>&1 || { echo "ERROR: '$bin' not found (use infra/devcontainer)." >&2; exit 1; }
done
# --- 1. install Flux controllers ---------------------------------------------
echo "==> [1/3] flux install (controllers only)"
flux install --namespace="${FLUX_NAMESPACE}"
# --- 2. verify the out-of-band age key Secret exists -------------------------
# We DO NOT create it here (it holds the private key). The operator installs it per
# ../sops/age-key-secret.md. Fail fast if missing so decryption can't silently no-op.
echo "==> [2/3] verifying ${SOPS_SECRET_NAME} Secret exists in ${FLUX_NAMESPACE}"
if ! kubectl -n "${FLUX_NAMESPACE}" get secret "${SOPS_SECRET_NAME}" >/dev/null 2>&1; then
echo "ERROR: Secret ${FLUX_NAMESPACE}/${SOPS_SECRET_NAME} not found." >&2
echo " Install the age private key first: bootstrap/sops/age-key-secret.md" >&2
exit 1
fi
# --- 3. apply the authored dev entrypoint ------------------------------------
echo "==> [3/3] kubectl apply -k ${FLUX_ENV_PATH}"
kubectl apply -k "${repo_root}/${FLUX_ENV_PATH}"
echo
echo "==> Bootstrap applied. Watch reconciliation with (read-only):"
echo " flux get kustomizations --watch"
echo " flux get sources git -n ${FLUX_NAMESPACE}"
echo " 'platform' should go Ready, then 'apps' (dependsOn platform)."
@@ -0,0 +1,21 @@
---
# Enable SOPS decryption on the `apps` Flux Kustomization (ADR-0007, R-SEC-01).
#
# REALISES the commented TODO in clusters/base/apps.yaml (same pattern as the platform patch).
# Strategic-merge patch — adds the decryption stanza to the base `apps` Kustomization without
# forking it (R-ENV-02). `sops-age` Secret referenced BY NAME ONLY (created out-of-band).
#
# WIRING (clusters owner / PM): add to clusters/dev/kustomization.yaml `patches:` alongside the
# platform patch, e.g.
# patches:
# - path: ../../bootstrap/flux/patches/apps-sops-decryption.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
decryption:
provider: sops
secretRef:
name: sops-age
@@ -0,0 +1,26 @@
---
# Set the reconciliation source URL on the `flux-system` GitRepository (R-GITOPS-01).
#
# clusters/base/flux-system.yaml ships a deliberate placeholder:
# url: https://REPLACE-ME.invalid/home-network-k3s-impl.git
# because the bootstrap-source URL is environment-specific and unknown at authoring time (Phase 1
# has no remote git host yet — spec docs/05 "Source-of-truth", CONTRIBUTING.md).
#
# This strategic-merge patch overrides that URL at bootstrap WITHOUT editing clusters/base
# (R-ENV-02). Fill spec.url with the actual bootstrap source, then wire this file into
# clusters/dev/kustomization.yaml `patches:` (clusters owner / PM). See ../README.md.
#
# PHASE 6 CUTOVER (WS-GITOPS): change this URL to
# https://gitea.home.keithjackson.dev/<org>/home-network-k3s-impl.git
# and keep an off-cluster mirror (docs/05 "Circularity & DR").
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: flux-system
namespace: flux-system
spec:
# >>> REPLACE-ME <<< actual bootstrap source URL (how the local repo is served to the cluster:
# git-over-ssh, a git daemon on the workstation NAT gateway, etc. — set by WS-INFRA/operator).
url: https://REPLACE-ME.invalid/home-network-k3s-impl.git
ref:
branch: main
@@ -0,0 +1,29 @@
---
# Enable SOPS decryption on the `platform` Flux Kustomization (ADR-0007, R-SEC-01).
#
# This REALISES the commented TODO in clusters/base/platform.yaml:
# # decryption:
# # provider: sops
# # secretRef:
# # name: sops-age
# It is a strategic-merge patch (matched by apiVersion/kind/name/namespace) that ADDS the
# decryption stanza to the existing base Kustomization WITHOUT forking it (R-ENV-02).
#
# The `sops-age` Secret is created out-of-band (../../sops/age-key-secret.md); it is referenced
# BY NAME ONLY here — no key material in git.
#
# WIRING (clusters owner / PM — outside the security engineer's subtree): add this file to
# clusters/dev/kustomization.yaml `patches:` (path-relative), e.g.
# patches:
# - path: ../../bootstrap/flux/patches/platform-sops-decryption.yaml
# so the dev overlay reconciles platform manifests with decryption enabled. See ../README.md.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: platform
namespace: flux-system
spec:
decryption:
provider: sops
secretRef:
name: sops-age