Solution
Reference solution
A working solution consists of:
- A real GitHub pull request on
OE_ENV_REPOwhose head branch issandcastle/hello-world-pico-<timestamp>and whose base is the repo’s default branch. The head branch carries the hand-off’sxr.yamlatenvs/dev/xr.yaml. - A merge commit for that PR on the default branch, whose SHA is captured in
work/outer-delivery-loop/state/merge-sha.txt. - Either:
- a Flux
GitRepository+Kustomizationpair (oe-env-hello-world-picoin namespaceflux-system) reconcilingenvs/dev/on the merged branch, or - a fresh clone of the merged branch that a
kubectl applyofenvs/dev/xr.yamlsucceeded from (equivalent to one Flux iteration).
- a Flux
- The Kubernetes
Jobcomposed by Crossplane from the reconciled XR, verified by the unchangedlabs/hello-pico-on-kubernetes/downloads/verify.shprinting its ownverify: OKline. - A clean
verify.shexit (exit code 0) with one of the two documented final lines.
Merged XR shape (byte-equivalent to the hand-off XR)
apiVersion: oe.academy/v1alpha1
kind: XHelloWorldPico
metadata:
name: hello
spec:
value: "Hello, Pico!"Commands
Run from the root of your local clone of the academy repository:
export OE_ENV_REPO=your-handle/oe-env-hello-world-pico
bash labs/outer-delivery-loop/downloads/verify.shExpected verify.sh output (final line)
Stages 1–2 only (no cluster):
verify: OK — outer loop stages 1–2 complete on <repo> PR #<n> (cluster stages skipped)
Full stages 1–4 (with a Crossplane cluster):
verify: OK — outer loop stages 1–4 complete on <repo> PR #<n> (merge sha <sha>)
Why this satisfies the objectives
- Environment repo as source of truth. The merged commit on the default branch — not the Sandcastle, not the hand-off — is the cluster’s desired state. If you delete the local
work/outer-delivery-loop/directory the loop is still complete; the truth lives on GitHub. - Guardrails outside the Sandcastle boundary. Review, checks, and branch protection attach to the PR on GitHub. Policy (OPA, Kyverno, Gatekeeper) attaches at cluster admission before Crossplane sees the XR. Neither surface is inside the Sandcastle.
- GitOps delivery is a pull, not a push from the Sandcastle. In Flux mode, Flux polls the env repo and applies the XR on every change. In
kubectlmode, the same effect happens once, from a fresh clone of the merged branch. Both preserve the rule that the merged branch is the only outer-loop input. - Crossplane closes the loop. The composed
Jobruns and printsHello, Pico!, verified by the existing Kubernetes lab’sverify.sh. The outer loop hands the XR off once and Crossplane keeps reconciling long after this lab exits.
Verification
You can verify the solution matches this reference by:
Running
bash labs/outer-delivery-loop/downloads/verify.shand checking the finalverify: OKline and exit status0.Opening the PR URL from
work/outer-delivery-loop/state/pr-url.txton GitHub and confirming it is merged and the merge commit matcheswork/outer-delivery-loop/state/merge-sha.txt.Fetching the merged XR from GitHub and diffing it against the hand-off XR on the boundary fields:
gh api "repos/$OE_ENV_REPO/contents/envs/dev/xr.yaml?ref=$(cat work/outer-delivery-loop/state/base.txt)" \ --jq .content | base64 -d \ | grep -E '^(apiVersion|kind| name| value):' > /tmp/remote-xr.txt grep -E '^(apiVersion|kind| name| value):' \ work/handoff-sandcastle-to-kubernetes/build/xr.yaml > /tmp/local-xr.txt diff -u /tmp/local-xr.txt /tmp/remote-xr.txtAn empty diff (exit code 0) is the pass.
On the cluster, running the Kubernetes lab’s
verify.shand confirming its ownverify: OK — job=... value="Hello, Pico!"line.
Variations
- Different greeting: change the agent’s greeting per the Sandcastle lab’s Variations, rerun the hand-off, then rerun this lab’s
verify.sh(EXPECTED_VALUE='Hello, World!' ...). Each run gets a fresh timestamped feature branch, so previous PRs stay in the history. - Realistic branch protection: on a shared env repo, remove
--adminfromMERGE_FLAGSand rely on required reviewers and required checks (for example a re-run oflabs/handoff-sandcastle-to-kubernetes/downloads/verify.shin GitHub Actions) to gate the merge. - Flux vs
kubectl: forceMODE=kubectl bash .../gitops-sync.shto see the single-shot form of the reconciler, then reinstall Flux and re-run in Flux mode to see the continuous-poll form. The cluster-side outcome is identical.
Not covered by this lab (intentional)
- Bootstrapping Flux or a cluster. Flux installation and the Crossplane cluster itself are operator-controlled prerequisites, documented under Objectives. The lab uses them when present but does not manage their lifecycle.
- Multi-artifact / multi-environment promotion. This lab promotes a single XR to a single env directory (
envs/dev/xr.yaml). Multi-env promotion (dev → staging → prod) and multi-artifact hand-offs are natural follow-on Part 3 lessons. - Full policy demo. The lab makes the policy guardrail slot concrete (cluster admission before Crossplane sees the XR) but does not ship an OPA/Kyverno/Gatekeeper policy. Adding one is a reasonable extension exercise.