Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b27417ff02 | |||
| b67ada9c65 | |||
| f6616a5863 | |||
| ff2b8dabdc | |||
| 8f2224deca | |||
| cf0295383b | |||
| d443f9ad6b | |||
| 7f7035b98f | |||
| 08e80d9f9f | |||
| 7fd24d0343 | |||
| 5076f52b21 | |||
| 847dd1424b | |||
| f155061f35 | |||
| 20b1dcfb9a | |||
| 26275c6262 | |||
| 4849627131 | |||
| 50993bedec | |||
| b41564d462 | |||
| 2e38b27d5f | |||
| 985c442feb | |||
| 98037d7550 | |||
| 7613bfa290 | |||
| e769007e0d | |||
| fee0de72e6 | |||
| d8a39c6a6c | |||
| b12e435003 | |||
| a201af9d29 | |||
| d0efbe0194 | |||
| 8b2a37d840 | |||
| 5a56ed6434 | |||
| 5a7b45f1b9 | |||
| e69d0854b3 | |||
| c0b9359e27 | |||
| 7a82fca26e | |||
| 0624419b90 | |||
| d50c9432e0 | |||
| 39ef04b38a | |||
| 7aa50d1f90 | |||
| 7caa9d5342 | |||
| 5cce8c5cd3 | |||
| dcbf896222 | |||
| ef937e8b07 | |||
| c7b5f862d5 | |||
| a3c6d33ecf |
@@ -1,3 +1,6 @@
|
||||
node_modules/
|
||||
*.log
|
||||
assets/work/
|
||||
/dist/
|
||||
pilot/course-1.html
|
||||
pilot/arc.html
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Static teaching site: build.sh runs the node assembler (pilot/assemble-arc.mjs) to
|
||||
# produce dist/, then a pinned nginx-unprivileged serves it. No package installs - the
|
||||
# assembler is dependency-free node.
|
||||
|
||||
# ---- build stage ----------------------------------------------------------
|
||||
# A bookworm-slim node base, digest-pinned and typically served from the zot
|
||||
# pull-through mirror; node runs the assembler.
|
||||
FROM node:22-bookworm-slim@sha256:d9f850096136edbc402debdd8729579a288aac64574ada0ff4db26b6ae58b0b2 AS build
|
||||
ARG BUILD_REV=dev
|
||||
ENV BUILD_REV=$BUILD_REV
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN bash build.sh
|
||||
|
||||
# ---- runtime stage --------------------------------------------------------
|
||||
# Same vetted digest as the site. Renovate keeps it current.
|
||||
FROM ghcr.io/nginx/nginx-unprivileged:1.28.0-alpine@sha256:c97ff0bf7cbae369953c6da1232ec14ad9f971d66360c5698db0856a4cd657a0
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY nginx/security-headers.conf /etc/nginx/security-headers.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8080
|
||||
@@ -1,26 +1,52 @@
|
||||
# The Exploded Cluster
|
||||
# The Teaching Lab (learn.bztmon.com)
|
||||
|
||||
Interactive scroll-driven teardown of Kubernetes / OpenShift / Docker, in the estate's
|
||||
Bat-Computer visual world (void `#070b14`, cyan `#3fbaf5` → magenta `#e879f9`).
|
||||
A static learning site documenting infrastructure I operate in my homelab - the decisions,
|
||||
the failures, and the validation still outstanding. Dark engineering-dossier aesthetic (void `#070b14`, cyan `#3fbaf5`,
|
||||
magenta `#e879f9`), dependency-light by design, strict security posture.
|
||||
|
||||
- **v2** (tag `v2`) — CSS `preserve-3d` diagrams. Parked: eyeballed px positions, misaligned at some widths.
|
||||
- **v3** (in progress) — Nano Banana 2 exploded hero shots, hand-sliced into `clip-path` parts and
|
||||
scrubbed collapsed → hero with GSAP. Spec: `homelab/specs/SPEC-exploded-cluster-v3.md`.
|
||||
## Pages
|
||||
| Route | Page | Source |
|
||||
|---|---|---|
|
||||
| `/` | The Teaching Lab - chapter directory | `pilot/index.tpl.html` |
|
||||
| `/cluster` | Chapter 01: The Exploded Cluster - eleven-course container-platform teardown | `pilot/arc.tpl.html` + `pilot/manifest-*.json` |
|
||||
| `/mirror` | Chapter 02: The Mirror - a zot pull-through registry, its failure modes and operations | `pilot/mirror.tpl.html` + `pilot/mirror.js` |
|
||||
|
||||
## Build
|
||||
`bash build.sh` -> `dist/`. The assembler (`pilot/assemble-arc.mjs`, plain node, no
|
||||
dependencies) wraps each page in a shared document shell (canonical/OG metadata, build
|
||||
revision, skip link), content-fingerprints every file under `/assets/` (immutable-cached
|
||||
by nginx), splits page scripts into external fingerprinted files, injects intrinsic image
|
||||
dimensions (WebP and JPEG parsed from the actual bytes) with deliberate lazy/eager loading,
|
||||
and runs per-page preflight gates: no inline scripts (CSP `script-src 'self'` with no
|
||||
exceptions), exactly one `h1`, unique ids, no dangling anchors, no unresolved placeholders,
|
||||
and page-specific content assertions. A failed gate fails the build.
|
||||
|
||||
Ship: `podman build` (multi-stage: node builds `dist/`, `nginx-unprivileged` serves it,
|
||||
both digest-pinned) -> push to the estate registry -> digest-pinned deploy via GitOps.
|
||||
`nginx/` carries the runtime config: strict CSP, HSTS, no-cache HTML with immutable
|
||||
fingerprinted assets, security headers on every location.
|
||||
|
||||
## Accessibility and performance
|
||||
Skip link on every page (hidden until keyboard focus), one `h1` per page, stable section
|
||||
anchors, reduced-motion honoured live (effects also pause when the tab is hidden or the
|
||||
scene is off screen), intrinsic image dimensions to prevent layout shift, below-fold images
|
||||
lazy-loaded, system font stacks (CSP permits no third-party fonts).
|
||||
|
||||
## Layout
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `exploded-cluster.html` | v2 assembled single file (174KB) |
|
||||
| `ec2-part1.html` / `ec2-part2.html` | v2 source split — markup+CSS / app JS |
|
||||
| `gsap.min.js` · `st.min.js` | GSAP 3.13 + ScrollTrigger, inlined at assemble time |
|
||||
| `prompts/` | NB2 prompt contract — master style block + per-scene prompts |
|
||||
| `tools/slicer.html` | slice editor: load a hero, tune cut lines, export the manifest |
|
||||
| `assets/raw/` | curated NB2 heroes, straight from the generator |
|
||||
| `pilot/` | page templates, page scripts, scene manifests, the assembler |
|
||||
| `assets/dist/` | production heroes and diagrams for `/cluster` |
|
||||
| `assets/mirror/` | plates for `/mirror` |
|
||||
| `content/` | course source notes |
|
||||
| `nginx/` | runtime config baked into the image |
|
||||
| `tools/slicer.html` | slice editor: load a hero, tune cut lines, export a scene manifest |
|
||||
| `prompts/` | image prompt contract for the house visual style |
|
||||
| `exploded-cluster.html`, `ec2-part*.html` | v2 (parked, superseded by `/cluster`) |
|
||||
| `gsap.min.js` / `st.min.js` | GSAP 3.13 + ScrollTrigger, fingerprinted at assemble time |
|
||||
|
||||
Rebuild v2: `cat ec2-part1.html <(echo "<script>") gsap.min.js <(echo "</script><script>") st.min.js <(echo "</script>") ec2-part2.html > exploded-cluster.html`
|
||||
|
||||
## The slicing rule (load-bearing — read before touching the engine)
|
||||
Parts occlude each other in 2D, so every slice carries a hidden notch where the part in front
|
||||
covers it. Animating **collapsed → hero** only ever increases occlusion, so notches stay buried.
|
||||
**Never overshoot past the hero layout and never fan parts sideways** — either exposes the tears.
|
||||
Region boundaries overlap upward by `EPS`; expanding downward would ghost.
|
||||
## The slicing rule (load-bearing - read before touching the arc engine)
|
||||
Parts occlude each other in 2D, so every slice carries a hidden notch where the part in
|
||||
front covers it. Animating **collapsed -> hero** only ever increases occlusion, so notches
|
||||
stay buried. **Never overshoot past the hero layout and never fan parts sideways** - either
|
||||
exposes the tears. Region boundaries overlap upward by `EPS`; expanding downward would ghost.
|
||||
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1,118 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="660" viewBox="0 0 1180 660" role="img" aria-label="End-to-end delivery flow in four lanes converging on one running workload. Change lane: a commit lands in the GitHub repository, Argo CD renders and diffs it against the live cluster and syncs the difference - the cluster pulls, nothing pushes. Shape lane: the Helm chart plus a per-cluster values context passes the schema gate, which refuses to render if a required value is missing, and produces the manifests Argo CD applies. Artefact lane: CI builds once and pushes to Azure Container Registry; a central pull-through mirror in the cloud fronts upstream, per-site mirrors pull from the centre, and each node pulls by digest - the load fans out in tiers so no thundering herd ever reaches upstream. Secret lane: Azure Key Vault holds the values, the External Secrets operator syncs them into the cluster, and git only ever holds the reference. The workload that runs is the sum of all four: synced state, rendered shape, digest-pinned image, injected secret.">
|
||||
<defs>
|
||||
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b"></path>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="1180" height="660" fill="#ffffff"></rect>
|
||||
|
||||
<!-- title -->
|
||||
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">The delivery flow, end to end</text>
|
||||
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">four lanes, one workload - courses VI, VIII and VII, in the order the bytes travel</text>
|
||||
|
||||
<!-- convergence -->
|
||||
<rect x="950" y="110" width="170" height="470" rx="14" fill="#0c8fce" fill-opacity="0.06" stroke="#0c8fce" stroke-width="3"></rect>
|
||||
<text x="1035" y="146" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0c8fce">THE WORKLOAD</text>
|
||||
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
|
||||
<text x="970" y="186">synced state</text>
|
||||
<text x="970" y="208">(C-VI)</text>
|
||||
<text x="970" y="286">rendered shape</text>
|
||||
<text x="970" y="308">(C-VIII)</text>
|
||||
<text x="970" y="416">digest-pinned</text>
|
||||
<text x="970" y="438">image (C-VII)</text>
|
||||
<text x="970" y="516">injected secret</text>
|
||||
<text x="970" y="538">(C-VI)</text>
|
||||
</g>
|
||||
|
||||
<!-- CHANGE lane -->
|
||||
<text x="60" y="122" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" letter-spacing="2" fill="#0c8fce">CHANGE</text>
|
||||
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
|
||||
<rect x="60" y="136" width="150" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="135" y="165" text-anchor="middle">a commit</text>
|
||||
<rect x="270" y="136" width="180" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="360" y="165" text-anchor="middle">GitHub repo</text>
|
||||
<rect x="510" y="136" width="230" height="48" rx="10" fill="#0c8fce" fill-opacity="0.06" stroke="#0c8fce" stroke-width="2"></rect>
|
||||
<text x="625" y="165" text-anchor="middle" fill="#0c8fce">Argo CD: render + diff + sync</text>
|
||||
</g>
|
||||
<g stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)">
|
||||
<line x1="210" y1="160" x2="264" y2="160"></line>
|
||||
<line x1="740" y1="160" x2="944" y2="160"></line>
|
||||
</g>
|
||||
<line x1="450" y1="160" x2="504" y2="160" stroke="#64748b" stroke-width="1.5" stroke-dasharray="6 5" marker-end="url(#arr)"></line>
|
||||
<text x="477" y="150" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">pull</text>
|
||||
<text x="510" y="206" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">the cluster pulls - no CI credential can touch it; drift is put back, rollback is git revert</text>
|
||||
|
||||
<!-- SHAPE lane -->
|
||||
<text x="60" y="252" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" letter-spacing="2" fill="#7c3aed">SHAPE</text>
|
||||
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
|
||||
<rect x="60" y="266" width="150" height="48" rx="10" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="1.5"></rect>
|
||||
<text x="135" y="295" text-anchor="middle">the chart</text>
|
||||
<rect x="230" y="266" width="170" height="48" rx="10" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="1.5"></rect>
|
||||
<text x="315" y="288" text-anchor="middle">values context</text>
|
||||
<text x="315" y="305" text-anchor="middle" font-size="10" fill="#64748b">one file per cluster</text>
|
||||
<rect x="460" y="266" width="130" height="48" rx="10" fill="#e8b44a" fill-opacity="0.08" stroke="#e8b44a" stroke-width="2"></rect>
|
||||
<text x="525" y="288" text-anchor="middle" fill="#8a6414">schema gate</text>
|
||||
<text x="525" y="305" text-anchor="middle" font-size="10" fill="#8a6414">required, or refuse</text>
|
||||
<rect x="650" y="266" width="180" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="740" y="295" text-anchor="middle">rendered manifests</text>
|
||||
</g>
|
||||
<g stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)">
|
||||
<line x1="210" y1="290" x2="224" y2="290"></line>
|
||||
<line x1="400" y1="290" x2="454" y2="290"></line>
|
||||
<line x1="590" y1="290" x2="644" y2="290"></line>
|
||||
<path d="M 740 266 L 740 236 L 660 236 L 660 190" fill="none"></path>
|
||||
</g>
|
||||
<text x="756" y="242" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">into the sync</text>
|
||||
|
||||
<!-- ARTEFACT lane -->
|
||||
<text x="60" y="392" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" letter-spacing="2" fill="#c026d3">ARTEFACT</text>
|
||||
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
|
||||
<rect x="60" y="406" width="130" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="125" y="428" text-anchor="middle">CI builds</text>
|
||||
<text x="125" y="445" text-anchor="middle" font-size="10" fill="#64748b">once, only once</text>
|
||||
<rect x="228" y="406" width="150" height="48" rx="10" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="1.5"></rect>
|
||||
<text x="303" y="428" text-anchor="middle">ACR upstream</text>
|
||||
<text x="303" y="445" text-anchor="middle" font-size="10" fill="#64748b">tag@sha256 minted</text>
|
||||
<rect x="416" y="406" width="160" height="48" rx="10" fill="#c026d3" fill-opacity="0.06" stroke="#c026d3" stroke-width="2"></rect>
|
||||
<text x="496" y="428" text-anchor="middle">central mirror</text>
|
||||
<text x="496" y="445" text-anchor="middle" font-size="10" fill="#64748b">one cloud cache, fronts upstream</text>
|
||||
<rect x="614" y="406" width="150" height="48" rx="10" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="1.5"></rect>
|
||||
<text x="689" y="428" text-anchor="middle">site mirror</text>
|
||||
<text x="689" y="445" text-anchor="middle" font-size="10" fill="#64748b">zot at the edge</text>
|
||||
<rect x="802" y="406" width="118" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="861" y="428" text-anchor="middle">node pulls</text>
|
||||
<text x="861" y="445" text-anchor="middle" font-size="10" fill="#64748b">by digest</text>
|
||||
</g>
|
||||
<g stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)">
|
||||
<line x1="190" y1="430" x2="222" y2="430"></line>
|
||||
<line x1="378" y1="430" x2="410" y2="430"></line>
|
||||
<line x1="576" y1="430" x2="608" y2="430"></line>
|
||||
<line x1="764" y1="430" x2="796" y2="430"></line>
|
||||
<line x1="920" y1="430" x2="944" y2="430"></line>
|
||||
</g>
|
||||
<text x="60" y="480" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">promotion is a copy of the same digest - if the digest changed, it is a new candidate, not a promotion</text>
|
||||
<text x="60" y="498" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">the tiers layer the load: upstream sees one consumer, each site sees one hop, nodes never leave the LAN</text>
|
||||
|
||||
<!-- SECRET lane -->
|
||||
<text x="60" y="522" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" letter-spacing="2" fill="#8a6414">SECRET</text>
|
||||
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
|
||||
<rect x="60" y="536" width="180" height="48" rx="10" fill="#e8b44a" fill-opacity="0.08" stroke="#e8b44a" stroke-width="1.5"></rect>
|
||||
<text x="150" y="558" text-anchor="middle" fill="#8a6414">Azure Key Vault</text>
|
||||
<text x="150" y="575" text-anchor="middle" font-size="10" fill="#8a6414">holds the values</text>
|
||||
<rect x="300" y="536" width="180" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="390" y="558" text-anchor="middle">ESO operator</text>
|
||||
<text x="390" y="575" text-anchor="middle" font-size="10" fill="#64748b">exchanges + keeps synced</text>
|
||||
<rect x="540" y="536" width="180" height="48" rx="10" fill="none" stroke="#64748b" stroke-width="1.5"></rect>
|
||||
<text x="630" y="558" text-anchor="middle">Secret, in-cluster</text>
|
||||
<text x="630" y="575" text-anchor="middle" font-size="10" fill="#64748b">never in git</text>
|
||||
</g>
|
||||
<g stroke="#64748b" stroke-width="1.5" marker-end="url(#arr)">
|
||||
<line x1="240" y1="560" x2="294" y2="560"></line>
|
||||
<line x1="480" y1="560" x2="534" y2="560"></line>
|
||||
<line x1="720" y1="560" x2="944" y2="560"></line>
|
||||
</g>
|
||||
<text x="760" y="550" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">git holds only the reference</text>
|
||||
|
||||
<!-- footer -->
|
||||
<text x="590" y="636" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#0e1726">this page shipped through this exact flow - the delivery arc is deployed by a delivery arc</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble dist/ - the CSP-clean static build served at learn.bztmon.com.
|
||||
# The site's face is the delivery arc (index.html); scripts stay SEPARATE files so
|
||||
# `script-src 'self'` holds with no 'unsafe-inline' exception. Assembly logic lives
|
||||
# in pilot/assemble-arc.mjs (node - the build image carries no python).
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
trap 'printf "build failed at line %s\n" "$LINENO" >&2' ERR
|
||||
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd -- "$here"
|
||||
|
||||
for f in pilot/arc.tpl.html pilot/manifest-docker.json pilot/manifest-cluster.json \
|
||||
pilot/manifest-surround.json pilot/manifest-node.json pilot/manifest-pod.json \
|
||||
pilot/manifest-traffic.json pilot/manifest-orbit.json \
|
||||
pilot/manifest-gitops.json pilot/manifest-supply.json \
|
||||
pilot/manifest-helm.json gsap.min.js st.min.js \
|
||||
assets/dist/course-VI-gitops.webp assets/dist/course-VII-supply-chain.webp \
|
||||
assets/dist/course-VIII-helm-press.webp assets/dist/appendix-dependency-ledger.webp \
|
||||
assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \
|
||||
assets/dist/course-II-cluster.webp assets/dist/course-00-surround.webp \
|
||||
assets/dist/course-III-node.webp assets/dist/course-III-pod.webp \
|
||||
assets/dist/course-IV-traffic.webp assets/dist/course-V-orbit.webp \
|
||||
assets/dist/intro-establishing.webp \
|
||||
pilot/mirror.tpl.html pilot/mirror.js pilot/index.tpl.html \
|
||||
assets/mirror/20.jpg assets/mirror/21.jpg assets/mirror/22.jpg \
|
||||
assets/mirror/23.jpg assets/mirror/24.jpg assets/mirror/25.jpg; do
|
||||
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
|
||||
done
|
||||
|
||||
dist="$here/dist"
|
||||
rm -rf -- "$dist"
|
||||
node pilot/assemble-arc.mjs "$dist"
|
||||
|
||||
# No inline JS may survive into ANY served HTML, or the CSP silently kills the page.
|
||||
for page in "$dist"/*.html; do
|
||||
if grep -oiE '<script[^>]*>' -- "$page" | grep -qivE 'src='; then
|
||||
printf '%s contains an inline <script> - CSP script-src self would block it\n' "$page" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
printf 'built %s\n' "$dist"
|
||||
find "$dist" -type f -printf ' %-32P %8s bytes\n' | sort
|
||||
@@ -0,0 +1,86 @@
|
||||
# Appendix - The dependency ledger: GitHub, Azure Key Vault, zot
|
||||
|
||||
**Series position:** appendix to the delivery arc (VI-VIII) - sits after Course VIII,
|
||||
beside the phrasebook. DOM-only page: a ledger, not a set-piece - no hero, no scrub
|
||||
(art optional later; the arc's three scenes carry the visuals).
|
||||
**Safety gate:** public site - technologies only, zero employer/industry framing.
|
||||
Claims source from [[zot-registry]], [[homelab-pull-through-registry]],
|
||||
[[eso-infisical-secrets]] (pattern generalised to the AKV example), [[argocd-stacks]],
|
||||
public GitHub/Azure/zot docs.
|
||||
|
||||
## Title (thesis)
|
||||
**Every toolchain stands on services it does not run.**
|
||||
|
||||
## Hook - the uncomfortable default
|
||||
The delivery arc reads like a closed machine: repo to reconciler to registry to node.
|
||||
It is not closed. Three of its load-bearing pieces live outside the cluster - one of
|
||||
them outside the building entirely - and the honest move is to write that down. A
|
||||
dependency you have not written down is a dependency you will rediscover during an
|
||||
outage.
|
||||
|
||||
## The ledger
|
||||
Three columns of truth per dependency: what it is, what leans on it, and - the one
|
||||
everyone skips - what actually happens when it is down.
|
||||
|
||||
### GitHub - where the desired state lives
|
||||
A hosted git service; here it is the system of record the whole loop watches. ArgoCD
|
||||
holds a read-only deploy key and polls a repo + path + revision (Course VI); every
|
||||
change to the cluster is a commit here first, and the audit trail is the commit log.
|
||||
LEANS ON IT: ArgoCD sync, rollback (git revert), change review, the "who merged this"
|
||||
answer.
|
||||
WHEN IT IS DOWN: the cluster does not care. The reconciler keeps enforcing the last
|
||||
synced state indefinitely - workloads run, drift is still corrected from cache. What
|
||||
stops is CHANGE: nothing new can land, and rollback-via-revert waits with it. GitOps
|
||||
degrades to read-only, which is the graceful half of the design.
|
||||
|
||||
### Azure Key Vault - where the secret values live
|
||||
A managed secret store; here it is the vault half of the split from Course VI: git
|
||||
carries the ExternalSecret REFERENCE (a logical name), the vault carries the VALUE,
|
||||
and an operator inside the cluster exchanges one for the other and keeps it synced.
|
||||
Rotation happens here, never as a commit.
|
||||
LEANS ON IT: External Secrets sync, secret rotation, first deploy of anything that
|
||||
needs a credential.
|
||||
WHEN IT IS DOWN: already-synced Secrets keep working - the values are materialised
|
||||
in-cluster, so running workloads feel nothing. What stops is rotation and NEW secrets:
|
||||
a fresh app blocks at its ExternalSecret, and an expiring credential becomes a real
|
||||
deadline. Down is survivable; down during a rotation window is an incident.
|
||||
|
||||
### zot - where the fleet pulls from
|
||||
A self-hosted OCI registry running as a pull-through mirror (Course VII): the fleet
|
||||
asks zot, zot asks the upstream managed registry (ACR in the arc's example) once, and
|
||||
every later pull is local. It is the availability seam between the cluster and the
|
||||
internet - and the control point where what enters the estate can be gated and audited.
|
||||
LEANS ON IT: every image pull on every node - boot, reschedule, scale-up, node
|
||||
recovery.
|
||||
WHEN IT IS DOWN: the sharpest edge of the three. Upstream down + mirror up = nobody
|
||||
notices, cache serves (that is the mirror's whole argument). Mirror down = nodes fall
|
||||
back to upstream only if the pull-path config says they may - a disconnected or
|
||||
mirror-only fleet CANNOT PULL, so running pods keep running but nothing new schedules
|
||||
and a rebooting node may not come back whole. The mirror is the dependency the other
|
||||
two get to lean on.
|
||||
|
||||
## Close
|
||||
Write the ledger for your own stack and notice the asymmetry it exposes: none of these
|
||||
outages stop what is already running - they stop CHANGE, ROTATION and RECOVERY, in that
|
||||
order of pain. Design for the outage you will actually have: cache what you pull, split
|
||||
references from values, and let the cluster hold its last known truth without asking
|
||||
anyone's permission.
|
||||
|
||||
## Legend terms (DOM layer -> phrasebook additions)
|
||||
system of record / deploy key / read-only degradation / ExternalSecret / reference vs
|
||||
value / rotation window / pull-through cache / availability seam / mirror-only pull path
|
||||
|
||||
## Page shape (build note)
|
||||
Ledger layout, not prose: three cards (or one three-row table on wide viewports), each
|
||||
carrying the blurb + "leans on it" + "when it is down" strips - the amber FIELD-NOTE
|
||||
treatment fits the "when it is down" strip. Cross-link each card to its owning course
|
||||
(VI, VI, VII respectively). No slice scene; if art is ever wanted, a single small
|
||||
still of three unconnected objects (crystal slab / vault cube / way-station) reusing
|
||||
the arc heroes' silhouettes - do not generate anything new for this page now.
|
||||
|
||||
## Page footer - further reading (site convention)
|
||||
GitHub docs (https://docs.github.com/); Azure Key Vault
|
||||
(https://learn.microsoft.com/azure/key-vault/); zot registry
|
||||
(https://zotregistry.dev/); and the arc's canon trio: Kubernetes
|
||||
(https://kubernetes.io/docs/), Helm (https://helm.sh/docs/), Red Hat OpenShift
|
||||
(https://docs.redhat.com/en/documentation/openshift_container_platform).
|
||||
@@ -0,0 +1,81 @@
|
||||
# Course VI - GitOps: ArgoCD, the GitHub linkage, and the one thing git never holds
|
||||
|
||||
**Series position:** first course of the delivery arc (VI-VIII). The main menu taught the
|
||||
machine; this arc teaches how software REACHES it.
|
||||
**Safety gate:** public site, real name - technologies only, zero employer/industry framing.
|
||||
Claims source from [[argocd-stacks]], [[gitops-iac-rigor]], [[eso-infisical-secrets]]
|
||||
(generalised to the Azure Key Vault worked example), [[authentik-oidc]] not needed here.
|
||||
|
||||
## Title (thesis)
|
||||
**Nobody deploys anything. The cluster syncs itself.**
|
||||
|
||||
## Hook - the uncomfortable default
|
||||
The mental model everyone arrives with: a person (or a pipeline) with credentials pushes
|
||||
manifests at the cluster. Deploy = an action someone performs, kubectl apply = the verb.
|
||||
The inversion: in GitOps nothing is pushed. A repository holds the desired state, an agent
|
||||
inside the cluster watches it, and the cluster pulls its own future from git. kubectl is
|
||||
for looking, git is for changing.
|
||||
|
||||
## Move 1 - The loop you already know, one level up
|
||||
Course II taught the reconciliation loop: desired vs actual, fix the difference, repeat.
|
||||
ArgoCD is the same habit applied to DELIVERY. An Application names a repo + path + revision
|
||||
(the GitHub linkage: watch this branch of this repository); the controller renders what it
|
||||
finds there, compares it against the live cluster, and shows the diff. Sync = make actual
|
||||
match desired. The deploy button is a git commit; the change history is git log; code review IS
|
||||
change control. (Accuracy: git records INTENT - the cluster's own audit log + the reconciler's
|
||||
sync history still hold what actually happened.)
|
||||
Say it like you mean it: **kubectl apply is for archaeology, not for change.**
|
||||
|
||||
## Move 2 - Pull, not push - the security inversion
|
||||
The load-bearing detail everyone misses: the cluster PULLS. No CI system, no laptop, no
|
||||
GitHub Action holds a credential that can touch the cluster. The agent inside the cluster
|
||||
holds a read-only deploy key to the repo - the trust arrow points OUT. Compromise the build
|
||||
system and you can propose a change (a commit, which is visible); you cannot reach into
|
||||
production. Drift works the same way: hand-edit a live object and the controller flags it
|
||||
OutOfSync - with selfHeal ENABLED it puts it back; with prune ENABLED what leaves git leaves
|
||||
the cluster. Both are opt-in (2026-08-17 review): without them the reconciler reports drift and
|
||||
waits for a human. Scope the pull-model claims as 'in this architecture' - push-based delivery
|
||||
is still GitOps to many. Rollback is `git revert`, which is why commit hygiene is an operational skill.
|
||||
FIELD NOTE (amber): fighting the reconciler by SSH/kubectl is arm-wrestling a machine that
|
||||
does not get tired (Course II's note, now with a face). The fix is always upstream, in git.
|
||||
|
||||
## Move 3 - The one thing git never holds
|
||||
Git holds everything - manifests, charts, config, the whole desired state - except secrets.
|
||||
A plaintext secret committed to git is COMPROMISED from that moment - deleting it later does
|
||||
not guarantee it is gone from history, forks, clones, CI caches or backups. So the pattern splits
|
||||
the reference from the value: git carries an ExternalSecret manifest naming a LOGICAL key
|
||||
("this app needs db-password from the vault"); a vault - Azure Key Vault as the worked
|
||||
example, any ESO-supported store works - holds the value; an operator inside the cluster
|
||||
exchanges the reference for the value at runtime and keeps it synced. Rotation happens in
|
||||
the vault, never as a commit.
|
||||
Say it like you mean it: **git holds the shape of the secret, the vault holds the secret.**
|
||||
FIELD NOTE: the day a real credential lands in a commit, it is spent - history rewrites do
|
||||
not un-publish it. Rotate first, tidy git second.
|
||||
|
||||
## Close
|
||||
Delivery stops being an event and becomes a property: the cluster is always converging on
|
||||
what the repository says. The question "who deployed this?" becomes "who merged this?" -
|
||||
and that question always has an answer.
|
||||
|
||||
## Legend terms (DOM layer)
|
||||
Application (repo + path + revision) / desired vs actual / sync / drift / selfHeal /
|
||||
prune / pull model / deploy key / ExternalSecret / vault reference / rotation
|
||||
|
||||
## Scene concept (hero + slices, the v3 mechanism)
|
||||
Exploded chain, upper-left to lower-right: a faceted crystal SLAB etched with branching
|
||||
lattice (the repository) -> a compare-loop ENGINE with two counter-rotating ring motifs
|
||||
(ArgoCD: desired vs actual) -> a stack of thin rendered SHEETS (manifests) -> the cluster
|
||||
CORE (Course II's command slab, familiar silhouette) -> off-axis, drifting away dimmed: a
|
||||
small GHOST SHARD (the hand-edit being reverted). Separate armoured VAULT BOX floating
|
||||
above the chain, apart from everything (the secret store - deliberately not in line).
|
||||
6 parts, no connectors. Slice plan: collapse packs chain into the engine; vault stays
|
||||
apart even collapsed (it is never absorbed into the pipeline - the visual argument).
|
||||
|
||||
## Page footer - pre-reads + further reading (site convention: every course carries this)
|
||||
Pre-reads: Course II (the reconciliation loop - this course is that loop one level up);
|
||||
Kubernetes concepts (https://kubernetes.io/docs/concepts/); git + pull requests
|
||||
(https://docs.github.com/en/get-started).
|
||||
Further reading: Argo CD docs (https://argo-cd.readthedocs.io/); Red Hat OpenShift
|
||||
GitOps (via https://docs.redhat.com/en/documentation/openshift_container_platform);
|
||||
External Secrets Operator (https://external-secrets.io/); Azure Key Vault
|
||||
(https://learn.microsoft.com/azure/key-vault/); Helm docs (https://helm.sh/docs/).
|
||||
@@ -0,0 +1,91 @@
|
||||
# Course VII - The image supply chain: registries, mirrors, tags and digests
|
||||
|
||||
**Series position:** second course of the delivery arc. Course I taught what an image IS;
|
||||
this course teaches how one travels - and how to say its name precisely.
|
||||
**Safety gate:** public site - technologies only. Claims source from [[zot-registry]],
|
||||
[[homelab-pull-through-registry]], [[work-container-image-tagging]] (generalised: the
|
||||
calendar-rev pattern taught as "a human tag scheme", no corp framing), Azure Container
|
||||
Registry as the generic managed-registry example, OpenShift IDMS behaviour from public
|
||||
Red Hat docs.
|
||||
|
||||
## Title (thesis)
|
||||
**A tag is a promise. A digest is a fact.**
|
||||
|
||||
## Hook - the uncomfortable default
|
||||
`myapp:latest` feels like a name. It is not - it is a sticky note. A tag is a mutable
|
||||
pointer anyone with push rights can peel off one image and press onto another, and
|
||||
nothing anywhere records that it moved. The digest - sha256 of the content - is the
|
||||
image's actual name: same bytes, same digest, forever. Most outages in this territory
|
||||
begin with two machines both holding "the same tag" and running different code.
|
||||
|
||||
## Move 1 - Say the name properly
|
||||
Three ways to name an image, in rising order of honesty: `:latest` (a moving target),
|
||||
`:1.4.2` (a promise somebody keeps, until they re-push it), `name:1.4.2@sha256:...`
|
||||
(a fact - the tag stays for human eyes, the digest does the pulling). Content-addressing
|
||||
is the same idea git uses for commits: the name IS the bytes. Pin by digest and "what is
|
||||
running?" has exactly one answer.
|
||||
Say it like you mean it: **the tag is for reading, the digest is for pulling.**
|
||||
FIELD NOTE (amber): `:latest` is how two nodes run different code from one manifest -
|
||||
the second node pulled an hour later, after a re-push. Nobody changed the YAML.
|
||||
|
||||
## Move 2 - Why a fleet pulls once
|
||||
Between the build and the node sits the registry chain. Upstream, a managed registry
|
||||
(Azure Container Registry as the worked example) holds what CI built. In front of the
|
||||
cluster sits a MIRROR - a pull-through cache like zot: the fleet asks the mirror, the
|
||||
mirror asks upstream once, everything after is local. Three reasons this exists: rate
|
||||
limits and egress cost (a hundred nodes should not make a hundred internet pulls),
|
||||
availability (the fleet boots even when upstream is down or the site is disconnected),
|
||||
and control (one place to gate, scan and audit what enters). OpenShift formalises the
|
||||
re-route with image mirror rules - and carries a sharp edge: its digest-mirror rules
|
||||
rewrite DIGEST pulls only, so a by-tag pull skips them - UNLESS an ImageTagMirrorSet is also
|
||||
added (the rule type for tag pulls; corrected 2026-08-17). The pinning habit
|
||||
from Move 1 is what makes the mirror actually catch the traffic.
|
||||
FIELD NOTE: a mirror can also serve a STALE tag->digest mapping while upstream moved the
|
||||
tag. Pin by digest and the whole chain becomes content-addressed - the mirror cannot be
|
||||
wrong about a hash.
|
||||
TIERING (added 2026-08-17, owner ask): at real fleet scale the mirror itself tiers - a
|
||||
central pull-through in the cloud fronts upstream once; per-site mirrors pull from the
|
||||
centre; nodes pull from their site over the LAN. The load fans out in layers instead of
|
||||
every site hammering upstream at once. Say it: mirrors tier - the load fans out in
|
||||
layers, never all at once.
|
||||
|
||||
## Move 3 - Build once, promote by copy
|
||||
The anti-pattern: rebuild the image for each environment ("the prod build"). Every
|
||||
rebuild is a different artefact - different digest, different bytes, untested by the
|
||||
stages before it. The discipline: build ONCE, then PROMOTE the same digest through
|
||||
environments by copying (skopeo copy --all --preserve-digests for multi-arch, or the copy takes
|
||||
one arch and the promoted digest is not the one that lands) - dev proves the exact bytes
|
||||
prod will run. Human-readable tags ride along (a calendar-plus-revision scheme reads well:
|
||||
month, year, revision), but the digest is the through-line; promotion never changes it.
|
||||
Say it like you mean it: **if the digest changed, it is not a promotion - it is a new
|
||||
candidate.**
|
||||
|
||||
## Close
|
||||
Everything in this course is one idea wearing three coats: names that can move are
|
||||
convenient exactly until they move. Address content by what it IS, let humans keep their
|
||||
readable labels, and the supply chain stops being a chain of trust and becomes a chain
|
||||
of arithmetic.
|
||||
|
||||
## Legend terms (DOM layer)
|
||||
tag / digest / content-addressing / pull-through mirror / disconnected pull / rate limit /
|
||||
image mirror rule (digest-only rewrite) / build-once / promotion / skopeo copy
|
||||
|
||||
## Scene concept (hero + slices, the v3 mechanism)
|
||||
Exploded left-to-right journey: the layered image CUBE from Course I (familiar - four
|
||||
thin slabs, tight formation, reads as one unit now) -> upstream registry TOWER (tall
|
||||
managed vault, cool white-cyan) -> the mirror WAY-STATION (squat wide cache block, warm
|
||||
cyan, visibly closer to the cluster side) -> the node/cluster CORE. Floating above the
|
||||
journey, two seal plates apart from each other: a GHOST RIBBON plate (the tag - faint,
|
||||
translucent, edges blurring) and an ENGRAVED SEAL plate (the digest - dense, crisp,
|
||||
crystalline). 6 parts, generous void, no connectors. Slice plan: collapse draws the
|
||||
journey into the mirror block (everything local); the two seals collapse toward each
|
||||
other but never merge - ghost stays ghost.
|
||||
|
||||
## Page footer - pre-reads + further reading (site convention: every course carries this)
|
||||
Pre-reads: Course I (what an image IS - layers, this course ships that object);
|
||||
Kubernetes images concept (https://kubernetes.io/docs/concepts/containers/images/).
|
||||
Further reading: zot registry docs (https://zotregistry.dev/); Azure Container
|
||||
Registry (https://learn.microsoft.com/azure/container-registry/); OpenShift image
|
||||
configuration + mirroring (via
|
||||
https://docs.redhat.com/en/documentation/openshift_container_platform); skopeo
|
||||
(https://github.com/containers/skopeo).
|
||||
@@ -0,0 +1,83 @@
|
||||
# Course VIII - Helm: Go templates, values contexts, one chart for a fleet
|
||||
|
||||
**Series position:** third course of the delivery arc. VI taught how change travels,
|
||||
VII how images are named; this course teaches how ONE definition serves MANY clusters.
|
||||
**Safety gate:** public site - technologies only. Claims source from [[helm-charts]],
|
||||
[[yaml-manifest-style]], [[kustomize-inflate-helm]] (the ArgoCD render caveat), the
|
||||
one-value-many-enforcers post (live on the blog - cross-link it), Helm/Go public docs.
|
||||
|
||||
## Title (thesis)
|
||||
**A chart is a function. The values file is the cluster speaking.**
|
||||
|
||||
## Hook - the uncomfortable default
|
||||
The default way to run one app on five clusters is five copies of the YAML, and the
|
||||
default result is five slightly different apps - each copy drifting on its own schedule,
|
||||
each "quick fix" applied to some of them. The inversion: stop copying outputs and ship
|
||||
the FUNCTION instead. A Helm chart is a template with holes; each cluster supplies one
|
||||
small values file that fills them; the rendered manifests are build artefacts nobody
|
||||
hand-edits, ever.
|
||||
|
||||
## Move 1 - It's Go under the braces
|
||||
Helm templates are Go text/template: `{{ .Values.camera.address }}` is a pipeline
|
||||
walking a values object, `_helpers.tpl` holds named template functions (labels, names)
|
||||
every manifest calls, and `include`/`toYaml`/`default` compose them. The mental shift:
|
||||
you are not writing YAML, you are writing a program whose OUTPUT is YAML - so render
|
||||
locally (`helm template`), read the output, and lint what came out, not what went in.
|
||||
Say it like you mean it: **review the render, not just the template.**
|
||||
FIELD NOTE (amber, CORRECTED 2026-08-17): ArgoCD renders with `helm template` rather than
|
||||
running `helm install`, so the lifecycle differs. `lookup` comes back empty (no live cluster at
|
||||
render). Hooks are NOT dead: Argo maps Helm hooks onto its sync phases (pre-install/pre-upgrade
|
||||
-> PreSync, post-install/post-upgrade -> PostSync); rollback and test hooks have no equivalent.
|
||||
Render the way your deployer renders and check where hooks land.
|
||||
|
||||
## Move 2 - Contexts: the cluster's whole voice is one small file
|
||||
The fleet pattern: the chart owns EVERYTHING structural - resources, probes, security
|
||||
contexts, policy - and each cluster owns one values file (its context): names, addresses,
|
||||
sizes, feature flags. The context is deliberately values-only; the moment a context
|
||||
carries its own manifests, there are two owners for one object and they will disagree.
|
||||
One value can feed many rendered artefacts - an address appearing in the app config, the
|
||||
network attachment and two policies renders from ONE field, so the copies cannot diverge
|
||||
(the blog post "Consistency by construction beats consistency by discipline" is this
|
||||
move, lived).
|
||||
Say it like you mean it: **the chart owns the shape; the context owns the numbers.**
|
||||
|
||||
## Move 3 - Make the template refuse
|
||||
A template that renders whatever it is given just moves the failure downstream. The
|
||||
grown-up chart carries a values.schema.json: required fields, types, enums - and a
|
||||
cluster whose context omits a required value FAILS AT RENDER TIME with a message naming
|
||||
the field, in the pipeline, before anything ships. Same discipline inside the template:
|
||||
`required "camera.address is required" .Values.camera.address` beats a silent empty
|
||||
string that becomes a policy pointed at nothing.
|
||||
FIELD NOTE: the failure you want is the render that refuses - it costs a red pipeline.
|
||||
The failure you get otherwise is enforcement aimed at an address nothing lives at,
|
||||
months later, reporting healthy the whole time.
|
||||
|
||||
## Close
|
||||
Fleet consistency is not a discipline problem, it is a construction problem. Ship one
|
||||
function, let each cluster speak one small file of facts, and make the function refuse
|
||||
to run on missing facts. Divergence stops being something you police and becomes
|
||||
something the tooling cannot express.
|
||||
|
||||
## Legend terms (DOM layer)
|
||||
chart / template / Go text-template / pipeline / _helpers.tpl / values / context /
|
||||
values.schema.json / required / render (helm template) / one source many renders
|
||||
|
||||
## Scene concept (hero + slices, the v3 mechanism)
|
||||
Exploded press-works, centre-out: an engraved PRESS PLATE (the chart - dark slab, its
|
||||
face carrying an abstract lattice of rectangular voids, the "holes" clearly empty) ->
|
||||
three small VALUES CRYSTALS (per-cluster contexts - compact faceted gems, cyan, one
|
||||
subtly larger) feeding toward it from the left -> on the right, three thin RENDERED
|
||||
SHEETS fanned apart, identical framework but each carrying a different accent pattern
|
||||
(same shape, different numbers) -> below, a GATE WEDGE (the schema - amber-white edge,
|
||||
Course III's gatekeeper silhouette re-used) with one dim REJECTED SHEET stopped at it.
|
||||
7 parts, generous void, no connectors. Slice plan: collapse draws crystals + sheets into
|
||||
the press (one machine at rest); the rejected sheet collapses BEHIND the gate, never
|
||||
past it - the visual argument for Move 3.
|
||||
|
||||
## Page footer - pre-reads + further reading (site convention: every course carries this)
|
||||
Pre-reads: Course II (desired state - a chart renders it); working with Kubernetes
|
||||
objects (https://kubernetes.io/docs/concepts/overview/working-with-objects/).
|
||||
Further reading: Helm docs (https://helm.sh/docs/) and the chart template guide
|
||||
(https://helm.sh/docs/chart_template_guide/); Go text/template
|
||||
(https://pkg.go.dev/text/template); Helm on OpenShift (via
|
||||
https://docs.redhat.com/en/documentation/openshift_container_platform).
|
||||
@@ -0,0 +1,20 @@
|
||||
# Site convention - the further-reading footer (all courses)
|
||||
|
||||
Every course page ends with a "Pre-reads + further reading" strip: official docs only
|
||||
(kubernetes.io / helm.sh / docs.redhat.com OpenShift + per-topic), two lines, quiet
|
||||
styling - a service to the reader, never a wall of links. The delivery-arc briefs
|
||||
(VI-VIII + appendix) carry theirs inline; the main-menu courses get these at build:
|
||||
|
||||
- 00 aperitif: none (the terminal IS the hook).
|
||||
- I Docker: https://docs.docker.com/get-started/ ; image layers via
|
||||
https://docs.docker.com/build/
|
||||
- II Machine: https://kubernetes.io/docs/concepts/overview/ ;
|
||||
https://kubernetes.io/docs/concepts/architecture/
|
||||
- III Runtime: https://kubernetes.io/docs/concepts/workloads/pods/ ;
|
||||
https://kubernetes.io/docs/concepts/architecture/#node-components
|
||||
- IV Traffic: https://kubernetes.io/docs/concepts/services-networking/
|
||||
- V OpenShift: https://docs.redhat.com/en/documentation/openshift_container_platform ;
|
||||
https://www.redhat.com/en/technologies/cloud-computing/openshift
|
||||
- VI-VIII + appendix: inline in their briefs (content/).
|
||||
|
||||
Pre-reads point BACKWARD into the site first (courses build on courses), outward second.
|
||||
@@ -0,0 +1,60 @@
|
||||
# nginx config for the static teaching site, baked into the image.
|
||||
# Base: nginxinc/nginx-unprivileged (uid 101, listens on 8080).
|
||||
# Read-only rootfs in k8s: /tmp and /var/cache/nginx are emptyDir mounts.
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# Behind the Cloudflare Tunnel on :443 while nginx listens on :8080 - without these,
|
||||
# nginx emits ABSOLUTE redirects to http://host:8080/... and breaks every link.
|
||||
absolute_redirect off;
|
||||
port_in_redirect off;
|
||||
|
||||
include /etc/nginx/security-headers.conf;
|
||||
|
||||
# ---- Caching -------------------------------------------------------------
|
||||
# Everything under /assets/ is content-fingerprinted at build time -> cache hard.
|
||||
# `^~` gives this prefix priority over the regex `.js` location below, so fingerprinted
|
||||
# JavaScript gets the immutable policy rather than no-cache.
|
||||
location ^~ /assets/ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
}
|
||||
|
||||
# Any JS OUTSIDE /assets/ (there is none today) revalidates; fingerprinted app JS is
|
||||
# handled immutably by the ^~ /assets/ block above.
|
||||
location ~* \.js$ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
location ~* \.html$ {
|
||||
include /etc/nginx/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
# Probe endpoint - no file IO, no cache headers, invisible to the CDN.
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
return 200 "ok";
|
||||
}
|
||||
|
||||
# ---- Routing -------------------------------------------------------------
|
||||
location / {
|
||||
try_files $uri $uri/ $uri.html =404;
|
||||
}
|
||||
|
||||
# ---- Compression ---------------------------------------------------------
|
||||
# WebP/PNG are already compressed - gzipping them burns CPU for nothing.
|
||||
gzip on;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_vary on;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Shared security headers. `include`d in the server block AND in every location that
|
||||
# sets its own add_header - a location-level add_header REPLACES inherited headers
|
||||
# rather than merging them (the classic nginx footgun).
|
||||
#
|
||||
# HSTS is host-scoped: NO includeSubDomains / preload, because the *.bztmon.com
|
||||
# wildcard points elsewhere and forcing HTTPS estate-wide from here would be reckless.
|
||||
# HTTP->HTTPS upgrade is Cloudflare's job.
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
# script-src 'self' with NO 'unsafe-inline'. build.sh externalises every script and
|
||||
# hard-fails if an inline <script> reaches dist/, so this holds without an exception.
|
||||
# style-src needs 'unsafe-inline': GSAP animates by writing inline style attributes.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; worker-src 'self'; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()" always;
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env node
|
||||
// Site build for the delivery-arc page (CSP-clean: script-src 'self', no inline JS).
|
||||
// Reads pilot/arc.tpl.html, splits the app <script> out to a fingerprinted arc.js, injects
|
||||
// the scene manifests, fingerprints every /assets/ file, sets intrinsic image dimensions,
|
||||
// wraps a full HTML document, writes build.json, and runs preflight gates.
|
||||
// Runs on bare node (the build image has no python). Superseded the earlier python assembler.
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const DIST = process.argv[2] || join(ROOT, "dist");
|
||||
const SITE = "https://learn.bztmon.com";
|
||||
const BUILD_REV = process.env.BUILD_REV || "dev";
|
||||
|
||||
const man = t => JSON.stringify(JSON.parse(readFileSync(join(ROOT, "pilot", `manifest-${t}.json`), "utf8")));
|
||||
|
||||
// --- intrinsic WebP dimensions (VP8 / VP8L / VP8X), so the browser reserves layout space ---
|
||||
function webpSize(buf) {
|
||||
if (buf.length < 30 || buf.toString("ascii", 0, 4) !== "RIFF") return null;
|
||||
const fourcc = buf.toString("ascii", 12, 16);
|
||||
try {
|
||||
if (fourcc === "VP8 ") {
|
||||
return { w: (buf.readUInt16LE(26) & 0x3fff), h: (buf.readUInt16LE(28) & 0x3fff) };
|
||||
}
|
||||
if (fourcc === "VP8L") {
|
||||
const b = buf; const o = 21;
|
||||
const w = 1 + (((b[o + 1] & 0x3f) << 8) | b[o]);
|
||||
const h = 1 + (((b[o + 3] & 0x0f) << 10) | (b[o + 2] << 2) | ((b[o + 1] & 0xc0) >> 6));
|
||||
return { w, h };
|
||||
}
|
||||
if (fourcc === "VP8X") {
|
||||
const o = 24;
|
||||
const w = 1 + (buf[o] | (buf[o + 1] << 8) | (buf[o + 2] << 16));
|
||||
const h = 1 + (buf[o + 3] | (buf[o + 4] << 8) | (buf[o + 5] << 16));
|
||||
return { w, h };
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- intrinsic JPEG dimensions (SOF0/SOF2 markers) for the mirror plates ---
|
||||
function jpegSize(buf) {
|
||||
if (buf[0] !== 0xff || buf[1] !== 0xd8) return null;
|
||||
let o = 2;
|
||||
while (o + 9 < buf.length) {
|
||||
if (buf[o] !== 0xff) { o++; continue; }
|
||||
const marker = buf[o + 1];
|
||||
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
||||
return { h: buf.readUInt16BE(o + 5), w: buf.readUInt16BE(o + 7) };
|
||||
}
|
||||
o += 2 + buf.readUInt16BE(o + 2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// skip link: emitted by the shared shell, so its accessibility CSS lives in the shell
|
||||
// too - visually hidden until keyboard focus, visible and prominent when focused.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared masthead: ONE opening treatment for every page on the site. Pages used
|
||||
// to declare their own tokens, type scale and alignment, which is why the three
|
||||
// headers drifted apart. This block owns the family - tokens, the centred
|
||||
// masthead, eyebrow, title, lede and scroll cue - and pages simply use it.
|
||||
// ---------------------------------------------------------------------------
|
||||
const MAST_CSS = `<style>
|
||||
:root{
|
||||
--void:#070b14; --ink:#c9d7ef; --dim:#7286a8; --ink-dim:#7286a8; --line:#1b2740;
|
||||
--cyan:#3fbaf5; --magenta:#e879f9; --amber:#e8b44a;
|
||||
--dur-fast:.2s; --dur-base:.35s; --dur-slow:.6s;
|
||||
--sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
}
|
||||
.masthead{position:relative;max-width:64ch;margin:0 auto;padding:16vh 24px 9vh;text-align:center}
|
||||
.masthead > :not(.introbg){position:relative}
|
||||
.chapnav{font-family:var(--mono);font-size:.72rem;letter-spacing:.14em;margin:0 0 2.2rem}
|
||||
.chapnav a{color:var(--dim);text-decoration:none;border-bottom:1px solid transparent;
|
||||
padding-bottom:2px;transition:color var(--dur-fast) ease,border-color var(--dur-fast) ease}
|
||||
.chapnav a:hover,.chapnav a:focus-visible{color:var(--cyan);border-color:var(--cyan)}
|
||||
.eyebrow{font-family:var(--mono);font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
|
||||
color:var(--cyan);margin:0 0 1.5rem}
|
||||
.masthead h1,.thesis{font-size:clamp(1.9rem,4.8vw,3.2rem);line-height:1.1;margin:0 0 1.3rem;
|
||||
font-weight:600;letter-spacing:-.02em;text-wrap:balance;font-family:var(--sans);color:var(--ink)}
|
||||
.masthead h1 em,.thesis em{font-style:normal;color:var(--magenta)}
|
||||
.lede{color:var(--dim);font-size:1.02rem;line-height:1.7;margin:0 auto;max-width:58ch}
|
||||
.scrollcue{font-family:var(--mono);font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;
|
||||
color:var(--dim);margin-top:4.5rem;display:flex;flex-direction:column;align-items:center;gap:.7rem}
|
||||
.scrollcue span{display:block;width:1px;height:46px;background:linear-gradient(var(--cyan),transparent)}
|
||||
.mastfacts{display:flex;flex-wrap:wrap;justify-content:center;gap:0;margin:2.6rem auto 0;
|
||||
border-top:1px solid var(--line);border-bottom:1px solid var(--line);max-width:52ch}
|
||||
.mastfacts div{flex:1 1 8rem;padding:.85rem 1rem;border-right:1px solid var(--line)}
|
||||
.mastfacts div:last-child{border-right:0}
|
||||
.mastfacts dt{font-family:var(--mono);font-size:.64rem;letter-spacing:.16em;text-transform:uppercase;
|
||||
color:var(--dim);margin:0 0 .3rem}
|
||||
.mastfacts dd{margin:0;font-family:var(--mono);font-size:.88rem;color:var(--cyan)}
|
||||
.toc{max-width:760px;margin:0 auto;padding:0 24px 5vh}
|
||||
.toc .kt{font-family:var(--mono);font-size:.68rem;letter-spacing:.16em;
|
||||
text-transform:uppercase;color:var(--dim);text-align:center;margin:0 0 1rem}
|
||||
.toc ol{list-style:none;margin:0;padding:0;counter-reset:toc;display:flex;flex-direction:column;gap:8px}
|
||||
.toc li{counter-increment:toc}
|
||||
.toc a{display:grid;grid-template-columns:34px 110px 1fr;gap:12px;align-items:baseline;
|
||||
border:1px solid var(--line);border-radius:8px;padding:10px 16px;background:#0a1020;text-decoration:none}
|
||||
.toc a::before{content:counter(toc,decimal-leading-zero);font-family:var(--mono);font-size:.68rem;color:var(--dim)}
|
||||
.toc a:hover,.toc a:focus-visible{border-color:var(--cyan)}
|
||||
.toc b{font-family:var(--mono);font-size:.72rem;letter-spacing:.08em;color:var(--cyan);font-weight:600}
|
||||
.toc span{font-size:.82rem;line-height:1.5;color:var(--dim)}
|
||||
@media (max-width:640px){.toc a{grid-template-columns:34px 1fr}.toc span{grid-column:2}}
|
||||
@media (max-width:900px){.masthead{padding:9vh 22px 6vh}}
|
||||
</style>`;
|
||||
|
||||
const SKIP_CSS = `<style>
|
||||
a.skip{position:absolute;left:-9999px;top:0;z-index:99}
|
||||
a.skip:focus{left:1rem;top:1rem;background:#070b14;color:#3fbaf5;
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.8rem;
|
||||
padding:10px 16px;border:1px solid #3fbaf5;border-radius:6px;outline:none}
|
||||
</style>`;
|
||||
|
||||
let tpl = readFileSync(join(ROOT, "pilot/arc.tpl.html"), "utf8");
|
||||
|
||||
// split the app script (the last <script> block) out of the template
|
||||
const i = tpl.lastIndexOf("<script>");
|
||||
const j = tpl.lastIndexOf("</script>");
|
||||
if (i < 0 || j < i) throw new Error("app script block not found");
|
||||
let app = tpl.slice(i + 8, j)
|
||||
.replace("__MAN_SURROUND__", man("surround"))
|
||||
.replace("__MAN_NODE__", man("node"))
|
||||
.replace("__MAN_POD__", man("pod"))
|
||||
.replace("__MAN_TRAFFIC__", man("traffic"))
|
||||
.replace("__MAN_ORBIT__", man("orbit"))
|
||||
.replace("__MAN_DOCKER__", man("docker"))
|
||||
.replace("__MAN_CLUSTER__", man("cluster"))
|
||||
.replace("__MAN_GITOPS__", man("gitops"))
|
||||
.replace("__MAN_SUPPLY__", man("supply"))
|
||||
.replace("__MAN_HELM__", man("helm"));
|
||||
let body = tpl.slice(0, i) + tpl.slice(j + 9);
|
||||
|
||||
const heroes = {
|
||||
__HERO_INTRO__: "intro-establishing.webp",
|
||||
__HERO_SURROUND__: "course-00-surround.webp",
|
||||
__HERO_NODE__: "course-III-node.webp",
|
||||
__HERO_POD__: "course-III-pod.webp",
|
||||
__HERO_TRAFFIC__: "course-IV-traffic.webp",
|
||||
__HERO_ORBIT__: "course-V-orbit.webp",
|
||||
__HERO_DOCKER__: "docker-layers.webp",
|
||||
__HERO_CLUSTER__: "course-II-cluster.webp",
|
||||
__HERO_GITOPS__: "course-VI-gitops.webp",
|
||||
__HERO_SUPPLY__: "course-VII-supply-chain.webp",
|
||||
__HERO_HELM__: "course-VIII-helm-press.webp",
|
||||
__HERO_APPENDIX__: "appendix-dependency-ledger.webp",
|
||||
};
|
||||
// content-fingerprint everything under /assets/ (immutable-cached by nginx): a fresh
|
||||
// index.html can then never pair with a stale script or image from a previous deploy.
|
||||
mkdirSync(join(DIST, "assets"), { recursive: true });
|
||||
const fp = buf => createHash("sha256").update(buf).digest("hex").slice(0, 8);
|
||||
const emit = (srcPath, base, ext) => {
|
||||
const buf = readFileSync(srcPath);
|
||||
const name = `${base}.${fp(buf)}.${ext}`;
|
||||
writeFileSync(join(DIST, "assets", name), buf);
|
||||
return { url: "/assets/" + name, buf };
|
||||
};
|
||||
|
||||
const heroUrls = {};
|
||||
for (const [ph, file] of Object.entries(heroes)) {
|
||||
const base = file.replace(/\.webp$/, "");
|
||||
const { url, buf } = emit(join(ROOT, "assets/dist", file), base, "webp");
|
||||
heroUrls[ph] = url;
|
||||
const dims = webpSize(buf);
|
||||
const dimAttr = dims ? ` width="${dims.w}" height="${dims.h}"` : "";
|
||||
// The intro backdrop is the above-the-fold LCP candidate -> eager. Everything else,
|
||||
// including Course 00's hero, sits below the fold and lazy-loads.
|
||||
const load = ph === "__HERO_INTRO__" ? "eager" : "lazy";
|
||||
body = body.replace(`src="${ph}"`, `src="${url}"${dimAttr} decoding="async" loading="${load}"`);
|
||||
}
|
||||
{
|
||||
const { url } = emit(join(ROOT, "assets/dist/delivery-flow.svg"), "delivery-flow", "svg");
|
||||
body = body.replace(`src="__DIAG_FLOW__"`,
|
||||
`src="${url}" width="1180" height="660" decoding="async" loading="lazy"`);
|
||||
}
|
||||
if (body.includes("__HERO_") || body.includes("__DIAG_") || app.includes("__MAN_")) throw new Error("unfilled placeholder");
|
||||
|
||||
const favicon = "data:image/svg+xml," + encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#070b14"/><rect x="6" y="4" width="14" height="7" rx="1.5" fill="#3fbaf5"/><rect x="11" y="13" width="14" height="7" rx="1.5" fill="#7dd0f9"/><rect x="6" y="22" width="14" height="7" rx="1.5" fill="#e879f9"/></svg>`);
|
||||
|
||||
const gsapRef = emit(join(ROOT, "gsap.min.js"), "gsap.min", "js").url;
|
||||
const stRef = emit(join(ROOT, "st.min.js"), "st.min", "js").url;
|
||||
const appName = `arc.${fp(app)}.js`;
|
||||
writeFileSync(join(DIST, "assets", appName), app);
|
||||
const appRef = "/assets/" + appName;
|
||||
|
||||
const DESC = "Interactive teardown of Kubernetes and OpenShift delivery: containers, Services, GitOps, image supply chains and Helm, explained with scroll-driven exploded diagrams.";
|
||||
const OG_IMG = SITE + heroUrls["__HERO_GITOPS__"];
|
||||
|
||||
// shared document shell: every page on the site gets the same head discipline
|
||||
const shell = ({ title, desc, path, ogImg, scripts, bodyHtml }) => `<!doctype html>
|
||||
<html lang="en-AU">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${title}</title>
|
||||
<meta name="description" content="${desc}">
|
||||
<meta name="theme-color" content="#070b14">
|
||||
<meta name="build-revision" content="${BUILD_REV}">
|
||||
<link rel="canonical" href="${SITE}${path}">
|
||||
<meta property="og:title" content="${title}">
|
||||
<meta property="og:description" content="${desc}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="${SITE}${path}">
|
||||
<meta property="og:image" content="${ogImg}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="${title}">
|
||||
<meta name="twitter:description" content="${desc}">
|
||||
<meta name="twitter:image" content="${ogImg}">
|
||||
<link rel="icon" href="${favicon}">
|
||||
${SKIP_CSS}
|
||||
${MAST_CSS}
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main-content">Skip to content</a>
|
||||
<main id="main-content">
|
||||
${bodyHtml}
|
||||
</main>
|
||||
${scripts.map(s => `<script src="${s}"></script>`).join("\n")}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const html = shell({
|
||||
title: "The Exploded Cluster", desc: DESC, path: "/cluster", ogImg: OG_IMG,
|
||||
scripts: [gsapRef, stRef, appRef], bodyHtml: body,
|
||||
});
|
||||
|
||||
// ---- The Mirror (chapter 02): fingerprint its plates + its app script -------
|
||||
const MIRROR_DESC = "How a zot pull-through registry behaves under Talos and containerd: upstream routing, digest preservation, garbage collection, authentication rollout, failure modes, and the lab evidence behind each decision.";
|
||||
let mirror = readFileSync(join(ROOT, "pilot/mirror.tpl.html"), "utf8");
|
||||
const mirrorPlates = {};
|
||||
for (const n of ["20", "21", "22", "23", "24", "25"]) {
|
||||
const { url, buf } = emit(join(ROOT, "assets/mirror", `${n}.jpg`), `mirror-${n}`, "jpg");
|
||||
mirrorPlates[n] = url;
|
||||
const dims = jpegSize(buf);
|
||||
if (!dims) throw new Error(`mirror plate ${n}: JPEG dimensions unreadable`);
|
||||
// every plate sits below the fold (fact panel + contents precede the first scene),
|
||||
// so all six lazy-load; intrinsic dims come from the actual bytes.
|
||||
mirror = mirror.replace(`__M${n}__`,
|
||||
`${url}" width="${dims.w}" height="${dims.h}" decoding="async" loading="lazy`);
|
||||
}
|
||||
const mirrorJs = readFileSync(join(ROOT, "pilot/mirror.js"));
|
||||
const mirrorJsName = `mirror.${fp(mirrorJs)}.js`;
|
||||
writeFileSync(join(DIST, "assets", mirrorJsName), mirrorJs);
|
||||
mirror = mirror.replace("__MIRROR_JS__", "/assets/" + mirrorJsName);
|
||||
if (mirror.includes("__M")) throw new Error("unfilled mirror placeholder");
|
||||
const mirrorHtml = shell({
|
||||
title: "The Mirror", desc: MIRROR_DESC, path: "/mirror",
|
||||
ogImg: SITE + mirrorPlates["20"], scripts: [], bodyHtml: mirror,
|
||||
});
|
||||
|
||||
// ---- the directory (index): the chapters of what this place teaches ---------
|
||||
const DIR_DESC = "The Teaching Lab: scroll-driven teardowns of real infrastructure - container platforms, registries and the machinery of delivery, one exploded system per chapter.";
|
||||
let dir = readFileSync(join(ROOT, "pilot/index.tpl.html"), "utf8")
|
||||
.replace("__THUMB_CLUSTER__", heroUrls["__HERO_GITOPS__"])
|
||||
.replace("__THUMB_MIRROR__", mirrorPlates["20"]);
|
||||
if (dir.includes("__THUMB")) throw new Error("unfilled directory placeholder");
|
||||
const dirHtml = shell({
|
||||
title: "The Teaching Lab", desc: DIR_DESC, path: "/",
|
||||
ogImg: SITE + mirrorPlates["20"], scripts: [], bodyHtml: dir,
|
||||
});
|
||||
|
||||
writeFileSync(join(DIST, "index.html"), dirHtml);
|
||||
writeFileSync(join(DIST, "cluster.html"), html);
|
||||
writeFileSync(join(DIST, "mirror.html"), mirrorHtml);
|
||||
writeFileSync(join(DIST, "build.json"), JSON.stringify({ revision: BUILD_REV, app: appName, mirror: mirrorJsName }) + "\n");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preflight gates: fail the build loudly rather than ship a known defect.
|
||||
// ---------------------------------------------------------------------------
|
||||
const fail = [];
|
||||
const must = (cond, msg) => { if (!cond) fail.push(msg); };
|
||||
|
||||
// per-page structural gates (CSP + document hygiene) for every page shipped
|
||||
for (const [pg, doc] of [["cluster", html], ["mirror", mirrorHtml], ["index", dirHtml]]) {
|
||||
must(!(doc.match(/<script(?![^>]*src=)[^>]*>/gi) || []).length, `${pg}: inline <script> present`);
|
||||
must(!/__[A-Z0-9_]+__/.test(doc), `${pg}: unresolved __PLACEHOLDER__ in output`);
|
||||
must((doc.match(/<h1[\s>]/gi) || []).length === 1, `${pg}: expected exactly one <h1>`);
|
||||
const pids = [...doc.matchAll(/\sid="([^"]+)"/g)].map(m => m[1]);
|
||||
must(new Set(pids).size === pids.length, `${pg}: duplicate id attribute(s): ` +
|
||||
pids.filter((v, k) => pids.indexOf(v) !== k).join(", "));
|
||||
const pidset = new Set(pids);
|
||||
for (const m of doc.matchAll(/href="#([^"]+)"/g)) {
|
||||
if (m[1] && !pidset.has(m[1])) fail.push(`${pg}: dangling internal link #` + m[1]);
|
||||
}
|
||||
}
|
||||
// mirror-page content gates: retired claims may be DISCUSSED as corrections, so these
|
||||
// target the assertive phrasings and internal production jargon, not the topic words.
|
||||
const mirrorBanned = [
|
||||
[/never leaves the building/i, "retired absolute cache claim"],
|
||||
[/SAY IT/, "removed rhetorical block resurfaced"],
|
||||
[/HOMELAB CLAUSE/, "removed label resurfaced"],
|
||||
[/Nothing on v2\.1\.17\/18 protects/, "over-broad retention annotation resurfaced"],
|
||||
[/100 manifest requests per six hours/, "imprecise Hub limit phrasing"],
|
||||
[/a tag is a suggestion, a digest is a fact/i, "retired slogan"],
|
||||
[/up to twelve|twelve tries|12 attempts/i, "stale retry arithmetic (correct figure: sixteen)"],
|
||||
[/plate-level/i, "internal production note leaked to readers"],
|
||||
[/· motion:/i, "art-direction caption leaked to readers"],
|
||||
[/Phase B/, "internal phase jargon without explanation"],
|
||||
[/RAID .*(is|are) (a )?backup/i, "snapshots/RAID-as-backup claim"],
|
||||
];
|
||||
for (const [re, msg] of mirrorBanned) if (re.test(mirrorHtml)) fail.push("mirror: " + msg);
|
||||
const mirrorRequired = [
|
||||
["zotregistry.dev", "primary zot references missing"],
|
||||
["docs.docker.com/docker-hub/usage/pulls", "Docker Hub pulls reference missing"],
|
||||
["v1alpha1", "Talos auth-reboot source link missing"],
|
||||
["deleteUntagged", "blanket untagged lever discussion missing"],
|
||||
["keepUntagged", "version-gated retention discussion missing"],
|
||||
["/livez", "real health endpoints missing"],
|
||||
['id="refs"', "references section missing"],
|
||||
['id="open"', "open-verification register missing"],
|
||||
['width="1600" height="872"', "plate intrinsic dimensions missing/incorrect"],
|
||||
];
|
||||
for (const [needle, msg] of mirrorRequired) if (!mirrorHtml.includes(needle)) fail.push("mirror: " + msg);
|
||||
must((mirrorHtml.match(/width="1600" height="872"/g) || []).length === 6, "mirror: expected six correctly-dimensioned plates");
|
||||
must((mirrorHtml.match(/loading="lazy"/g) || []).length === 6, "mirror: expected all six plates lazy-loaded");
|
||||
must((mirrorHtml.match(/<caption/g) || []).length === 5, "mirror: expected five table captions");
|
||||
must(!/<th>(?!<)/.test(mirrorHtml.replace(/<th scope="col">/g, "")), "mirror: th without scope=col");
|
||||
must(!/100vw/.test(mirrorHtml), "mirror: viewport-width unit reintroduces scrollbar overflow");
|
||||
must(!mirrorHtml.includes("gsap"), "mirror: unexpected animation library reference");
|
||||
|
||||
// banned / obsolete strings (user-visible copy)
|
||||
const banned = [
|
||||
[/ArgoCD/, "user-visible 'ArgoCD' (use 'Argo CD')"],
|
||||
[/random non-root UID/, "obsolete 'random non-root UID'"],
|
||||
[/nothing anywhere records that it moved/, "obsolete tag-immutability claim"],
|
||||
[/no interface owns that address/, "obsolete universal ClusterIP claim"],
|
||||
[/Three commands/, "'Three commands' contradicts four shown"],
|
||||
[/selfHeal puts it back; what leaves git leaves the cluster/, "stale unconditional self-heal legend"],
|
||||
[/every manifest <span class="mono">include<\/span>s/, "broken 'include s' construction"],
|
||||
[/two policies renders from one field/, "subject/verb error 'renders from one field'"],
|
||||
[/grown-up chart/, "'grown-up chart' (use 'production-grade chart')"],
|
||||
[/journalctl -u kubelet/, "node-shell instruction (use oc describe / oc debug node)"],
|
||||
];
|
||||
// legend blurbs are inlined into the app JS (the SCENES const), so scan both surfaces
|
||||
const visible = html + "\n" + app;
|
||||
for (const [re, msg] of banned) if (re.test(visible)) fail.push(msg);
|
||||
if (/\bnever by SSH\b/.test(visible)) fail.push("obsolete 'never by SSH'");
|
||||
if (/random non-root UID/.test(visible)) fail.push("obsolete 'random non-root UID' (also check legends)");
|
||||
|
||||
if (fail.length) { console.error("PREFLIGHT FAILED:\n - " + fail.join("\n - ")); process.exit(1); }
|
||||
console.log(`site build -> ${DIST}: index.html ${(html.length / 1024).toFixed(0)}KB, rev ${BUILD_REV}, preflight OK (${appName})`);
|
||||
@@ -0,0 +1,93 @@
|
||||
<style>
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--void);color:var(--ink);font-family:var(--sans);
|
||||
-webkit-font-smoothing:antialiased}
|
||||
.wrap{max-width:56rem;margin:0 auto;padding:0 1.25rem}
|
||||
h1 em{color:transparent;background:linear-gradient(100deg,var(--cyan) 10%,var(--magenta));
|
||||
-webkit-background-clip:text;background-clip:text;padding-bottom:.12em}
|
||||
.card{position:relative}
|
||||
.card::before{content:"";position:absolute;inset:0;pointer-events:none;opacity:0;
|
||||
background:linear-gradient(120deg,rgba(63,186,245,.10),transparent 55%);transition:opacity .3s ease}
|
||||
.card:hover::before,.card:focus-visible::before{opacity:1}
|
||||
h1 em{color:var(--cyan);font-style:normal}
|
||||
.chapters{display:grid;gap:1.4rem;margin:3rem 0 5rem}
|
||||
.card{display:grid;grid-template-columns:minmax(180px,300px) 1fr;gap:0;border:1px solid var(--line);
|
||||
border-radius:.6rem;overflow:hidden;background:#0a101d;text-decoration:none;color:inherit;
|
||||
transition:border-color .25s}
|
||||
.card:hover,.card:focus-visible{border-color:var(--cyan)}
|
||||
.card .thumb{background:#050810;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
||||
.card .thumb img{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.card .meta{padding:1.4rem 1.5rem}
|
||||
.card .k{font-family:var(--mono);font-size:.72rem;letter-spacing:.18em;color:var(--dim);text-transform:uppercase}
|
||||
.card h2{margin:.5rem 0 .5rem;font-size:1.6rem;line-height:1.15}
|
||||
.card h2 .arrow{color:var(--cyan);transition:transform .25s;display:inline-block}
|
||||
.card:hover h2 .arrow{transform:translateX(6px)}
|
||||
.card .outcome{margin:0;color:var(--dim);font-size:.98rem;line-height:1.55}
|
||||
.card .outcome b{color:var(--ink);font-weight:600;font-family:var(--mono);font-size:.78rem;
|
||||
letter-spacing:.14em;text-transform:uppercase;color:var(--cyan)}
|
||||
.card.ghost{border-style:dashed;opacity:.55}
|
||||
.card.ghost .thumb{min-height:130px;font-family:var(--mono);color:var(--dim);font-size:.8rem;letter-spacing:.15em}
|
||||
footer{border-top:1px solid var(--line);padding:1.4rem 0 3rem;color:var(--dim);
|
||||
font-family:var(--mono);font-size:.82rem}
|
||||
footer a{color:var(--cyan)}
|
||||
.sig{margin:0;letter-spacing:.12em;line-height:1.9;color:var(--dim)}
|
||||
:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
|
||||
@media (max-width:640px){.card{grid-template-columns:1fr}.card .thumb{max-height:170px}}
|
||||
</style>
|
||||
|
||||
<header class="masthead">
|
||||
<p class="eyebrow">bztmon · the teaching lab</p>
|
||||
<h1>Infrastructure is easier to trust<br><em>when you can see it come apart.</em></h1>
|
||||
<p class="lede">Scroll-driven teardowns of machinery this lab actually runs. Each chapter is
|
||||
one system, exploded - the diagrams move while the words explain them, and every claim traces
|
||||
back to a running estate: configs lifted from the fleet, incidents told with their receipts.</p>
|
||||
<dl class="mastfacts">
|
||||
<div><dt>Chapters live</dt><dd>02</dd></div>
|
||||
<div><dt>Subject</dt><dd>container platforms</dd></div>
|
||||
<div><dt>Evidence</dt><dd>labelled per claim</dd></div>
|
||||
</dl>
|
||||
<p class="scrollcue" aria-hidden="true">chapters<span></span></p>
|
||||
</header>
|
||||
|
||||
<nav class="wrap" aria-label="Chapters">
|
||||
<div class="chapters">
|
||||
|
||||
<a class="card" href="/cluster">
|
||||
<span class="thumb"><img src="__THUMB_CLUSTER__" alt="" width="1200" height="675" loading="eager" decoding="async"></span>
|
||||
<span class="meta">
|
||||
<span class="k">Chapter 01 · Dossier 004</span>
|
||||
<h2>The Exploded Cluster <span class="arrow">→</span></h2>
|
||||
<p class="outcome"><b>Outcome</b> — how software reaches a container platform: images,
|
||||
clusters, nodes, pods, traffic, GitOps, the image supply chain and Helm - eleven courses,
|
||||
each one machine drawn apart and wired to your scroll.</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a class="card" href="/mirror">
|
||||
<span class="thumb"><img src="__THUMB_MIRROR__" alt="" width="1600" height="872" loading="eager" decoding="async"></span>
|
||||
<span class="meta">
|
||||
<span class="k">Chapter 02 · Dossier 005</span>
|
||||
<h2>The Mirror <span class="arrow">→</span></h2>
|
||||
<p class="outcome"><b>Outcome</b> — how a zot pull-through registry behaves under Talos and
|
||||
containerd: upstream routing, digest preservation, garbage collection, authentication
|
||||
rollout and failure modes, with the lab evidence behind each decision.</p>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="card ghost" aria-hidden="true">
|
||||
<span class="thumb">IN THE WORKSHOP</span>
|
||||
<span class="meta">
|
||||
<span class="k">Chapter 03</span>
|
||||
<h2>Coming soon</h2>
|
||||
<p class="outcome">In the workshop now. The lab keeps running, so the chapters keep forming.</p>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<footer class="wrap">
|
||||
<p style="margin:0 0 .9rem">The Teaching Lab · each chapter documents infrastructure I run.</p>
|
||||
<p class="sig">© 2026 Jonathon Wright · <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>
|
||||
· hand-built static, shipped via GitOps · <a href="https://www.bztmon.com/">bztmon.com</a></p>
|
||||
</footer>
|
||||
@@ -0,0 +1,439 @@
|
||||
{
|
||||
"scene": "cluster",
|
||||
"source": "assets/raw/course-II-cluster.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "command",
|
||||
"title": "The control plane",
|
||||
"blurb": "Decides - holds the truth, schedules, reconciles. The only authority in the room.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
70.1,
|
||||
23.15
|
||||
],
|
||||
[
|
||||
70.02,
|
||||
28.95
|
||||
],
|
||||
[
|
||||
68.86,
|
||||
30.8
|
||||
],
|
||||
[
|
||||
63.92,
|
||||
34.78
|
||||
],
|
||||
[
|
||||
55.0,
|
||||
42.6
|
||||
],
|
||||
[
|
||||
51.76,
|
||||
44.47
|
||||
],
|
||||
[
|
||||
49.56,
|
||||
45.13
|
||||
],
|
||||
[
|
||||
47.87,
|
||||
44.2
|
||||
],
|
||||
[
|
||||
43.26,
|
||||
41.53
|
||||
],
|
||||
[
|
||||
32.23,
|
||||
31.68
|
||||
],
|
||||
[
|
||||
30.85,
|
||||
30.52
|
||||
],
|
||||
[
|
||||
29.98,
|
||||
29.1
|
||||
],
|
||||
[
|
||||
29.83,
|
||||
27.54
|
||||
],
|
||||
[
|
||||
30.63,
|
||||
21.55
|
||||
],
|
||||
[
|
||||
47.43,
|
||||
9.27
|
||||
],
|
||||
[
|
||||
49.73,
|
||||
8.07
|
||||
],
|
||||
[
|
||||
50.71,
|
||||
8.2
|
||||
],
|
||||
[
|
||||
68.5,
|
||||
20.84
|
||||
],
|
||||
[
|
||||
69.3,
|
||||
21.57
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "worker-1",
|
||||
"title": "Worker node",
|
||||
"blurb": "Runs pods. Identical and replaceable by design - cattle, not pets.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
32.59,
|
||||
76.73
|
||||
],
|
||||
[
|
||||
30.36,
|
||||
76.47
|
||||
],
|
||||
[
|
||||
23.8,
|
||||
71.93
|
||||
],
|
||||
[
|
||||
23.29,
|
||||
71.16
|
||||
],
|
||||
[
|
||||
22.71,
|
||||
69.64
|
||||
],
|
||||
[
|
||||
22.56,
|
||||
68.69
|
||||
],
|
||||
[
|
||||
22.35,
|
||||
51.98
|
||||
],
|
||||
[
|
||||
22.42,
|
||||
49.83
|
||||
],
|
||||
[
|
||||
22.56,
|
||||
49.04
|
||||
],
|
||||
[
|
||||
23.73,
|
||||
46.8
|
||||
],
|
||||
[
|
||||
30.59,
|
||||
42.2
|
||||
],
|
||||
[
|
||||
31.6,
|
||||
41.67
|
||||
],
|
||||
[
|
||||
32.52,
|
||||
41.8
|
||||
],
|
||||
[
|
||||
33.62,
|
||||
42.47
|
||||
],
|
||||
[
|
||||
39.21,
|
||||
46.19
|
||||
],
|
||||
[
|
||||
40.01,
|
||||
47.18
|
||||
],
|
||||
[
|
||||
40.88,
|
||||
49.36
|
||||
],
|
||||
[
|
||||
41.03,
|
||||
52.59
|
||||
],
|
||||
[
|
||||
41.1,
|
||||
68.65
|
||||
],
|
||||
[
|
||||
40.95,
|
||||
69.59
|
||||
],
|
||||
[
|
||||
40.37,
|
||||
71.1
|
||||
],
|
||||
[
|
||||
40.01,
|
||||
71.66
|
||||
]
|
||||
],
|
||||
"dx": 11.06,
|
||||
"dy": -19.39
|
||||
},
|
||||
{
|
||||
"name": "worker-2",
|
||||
"title": "Worker node",
|
||||
"blurb": "Interchangeable with its neighbours - lose one and the promise re-lands its pods elsewhere.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
59.78,
|
||||
62.5
|
||||
],
|
||||
[
|
||||
59.78,
|
||||
74.77
|
||||
],
|
||||
[
|
||||
59.7,
|
||||
81.75
|
||||
],
|
||||
[
|
||||
59.63,
|
||||
82.22
|
||||
],
|
||||
[
|
||||
59.56,
|
||||
82.53
|
||||
],
|
||||
[
|
||||
58.83,
|
||||
84.19
|
||||
],
|
||||
[
|
||||
58.54,
|
||||
84.56
|
||||
],
|
||||
[
|
||||
52.87,
|
||||
88.87
|
||||
],
|
||||
[
|
||||
51.1,
|
||||
90.07
|
||||
],
|
||||
[
|
||||
50.85,
|
||||
90.2
|
||||
],
|
||||
[
|
||||
49.12,
|
||||
90.2
|
||||
],
|
||||
[
|
||||
48.87,
|
||||
90.07
|
||||
],
|
||||
[
|
||||
48.29,
|
||||
89.67
|
||||
],
|
||||
[
|
||||
41.39,
|
||||
84.65
|
||||
],
|
||||
[
|
||||
41.1,
|
||||
84.27
|
||||
],
|
||||
[
|
||||
40.44,
|
||||
82.76
|
||||
],
|
||||
[
|
||||
40.3,
|
||||
82.12
|
||||
],
|
||||
[
|
||||
40.22,
|
||||
81.65
|
||||
],
|
||||
[
|
||||
40.15,
|
||||
72.96
|
||||
],
|
||||
[
|
||||
40.15,
|
||||
62.14
|
||||
],
|
||||
[
|
||||
40.22,
|
||||
61.52
|
||||
],
|
||||
[
|
||||
40.37,
|
||||
60.89
|
||||
],
|
||||
[
|
||||
41.02,
|
||||
59.38
|
||||
],
|
||||
[
|
||||
41.24,
|
||||
59.02
|
||||
],
|
||||
[
|
||||
41.53,
|
||||
58.63
|
||||
],
|
||||
[
|
||||
45.13,
|
||||
56.07
|
||||
],
|
||||
[
|
||||
49.37,
|
||||
53.4
|
||||
],
|
||||
[
|
||||
49.7,
|
||||
53.27
|
||||
],
|
||||
[
|
||||
50.28,
|
||||
53.27
|
||||
],
|
||||
[
|
||||
50.61,
|
||||
53.4
|
||||
],
|
||||
[
|
||||
54.86,
|
||||
56.07
|
||||
],
|
||||
[
|
||||
58.54,
|
||||
58.92
|
||||
],
|
||||
[
|
||||
58.76,
|
||||
59.27
|
||||
],
|
||||
[
|
||||
59.63,
|
||||
61.26
|
||||
],
|
||||
[
|
||||
59.7,
|
||||
61.73
|
||||
]
|
||||
],
|
||||
"dx": 0.28,
|
||||
"dy": -26.71
|
||||
},
|
||||
{
|
||||
"name": "worker-3",
|
||||
"title": "Worker node",
|
||||
"blurb": "The fleet grows by adding more of exactly this.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
58.9,
|
||||
69.09
|
||||
],
|
||||
[
|
||||
58.97,
|
||||
50.11
|
||||
],
|
||||
[
|
||||
59.19,
|
||||
49.18
|
||||
],
|
||||
[
|
||||
60.21,
|
||||
47.22
|
||||
],
|
||||
[
|
||||
66.51,
|
||||
42.2
|
||||
],
|
||||
[
|
||||
67.2,
|
||||
41.8
|
||||
],
|
||||
[
|
||||
68.05,
|
||||
41.67
|
||||
],
|
||||
[
|
||||
68.73,
|
||||
41.93
|
||||
],
|
||||
[
|
||||
73.28,
|
||||
44.6
|
||||
],
|
||||
[
|
||||
76.2,
|
||||
46.67
|
||||
],
|
||||
[
|
||||
77.22,
|
||||
48.69
|
||||
],
|
||||
[
|
||||
77.44,
|
||||
49.51
|
||||
],
|
||||
[
|
||||
77.58,
|
||||
50.94
|
||||
],
|
||||
[
|
||||
77.51,
|
||||
58.91
|
||||
],
|
||||
[
|
||||
77.36,
|
||||
69.28
|
||||
],
|
||||
[
|
||||
77.14,
|
||||
70.44
|
||||
],
|
||||
[
|
||||
75.71,
|
||||
72.2
|
||||
],
|
||||
[
|
||||
70.98,
|
||||
75.4
|
||||
],
|
||||
[
|
||||
69.0,
|
||||
76.73
|
||||
],
|
||||
[
|
||||
67.19,
|
||||
76.73
|
||||
],
|
||||
[
|
||||
59.92,
|
||||
71.54
|
||||
],
|
||||
[
|
||||
59.05,
|
||||
69.71
|
||||
]
|
||||
],
|
||||
"dx": -11.12,
|
||||
"dy": -19.34
|
||||
}
|
||||
],
|
||||
"stagger": 0.28
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
{
|
||||
"scene": "docker",
|
||||
"source": "assets/dist/docker-layers.webp",
|
||||
"stagger": 0.35,
|
||||
"parts": [
|
||||
{
|
||||
"name": "base",
|
||||
"title": "Base image",
|
||||
"blurb": "The foundation everything stacks on. Layers are content-addressed - an identical layer is stored once and reused by every image that references it.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
55.66
|
||||
],
|
||||
[
|
||||
27.88,
|
||||
55.66
|
||||
],
|
||||
[
|
||||
49.48,
|
||||
75.51
|
||||
],
|
||||
[
|
||||
71.88,
|
||||
55.6
|
||||
],
|
||||
[
|
||||
100,
|
||||
55.6
|
||||
],
|
||||
[
|
||||
100,
|
||||
100
|
||||
],
|
||||
[
|
||||
0,
|
||||
100
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "deps",
|
||||
"title": "Dependencies",
|
||||
"blurb": "Your packages and runtime. Change these and every layer above has to be rebuilt.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
40.64
|
||||
],
|
||||
[
|
||||
28.38,
|
||||
40.64
|
||||
],
|
||||
[
|
||||
49.76,
|
||||
55.45
|
||||
],
|
||||
[
|
||||
71.5,
|
||||
40.32
|
||||
],
|
||||
[
|
||||
100,
|
||||
40.32
|
||||
],
|
||||
[
|
||||
100,
|
||||
56.3
|
||||
],
|
||||
[
|
||||
71.88,
|
||||
56.3
|
||||
],
|
||||
[
|
||||
49.48,
|
||||
76.21
|
||||
],
|
||||
[
|
||||
27.88,
|
||||
56.36
|
||||
],
|
||||
[
|
||||
0,
|
||||
56.36
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 13.49
|
||||
},
|
||||
{
|
||||
"name": "code",
|
||||
"title": "Application code",
|
||||
"blurb": "Your actual program - usually the smallest layer, and the one that changes every single build.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
26.54
|
||||
],
|
||||
[
|
||||
27.75,
|
||||
26.54
|
||||
],
|
||||
[
|
||||
50.77,
|
||||
40.03
|
||||
],
|
||||
[
|
||||
72.0,
|
||||
26.17
|
||||
],
|
||||
[
|
||||
100,
|
||||
26.17
|
||||
],
|
||||
[
|
||||
100,
|
||||
41.02
|
||||
],
|
||||
[
|
||||
71.5,
|
||||
41.02
|
||||
],
|
||||
[
|
||||
49.76,
|
||||
56.15
|
||||
],
|
||||
[
|
||||
28.38,
|
||||
41.34
|
||||
],
|
||||
[
|
||||
0,
|
||||
41.34
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 28.54
|
||||
},
|
||||
{
|
||||
"name": "writable",
|
||||
"title": "Writable layer",
|
||||
"blurb": "NOT part of the image. The runtime adds it when it creates the container, and discards it when that container dies.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
100,
|
||||
0
|
||||
],
|
||||
[
|
||||
100,
|
||||
26.87
|
||||
],
|
||||
[
|
||||
72.0,
|
||||
26.87
|
||||
],
|
||||
[
|
||||
50.77,
|
||||
40.73
|
||||
],
|
||||
[
|
||||
27.75,
|
||||
27.24
|
||||
],
|
||||
[
|
||||
0,
|
||||
27.24
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 40.1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
{
|
||||
"scene": "gitops",
|
||||
"source": "assets/raw/course-VI-gitops.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "repo",
|
||||
"title": "The repository",
|
||||
"blurb": "Desired state lives here. The deploy button is a git commit; the audit log is git log.",
|
||||
"z": 3,
|
||||
"points": [
|
||||
[
|
||||
13.7,
|
||||
36.46
|
||||
],
|
||||
[
|
||||
13.26,
|
||||
35.57
|
||||
],
|
||||
[
|
||||
13.19,
|
||||
33.7
|
||||
],
|
||||
[
|
||||
13.19,
|
||||
32.55
|
||||
],
|
||||
[
|
||||
13.26,
|
||||
29.68
|
||||
],
|
||||
[
|
||||
13.84,
|
||||
28.52
|
||||
],
|
||||
[
|
||||
14.35,
|
||||
27.94
|
||||
],
|
||||
[
|
||||
15.66,
|
||||
26.44
|
||||
],
|
||||
[
|
||||
16.46,
|
||||
25.51
|
||||
],
|
||||
[
|
||||
23.36,
|
||||
17.9
|
||||
],
|
||||
[
|
||||
26.79,
|
||||
15.17
|
||||
],
|
||||
[
|
||||
28.04,
|
||||
14.13
|
||||
],
|
||||
[
|
||||
31.24,
|
||||
14.13
|
||||
],
|
||||
[
|
||||
33.21,
|
||||
15.56
|
||||
],
|
||||
[
|
||||
35.14,
|
||||
17.83
|
||||
],
|
||||
[
|
||||
36.52,
|
||||
19.54
|
||||
],
|
||||
[
|
||||
37.46,
|
||||
20.66
|
||||
],
|
||||
[
|
||||
39.06,
|
||||
22.51
|
||||
],
|
||||
[
|
||||
39.57,
|
||||
23.54
|
||||
],
|
||||
[
|
||||
39.64,
|
||||
25.68
|
||||
],
|
||||
[
|
||||
39.64,
|
||||
29.39
|
||||
],
|
||||
[
|
||||
39.06,
|
||||
30.54
|
||||
],
|
||||
[
|
||||
38.7,
|
||||
30.97
|
||||
],
|
||||
[
|
||||
38.19,
|
||||
31.56
|
||||
],
|
||||
[
|
||||
35.94,
|
||||
34.12
|
||||
],
|
||||
[
|
||||
26.02,
|
||||
44.21
|
||||
],
|
||||
[
|
||||
25.17,
|
||||
44.86
|
||||
],
|
||||
[
|
||||
24.92,
|
||||
44.99
|
||||
],
|
||||
[
|
||||
21.96,
|
||||
44.99
|
||||
],
|
||||
[
|
||||
21.35,
|
||||
44.6
|
||||
],
|
||||
[
|
||||
20.45,
|
||||
43.95
|
||||
],
|
||||
[
|
||||
15.15,
|
||||
38.17
|
||||
],
|
||||
[
|
||||
13.92,
|
||||
36.76
|
||||
]
|
||||
],
|
||||
"dx": 9.27,
|
||||
"dy": 8.52
|
||||
},
|
||||
{
|
||||
"name": "engine",
|
||||
"title": "Argo CD",
|
||||
"blurb": "Watches repo + path + revision, compares desired against actual, and syncs the difference.",
|
||||
"z": 5,
|
||||
"points": [
|
||||
[
|
||||
48.29,
|
||||
31.4
|
||||
],
|
||||
[
|
||||
48.29,
|
||||
43.47
|
||||
],
|
||||
[
|
||||
48.22,
|
||||
43.78
|
||||
],
|
||||
[
|
||||
47.93,
|
||||
44.69
|
||||
],
|
||||
[
|
||||
47.35,
|
||||
45.94
|
||||
],
|
||||
[
|
||||
47.13,
|
||||
46.27
|
||||
],
|
||||
[
|
||||
45.46,
|
||||
48.59
|
||||
],
|
||||
[
|
||||
39.41,
|
||||
54.23
|
||||
],
|
||||
[
|
||||
35.64,
|
||||
57.1
|
||||
],
|
||||
[
|
||||
35.3,
|
||||
57.1
|
||||
],
|
||||
[
|
||||
31.94,
|
||||
54.49
|
||||
],
|
||||
[
|
||||
31.29,
|
||||
53.97
|
||||
],
|
||||
[
|
||||
30.63,
|
||||
52.4
|
||||
],
|
||||
[
|
||||
30.56,
|
||||
52.08
|
||||
],
|
||||
[
|
||||
30.05,
|
||||
45.02
|
||||
],
|
||||
[
|
||||
30.05,
|
||||
43.21
|
||||
],
|
||||
[
|
||||
30.12,
|
||||
42.46
|
||||
],
|
||||
[
|
||||
30.27,
|
||||
41.25
|
||||
],
|
||||
[
|
||||
30.41,
|
||||
40.49
|
||||
],
|
||||
[
|
||||
30.78,
|
||||
38.96
|
||||
],
|
||||
[
|
||||
31.07,
|
||||
38.03
|
||||
],
|
||||
[
|
||||
31.29,
|
||||
37.39
|
||||
],
|
||||
[
|
||||
32.3,
|
||||
35.06
|
||||
],
|
||||
[
|
||||
32.74,
|
||||
34.32
|
||||
],
|
||||
[
|
||||
39.51,
|
||||
27.93
|
||||
],
|
||||
[
|
||||
39.95,
|
||||
27.67
|
||||
],
|
||||
[
|
||||
43.5,
|
||||
25.98
|
||||
],
|
||||
[
|
||||
43.84,
|
||||
25.98
|
||||
],
|
||||
[
|
||||
45.48,
|
||||
27.15
|
||||
],
|
||||
[
|
||||
46.84,
|
||||
28.71
|
||||
],
|
||||
[
|
||||
47.35,
|
||||
29.44
|
||||
],
|
||||
[
|
||||
47.71,
|
||||
29.96
|
||||
],
|
||||
[
|
||||
48.22,
|
||||
31.09
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "sheets",
|
||||
"title": "Rendered manifests",
|
||||
"blurb": "What the repo's charts and manifests render to - build artefacts nobody hand-edits.",
|
||||
"z": 2,
|
||||
"points": [
|
||||
[
|
||||
39.57,
|
||||
59.81
|
||||
],
|
||||
[
|
||||
39.57,
|
||||
52.61
|
||||
],
|
||||
[
|
||||
40.08,
|
||||
51.59
|
||||
],
|
||||
[
|
||||
40.59,
|
||||
51.01
|
||||
],
|
||||
[
|
||||
42.04,
|
||||
49.36
|
||||
],
|
||||
[
|
||||
47.79,
|
||||
42.25
|
||||
],
|
||||
[
|
||||
51.84,
|
||||
39.13
|
||||
],
|
||||
[
|
||||
54.79,
|
||||
36.65
|
||||
],
|
||||
[
|
||||
54.96,
|
||||
36.52
|
||||
],
|
||||
[
|
||||
55.04,
|
||||
36.52
|
||||
],
|
||||
[
|
||||
56.61,
|
||||
37.69
|
||||
],
|
||||
[
|
||||
61.59,
|
||||
42.77
|
||||
],
|
||||
[
|
||||
64.35,
|
||||
45.97
|
||||
],
|
||||
[
|
||||
64.57,
|
||||
46.27
|
||||
],
|
||||
[
|
||||
65.01,
|
||||
47.15
|
||||
],
|
||||
[
|
||||
65.01,
|
||||
54.33
|
||||
],
|
||||
[
|
||||
64.57,
|
||||
55.2
|
||||
],
|
||||
[
|
||||
64.35,
|
||||
55.49
|
||||
],
|
||||
[
|
||||
51.02,
|
||||
69.08
|
||||
],
|
||||
[
|
||||
49.95,
|
||||
69.99
|
||||
],
|
||||
[
|
||||
49.79,
|
||||
70.12
|
||||
],
|
||||
[
|
||||
49.54,
|
||||
70.12
|
||||
],
|
||||
[
|
||||
48.69,
|
||||
69.47
|
||||
],
|
||||
[
|
||||
41.24,
|
||||
62.12
|
||||
],
|
||||
[
|
||||
40.01,
|
||||
60.7
|
||||
]
|
||||
],
|
||||
"dx": -9.34,
|
||||
"dy": -8.61
|
||||
},
|
||||
{
|
||||
"name": "cluster",
|
||||
"title": "The cluster",
|
||||
"blurb": "Actual state. It pulls its own future - no CI system holds a credential that can touch it.",
|
||||
"z": 1,
|
||||
"points": [
|
||||
[
|
||||
52.8,
|
||||
72.93
|
||||
],
|
||||
[
|
||||
52.29,
|
||||
71.93
|
||||
],
|
||||
[
|
||||
52.22,
|
||||
70.38
|
||||
],
|
||||
[
|
||||
52.22,
|
||||
68.69
|
||||
],
|
||||
[
|
||||
52.29,
|
||||
68.13
|
||||
],
|
||||
[
|
||||
52.94,
|
||||
66.85
|
||||
],
|
||||
[
|
||||
53.16,
|
||||
66.57
|
||||
],
|
||||
[
|
||||
54.61,
|
||||
64.7
|
||||
],
|
||||
[
|
||||
61.55,
|
||||
55.92
|
||||
],
|
||||
[
|
||||
64.87,
|
||||
53.32
|
||||
],
|
||||
[
|
||||
67.36,
|
||||
51.37
|
||||
],
|
||||
[
|
||||
69.43,
|
||||
49.93
|
||||
],
|
||||
[
|
||||
70.76,
|
||||
49.93
|
||||
],
|
||||
[
|
||||
74.11,
|
||||
52.28
|
||||
],
|
||||
[
|
||||
75.36,
|
||||
53.19
|
||||
],
|
||||
[
|
||||
78.6,
|
||||
56.99
|
||||
],
|
||||
[
|
||||
82.09,
|
||||
61.01
|
||||
],
|
||||
[
|
||||
82.45,
|
||||
61.46
|
||||
],
|
||||
[
|
||||
83.76,
|
||||
63.05
|
||||
],
|
||||
[
|
||||
83.98,
|
||||
63.34
|
||||
],
|
||||
[
|
||||
84.56,
|
||||
64.48
|
||||
],
|
||||
[
|
||||
84.56,
|
||||
68.27
|
||||
],
|
||||
[
|
||||
83.98,
|
||||
69.39
|
||||
],
|
||||
[
|
||||
83.76,
|
||||
69.68
|
||||
],
|
||||
[
|
||||
81.8,
|
||||
71.84
|
||||
],
|
||||
[
|
||||
74.1,
|
||||
81.06
|
||||
],
|
||||
[
|
||||
69.78,
|
||||
84.57
|
||||
],
|
||||
[
|
||||
67.95,
|
||||
86.13
|
||||
],
|
||||
[
|
||||
67.62,
|
||||
86.39
|
||||
],
|
||||
[
|
||||
66.3,
|
||||
86.39
|
||||
],
|
||||
[
|
||||
64.8,
|
||||
85.22
|
||||
],
|
||||
[
|
||||
58.54,
|
||||
79.49
|
||||
],
|
||||
[
|
||||
53.81,
|
||||
74.11
|
||||
],
|
||||
[
|
||||
53.02,
|
||||
73.22
|
||||
]
|
||||
],
|
||||
"dx": -16.01,
|
||||
"dy": -14.69
|
||||
},
|
||||
{
|
||||
"name": "shard",
|
||||
"title": "Drift",
|
||||
"blurb": "A hand-edited live object becomes drift. With self-heal enabled Argo CD can restore desired state; with prune enabled removed desired objects can be deleted.",
|
||||
"z": 4,
|
||||
"points": [
|
||||
[
|
||||
80.04,
|
||||
94.73
|
||||
],
|
||||
[
|
||||
77.39,
|
||||
94.21
|
||||
],
|
||||
[
|
||||
76.89,
|
||||
94.08
|
||||
],
|
||||
[
|
||||
76.58,
|
||||
93.95
|
||||
],
|
||||
[
|
||||
76.37,
|
||||
93.82
|
||||
],
|
||||
[
|
||||
75.51,
|
||||
92.91
|
||||
],
|
||||
[
|
||||
74.82,
|
||||
91.82
|
||||
],
|
||||
[
|
||||
72.35,
|
||||
82.43
|
||||
],
|
||||
[
|
||||
72.35,
|
||||
82.26
|
||||
],
|
||||
[
|
||||
72.42,
|
||||
81.76
|
||||
],
|
||||
[
|
||||
72.93,
|
||||
80.47
|
||||
],
|
||||
[
|
||||
73.15,
|
||||
80.05
|
||||
],
|
||||
[
|
||||
73.51,
|
||||
79.38
|
||||
],
|
||||
[
|
||||
75.28,
|
||||
77.67
|
||||
],
|
||||
[
|
||||
75.49,
|
||||
77.54
|
||||
],
|
||||
[
|
||||
75.58,
|
||||
77.54
|
||||
],
|
||||
[
|
||||
79.69,
|
||||
78.32
|
||||
],
|
||||
[
|
||||
80.2,
|
||||
78.45
|
||||
],
|
||||
[
|
||||
81.18,
|
||||
79.49
|
||||
],
|
||||
[
|
||||
81.63,
|
||||
80.01
|
||||
],
|
||||
[
|
||||
82.01,
|
||||
81.62
|
||||
],
|
||||
[
|
||||
83.9,
|
||||
88.5
|
||||
],
|
||||
[
|
||||
84.05,
|
||||
88.98
|
||||
],
|
||||
[
|
||||
84.05,
|
||||
89.31
|
||||
],
|
||||
[
|
||||
83.98,
|
||||
89.65
|
||||
],
|
||||
[
|
||||
83.39,
|
||||
91.11
|
||||
],
|
||||
[
|
||||
83.18,
|
||||
91.52
|
||||
],
|
||||
[
|
||||
80.34,
|
||||
94.6
|
||||
],
|
||||
[
|
||||
80.14,
|
||||
94.73
|
||||
]
|
||||
],
|
||||
"dx": -13.61,
|
||||
"dy": -15.62
|
||||
},
|
||||
{
|
||||
"name": "vault",
|
||||
"title": "The vault",
|
||||
"blurb": "Azure Key Vault in the worked example. Git carries the reference; the vault carries the value. Never absorbed into the pipeline.",
|
||||
"z": 6,
|
||||
"points": [
|
||||
[
|
||||
56.07,
|
||||
10.57
|
||||
],
|
||||
[
|
||||
56.07,
|
||||
23.29
|
||||
],
|
||||
[
|
||||
55.63,
|
||||
24.38
|
||||
],
|
||||
[
|
||||
55.42,
|
||||
24.79
|
||||
],
|
||||
[
|
||||
55.05,
|
||||
25.44
|
||||
],
|
||||
[
|
||||
51.4,
|
||||
28.32
|
||||
],
|
||||
[
|
||||
50.13,
|
||||
29.23
|
||||
],
|
||||
[
|
||||
49.87,
|
||||
29.23
|
||||
],
|
||||
[
|
||||
49.33,
|
||||
28.84
|
||||
],
|
||||
[
|
||||
45.24,
|
||||
25.98
|
||||
],
|
||||
[
|
||||
44.51,
|
||||
24.92
|
||||
],
|
||||
[
|
||||
44.29,
|
||||
24.5
|
||||
],
|
||||
[
|
||||
43.86,
|
||||
23.37
|
||||
],
|
||||
[
|
||||
43.86,
|
||||
10.48
|
||||
],
|
||||
[
|
||||
44.29,
|
||||
9.35
|
||||
],
|
||||
[
|
||||
44.51,
|
||||
8.92
|
||||
],
|
||||
[
|
||||
44.9,
|
||||
8.27
|
||||
],
|
||||
[
|
||||
48.89,
|
||||
5.4
|
||||
],
|
||||
[
|
||||
49.78,
|
||||
4.75
|
||||
],
|
||||
[
|
||||
50.3,
|
||||
4.75
|
||||
],
|
||||
[
|
||||
50.66,
|
||||
5.01
|
||||
],
|
||||
[
|
||||
55.05,
|
||||
8.39
|
||||
],
|
||||
[
|
||||
55.56,
|
||||
9.28
|
||||
]
|
||||
],
|
||||
"dx": -2.34,
|
||||
"dy": 5.37
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"scene": "helm",
|
||||
"source": "assets/raw/course-VIII-helm-press.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "press",
|
||||
"title": "The chart",
|
||||
"blurb": "A function with holes - the template owns every structural decision, and renders whatever fills it.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
28.09,
|
||||
44.03
|
||||
],
|
||||
[
|
||||
28.23,
|
||||
43.34
|
||||
],
|
||||
[
|
||||
28.81,
|
||||
42.23
|
||||
],
|
||||
[
|
||||
41.58,
|
||||
28.45
|
||||
],
|
||||
[
|
||||
47.7,
|
||||
22.85
|
||||
],
|
||||
[
|
||||
49.1,
|
||||
21.55
|
||||
],
|
||||
[
|
||||
49.41,
|
||||
21.29
|
||||
],
|
||||
[
|
||||
49.88,
|
||||
21.29
|
||||
],
|
||||
[
|
||||
50.11,
|
||||
21.42
|
||||
],
|
||||
[
|
||||
60.78,
|
||||
31.05
|
||||
],
|
||||
[
|
||||
69.66,
|
||||
40.45
|
||||
],
|
||||
[
|
||||
70.02,
|
||||
40.89
|
||||
],
|
||||
[
|
||||
70.6,
|
||||
42.04
|
||||
],
|
||||
[
|
||||
70.68,
|
||||
42.6
|
||||
],
|
||||
[
|
||||
71.4,
|
||||
68.03
|
||||
],
|
||||
[
|
||||
71.4,
|
||||
69.14
|
||||
],
|
||||
[
|
||||
71.33,
|
||||
69.42
|
||||
],
|
||||
[
|
||||
70.89,
|
||||
70.27
|
||||
],
|
||||
[
|
||||
70.02,
|
||||
71.43
|
||||
],
|
||||
[
|
||||
65.08,
|
||||
78.29
|
||||
],
|
||||
[
|
||||
60.99,
|
||||
82.75
|
||||
],
|
||||
[
|
||||
58.16,
|
||||
85.74
|
||||
],
|
||||
[
|
||||
57.76,
|
||||
86.13
|
||||
],
|
||||
[
|
||||
57.37,
|
||||
86.26
|
||||
],
|
||||
[
|
||||
56.27,
|
||||
86.26
|
||||
],
|
||||
[
|
||||
55.5,
|
||||
85.61
|
||||
],
|
||||
[
|
||||
53.39,
|
||||
83.66
|
||||
],
|
||||
[
|
||||
44.95,
|
||||
76.11
|
||||
],
|
||||
[
|
||||
42.79,
|
||||
74.29
|
||||
],
|
||||
[
|
||||
41.68,
|
||||
73.03
|
||||
],
|
||||
[
|
||||
41.46,
|
||||
72.69
|
||||
],
|
||||
[
|
||||
28.74,
|
||||
53.35
|
||||
],
|
||||
[
|
||||
28.31,
|
||||
52.53
|
||||
],
|
||||
[
|
||||
28.23,
|
||||
52.26
|
||||
],
|
||||
[
|
||||
28.09,
|
||||
44.72
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "contexts",
|
||||
"title": "Values contexts",
|
||||
"blurb": "One small file per cluster - names, addresses, sizes. The cluster's whole voice, values only.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
24.11,
|
||||
21.94
|
||||
],
|
||||
[
|
||||
34.92,
|
||||
19.22
|
||||
],
|
||||
[
|
||||
35.07,
|
||||
19.25
|
||||
],
|
||||
[
|
||||
36.67,
|
||||
21.02
|
||||
],
|
||||
[
|
||||
36.81,
|
||||
21.19
|
||||
],
|
||||
[
|
||||
37.39,
|
||||
22.28
|
||||
],
|
||||
[
|
||||
38.12,
|
||||
23.8
|
||||
],
|
||||
[
|
||||
38.19,
|
||||
24.23
|
||||
],
|
||||
[
|
||||
38.48,
|
||||
28.14
|
||||
],
|
||||
[
|
||||
38.48,
|
||||
28.43
|
||||
],
|
||||
[
|
||||
38.34,
|
||||
30.15
|
||||
],
|
||||
[
|
||||
38.26,
|
||||
30.44
|
||||
],
|
||||
[
|
||||
37.68,
|
||||
31.58
|
||||
],
|
||||
[
|
||||
37.32,
|
||||
32.01
|
||||
],
|
||||
[
|
||||
15.52,
|
||||
51.59
|
||||
],
|
||||
[
|
||||
14.64,
|
||||
52.14
|
||||
],
|
||||
[
|
||||
14.35,
|
||||
52.08
|
||||
],
|
||||
[
|
||||
14.06,
|
||||
51.74
|
||||
],
|
||||
[
|
||||
13.41,
|
||||
50.91
|
||||
],
|
||||
[
|
||||
13.19,
|
||||
50.59
|
||||
],
|
||||
[
|
||||
12.68,
|
||||
49.51
|
||||
],
|
||||
[
|
||||
12.68,
|
||||
46.65
|
||||
],
|
||||
[
|
||||
14.5,
|
||||
35.5
|
||||
],
|
||||
[
|
||||
14.57,
|
||||
35.06
|
||||
],
|
||||
[
|
||||
14.72,
|
||||
34.63
|
||||
],
|
||||
[
|
||||
15.3,
|
||||
33.46
|
||||
],
|
||||
[
|
||||
22.06,
|
||||
22.98
|
||||
],
|
||||
[
|
||||
22.35,
|
||||
22.72
|
||||
],
|
||||
[
|
||||
22.82,
|
||||
22.46
|
||||
]
|
||||
],
|
||||
"dx": 17.79,
|
||||
"dy": 14.42
|
||||
},
|
||||
{
|
||||
"name": "renders",
|
||||
"title": "The renders",
|
||||
"blurb": "Same framework, different numbers - one function, many clusters. Review the render, not just the template.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
54.4,
|
||||
28.31
|
||||
],
|
||||
[
|
||||
53.81,
|
||||
26.93
|
||||
],
|
||||
[
|
||||
53.67,
|
||||
25.14
|
||||
],
|
||||
[
|
||||
54.76,
|
||||
22.23
|
||||
],
|
||||
[
|
||||
60.5,
|
||||
7.22
|
||||
],
|
||||
[
|
||||
61.23,
|
||||
5.85
|
||||
],
|
||||
[
|
||||
62.48,
|
||||
5.4
|
||||
],
|
||||
[
|
||||
66.73,
|
||||
8.79
|
||||
],
|
||||
[
|
||||
71.55,
|
||||
12.69
|
||||
],
|
||||
[
|
||||
77.03,
|
||||
17.51
|
||||
],
|
||||
[
|
||||
81.94,
|
||||
22.56
|
||||
],
|
||||
[
|
||||
84.63,
|
||||
26.52
|
||||
],
|
||||
[
|
||||
88.34,
|
||||
32.13
|
||||
],
|
||||
[
|
||||
91.03,
|
||||
36.33
|
||||
],
|
||||
[
|
||||
94.3,
|
||||
41.38
|
||||
],
|
||||
[
|
||||
94.88,
|
||||
42.61
|
||||
],
|
||||
[
|
||||
94.73,
|
||||
44.56
|
||||
],
|
||||
[
|
||||
92.92,
|
||||
47.61
|
||||
],
|
||||
[
|
||||
86.33,
|
||||
55.8
|
||||
],
|
||||
[
|
||||
82.05,
|
||||
60.22
|
||||
],
|
||||
[
|
||||
80.26,
|
||||
59.96
|
||||
]
|
||||
],
|
||||
"dx": -15.11,
|
||||
"dy": 12.16
|
||||
},
|
||||
{
|
||||
"name": "gate",
|
||||
"title": "The schema gate",
|
||||
"blurb": "values.schema.json + required: a context missing a value fails at render time, in the pipeline.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
52.58,
|
||||
82.79
|
||||
],
|
||||
[
|
||||
52.58,
|
||||
84.22
|
||||
],
|
||||
[
|
||||
49.17,
|
||||
92.01
|
||||
],
|
||||
[
|
||||
48.73,
|
||||
92.99
|
||||
],
|
||||
[
|
||||
48.44,
|
||||
93.51
|
||||
],
|
||||
[
|
||||
48.04,
|
||||
93.56
|
||||
],
|
||||
[
|
||||
47.89,
|
||||
93.43
|
||||
],
|
||||
[
|
||||
29.76,
|
||||
74.97
|
||||
],
|
||||
[
|
||||
28.74,
|
||||
73.82
|
||||
],
|
||||
[
|
||||
28.31,
|
||||
72.96
|
||||
],
|
||||
[
|
||||
28.23,
|
||||
72.68
|
||||
],
|
||||
[
|
||||
28.23,
|
||||
68.29
|
||||
],
|
||||
[
|
||||
28.67,
|
||||
67.36
|
||||
],
|
||||
[
|
||||
29.03,
|
||||
66.72
|
||||
],
|
||||
[
|
||||
29.69,
|
||||
66.48
|
||||
],
|
||||
[
|
||||
31.58,
|
||||
65.77
|
||||
],
|
||||
[
|
||||
35.11,
|
||||
65.04
|
||||
],
|
||||
[
|
||||
36.17,
|
||||
65.04
|
||||
],
|
||||
[
|
||||
36.51,
|
||||
65.3
|
||||
],
|
||||
[
|
||||
51.56,
|
||||
81.1
|
||||
],
|
||||
[
|
||||
52.0,
|
||||
81.66
|
||||
]
|
||||
],
|
||||
"dx": 4.13,
|
||||
"dy": -10.4
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
{
|
||||
"scene": "node",
|
||||
"source": "assets/raw/course-III-node.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "kubelet",
|
||||
"title": "kubelet",
|
||||
"blurb": "The node agent. It ensures the PodSpecs assigned to this node are running and healthy, using CRI to instruct the container runtime.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
17.33,
|
||||
29.89
|
||||
],
|
||||
[
|
||||
18.28,
|
||||
22.52
|
||||
],
|
||||
[
|
||||
18.71,
|
||||
19.27
|
||||
],
|
||||
[
|
||||
18.79,
|
||||
18.95
|
||||
],
|
||||
[
|
||||
19.51,
|
||||
17.27
|
||||
],
|
||||
[
|
||||
21.12,
|
||||
14.65
|
||||
],
|
||||
[
|
||||
25.04,
|
||||
11.52
|
||||
],
|
||||
[
|
||||
25.39,
|
||||
11.26
|
||||
],
|
||||
[
|
||||
25.9,
|
||||
11.0
|
||||
],
|
||||
[
|
||||
25.99,
|
||||
11.0
|
||||
],
|
||||
[
|
||||
27.19,
|
||||
11.91
|
||||
],
|
||||
[
|
||||
27.99,
|
||||
12.56
|
||||
],
|
||||
[
|
||||
28.62,
|
||||
13.08
|
||||
],
|
||||
[
|
||||
30.4,
|
||||
14.52
|
||||
],
|
||||
[
|
||||
30.89,
|
||||
14.91
|
||||
],
|
||||
[
|
||||
34.56,
|
||||
22.64
|
||||
],
|
||||
[
|
||||
34.92,
|
||||
24.03
|
||||
],
|
||||
[
|
||||
34.99,
|
||||
24.34
|
||||
],
|
||||
[
|
||||
35.14,
|
||||
25.25
|
||||
],
|
||||
[
|
||||
35.21,
|
||||
25.86
|
||||
],
|
||||
[
|
||||
35.21,
|
||||
26.46
|
||||
],
|
||||
[
|
||||
34.99,
|
||||
34.83
|
||||
],
|
||||
[
|
||||
34.92,
|
||||
35.15
|
||||
],
|
||||
[
|
||||
34.41,
|
||||
36.34
|
||||
],
|
||||
[
|
||||
30.15,
|
||||
40.43
|
||||
],
|
||||
[
|
||||
28.6,
|
||||
41.6
|
||||
],
|
||||
[
|
||||
28.43,
|
||||
41.6
|
||||
],
|
||||
[
|
||||
22.84,
|
||||
39.91
|
||||
],
|
||||
[
|
||||
22.49,
|
||||
39.65
|
||||
],
|
||||
[
|
||||
18.49,
|
||||
35.48
|
||||
],
|
||||
[
|
||||
18.13,
|
||||
34.95
|
||||
],
|
||||
[
|
||||
17.91,
|
||||
34.61
|
||||
],
|
||||
[
|
||||
17.33,
|
||||
33.32
|
||||
]
|
||||
],
|
||||
"dx": 10.84,
|
||||
"dy": 11.97
|
||||
},
|
||||
{
|
||||
"name": "engine",
|
||||
"title": "containerd / CRI-O",
|
||||
"blurb": "containerd or CRI-O: pulls the image and actually creates and starts the container.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
55.56,
|
||||
40.75
|
||||
],
|
||||
[
|
||||
56.07,
|
||||
42.91
|
||||
],
|
||||
[
|
||||
55.85,
|
||||
54.8
|
||||
],
|
||||
[
|
||||
54.62,
|
||||
58.67
|
||||
],
|
||||
[
|
||||
52.58,
|
||||
62.25
|
||||
],
|
||||
[
|
||||
49.92,
|
||||
64.65
|
||||
],
|
||||
[
|
||||
48.82,
|
||||
65.56
|
||||
],
|
||||
[
|
||||
46.78,
|
||||
66.47
|
||||
],
|
||||
[
|
||||
45.1,
|
||||
65.04
|
||||
],
|
||||
[
|
||||
41.99,
|
||||
62.18
|
||||
],
|
||||
[
|
||||
34.27,
|
||||
54.48
|
||||
],
|
||||
[
|
||||
33.03,
|
||||
52.35
|
||||
],
|
||||
[
|
||||
31.79,
|
||||
44.03
|
||||
],
|
||||
[
|
||||
31.72,
|
||||
40.09
|
||||
],
|
||||
[
|
||||
31.94,
|
||||
37.88
|
||||
],
|
||||
[
|
||||
32.67,
|
||||
34.56
|
||||
],
|
||||
[
|
||||
33.18,
|
||||
32.99
|
||||
],
|
||||
[
|
||||
34.69,
|
||||
29.88
|
||||
],
|
||||
[
|
||||
36.13,
|
||||
28.58
|
||||
],
|
||||
[
|
||||
36.74,
|
||||
28.19
|
||||
],
|
||||
[
|
||||
37.5,
|
||||
27.93
|
||||
],
|
||||
[
|
||||
42.66,
|
||||
27.28
|
||||
],
|
||||
[
|
||||
44.32,
|
||||
28.32
|
||||
],
|
||||
[
|
||||
47.1,
|
||||
30.66
|
||||
],
|
||||
[
|
||||
53.38,
|
||||
38.02
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "cni",
|
||||
"title": "CNI plugin",
|
||||
"blurb": "Hands each pod a real IP - invoked by the container runtime, not by the kubelet.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
53.7,
|
||||
76.63
|
||||
],
|
||||
[
|
||||
52.65,
|
||||
74.87
|
||||
],
|
||||
[
|
||||
50.83,
|
||||
68.61
|
||||
],
|
||||
[
|
||||
50.76,
|
||||
67.43
|
||||
],
|
||||
[
|
||||
51.56,
|
||||
61.09
|
||||
],
|
||||
[
|
||||
52.22,
|
||||
59.42
|
||||
],
|
||||
[
|
||||
54.11,
|
||||
55.55
|
||||
],
|
||||
[
|
||||
54.69,
|
||||
54.63
|
||||
],
|
||||
[
|
||||
55.92,
|
||||
52.86
|
||||
],
|
||||
[
|
||||
57.48,
|
||||
52.15
|
||||
],
|
||||
[
|
||||
58.9,
|
||||
51.76
|
||||
],
|
||||
[
|
||||
60.29,
|
||||
51.5
|
||||
],
|
||||
[
|
||||
63.68,
|
||||
51.63
|
||||
],
|
||||
[
|
||||
64.8,
|
||||
51.89
|
||||
],
|
||||
[
|
||||
66.61,
|
||||
52.63
|
||||
],
|
||||
[
|
||||
67.55,
|
||||
53.81
|
||||
],
|
||||
[
|
||||
68.5,
|
||||
55.13
|
||||
],
|
||||
[
|
||||
69.22,
|
||||
56.32
|
||||
],
|
||||
[
|
||||
71.19,
|
||||
60.35
|
||||
],
|
||||
[
|
||||
71.48,
|
||||
61.69
|
||||
],
|
||||
[
|
||||
72.35,
|
||||
67.96
|
||||
],
|
||||
[
|
||||
70.46,
|
||||
73.49
|
||||
],
|
||||
[
|
||||
64.94,
|
||||
78.84
|
||||
],
|
||||
[
|
||||
59.2,
|
||||
79.23
|
||||
],
|
||||
[
|
||||
58.54,
|
||||
79.1
|
||||
]
|
||||
],
|
||||
"dx": -10.89,
|
||||
"dy": -12.01
|
||||
},
|
||||
{
|
||||
"name": "proxy",
|
||||
"title": "kube-proxy / eBPF",
|
||||
"blurb": "Makes Service addresses route to real pods - the datapath's switchboard.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
69.95,
|
||||
71.38
|
||||
],
|
||||
[
|
||||
70.68,
|
||||
69.46
|
||||
],
|
||||
[
|
||||
76.08,
|
||||
65.17
|
||||
],
|
||||
[
|
||||
76.76,
|
||||
64.65
|
||||
],
|
||||
[
|
||||
76.85,
|
||||
64.65
|
||||
],
|
||||
[
|
||||
77.36,
|
||||
64.91
|
||||
],
|
||||
[
|
||||
77.71,
|
||||
65.17
|
||||
],
|
||||
[
|
||||
78.34,
|
||||
66.08
|
||||
],
|
||||
[
|
||||
79.11,
|
||||
67.77
|
||||
],
|
||||
[
|
||||
82.74,
|
||||
79.93
|
||||
],
|
||||
[
|
||||
83.32,
|
||||
82.26
|
||||
],
|
||||
[
|
||||
83.98,
|
||||
84.98
|
||||
],
|
||||
[
|
||||
84.05,
|
||||
85.43
|
||||
],
|
||||
[
|
||||
84.12,
|
||||
86.19
|
||||
],
|
||||
[
|
||||
84.12,
|
||||
87.57
|
||||
],
|
||||
[
|
||||
83.69,
|
||||
88.58
|
||||
],
|
||||
[
|
||||
82.96,
|
||||
89.25
|
||||
],
|
||||
[
|
||||
82.67,
|
||||
89.5
|
||||
],
|
||||
[
|
||||
81.66,
|
||||
89.78
|
||||
],
|
||||
[
|
||||
80.39,
|
||||
89.65
|
||||
],
|
||||
[
|
||||
74.21,
|
||||
89.0
|
||||
],
|
||||
[
|
||||
68.71,
|
||||
87.52
|
||||
],
|
||||
[
|
||||
68.28,
|
||||
87.28
|
||||
],
|
||||
[
|
||||
67.7,
|
||||
85.97
|
||||
],
|
||||
[
|
||||
67.33,
|
||||
85.02
|
||||
],
|
||||
[
|
||||
67.33,
|
||||
84.72
|
||||
],
|
||||
[
|
||||
67.48,
|
||||
83.83
|
||||
],
|
||||
[
|
||||
68.42,
|
||||
78.83
|
||||
],
|
||||
[
|
||||
69.44,
|
||||
73.26
|
||||
],
|
||||
[
|
||||
69.58,
|
||||
72.6
|
||||
]
|
||||
],
|
||||
"dx": -19.91,
|
||||
"dy": -20.5
|
||||
},
|
||||
{
|
||||
"name": "pod",
|
||||
"title": "The pod",
|
||||
"blurb": "Lands here. Everything else in this scene exists to run it.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
52.27,
|
||||
13.48
|
||||
],
|
||||
[
|
||||
55.2,
|
||||
25.14
|
||||
],
|
||||
[
|
||||
55.27,
|
||||
28.0
|
||||
],
|
||||
[
|
||||
54.91,
|
||||
30.82
|
||||
],
|
||||
[
|
||||
53.7,
|
||||
33.01
|
||||
],
|
||||
[
|
||||
52.83,
|
||||
33.92
|
||||
],
|
||||
[
|
||||
51.74,
|
||||
34.57
|
||||
],
|
||||
[
|
||||
50.78,
|
||||
34.7
|
||||
],
|
||||
[
|
||||
49.83,
|
||||
34.44
|
||||
],
|
||||
[
|
||||
48.87,
|
||||
33.92
|
||||
],
|
||||
[
|
||||
47.51,
|
||||
32.1
|
||||
],
|
||||
[
|
||||
46.71,
|
||||
30.8
|
||||
],
|
||||
[
|
||||
43.06,
|
||||
20.97
|
||||
],
|
||||
[
|
||||
42.4,
|
||||
18.73
|
||||
],
|
||||
[
|
||||
42.11,
|
||||
13.82
|
||||
],
|
||||
[
|
||||
42.48,
|
||||
11.94
|
||||
],
|
||||
[
|
||||
43.12,
|
||||
10.35
|
||||
],
|
||||
[
|
||||
44.6,
|
||||
8.66
|
||||
],
|
||||
[
|
||||
45.67,
|
||||
8.01
|
||||
],
|
||||
[
|
||||
46.89,
|
||||
7.75
|
||||
],
|
||||
[
|
||||
48.35,
|
||||
8.4
|
||||
],
|
||||
[
|
||||
49.95,
|
||||
10.09
|
||||
],
|
||||
[
|
||||
51.44,
|
||||
12.3
|
||||
]
|
||||
],
|
||||
"dx": -2.19,
|
||||
"dy": 11.2
|
||||
}
|
||||
],
|
||||
"stagger": 0.3
|
||||
}
|
||||
@@ -0,0 +1,834 @@
|
||||
{
|
||||
"scene": "orbit",
|
||||
"source": "assets/raw/course-V-orbit.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "core",
|
||||
"title": "The Kubernetes core",
|
||||
"blurb": "Same engine underneath - everything in orbit is opinion, not replacement.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
40.08,
|
||||
49.98
|
||||
],
|
||||
[
|
||||
40.95,
|
||||
42.34
|
||||
],
|
||||
[
|
||||
41.53,
|
||||
40.42
|
||||
],
|
||||
[
|
||||
44.17,
|
||||
35.13
|
||||
],
|
||||
[
|
||||
45.64,
|
||||
33.4
|
||||
],
|
||||
[
|
||||
46.34,
|
||||
33.0
|
||||
],
|
||||
[
|
||||
48.12,
|
||||
32.33
|
||||
],
|
||||
[
|
||||
50.21,
|
||||
32.07
|
||||
],
|
||||
[
|
||||
52.9,
|
||||
32.73
|
||||
],
|
||||
[
|
||||
53.66,
|
||||
33.0
|
||||
],
|
||||
[
|
||||
54.28,
|
||||
33.53
|
||||
],
|
||||
[
|
||||
57.6,
|
||||
37.75
|
||||
],
|
||||
[
|
||||
58.61,
|
||||
40.97
|
||||
],
|
||||
[
|
||||
59.78,
|
||||
49.07
|
||||
],
|
||||
[
|
||||
59.85,
|
||||
50.9
|
||||
],
|
||||
[
|
||||
58.83,
|
||||
58.37
|
||||
],
|
||||
[
|
||||
58.54,
|
||||
59.52
|
||||
],
|
||||
[
|
||||
55.88,
|
||||
64.6
|
||||
],
|
||||
[
|
||||
54.63,
|
||||
66.2
|
||||
],
|
||||
[
|
||||
51.53,
|
||||
67.67
|
||||
],
|
||||
[
|
||||
50.38,
|
||||
68.07
|
||||
],
|
||||
[
|
||||
49.47,
|
||||
67.93
|
||||
],
|
||||
[
|
||||
45.8,
|
||||
66.47
|
||||
],
|
||||
[
|
||||
45.2,
|
||||
66.07
|
||||
],
|
||||
[
|
||||
41.53,
|
||||
59.74
|
||||
],
|
||||
[
|
||||
40.73,
|
||||
55.02
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "scc",
|
||||
"title": "SCC",
|
||||
"blurb": "Admission: what a pod may BE. Under the restricted SCCs a workload runs as a non-root UID from the project's allocated range, so the image must cope with an arbitrary UID.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
52.91,
|
||||
28.07
|
||||
],
|
||||
[
|
||||
48.91,
|
||||
29.0
|
||||
],
|
||||
[
|
||||
43.57,
|
||||
28.46
|
||||
],
|
||||
[
|
||||
42.99,
|
||||
27.13
|
||||
],
|
||||
[
|
||||
42.19,
|
||||
22.68
|
||||
],
|
||||
[
|
||||
42.11,
|
||||
18.9
|
||||
],
|
||||
[
|
||||
42.62,
|
||||
15.27
|
||||
],
|
||||
[
|
||||
43.13,
|
||||
13.62
|
||||
],
|
||||
[
|
||||
44.44,
|
||||
10.72
|
||||
],
|
||||
[
|
||||
48.82,
|
||||
5.93
|
||||
],
|
||||
[
|
||||
49.72,
|
||||
5.53
|
||||
],
|
||||
[
|
||||
50.7,
|
||||
5.67
|
||||
],
|
||||
[
|
||||
51.88,
|
||||
6.07
|
||||
],
|
||||
[
|
||||
56.36,
|
||||
9.1
|
||||
],
|
||||
[
|
||||
57.89,
|
||||
12.61
|
||||
],
|
||||
[
|
||||
59.34,
|
||||
17.03
|
||||
],
|
||||
[
|
||||
59.41,
|
||||
19.47
|
||||
],
|
||||
[
|
||||
58.54,
|
||||
21.06
|
||||
],
|
||||
[
|
||||
54.3,
|
||||
27.13
|
||||
],
|
||||
[
|
||||
53.2,
|
||||
27.93
|
||||
]
|
||||
],
|
||||
"dx": -0.22,
|
||||
"dy": 23.11
|
||||
},
|
||||
{
|
||||
"name": "routes",
|
||||
"title": "Routes",
|
||||
"blurb": "L7 exposure that predates Ingress - and still rules here.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
63.33,
|
||||
31.51
|
||||
],
|
||||
[
|
||||
63.33,
|
||||
13.15
|
||||
],
|
||||
[
|
||||
63.41,
|
||||
12.63
|
||||
],
|
||||
[
|
||||
63.84,
|
||||
11.46
|
||||
],
|
||||
[
|
||||
64.71,
|
||||
10.2
|
||||
],
|
||||
[
|
||||
65.21,
|
||||
9.8
|
||||
],
|
||||
[
|
||||
65.89,
|
||||
9.27
|
||||
],
|
||||
[
|
||||
66.63,
|
||||
8.73
|
||||
],
|
||||
[
|
||||
66.9,
|
||||
8.6
|
||||
],
|
||||
[
|
||||
67.42,
|
||||
9.0
|
||||
],
|
||||
[
|
||||
68.65,
|
||||
10.07
|
||||
],
|
||||
[
|
||||
71.1,
|
||||
12.07
|
||||
],
|
||||
[
|
||||
73.66,
|
||||
14.15
|
||||
],
|
||||
[
|
||||
74.38,
|
||||
15.61
|
||||
],
|
||||
[
|
||||
74.96,
|
||||
17.16
|
||||
],
|
||||
[
|
||||
75.11,
|
||||
17.7
|
||||
],
|
||||
[
|
||||
75.47,
|
||||
19.25
|
||||
],
|
||||
[
|
||||
75.69,
|
||||
20.26
|
||||
],
|
||||
[
|
||||
75.24,
|
||||
37.8
|
||||
],
|
||||
[
|
||||
74.7,
|
||||
38.6
|
||||
],
|
||||
[
|
||||
73.59,
|
||||
39.8
|
||||
],
|
||||
[
|
||||
72.67,
|
||||
40.2
|
||||
],
|
||||
[
|
||||
72.43,
|
||||
40.07
|
||||
],
|
||||
[
|
||||
65.27,
|
||||
33.93
|
||||
],
|
||||
[
|
||||
64.0,
|
||||
32.6
|
||||
]
|
||||
],
|
||||
"dx": -13.89,
|
||||
"dy": 19.09
|
||||
},
|
||||
{
|
||||
"name": "operators",
|
||||
"title": "Operators",
|
||||
"blurb": "Machinery that runs itself: the Cluster Version Operator drives the platform's own, OLM installs the add-ons you choose.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
70.46,
|
||||
52.73
|
||||
],
|
||||
[
|
||||
70.46,
|
||||
50.07
|
||||
],
|
||||
[
|
||||
70.53,
|
||||
49.59
|
||||
],
|
||||
[
|
||||
70.75,
|
||||
48.32
|
||||
],
|
||||
[
|
||||
70.82,
|
||||
48.0
|
||||
],
|
||||
[
|
||||
71.62,
|
||||
46.14
|
||||
],
|
||||
[
|
||||
74.79,
|
||||
41.27
|
||||
],
|
||||
[
|
||||
76.32,
|
||||
40.47
|
||||
],
|
||||
[
|
||||
76.87,
|
||||
40.2
|
||||
],
|
||||
[
|
||||
77.78,
|
||||
39.93
|
||||
],
|
||||
[
|
||||
78.59,
|
||||
39.93
|
||||
],
|
||||
[
|
||||
79.67,
|
||||
40.07
|
||||
],
|
||||
[
|
||||
81.24,
|
||||
40.6
|
||||
],
|
||||
[
|
||||
81.81,
|
||||
40.87
|
||||
],
|
||||
[
|
||||
82.49,
|
||||
41.27
|
||||
],
|
||||
[
|
||||
83.37,
|
||||
42.2
|
||||
],
|
||||
[
|
||||
84.12,
|
||||
44.21
|
||||
],
|
||||
[
|
||||
84.27,
|
||||
44.77
|
||||
],
|
||||
[
|
||||
84.56,
|
||||
46.34
|
||||
],
|
||||
[
|
||||
84.56,
|
||||
47.17
|
||||
],
|
||||
[
|
||||
84.05,
|
||||
61.15
|
||||
],
|
||||
[
|
||||
83.94,
|
||||
61.8
|
||||
],
|
||||
[
|
||||
83.32,
|
||||
63.27
|
||||
],
|
||||
[
|
||||
83.01,
|
||||
63.8
|
||||
],
|
||||
[
|
||||
82.14,
|
||||
65.0
|
||||
],
|
||||
[
|
||||
80.72,
|
||||
65.93
|
||||
],
|
||||
[
|
||||
80.46,
|
||||
66.07
|
||||
],
|
||||
[
|
||||
80.12,
|
||||
66.07
|
||||
],
|
||||
[
|
||||
79.01,
|
||||
65.67
|
||||
],
|
||||
[
|
||||
78.5,
|
||||
65.27
|
||||
],
|
||||
[
|
||||
71.77,
|
||||
56.68
|
||||
],
|
||||
[
|
||||
70.82,
|
||||
54.49
|
||||
]
|
||||
],
|
||||
"dx": -20.22,
|
||||
"dy": -1.54
|
||||
},
|
||||
{
|
||||
"name": "console",
|
||||
"title": "oc + console",
|
||||
"blurb": "The CLI and the web console - the operator's handheld.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
54.69,
|
||||
83.27
|
||||
],
|
||||
[
|
||||
53.96,
|
||||
81.7
|
||||
],
|
||||
[
|
||||
53.89,
|
||||
80.33
|
||||
],
|
||||
[
|
||||
53.89,
|
||||
80.03
|
||||
],
|
||||
[
|
||||
53.96,
|
||||
79.73
|
||||
],
|
||||
[
|
||||
54.69,
|
||||
78.23
|
||||
],
|
||||
[
|
||||
58.76,
|
||||
71.64
|
||||
],
|
||||
[
|
||||
59.88,
|
||||
69.4
|
||||
],
|
||||
[
|
||||
61.41,
|
||||
68.2
|
||||
],
|
||||
[
|
||||
64.26,
|
||||
66.2
|
||||
],
|
||||
[
|
||||
65.15,
|
||||
65.67
|
||||
],
|
||||
[
|
||||
65.69,
|
||||
65.67
|
||||
],
|
||||
[
|
||||
66.5,
|
||||
65.8
|
||||
],
|
||||
[
|
||||
67.25,
|
||||
66.33
|
||||
],
|
||||
[
|
||||
69.73,
|
||||
68.62
|
||||
],
|
||||
[
|
||||
71.11,
|
||||
70.86
|
||||
],
|
||||
[
|
||||
71.84,
|
||||
71.93
|
||||
],
|
||||
[
|
||||
72.06,
|
||||
72.27
|
||||
],
|
||||
[
|
||||
72.35,
|
||||
72.78
|
||||
],
|
||||
[
|
||||
72.78,
|
||||
73.75
|
||||
],
|
||||
[
|
||||
72.93,
|
||||
74.37
|
||||
],
|
||||
[
|
||||
72.93,
|
||||
75.46
|
||||
],
|
||||
[
|
||||
72.86,
|
||||
76.07
|
||||
],
|
||||
[
|
||||
72.2,
|
||||
77.46
|
||||
],
|
||||
[
|
||||
71.77,
|
||||
78.1
|
||||
],
|
||||
[
|
||||
70.31,
|
||||
80.12
|
||||
],
|
||||
[
|
||||
67.17,
|
||||
84.87
|
||||
],
|
||||
[
|
||||
63.61,
|
||||
87.53
|
||||
],
|
||||
[
|
||||
61.58,
|
||||
89.27
|
||||
],
|
||||
[
|
||||
61.07,
|
||||
89.67
|
||||
],
|
||||
[
|
||||
60.81,
|
||||
89.8
|
||||
],
|
||||
[
|
||||
60.19,
|
||||
89.8
|
||||
],
|
||||
[
|
||||
59.83,
|
||||
89.67
|
||||
],
|
||||
[
|
||||
59.64,
|
||||
89.53
|
||||
],
|
||||
[
|
||||
56.58,
|
||||
85.86
|
||||
]
|
||||
],
|
||||
"dx": -9.64,
|
||||
"dy": -19.97
|
||||
},
|
||||
{
|
||||
"name": "machineconfig",
|
||||
"title": "MachineConfig",
|
||||
"blurb": "Node OS configuration goes through MachineConfig declaratively; ad-hoc SSH changes create drift and are not the intended model.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
22.93,
|
||||
78.51
|
||||
],
|
||||
[
|
||||
22.93,
|
||||
69.78
|
||||
],
|
||||
[
|
||||
23.51,
|
||||
68.51
|
||||
],
|
||||
[
|
||||
23.73,
|
||||
68.18
|
||||
],
|
||||
[
|
||||
28.0,
|
||||
62.6
|
||||
],
|
||||
[
|
||||
31.84,
|
||||
59.4
|
||||
],
|
||||
[
|
||||
32.19,
|
||||
59.13
|
||||
],
|
||||
[
|
||||
32.36,
|
||||
59.13
|
||||
],
|
||||
[
|
||||
32.88,
|
||||
59.53
|
||||
],
|
||||
[
|
||||
36.94,
|
||||
62.87
|
||||
],
|
||||
[
|
||||
37.66,
|
||||
63.4
|
||||
],
|
||||
[
|
||||
39.06,
|
||||
65.68
|
||||
],
|
||||
[
|
||||
39.28,
|
||||
66.06
|
||||
],
|
||||
[
|
||||
39.86,
|
||||
67.44
|
||||
],
|
||||
[
|
||||
39.86,
|
||||
72.34
|
||||
],
|
||||
[
|
||||
39.79,
|
||||
76.62
|
||||
],
|
||||
[
|
||||
39.28,
|
||||
77.79
|
||||
],
|
||||
[
|
||||
39.06,
|
||||
78.14
|
||||
],
|
||||
[
|
||||
38.34,
|
||||
79.26
|
||||
],
|
||||
[
|
||||
34.89,
|
||||
83.4
|
||||
],
|
||||
[
|
||||
31.83,
|
||||
85.93
|
||||
],
|
||||
[
|
||||
31.22,
|
||||
86.47
|
||||
],
|
||||
[
|
||||
30.45,
|
||||
87.13
|
||||
],
|
||||
[
|
||||
30.1,
|
||||
87.13
|
||||
],
|
||||
[
|
||||
29.57,
|
||||
86.73
|
||||
],
|
||||
[
|
||||
28.56,
|
||||
85.93
|
||||
],
|
||||
[
|
||||
25.47,
|
||||
82.69
|
||||
],
|
||||
[
|
||||
23.58,
|
||||
79.95
|
||||
]
|
||||
],
|
||||
"dx": 13.4,
|
||||
"dy": -16.73
|
||||
},
|
||||
{
|
||||
"name": "sno",
|
||||
"title": "SNO",
|
||||
"blurb": "The whole cluster on one box at the edge - same promises, one machine.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
24.24,
|
||||
24.33
|
||||
],
|
||||
[
|
||||
25.54,
|
||||
21.03
|
||||
],
|
||||
[
|
||||
29.21,
|
||||
17.53
|
||||
],
|
||||
[
|
||||
29.95,
|
||||
17.0
|
||||
],
|
||||
[
|
||||
30.94,
|
||||
16.6
|
||||
],
|
||||
[
|
||||
31.3,
|
||||
16.6
|
||||
],
|
||||
[
|
||||
31.66,
|
||||
16.87
|
||||
],
|
||||
[
|
||||
32.48,
|
||||
17.53
|
||||
],
|
||||
[
|
||||
36.23,
|
||||
20.91
|
||||
],
|
||||
[
|
||||
36.59,
|
||||
21.57
|
||||
],
|
||||
[
|
||||
37.17,
|
||||
23.07
|
||||
],
|
||||
[
|
||||
37.17,
|
||||
33.35
|
||||
],
|
||||
[
|
||||
37.1,
|
||||
33.69
|
||||
],
|
||||
[
|
||||
36.67,
|
||||
34.81
|
||||
],
|
||||
[
|
||||
35.94,
|
||||
36.66
|
||||
],
|
||||
[
|
||||
31.66,
|
||||
40.33
|
||||
],
|
||||
[
|
||||
31.48,
|
||||
40.47
|
||||
],
|
||||
[
|
||||
30.41,
|
||||
40.87
|
||||
],
|
||||
[
|
||||
29.88,
|
||||
40.73
|
||||
],
|
||||
[
|
||||
25.91,
|
||||
37.59
|
||||
],
|
||||
[
|
||||
24.74,
|
||||
35.62
|
||||
],
|
||||
[
|
||||
24.31,
|
||||
34.54
|
||||
],
|
||||
[
|
||||
24.24,
|
||||
33.72
|
||||
]
|
||||
],
|
||||
"dx": 13.85,
|
||||
"dy": 15.36
|
||||
}
|
||||
],
|
||||
"motion": "orbit",
|
||||
"orbit": 60
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
{
|
||||
"scene": "pod",
|
||||
"source": "assets/raw/course-III-pod.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "shell-l",
|
||||
"title": "The jacket",
|
||||
"blurb": "A pod is not a container - it is a jacket around one or more, sharing a network namespace.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
18.93,
|
||||
25.51
|
||||
],
|
||||
[
|
||||
19.37,
|
||||
18.71
|
||||
],
|
||||
[
|
||||
21.26,
|
||||
12.64
|
||||
],
|
||||
[
|
||||
23.35,
|
||||
9.44
|
||||
],
|
||||
[
|
||||
26.72,
|
||||
7.62
|
||||
],
|
||||
[
|
||||
29.25,
|
||||
7.88
|
||||
],
|
||||
[
|
||||
30.81,
|
||||
8.66
|
||||
],
|
||||
[
|
||||
35.8,
|
||||
12.3
|
||||
],
|
||||
[
|
||||
41.24,
|
||||
18.18
|
||||
],
|
||||
[
|
||||
44.01,
|
||||
23.92
|
||||
],
|
||||
[
|
||||
44.59,
|
||||
27.52
|
||||
],
|
||||
[
|
||||
44.37,
|
||||
30.68
|
||||
],
|
||||
[
|
||||
43.06,
|
||||
38.42
|
||||
],
|
||||
[
|
||||
40.88,
|
||||
44.99
|
||||
],
|
||||
[
|
||||
38.28,
|
||||
47.72
|
||||
],
|
||||
[
|
||||
35.52,
|
||||
47.72
|
||||
],
|
||||
[
|
||||
33.35,
|
||||
46.29
|
||||
],
|
||||
[
|
||||
23.22,
|
||||
38.21
|
||||
],
|
||||
[
|
||||
21.98,
|
||||
36.33
|
||||
],
|
||||
[
|
||||
20.53,
|
||||
33.23
|
||||
],
|
||||
[
|
||||
19.29,
|
||||
28.97
|
||||
]
|
||||
],
|
||||
"dx": 9.1,
|
||||
"dy": 10.23
|
||||
},
|
||||
{
|
||||
"name": "shell-r",
|
||||
"title": "One IP, shared fate",
|
||||
"blurb": "One address for everything inside; localhost between friends; volumes shared.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
59.05,
|
||||
29.58
|
||||
],
|
||||
[
|
||||
59.19,
|
||||
24.61
|
||||
],
|
||||
[
|
||||
59.63,
|
||||
22.68
|
||||
],
|
||||
[
|
||||
61.88,
|
||||
17.94
|
||||
],
|
||||
[
|
||||
65.68,
|
||||
13.21
|
||||
],
|
||||
[
|
||||
72.69,
|
||||
7.88
|
||||
],
|
||||
[
|
||||
75.23,
|
||||
7.88
|
||||
],
|
||||
[
|
||||
77.33,
|
||||
8.92
|
||||
],
|
||||
[
|
||||
79.4,
|
||||
11.13
|
||||
],
|
||||
[
|
||||
81.21,
|
||||
15.58
|
||||
],
|
||||
[
|
||||
82.38,
|
||||
22.19
|
||||
],
|
||||
[
|
||||
82.16,
|
||||
27.39
|
||||
],
|
||||
[
|
||||
81.29,
|
||||
31.03
|
||||
],
|
||||
[
|
||||
78.82,
|
||||
36.3
|
||||
],
|
||||
[
|
||||
71.07,
|
||||
43.69
|
||||
],
|
||||
[
|
||||
67.61,
|
||||
45.9
|
||||
],
|
||||
[
|
||||
65.55,
|
||||
45.64
|
||||
],
|
||||
[
|
||||
63.93,
|
||||
44.34
|
||||
],
|
||||
[
|
||||
61.52,
|
||||
40.91
|
||||
],
|
||||
[
|
||||
60.43,
|
||||
37.52
|
||||
],
|
||||
[
|
||||
59.41,
|
||||
32.83
|
||||
]
|
||||
],
|
||||
"dx": -10.39,
|
||||
"dy": 10.66
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"title": "The app container",
|
||||
"blurb": "Your process - the reason the jacket exists.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
57.3,
|
||||
73.63
|
||||
],
|
||||
[
|
||||
41.34,
|
||||
73.63
|
||||
],
|
||||
[
|
||||
43.35,
|
||||
28.47
|
||||
],
|
||||
[
|
||||
43.49,
|
||||
27.05
|
||||
],
|
||||
[
|
||||
44.15,
|
||||
24.92
|
||||
],
|
||||
[
|
||||
44.58,
|
||||
23.87
|
||||
],
|
||||
[
|
||||
44.88,
|
||||
23.3
|
||||
],
|
||||
[
|
||||
46.37,
|
||||
21.68
|
||||
],
|
||||
[
|
||||
47.62,
|
||||
21.29
|
||||
],
|
||||
[
|
||||
48.23,
|
||||
21.16
|
||||
],
|
||||
[
|
||||
51.06,
|
||||
21.16
|
||||
],
|
||||
[
|
||||
51.75,
|
||||
21.29
|
||||
],
|
||||
[
|
||||
52.25,
|
||||
21.42
|
||||
],
|
||||
[
|
||||
52.66,
|
||||
21.55
|
||||
],
|
||||
[
|
||||
54.83,
|
||||
22.47
|
||||
],
|
||||
[
|
||||
55.12,
|
||||
23.0
|
||||
],
|
||||
[
|
||||
55.34,
|
||||
23.46
|
||||
],
|
||||
[
|
||||
55.78,
|
||||
24.65
|
||||
],
|
||||
[
|
||||
56.21,
|
||||
26.08
|
||||
],
|
||||
[
|
||||
56.29,
|
||||
26.43
|
||||
],
|
||||
[
|
||||
56.51,
|
||||
27.76
|
||||
],
|
||||
[
|
||||
56.58,
|
||||
28.58
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "sidecar",
|
||||
"title": "The sidecar",
|
||||
"blurb": "Rides along: proxy, logs, reload. Same jacket, its own container.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
55.18,
|
||||
67.64
|
||||
],
|
||||
[
|
||||
54.79,
|
||||
39.65
|
||||
],
|
||||
[
|
||||
55.04,
|
||||
38.87
|
||||
],
|
||||
[
|
||||
56.01,
|
||||
37.56
|
||||
],
|
||||
[
|
||||
56.19,
|
||||
37.43
|
||||
],
|
||||
[
|
||||
56.82,
|
||||
37.04
|
||||
],
|
||||
[
|
||||
57.87,
|
||||
36.52
|
||||
],
|
||||
[
|
||||
58.3,
|
||||
36.39
|
||||
],
|
||||
[
|
||||
59.84,
|
||||
36.39
|
||||
],
|
||||
[
|
||||
60.27,
|
||||
36.52
|
||||
],
|
||||
[
|
||||
60.87,
|
||||
36.78
|
||||
],
|
||||
[
|
||||
61.66,
|
||||
37.17
|
||||
],
|
||||
[
|
||||
62.01,
|
||||
37.43
|
||||
],
|
||||
[
|
||||
62.76,
|
||||
38.48
|
||||
],
|
||||
[
|
||||
63.17,
|
||||
39.26
|
||||
],
|
||||
[
|
||||
63.28,
|
||||
39.52
|
||||
],
|
||||
[
|
||||
63.4,
|
||||
39.91
|
||||
],
|
||||
[
|
||||
63.14,
|
||||
65.69
|
||||
],
|
||||
[
|
||||
63.03,
|
||||
66.47
|
||||
],
|
||||
[
|
||||
62.75,
|
||||
67.64
|
||||
],
|
||||
[
|
||||
62.66,
|
||||
67.91
|
||||
],
|
||||
[
|
||||
61.89,
|
||||
69.08
|
||||
],
|
||||
[
|
||||
61.06,
|
||||
70.12
|
||||
],
|
||||
[
|
||||
60.73,
|
||||
70.25
|
||||
],
|
||||
[
|
||||
56.87,
|
||||
69.08
|
||||
],
|
||||
[
|
||||
55.79,
|
||||
68.69
|
||||
],
|
||||
[
|
||||
55.27,
|
||||
67.91
|
||||
]
|
||||
],
|
||||
"dx": -4.63,
|
||||
"dy": -2.57
|
||||
},
|
||||
{
|
||||
"name": "gate",
|
||||
"title": "Regular init container",
|
||||
"blurb": "Runs in order and completes before the app containers start. A native sidecar is the restartable exception.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
40.44,
|
||||
69.49
|
||||
],
|
||||
[
|
||||
58.32,
|
||||
70.86
|
||||
],
|
||||
[
|
||||
58.76,
|
||||
71.32
|
||||
],
|
||||
[
|
||||
59.27,
|
||||
72.35
|
||||
],
|
||||
[
|
||||
59.27,
|
||||
76.92
|
||||
],
|
||||
[
|
||||
58.83,
|
||||
77.77
|
||||
],
|
||||
[
|
||||
48.69,
|
||||
91.34
|
||||
],
|
||||
[
|
||||
48.43,
|
||||
91.47
|
||||
],
|
||||
[
|
||||
47.99,
|
||||
91.47
|
||||
],
|
||||
[
|
||||
46.94,
|
||||
91.34
|
||||
],
|
||||
[
|
||||
39.57,
|
||||
84.6
|
||||
],
|
||||
[
|
||||
36.74,
|
||||
80.73
|
||||
],
|
||||
[
|
||||
36.3,
|
||||
79.82
|
||||
],
|
||||
[
|
||||
36.3,
|
||||
79.22
|
||||
],
|
||||
[
|
||||
36.95,
|
||||
75.6
|
||||
],
|
||||
[
|
||||
37.03,
|
||||
75.3
|
||||
],
|
||||
[
|
||||
37.24,
|
||||
74.68
|
||||
],
|
||||
[
|
||||
37.68,
|
||||
73.72
|
||||
],
|
||||
[
|
||||
39.28,
|
||||
71.25
|
||||
]
|
||||
],
|
||||
"dx": 0.72,
|
||||
"dy": -7.57
|
||||
},
|
||||
{
|
||||
"name": "discs",
|
||||
"title": "Volumes",
|
||||
"blurb": "Declared once on the pod - but each container mounts the ones it needs. Sharing storage is opt-in.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
73.95,
|
||||
71.5
|
||||
],
|
||||
[
|
||||
73.87,
|
||||
84.4
|
||||
],
|
||||
[
|
||||
73.66,
|
||||
85.31
|
||||
],
|
||||
[
|
||||
71.92,
|
||||
87.83
|
||||
],
|
||||
[
|
||||
71.05,
|
||||
88.35
|
||||
],
|
||||
[
|
||||
70.13,
|
||||
88.74
|
||||
],
|
||||
[
|
||||
68.95,
|
||||
89.0
|
||||
],
|
||||
[
|
||||
66.53,
|
||||
88.87
|
||||
],
|
||||
[
|
||||
64.78,
|
||||
88.22
|
||||
],
|
||||
[
|
||||
63.9,
|
||||
87.7
|
||||
],
|
||||
[
|
||||
62.39,
|
||||
85.14
|
||||
],
|
||||
[
|
||||
62.1,
|
||||
83.43
|
||||
],
|
||||
[
|
||||
62.17,
|
||||
71.12
|
||||
],
|
||||
[
|
||||
62.39,
|
||||
70.24
|
||||
],
|
||||
[
|
||||
63.88,
|
||||
67.64
|
||||
],
|
||||
[
|
||||
64.96,
|
||||
66.99
|
||||
],
|
||||
[
|
||||
66.16,
|
||||
66.6
|
||||
],
|
||||
[
|
||||
69.32,
|
||||
66.47
|
||||
],
|
||||
[
|
||||
70.32,
|
||||
66.73
|
||||
],
|
||||
[
|
||||
71.64,
|
||||
67.38
|
||||
],
|
||||
[
|
||||
72.35,
|
||||
67.9
|
||||
],
|
||||
[
|
||||
73.66,
|
||||
70.07
|
||||
],
|
||||
[
|
||||
73.87,
|
||||
70.98
|
||||
]
|
||||
],
|
||||
"dx": -9.06,
|
||||
"dy": -14.92
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
{
|
||||
"scene": "supply",
|
||||
"source": "assets/raw/course-VII-supply-chain.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "image",
|
||||
"title": "The image",
|
||||
"blurb": "Course I's layers, shipped as one unit - the thing whose name we must say precisely.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
8.03,
|
||||
70.47
|
||||
],
|
||||
[
|
||||
8.03,
|
||||
51.54
|
||||
],
|
||||
[
|
||||
8.47,
|
||||
50.6
|
||||
],
|
||||
[
|
||||
8.68,
|
||||
50.26
|
||||
],
|
||||
[
|
||||
13.09,
|
||||
45.51
|
||||
],
|
||||
[
|
||||
17.11,
|
||||
42.12
|
||||
],
|
||||
[
|
||||
17.36,
|
||||
41.99
|
||||
],
|
||||
[
|
||||
17.61,
|
||||
42.12
|
||||
],
|
||||
[
|
||||
17.77,
|
||||
42.25
|
||||
],
|
||||
[
|
||||
23.01,
|
||||
46.68
|
||||
],
|
||||
[
|
||||
25.91,
|
||||
49.65
|
||||
],
|
||||
[
|
||||
26.27,
|
||||
50.2
|
||||
],
|
||||
[
|
||||
26.85,
|
||||
51.52
|
||||
],
|
||||
[
|
||||
26.85,
|
||||
70.67
|
||||
],
|
||||
[
|
||||
26.42,
|
||||
71.67
|
||||
],
|
||||
[
|
||||
26.2,
|
||||
72.03
|
||||
],
|
||||
[
|
||||
25.69,
|
||||
72.78
|
||||
],
|
||||
[
|
||||
18.76,
|
||||
78.71
|
||||
],
|
||||
[
|
||||
17.85,
|
||||
79.49
|
||||
],
|
||||
[
|
||||
17.36,
|
||||
79.88
|
||||
],
|
||||
[
|
||||
16.54,
|
||||
79.23
|
||||
],
|
||||
[
|
||||
14.7,
|
||||
77.67
|
||||
],
|
||||
[
|
||||
8.61,
|
||||
71.75
|
||||
]
|
||||
],
|
||||
"dx": 26.55,
|
||||
"dy": -0.01
|
||||
},
|
||||
{
|
||||
"name": "registry",
|
||||
"title": "Upstream registry",
|
||||
"blurb": "The managed registry CI pushes to - Azure Container Registry in the worked example.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
31.21,
|
||||
82.1
|
||||
],
|
||||
[
|
||||
31.11,
|
||||
38.48
|
||||
],
|
||||
[
|
||||
31.63,
|
||||
37.69
|
||||
],
|
||||
[
|
||||
31.88,
|
||||
37.43
|
||||
],
|
||||
[
|
||||
34.11,
|
||||
35.48
|
||||
],
|
||||
[
|
||||
37.16,
|
||||
32.75
|
||||
],
|
||||
[
|
||||
37.8,
|
||||
32.23
|
||||
],
|
||||
[
|
||||
38.43,
|
||||
32.23
|
||||
],
|
||||
[
|
||||
39.3,
|
||||
33.01
|
||||
],
|
||||
[
|
||||
43.66,
|
||||
36.91
|
||||
],
|
||||
[
|
||||
44.24,
|
||||
37.43
|
||||
],
|
||||
[
|
||||
44.49,
|
||||
37.69
|
||||
],
|
||||
[
|
||||
45.0,
|
||||
38.48
|
||||
],
|
||||
[
|
||||
45.48,
|
||||
45.25
|
||||
],
|
||||
[
|
||||
45.6,
|
||||
63.08
|
||||
],
|
||||
[
|
||||
44.92,
|
||||
81.97
|
||||
],
|
||||
[
|
||||
44.25,
|
||||
83.01
|
||||
],
|
||||
[
|
||||
43.12,
|
||||
84.05
|
||||
],
|
||||
[
|
||||
41.67,
|
||||
85.35
|
||||
],
|
||||
[
|
||||
38.51,
|
||||
88.22
|
||||
],
|
||||
[
|
||||
38.35,
|
||||
88.35
|
||||
],
|
||||
[
|
||||
37.8,
|
||||
88.48
|
||||
],
|
||||
[
|
||||
33.1,
|
||||
84.18
|
||||
],
|
||||
[
|
||||
32.54,
|
||||
83.66
|
||||
],
|
||||
[
|
||||
31.72,
|
||||
82.88
|
||||
]
|
||||
],
|
||||
"dx": 14.12,
|
||||
"dy": 0.34
|
||||
},
|
||||
{
|
||||
"name": "mirror",
|
||||
"title": "The mirror",
|
||||
"blurb": "zot, a pull-through cache: on a cache miss it fetches from upstream, then serves subsequent requests locally while the content stays cached.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
48.51,
|
||||
64.72
|
||||
],
|
||||
[
|
||||
48.87,
|
||||
52.91
|
||||
],
|
||||
[
|
||||
51.05,
|
||||
49.54
|
||||
],
|
||||
[
|
||||
60.12,
|
||||
40.69
|
||||
],
|
||||
[
|
||||
61.98,
|
||||
39.78
|
||||
],
|
||||
[
|
||||
64.72,
|
||||
42.12
|
||||
],
|
||||
[
|
||||
73.22,
|
||||
50.34
|
||||
],
|
||||
[
|
||||
74.38,
|
||||
52.54
|
||||
],
|
||||
[
|
||||
74.75,
|
||||
56.78
|
||||
],
|
||||
[
|
||||
74.46,
|
||||
68.54
|
||||
],
|
||||
[
|
||||
73.87,
|
||||
69.89
|
||||
],
|
||||
[
|
||||
73.29,
|
||||
70.67
|
||||
],
|
||||
[
|
||||
71.48,
|
||||
73.01
|
||||
],
|
||||
[
|
||||
65.04,
|
||||
79.23
|
||||
],
|
||||
[
|
||||
62.2,
|
||||
81.58
|
||||
],
|
||||
[
|
||||
61.46,
|
||||
81.71
|
||||
],
|
||||
[
|
||||
59.38,
|
||||
80.67
|
||||
],
|
||||
[
|
||||
51.27,
|
||||
72.31
|
||||
],
|
||||
[
|
||||
49.53,
|
||||
69.74
|
||||
],
|
||||
[
|
||||
48.8,
|
||||
67.85
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"title": "The node",
|
||||
"blurb": "Pulls local. Boot, reschedule and recovery all lean on the mirror being there.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
75.83,
|
||||
58.68
|
||||
],
|
||||
[
|
||||
75.91,
|
||||
50.73
|
||||
],
|
||||
[
|
||||
76.63,
|
||||
49.02
|
||||
],
|
||||
[
|
||||
76.93,
|
||||
48.46
|
||||
],
|
||||
[
|
||||
78.33,
|
||||
47.07
|
||||
],
|
||||
[
|
||||
80.6,
|
||||
45.25
|
||||
],
|
||||
[
|
||||
82.93,
|
||||
43.29
|
||||
],
|
||||
[
|
||||
84.1,
|
||||
42.38
|
||||
],
|
||||
[
|
||||
84.43,
|
||||
42.38
|
||||
],
|
||||
[
|
||||
85.43,
|
||||
43.16
|
||||
],
|
||||
[
|
||||
87.93,
|
||||
45.25
|
||||
],
|
||||
[
|
||||
89.38,
|
||||
46.42
|
||||
],
|
||||
[
|
||||
91.75,
|
||||
48.97
|
||||
],
|
||||
[
|
||||
92.55,
|
||||
50.81
|
||||
],
|
||||
[
|
||||
92.62,
|
||||
52.04
|
||||
],
|
||||
[
|
||||
92.62,
|
||||
68.68
|
||||
],
|
||||
[
|
||||
92.48,
|
||||
69.61
|
||||
],
|
||||
[
|
||||
92.26,
|
||||
70.26
|
||||
],
|
||||
[
|
||||
91.46,
|
||||
72.16
|
||||
],
|
||||
[
|
||||
90.05,
|
||||
73.5
|
||||
],
|
||||
[
|
||||
84.75,
|
||||
77.93
|
||||
],
|
||||
[
|
||||
84.51,
|
||||
78.06
|
||||
],
|
||||
[
|
||||
84.1,
|
||||
78.06
|
||||
],
|
||||
[
|
||||
83.68,
|
||||
77.8
|
||||
],
|
||||
[
|
||||
79.31,
|
||||
74.16
|
||||
],
|
||||
[
|
||||
76.93,
|
||||
72.13
|
||||
],
|
||||
[
|
||||
76.05,
|
||||
70.03
|
||||
],
|
||||
[
|
||||
75.98,
|
||||
69.7
|
||||
],
|
||||
[
|
||||
75.83,
|
||||
68.6
|
||||
]
|
||||
],
|
||||
"dx": -13.37,
|
||||
"dy": 0.57
|
||||
},
|
||||
{
|
||||
"name": "tag",
|
||||
"title": "The tag",
|
||||
"blurb": "A mutable reference. Registries may audit tag updates, but the tag itself carries no immutability guarantee.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
26.2,
|
||||
20.46
|
||||
],
|
||||
[
|
||||
26.63,
|
||||
19.59
|
||||
],
|
||||
[
|
||||
27.29,
|
||||
18.86
|
||||
],
|
||||
[
|
||||
32.81,
|
||||
12.02
|
||||
],
|
||||
[
|
||||
37.75,
|
||||
7.75
|
||||
],
|
||||
[
|
||||
37.93,
|
||||
7.62
|
||||
],
|
||||
[
|
||||
38.02,
|
||||
7.62
|
||||
],
|
||||
[
|
||||
39.43,
|
||||
8.66
|
||||
],
|
||||
[
|
||||
40.81,
|
||||
9.7
|
||||
],
|
||||
[
|
||||
43.64,
|
||||
11.66
|
||||
],
|
||||
[
|
||||
44.59,
|
||||
13.25
|
||||
],
|
||||
[
|
||||
49.17,
|
||||
18.99
|
||||
],
|
||||
[
|
||||
49.67,
|
||||
19.58
|
||||
],
|
||||
[
|
||||
50.11,
|
||||
20.46
|
||||
],
|
||||
[
|
||||
50.11,
|
||||
22.5
|
||||
],
|
||||
[
|
||||
49.67,
|
||||
23.39
|
||||
],
|
||||
[
|
||||
49.46,
|
||||
23.69
|
||||
],
|
||||
[
|
||||
47.06,
|
||||
26.57
|
||||
],
|
||||
[
|
||||
38.47,
|
||||
34.96
|
||||
],
|
||||
[
|
||||
38.3,
|
||||
35.09
|
||||
],
|
||||
[
|
||||
38.04,
|
||||
35.22
|
||||
],
|
||||
[
|
||||
37.36,
|
||||
34.7
|
||||
],
|
||||
[
|
||||
27.0,
|
||||
23.79
|
||||
],
|
||||
[
|
||||
26.63,
|
||||
23.35
|
||||
],
|
||||
[
|
||||
26.2,
|
||||
22.47
|
||||
]
|
||||
],
|
||||
"dx": 8.22,
|
||||
"dy": 13.81
|
||||
},
|
||||
{
|
||||
"name": "digest",
|
||||
"title": "The digest",
|
||||
"blurb": "A fact - sha256 of the content. Same bytes, same digest, forever. The digest does the pulling.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
49.82,
|
||||
20.3
|
||||
],
|
||||
[
|
||||
50.25,
|
||||
19.42
|
||||
],
|
||||
[
|
||||
51.05,
|
||||
18.51
|
||||
],
|
||||
[
|
||||
53.74,
|
||||
15.22
|
||||
],
|
||||
[
|
||||
56.92,
|
||||
11.39
|
||||
],
|
||||
[
|
||||
61.05,
|
||||
8.27
|
||||
],
|
||||
[
|
||||
61.91,
|
||||
7.62
|
||||
],
|
||||
[
|
||||
65.17,
|
||||
10.09
|
||||
],
|
||||
[
|
||||
67.62,
|
||||
12.55
|
||||
],
|
||||
[
|
||||
72.35,
|
||||
18.41
|
||||
],
|
||||
[
|
||||
73.15,
|
||||
19.3
|
||||
],
|
||||
[
|
||||
73.66,
|
||||
20.32
|
||||
],
|
||||
[
|
||||
73.66,
|
||||
22.33
|
||||
],
|
||||
[
|
||||
73.15,
|
||||
23.35
|
||||
],
|
||||
[
|
||||
69.95,
|
||||
27.03
|
||||
],
|
||||
[
|
||||
63.07,
|
||||
34.31
|
||||
],
|
||||
[
|
||||
62.01,
|
||||
35.09
|
||||
],
|
||||
[
|
||||
61.83,
|
||||
35.09
|
||||
],
|
||||
[
|
||||
60.62,
|
||||
34.18
|
||||
],
|
||||
[
|
||||
58.43,
|
||||
32.49
|
||||
],
|
||||
[
|
||||
53.6,
|
||||
27.21
|
||||
],
|
||||
[
|
||||
50.4,
|
||||
23.38
|
||||
],
|
||||
[
|
||||
49.89,
|
||||
22.34
|
||||
],
|
||||
[
|
||||
49.82,
|
||||
20.59
|
||||
]
|
||||
],
|
||||
"dx": -0.06,
|
||||
"dy": 13.82
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,680 @@
|
||||
{
|
||||
"scene": "surround",
|
||||
"source": "assets/raw/course-00-surround.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "frag-1",
|
||||
"title": "",
|
||||
"blurb": "",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
39.79,
|
||||
24.36
|
||||
],
|
||||
[
|
||||
24.29,
|
||||
36.92
|
||||
],
|
||||
[
|
||||
24.12,
|
||||
36.79
|
||||
],
|
||||
[
|
||||
20.82,
|
||||
32.32
|
||||
],
|
||||
[
|
||||
20.02,
|
||||
30.61
|
||||
],
|
||||
[
|
||||
19.95,
|
||||
30.17
|
||||
],
|
||||
[
|
||||
19.95,
|
||||
29.3
|
||||
],
|
||||
[
|
||||
20.02,
|
||||
29.02
|
||||
],
|
||||
[
|
||||
24.96,
|
||||
10.68
|
||||
],
|
||||
[
|
||||
25.33,
|
||||
9.18
|
||||
],
|
||||
[
|
||||
25.98,
|
||||
7.51
|
||||
],
|
||||
[
|
||||
26.2,
|
||||
7.09
|
||||
],
|
||||
[
|
||||
29.03,
|
||||
5.92
|
||||
],
|
||||
[
|
||||
29.39,
|
||||
5.79
|
||||
],
|
||||
[
|
||||
32.41,
|
||||
5.01
|
||||
],
|
||||
[
|
||||
35.97,
|
||||
4.36
|
||||
],
|
||||
[
|
||||
36.15,
|
||||
4.36
|
||||
],
|
||||
[
|
||||
36.96,
|
||||
4.75
|
||||
],
|
||||
[
|
||||
38.18,
|
||||
6.31
|
||||
],
|
||||
[
|
||||
41.03,
|
||||
13.84
|
||||
],
|
||||
[
|
||||
41.1,
|
||||
14.14
|
||||
],
|
||||
[
|
||||
41.17,
|
||||
14.59
|
||||
],
|
||||
[
|
||||
41.24,
|
||||
15.19
|
||||
],
|
||||
[
|
||||
41.24,
|
||||
16.09
|
||||
],
|
||||
[
|
||||
41.17,
|
||||
17.59
|
||||
],
|
||||
[
|
||||
40.44,
|
||||
22.6
|
||||
],
|
||||
[
|
||||
40.37,
|
||||
22.91
|
||||
],
|
||||
[
|
||||
40.23,
|
||||
23.38
|
||||
]
|
||||
],
|
||||
"dx": -22.34,
|
||||
"dy": -37.33
|
||||
},
|
||||
{
|
||||
"name": "frag-2",
|
||||
"title": "",
|
||||
"blurb": "",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
78.82,
|
||||
11.32
|
||||
],
|
||||
[
|
||||
79.76,
|
||||
19.27
|
||||
],
|
||||
[
|
||||
79.47,
|
||||
20.96
|
||||
],
|
||||
[
|
||||
78.6,
|
||||
25.58
|
||||
],
|
||||
[
|
||||
77.29,
|
||||
31.71
|
||||
],
|
||||
[
|
||||
76.27,
|
||||
34.18
|
||||
],
|
||||
[
|
||||
73.1,
|
||||
38.35
|
||||
],
|
||||
[
|
||||
72.43,
|
||||
39.13
|
||||
],
|
||||
[
|
||||
71.03,
|
||||
38.87
|
||||
],
|
||||
[
|
||||
68.94,
|
||||
37.96
|
||||
],
|
||||
[
|
||||
63.26,
|
||||
28.81
|
||||
],
|
||||
[
|
||||
60.36,
|
||||
16.47
|
||||
],
|
||||
[
|
||||
61.15,
|
||||
14.67
|
||||
],
|
||||
[
|
||||
63.99,
|
||||
10.4
|
||||
],
|
||||
[
|
||||
70.66,
|
||||
5.92
|
||||
],
|
||||
[
|
||||
72.86,
|
||||
4.75
|
||||
],
|
||||
[
|
||||
74.12,
|
||||
5.01
|
||||
],
|
||||
[
|
||||
75.16,
|
||||
6.05
|
||||
],
|
||||
[
|
||||
77.3,
|
||||
8.66
|
||||
]
|
||||
],
|
||||
"dx": 23.92,
|
||||
"dy": -35.63
|
||||
},
|
||||
{
|
||||
"name": "frag-3",
|
||||
"title": "",
|
||||
"blurb": "",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
81.09,
|
||||
35.74
|
||||
],
|
||||
[
|
||||
81.54,
|
||||
33.4
|
||||
],
|
||||
[
|
||||
82.29,
|
||||
32.36
|
||||
],
|
||||
[
|
||||
83.51,
|
||||
32.09
|
||||
],
|
||||
[
|
||||
84.57,
|
||||
33.4
|
||||
],
|
||||
[
|
||||
85.92,
|
||||
35.74
|
||||
],
|
||||
[
|
||||
86.19,
|
||||
36.65
|
||||
],
|
||||
[
|
||||
86.92,
|
||||
39.91
|
||||
],
|
||||
[
|
||||
88.7,
|
||||
58.69
|
||||
],
|
||||
[
|
||||
88.77,
|
||||
60.51
|
||||
],
|
||||
[
|
||||
87.65,
|
||||
62.83
|
||||
],
|
||||
[
|
||||
85.99,
|
||||
64.65
|
||||
],
|
||||
[
|
||||
82.83,
|
||||
68.43
|
||||
],
|
||||
[
|
||||
82.42,
|
||||
68.82
|
||||
],
|
||||
[
|
||||
82.0,
|
||||
68.95
|
||||
],
|
||||
[
|
||||
81.32,
|
||||
68.69
|
||||
],
|
||||
[
|
||||
79.51,
|
||||
67.51
|
||||
],
|
||||
[
|
||||
78.59,
|
||||
66.34
|
||||
],
|
||||
[
|
||||
78.35,
|
||||
65.43
|
||||
]
|
||||
],
|
||||
"dx": 38.73,
|
||||
"dy": -0.36
|
||||
},
|
||||
{
|
||||
"name": "frag-4",
|
||||
"title": "",
|
||||
"blurb": "",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
22.02,
|
||||
62.57
|
||||
],
|
||||
[
|
||||
19.25,
|
||||
62.31
|
||||
],
|
||||
[
|
||||
18.29,
|
||||
62.18
|
||||
],
|
||||
[
|
||||
17.25,
|
||||
61.52
|
||||
],
|
||||
[
|
||||
11.74,
|
||||
58.23
|
||||
],
|
||||
[
|
||||
10.94,
|
||||
57.04
|
||||
],
|
||||
[
|
||||
10.06,
|
||||
55.83
|
||||
],
|
||||
[
|
||||
8.83,
|
||||
53.01
|
||||
],
|
||||
[
|
||||
8.83,
|
||||
52.86
|
||||
],
|
||||
[
|
||||
8.97,
|
||||
52.25
|
||||
],
|
||||
[
|
||||
9.7,
|
||||
49.32
|
||||
],
|
||||
[
|
||||
10.36,
|
||||
46.59
|
||||
],
|
||||
[
|
||||
10.94,
|
||||
44.05
|
||||
],
|
||||
[
|
||||
11.37,
|
||||
42.08
|
||||
],
|
||||
[
|
||||
11.76,
|
||||
40.43
|
||||
],
|
||||
[
|
||||
12.97,
|
||||
39.0
|
||||
],
|
||||
[
|
||||
13.16,
|
||||
38.87
|
||||
],
|
||||
[
|
||||
14.2,
|
||||
38.34
|
||||
],
|
||||
[
|
||||
16.1,
|
||||
37.43
|
||||
],
|
||||
[
|
||||
16.45,
|
||||
37.3
|
||||
],
|
||||
[
|
||||
17.51,
|
||||
37.3
|
||||
],
|
||||
[
|
||||
18.22,
|
||||
37.56
|
||||
],
|
||||
[
|
||||
18.96,
|
||||
38.08
|
||||
],
|
||||
[
|
||||
23.58,
|
||||
42.54
|
||||
],
|
||||
[
|
||||
24.09,
|
||||
43.72
|
||||
],
|
||||
[
|
||||
24.53,
|
||||
45.17
|
||||
],
|
||||
[
|
||||
24.53,
|
||||
45.48
|
||||
],
|
||||
[
|
||||
24.17,
|
||||
58.32
|
||||
],
|
||||
[
|
||||
24.09,
|
||||
58.65
|
||||
],
|
||||
[
|
||||
23.8,
|
||||
59.66
|
||||
],
|
||||
[
|
||||
23.28,
|
||||
61.39
|
||||
],
|
||||
[
|
||||
22.57,
|
||||
62.31
|
||||
]
|
||||
],
|
||||
"dx": -37.8,
|
||||
"dy": -2.51
|
||||
},
|
||||
{
|
||||
"name": "frag-5",
|
||||
"title": "",
|
||||
"blurb": "",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
58.97,
|
||||
94.23
|
||||
],
|
||||
[
|
||||
59.63,
|
||||
87.99
|
||||
],
|
||||
[
|
||||
60.43,
|
||||
86.45
|
||||
],
|
||||
[
|
||||
72.75,
|
||||
66.34
|
||||
],
|
||||
[
|
||||
73.15,
|
||||
65.82
|
||||
],
|
||||
[
|
||||
74.44,
|
||||
64.26
|
||||
],
|
||||
[
|
||||
74.6,
|
||||
64.13
|
||||
],
|
||||
[
|
||||
74.94,
|
||||
64.13
|
||||
],
|
||||
[
|
||||
76.21,
|
||||
65.82
|
||||
],
|
||||
[
|
||||
76.32,
|
||||
66.08
|
||||
],
|
||||
[
|
||||
76.42,
|
||||
66.69
|
||||
],
|
||||
[
|
||||
76.49,
|
||||
67.5
|
||||
],
|
||||
[
|
||||
76.49,
|
||||
68.44
|
||||
],
|
||||
[
|
||||
76.42,
|
||||
69.21
|
||||
],
|
||||
[
|
||||
74.53,
|
||||
78.28
|
||||
],
|
||||
[
|
||||
74.46,
|
||||
78.62
|
||||
],
|
||||
[
|
||||
73.87,
|
||||
80.04
|
||||
],
|
||||
[
|
||||
73.51,
|
||||
80.82
|
||||
],
|
||||
[
|
||||
67.19,
|
||||
88.87
|
||||
],
|
||||
[
|
||||
61.45,
|
||||
96.16
|
||||
],
|
||||
[
|
||||
60.41,
|
||||
96.81
|
||||
],
|
||||
[
|
||||
60.33,
|
||||
96.81
|
||||
],
|
||||
[
|
||||
59.56,
|
||||
95.98
|
||||
],
|
||||
[
|
||||
58.97,
|
||||
94.67
|
||||
]
|
||||
],
|
||||
"dx": 20.61,
|
||||
"dy": 33.81
|
||||
},
|
||||
{
|
||||
"name": "frag-6",
|
||||
"title": "",
|
||||
"blurb": "",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
34.31,
|
||||
94.86
|
||||
],
|
||||
[
|
||||
25.75,
|
||||
88.87
|
||||
],
|
||||
[
|
||||
23.95,
|
||||
87.0
|
||||
],
|
||||
[
|
||||
23.51,
|
||||
85.9
|
||||
],
|
||||
[
|
||||
23.07,
|
||||
84.7
|
||||
],
|
||||
[
|
||||
23.0,
|
||||
84.38
|
||||
],
|
||||
[
|
||||
22.42,
|
||||
81.2
|
||||
],
|
||||
[
|
||||
20.97,
|
||||
73.32
|
||||
],
|
||||
[
|
||||
20.97,
|
||||
72.87
|
||||
],
|
||||
[
|
||||
21.55,
|
||||
71.59
|
||||
],
|
||||
[
|
||||
22.93,
|
||||
69.29
|
||||
],
|
||||
[
|
||||
23.07,
|
||||
69.09
|
||||
],
|
||||
[
|
||||
23.29,
|
||||
68.86
|
||||
],
|
||||
[
|
||||
24.06,
|
||||
68.16
|
||||
],
|
||||
[
|
||||
27.64,
|
||||
66.99
|
||||
],
|
||||
[
|
||||
29.15,
|
||||
66.73
|
||||
],
|
||||
[
|
||||
29.23,
|
||||
66.73
|
||||
],
|
||||
[
|
||||
34.41,
|
||||
69.33
|
||||
],
|
||||
[
|
||||
34.92,
|
||||
69.59
|
||||
],
|
||||
[
|
||||
35.13,
|
||||
69.73
|
||||
],
|
||||
[
|
||||
35.65,
|
||||
71.1
|
||||
],
|
||||
[
|
||||
36.96,
|
||||
76.04
|
||||
],
|
||||
[
|
||||
37.39,
|
||||
77.63
|
||||
],
|
||||
[
|
||||
37.46,
|
||||
77.94
|
||||
],
|
||||
[
|
||||
38.34,
|
||||
83.44
|
||||
],
|
||||
[
|
||||
38.34,
|
||||
84.04
|
||||
],
|
||||
[
|
||||
37.06,
|
||||
92.25
|
||||
],
|
||||
[
|
||||
36.85,
|
||||
92.64
|
||||
],
|
||||
[
|
||||
36.34,
|
||||
93.56
|
||||
],
|
||||
[
|
||||
35.5,
|
||||
94.73
|
||||
],
|
||||
[
|
||||
35.32,
|
||||
94.86
|
||||
]
|
||||
],
|
||||
"dx": -22.51,
|
||||
"dy": 31.74
|
||||
}
|
||||
],
|
||||
"reverse": true,
|
||||
"motion": "tumble"
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
{
|
||||
"scene": "traffic",
|
||||
"source": "assets/raw/course-IV-traffic.jpeg",
|
||||
"parts": [
|
||||
{
|
||||
"name": "client",
|
||||
"title": "The client",
|
||||
"blurb": "Knows one stable name. Never a pod.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
22.57,
|
||||
20.71
|
||||
],
|
||||
[
|
||||
21.98,
|
||||
23.54
|
||||
],
|
||||
[
|
||||
21.18,
|
||||
25.13
|
||||
],
|
||||
[
|
||||
20.04,
|
||||
25.93
|
||||
],
|
||||
[
|
||||
19.41,
|
||||
26.33
|
||||
],
|
||||
[
|
||||
18.61,
|
||||
26.47
|
||||
],
|
||||
[
|
||||
17.83,
|
||||
26.47
|
||||
],
|
||||
[
|
||||
17.23,
|
||||
26.33
|
||||
],
|
||||
[
|
||||
16.31,
|
||||
25.93
|
||||
],
|
||||
[
|
||||
15.88,
|
||||
25.67
|
||||
],
|
||||
[
|
||||
15.53,
|
||||
25.4
|
||||
],
|
||||
[
|
||||
14.35,
|
||||
23.73
|
||||
],
|
||||
[
|
||||
13.92,
|
||||
21.94
|
||||
],
|
||||
[
|
||||
13.77,
|
||||
21.15
|
||||
],
|
||||
[
|
||||
13.55,
|
||||
19.81
|
||||
],
|
||||
[
|
||||
13.48,
|
||||
18.9
|
||||
],
|
||||
[
|
||||
13.41,
|
||||
17.63
|
||||
],
|
||||
[
|
||||
13.55,
|
||||
15.62
|
||||
],
|
||||
[
|
||||
14.21,
|
||||
12.16
|
||||
],
|
||||
[
|
||||
15.23,
|
||||
10.6
|
||||
],
|
||||
[
|
||||
16.03,
|
||||
9.93
|
||||
],
|
||||
[
|
||||
16.84,
|
||||
9.53
|
||||
],
|
||||
[
|
||||
17.63,
|
||||
9.27
|
||||
],
|
||||
[
|
||||
18.8,
|
||||
9.27
|
||||
],
|
||||
[
|
||||
19.09,
|
||||
9.4
|
||||
],
|
||||
[
|
||||
19.81,
|
||||
9.8
|
||||
],
|
||||
[
|
||||
20.45,
|
||||
10.2
|
||||
],
|
||||
[
|
||||
20.79,
|
||||
10.47
|
||||
],
|
||||
[
|
||||
22.28,
|
||||
13.34
|
||||
],
|
||||
[
|
||||
22.49,
|
||||
14.53
|
||||
],
|
||||
[
|
||||
22.64,
|
||||
15.83
|
||||
],
|
||||
[
|
||||
22.71,
|
||||
17.09
|
||||
],
|
||||
[
|
||||
22.71,
|
||||
18.17
|
||||
]
|
||||
],
|
||||
"dx": 18.88,
|
||||
"dy": 17.42
|
||||
},
|
||||
{
|
||||
"name": "lb",
|
||||
"title": "LoadBalancer",
|
||||
"blurb": "The edge - divides incoming flow before anything else sees it.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
19.29,
|
||||
24.08
|
||||
],
|
||||
[
|
||||
26.55,
|
||||
16.47
|
||||
],
|
||||
[
|
||||
27.85,
|
||||
15.53
|
||||
],
|
||||
[
|
||||
28.89,
|
||||
15.8
|
||||
],
|
||||
[
|
||||
29.33,
|
||||
16.2
|
||||
],
|
||||
[
|
||||
36.81,
|
||||
29.42
|
||||
],
|
||||
[
|
||||
37.32,
|
||||
30.64
|
||||
],
|
||||
[
|
||||
37.25,
|
||||
32.31
|
||||
],
|
||||
[
|
||||
35.14,
|
||||
38.07
|
||||
],
|
||||
[
|
||||
33.61,
|
||||
40.42
|
||||
],
|
||||
[
|
||||
33.03,
|
||||
41.0
|
||||
],
|
||||
[
|
||||
31.16,
|
||||
41.8
|
||||
],
|
||||
[
|
||||
28.66,
|
||||
42.33
|
||||
],
|
||||
[
|
||||
17.48,
|
||||
33.98
|
||||
],
|
||||
[
|
||||
16.75,
|
||||
32.74
|
||||
],
|
||||
[
|
||||
16.53,
|
||||
31.08
|
||||
],
|
||||
[
|
||||
16.61,
|
||||
29.28
|
||||
],
|
||||
[
|
||||
17.77,
|
||||
26.68
|
||||
]
|
||||
],
|
||||
"dx": 13.92,
|
||||
"dy": 10.93
|
||||
},
|
||||
{
|
||||
"name": "ingress",
|
||||
"title": "Ingress / Gateway",
|
||||
"blurb": "Host- and path-routing above the Services - the open doorframe.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
47.67,
|
||||
14.2
|
||||
],
|
||||
[
|
||||
47.91,
|
||||
46.2
|
||||
],
|
||||
[
|
||||
47.75,
|
||||
47.13
|
||||
],
|
||||
[
|
||||
47.65,
|
||||
47.4
|
||||
],
|
||||
[
|
||||
46.73,
|
||||
48.73
|
||||
],
|
||||
[
|
||||
46.47,
|
||||
49.0
|
||||
],
|
||||
[
|
||||
46.04,
|
||||
49.4
|
||||
],
|
||||
[
|
||||
44.01,
|
||||
51.27
|
||||
],
|
||||
[
|
||||
38.43,
|
||||
56.6
|
||||
],
|
||||
[
|
||||
37.8,
|
||||
57.13
|
||||
],
|
||||
[
|
||||
37.64,
|
||||
57.13
|
||||
],
|
||||
[
|
||||
37.32,
|
||||
56.87
|
||||
],
|
||||
[
|
||||
36.75,
|
||||
56.33
|
||||
],
|
||||
[
|
||||
36.0,
|
||||
55.13
|
||||
],
|
||||
[
|
||||
35.28,
|
||||
31.31
|
||||
],
|
||||
[
|
||||
35.28,
|
||||
22.81
|
||||
],
|
||||
[
|
||||
35.36,
|
||||
22.13
|
||||
],
|
||||
[
|
||||
35.43,
|
||||
21.77
|
||||
],
|
||||
[
|
||||
35.57,
|
||||
21.22
|
||||
],
|
||||
[
|
||||
36.37,
|
||||
20.07
|
||||
],
|
||||
[
|
||||
36.66,
|
||||
19.8
|
||||
],
|
||||
[
|
||||
37.13,
|
||||
19.4
|
||||
],
|
||||
[
|
||||
38.69,
|
||||
18.07
|
||||
],
|
||||
[
|
||||
44.74,
|
||||
12.47
|
||||
],
|
||||
[
|
||||
45.3,
|
||||
11.93
|
||||
],
|
||||
[
|
||||
45.46,
|
||||
11.8
|
||||
],
|
||||
[
|
||||
45.78,
|
||||
11.8
|
||||
],
|
||||
[
|
||||
46.12,
|
||||
12.07
|
||||
],
|
||||
[
|
||||
46.79,
|
||||
12.73
|
||||
],
|
||||
[
|
||||
47.05,
|
||||
13.0
|
||||
],
|
||||
[
|
||||
47.57,
|
||||
13.8
|
||||
]
|
||||
],
|
||||
"dx": 5.89,
|
||||
"dy": 8.42
|
||||
},
|
||||
{
|
||||
"name": "prism",
|
||||
"title": "The Service",
|
||||
"blurb": "The stable name in front of the churn - a virtual IP that outlives every pod behind it.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
46.91,
|
||||
58.09
|
||||
],
|
||||
[
|
||||
46.91,
|
||||
41.92
|
||||
],
|
||||
[
|
||||
47.64,
|
||||
39.86
|
||||
],
|
||||
[
|
||||
48.18,
|
||||
39.27
|
||||
],
|
||||
[
|
||||
50.41,
|
||||
37.0
|
||||
],
|
||||
[
|
||||
51.6,
|
||||
35.67
|
||||
],
|
||||
[
|
||||
52.3,
|
||||
34.87
|
||||
],
|
||||
[
|
||||
52.56,
|
||||
34.87
|
||||
],
|
||||
[
|
||||
53.27,
|
||||
35.67
|
||||
],
|
||||
[
|
||||
55.73,
|
||||
38.33
|
||||
],
|
||||
[
|
||||
56.68,
|
||||
39.27
|
||||
],
|
||||
[
|
||||
57.23,
|
||||
39.84
|
||||
],
|
||||
[
|
||||
57.96,
|
||||
41.91
|
||||
],
|
||||
[
|
||||
57.96,
|
||||
58.1
|
||||
],
|
||||
[
|
||||
57.24,
|
||||
59.13
|
||||
],
|
||||
[
|
||||
56.43,
|
||||
59.93
|
||||
],
|
||||
[
|
||||
55.01,
|
||||
61.4
|
||||
],
|
||||
[
|
||||
52.56,
|
||||
64.07
|
||||
],
|
||||
[
|
||||
52.39,
|
||||
64.2
|
||||
],
|
||||
[
|
||||
49.2,
|
||||
60.73
|
||||
],
|
||||
[
|
||||
48.14,
|
||||
59.67
|
||||
],
|
||||
[
|
||||
47.73,
|
||||
59.27
|
||||
]
|
||||
],
|
||||
"dx": 0.0,
|
||||
"dy": 0.0
|
||||
},
|
||||
{
|
||||
"name": "ready",
|
||||
"title": "Pod - ready",
|
||||
"blurb": "Passing readiness: in the pool, receiving traffic.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
72.06,
|
||||
57.64
|
||||
],
|
||||
[
|
||||
72.42,
|
||||
62.82
|
||||
],
|
||||
[
|
||||
72.06,
|
||||
64.38
|
||||
],
|
||||
[
|
||||
71.04,
|
||||
66.97
|
||||
],
|
||||
[
|
||||
70.1,
|
||||
68.74
|
||||
],
|
||||
[
|
||||
64.94,
|
||||
74.47
|
||||
],
|
||||
[
|
||||
62.61,
|
||||
75.93
|
||||
],
|
||||
[
|
||||
60.78,
|
||||
76.47
|
||||
],
|
||||
[
|
||||
59.02,
|
||||
76.33
|
||||
],
|
||||
[
|
||||
57.08,
|
||||
75.21
|
||||
],
|
||||
[
|
||||
55.7,
|
||||
72.32
|
||||
],
|
||||
[
|
||||
55.34,
|
||||
69.17
|
||||
],
|
||||
[
|
||||
55.78,
|
||||
65.94
|
||||
],
|
||||
[
|
||||
56.43,
|
||||
62.01
|
||||
],
|
||||
[
|
||||
56.72,
|
||||
61.04
|
||||
],
|
||||
[
|
||||
58.25,
|
||||
57.7
|
||||
],
|
||||
[
|
||||
62.75,
|
||||
52.87
|
||||
],
|
||||
[
|
||||
66.21,
|
||||
50.33
|
||||
],
|
||||
[
|
||||
68.19,
|
||||
50.07
|
||||
],
|
||||
[
|
||||
69.45,
|
||||
50.6
|
||||
],
|
||||
[
|
||||
70.75,
|
||||
52.24
|
||||
],
|
||||
[
|
||||
71.33,
|
||||
53.87
|
||||
]
|
||||
],
|
||||
"dx": -6.34,
|
||||
"dy": -7.73
|
||||
},
|
||||
{
|
||||
"name": "notready",
|
||||
"title": "Pod - not ready",
|
||||
"blurb": "Failing readiness: removed from the endpoint set for new Service traffic. That is the feature - and the first place to look.",
|
||||
"z": 0,
|
||||
"points": [
|
||||
[
|
||||
67.48,
|
||||
83.79
|
||||
],
|
||||
[
|
||||
67.62,
|
||||
80.2
|
||||
],
|
||||
[
|
||||
68.2,
|
||||
77.99
|
||||
],
|
||||
[
|
||||
69.37,
|
||||
75.48
|
||||
],
|
||||
[
|
||||
70.31,
|
||||
73.95
|
||||
],
|
||||
[
|
||||
77.1,
|
||||
67.27
|
||||
],
|
||||
[
|
||||
78.65,
|
||||
66.2
|
||||
],
|
||||
[
|
||||
80.23,
|
||||
66.07
|
||||
],
|
||||
[
|
||||
81.04,
|
||||
66.33
|
||||
],
|
||||
[
|
||||
81.7,
|
||||
66.73
|
||||
],
|
||||
[
|
||||
83.25,
|
||||
69.93
|
||||
],
|
||||
[
|
||||
83.76,
|
||||
71.91
|
||||
],
|
||||
[
|
||||
83.9,
|
||||
73.17
|
||||
],
|
||||
[
|
||||
83.83,
|
||||
76.6
|
||||
],
|
||||
[
|
||||
83.54,
|
||||
78.0
|
||||
],
|
||||
[
|
||||
82.16,
|
||||
81.39
|
||||
],
|
||||
[
|
||||
81.36,
|
||||
82.65
|
||||
],
|
||||
[
|
||||
74.57,
|
||||
89.8
|
||||
],
|
||||
[
|
||||
73.08,
|
||||
90.87
|
||||
],
|
||||
[
|
||||
71.94,
|
||||
91.13
|
||||
],
|
||||
[
|
||||
70.96,
|
||||
91.0
|
||||
],
|
||||
[
|
||||
70.03,
|
||||
90.6
|
||||
],
|
||||
[
|
||||
69.37,
|
||||
90.0
|
||||
],
|
||||
[
|
||||
68.06,
|
||||
86.87
|
||||
],
|
||||
[
|
||||
67.7,
|
||||
85.39
|
||||
]
|
||||
],
|
||||
"dx": -9.32,
|
||||
"dy": -11.61
|
||||
}
|
||||
],
|
||||
"stagger": 0.22,
|
||||
"motion": "arc",
|
||||
"bow": 8
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// The Mirror - scroll effects. External file: script-src 'self' holds, no inline JS.
|
||||
// Design constraints: no perpetual work - scroll reads are coalesced into a single rAF,
|
||||
// per-scene work only runs while that scene intersects the viewport, the pulse loop
|
||||
// runs only while its scene is visible AND the tab is visible AND motion is allowed,
|
||||
// and a change to prefers-reduced-motion takes effect live without a reload.
|
||||
(function () {
|
||||
"use strict";
|
||||
var motionQuery = matchMedia("(prefers-reduced-motion: reduce)");
|
||||
var bar = document.getElementById("bar");
|
||||
var s20 = document.querySelector("#s20 img");
|
||||
var ring = document.getElementById("ring");
|
||||
var s22 = document.getElementById("s22");
|
||||
var glows = s22 ? [].slice.call(s22.querySelectorAll(".s22-glow")) : [];
|
||||
var s24 = document.querySelector("#s24 img");
|
||||
var pulse = document.getElementById("pulse");
|
||||
|
||||
// visibility ledger: IntersectionObserver marks which scenes are on screen
|
||||
var visible = {};
|
||||
var watched = [
|
||||
["s20", s20 && s20.closest(".scene")],
|
||||
["s21", ring && ring.closest(".scene")],
|
||||
["s22", s22 && s22.closest(".scene")],
|
||||
["s23", pulse && pulse.closest(".scene")],
|
||||
["s24", s24 && s24.closest(".scene")],
|
||||
].filter(function (w) { return w[1]; });
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
var hit = watched.find(function (w) { return w[1] === e.target; });
|
||||
if (hit) visible[hit[0]] = e.isIntersecting;
|
||||
});
|
||||
if (visible.s23) startPulse(); // (re)arm the breather when its scene returns
|
||||
onScroll(); // anchor jumps and observer-only transitions update without a scroll event
|
||||
}, { rootMargin: "10% 0px" });
|
||||
watched.forEach(function (w) { io.observe(w[1]); });
|
||||
|
||||
// progress of an element through the viewport, guarded against zero heights
|
||||
function prog(el) {
|
||||
var r = el.getBoundingClientRect();
|
||||
var span = innerHeight + r.height;
|
||||
if (!span) return 0;
|
||||
return Math.max(0, Math.min(1, (innerHeight - r.top) / span));
|
||||
}
|
||||
|
||||
// scroll work: one rAF per burst, only touching scenes that are on screen
|
||||
var ticking = false;
|
||||
function frame() {
|
||||
ticking = false;
|
||||
var d = document.documentElement;
|
||||
var denom = d.scrollHeight - innerHeight;
|
||||
if (bar) bar.style.width = (denom > 0 ? (100 * d.scrollTop / denom) : 0) + "%";
|
||||
if (motionQuery.matches) return;
|
||||
if (visible.s20 && s20) {
|
||||
var p20 = Math.min(1, prog(s20) * 1.6);
|
||||
s20.style.transform = "scale(" + (1.07 - 0.07 * p20) + ") translateY(" + (26 - 26 * p20) + "px)";
|
||||
}
|
||||
if (visible.s21 && ring) ring.style.transform = "rotate(" + (prog(ring) * 300) + "deg)";
|
||||
if (visible.s22 && glows.length) {
|
||||
var p22 = prog(s22);
|
||||
glows.forEach(function (g, i) {
|
||||
g.style.opacity = Math.max(0, Math.min(0.85, (p22 - (0.18 + i * 0.22)) * 6));
|
||||
});
|
||||
}
|
||||
if (visible.s24 && s24) s24.style.transform = "translateY(" + ((prog(s24) - 0.5) * -26) + "px) scale(1.03)";
|
||||
}
|
||||
function onScroll() {
|
||||
if (!ticking) { ticking = true; requestAnimationFrame(frame); }
|
||||
}
|
||||
addEventListener("scroll", onScroll, { passive: true });
|
||||
addEventListener("resize", onScroll);
|
||||
|
||||
// the jam's breathing glow: runs only while worth running, stops itself otherwise
|
||||
var breathing = false;
|
||||
function startPulse() {
|
||||
if (breathing || !pulse || motionQuery.matches || document.hidden || !visible.s23) return;
|
||||
breathing = true;
|
||||
var t0 = null;
|
||||
requestAnimationFrame(function breathe(t) {
|
||||
if (motionQuery.matches || document.hidden || !visible.s23) {
|
||||
breathing = false;
|
||||
pulse.style.opacity = 0.25;
|
||||
return;
|
||||
}
|
||||
if (t0 === null) t0 = t;
|
||||
pulse.style.opacity = 0.18 + 0.2 * (0.5 + 0.5 * Math.sin((t - t0) / 520));
|
||||
requestAnimationFrame(breathe);
|
||||
});
|
||||
}
|
||||
document.addEventListener("visibilitychange", function () { if (!document.hidden) { startPulse(); onScroll(); } });
|
||||
|
||||
// live preference change: clear transforms so the page settles to its static state
|
||||
function motionChanged() {
|
||||
if (motionQuery.matches) {
|
||||
[s20, ring, s24].forEach(function (el) { if (el) el.style.transform = ""; });
|
||||
glows.forEach(function (g) { g.style.opacity = 0.35; });
|
||||
if (pulse) pulse.style.opacity = 0.25;
|
||||
} else {
|
||||
startPulse();
|
||||
onScroll();
|
||||
}
|
||||
}
|
||||
if (motionQuery.addEventListener) motionQuery.addEventListener("change", motionChanged);
|
||||
|
||||
startPulse();
|
||||
onScroll();
|
||||
})();
|
||||
@@ -0,0 +1,834 @@
|
||||
<style>
|
||||
:root{--void-2:#0a0f1b;--card:#0d1424;--good:#7fe0a7;--bad:#ff8fa3}
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{margin:0;background:var(--void);color:var(--ink);
|
||||
font-family:var(--sans);font-size:1.05rem;line-height:1.65}
|
||||
#bar{position:fixed;top:0;left:0;height:2px;width:0;background:linear-gradient(90deg,var(--cyan),var(--magenta));z-index:9}
|
||||
/* full-bleed via named grid lines - no viewport units, so the vertical scrollbar
|
||||
can never make the document wider than the visible viewport */
|
||||
.layout{display:grid;
|
||||
grid-template-columns:[full-start] minmax(1.25rem,1fr) [main-start] min(46rem,calc(100% - 2.5rem)) [main-end] minmax(1.25rem,1fr) [full-end]}
|
||||
.layout>*{grid-column:main}
|
||||
.layout>.scene{grid-column:full}
|
||||
.eyebrow{font-family:var(--mono);font-size:.78rem;letter-spacing:.22em;color:var(--cyan);text-transform:uppercase}
|
||||
h1{font-family:var(--sans);font-weight:750;font-size:clamp(2.8rem,8vw,4.6rem);
|
||||
line-height:.98;margin:.5rem 0 1rem;text-wrap:balance;letter-spacing:-.01em}
|
||||
.chapnav{font-family:var(--mono);font-size:.72rem;letter-spacing:.14em;margin:0 0 2.4rem}
|
||||
.chapnav a{color:var(--ink-dim);text-decoration:none;border-bottom:1px solid transparent;
|
||||
padding-bottom:2px;transition:color .2s ease,border-color .2s ease}
|
||||
.chapnav a:hover,.chapnav a:focus-visible{color:var(--cyan);border-color:var(--cyan)}
|
||||
h1 em{font-style:normal;background:linear-gradient(100deg,var(--cyan),var(--magenta));
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent;padding-bottom:.12em}
|
||||
.standfirst{font-size:clamp(1.15rem,2.4vw,1.5rem);color:var(--ink);font-weight:600;
|
||||
margin:.2rem 0 1rem;letter-spacing:-.01em;max-width:34rem}
|
||||
h2{font-family:var(--sans);font-weight:700;font-size:1.85rem;letter-spacing:0;
|
||||
margin:4.2rem 0 1rem;text-wrap:balance;scroll-margin-top:2.5rem}
|
||||
h2 .n{color:var(--cyan);font-family:var(--mono);font-weight:500;font-size:1rem;vertical-align:.35em;margin-right:.65rem;letter-spacing:.1em}
|
||||
h3{font-weight:650;font-size:1.28rem;margin:2.2rem 0 .6rem;scroll-margin-top:2.5rem}
|
||||
p{margin:.9rem 0}
|
||||
code,.mono{font-family:var(--mono);font-size:.92em}
|
||||
.scene{margin:3rem 0;overflow:hidden;position:relative}
|
||||
.scene .inner{max-width:72rem;margin:0 auto;padding:0 1rem;position:relative}
|
||||
.scene img{width:100%;height:auto;display:block;border-radius:.4rem}
|
||||
.scene .cap{font-family:var(--mono);font-size:.74rem;color:var(--ink-dim);
|
||||
letter-spacing:.1em;text-align:center;margin-top:.7rem;padding:0 1rem}
|
||||
[tabindex="0"]:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}
|
||||
.note{background:var(--card);border:1px solid var(--line);border-left:3px solid var(--cyan);
|
||||
border-radius:.45rem;padding:1rem 1.2rem;margin:1.6rem 0;font-size:.98rem}
|
||||
.note .k{font-family:var(--mono);font-size:.72rem;letter-spacing:.18em;color:var(--cyan)}
|
||||
.note.warn{border-left-color:var(--amber)}
|
||||
.note.warn .k{color:var(--amber)}
|
||||
.clause{background:linear-gradient(135deg,rgba(232,121,249,.07),transparent 60%);
|
||||
border:1px solid var(--line);border-radius:.45rem;padding:1rem 1.2rem;margin:1.6rem 0;font-size:.98rem}
|
||||
.clause .k{font-family:var(--mono);font-size:.72rem;letter-spacing:.18em;color:var(--magenta)}
|
||||
pre{background:#050810;border:1px solid var(--line);border-radius:.5rem;padding:1rem 1.1rem;
|
||||
overflow-x:auto;font-family:var(--mono);font-size:.83rem;line-height:1.55;color:#c8d2e0}
|
||||
pre.pane{border-left:3px solid var(--cyan)}
|
||||
pre .cm{color:#8a95ab}
|
||||
pre .good{color:var(--good)}
|
||||
pre .bad{color:var(--bad)}
|
||||
.pane-k{font-family:var(--mono);font-size:.72rem;letter-spacing:.18em;color:var(--ink-dim);
|
||||
text-transform:uppercase;margin:1.6rem 0 .4rem}
|
||||
.diagram{background:var(--card);border:1px solid var(--line);border-radius:.55rem;
|
||||
padding:1.2rem;margin:2rem 0;overflow-x:auto}
|
||||
.diagram .k{font-family:var(--mono);font-size:.72rem;letter-spacing:.18em;color:var(--cyan);display:block;margin-bottom:.8rem}
|
||||
.diagram svg{width:100%;height:auto;display:block;min-width:560px}
|
||||
.diagram .why{font-size:.9rem;color:var(--ink-dim);margin:.8rem 0 0}
|
||||
.diagram .legend{font-family:var(--mono);font-size:.72rem;color:var(--ink-dim);margin:.6rem 0 0}
|
||||
table{border-collapse:collapse;width:100%;font-size:.88rem;margin:1.4rem 0}
|
||||
.tablewrap{overflow-x:auto;margin:1.4rem 0}
|
||||
.tablewrap table{margin:0;min-width:640px}
|
||||
th,td{border:1px solid var(--line);padding:.5rem .65rem;text-align:left;vertical-align:top}
|
||||
th{font-family:var(--mono);font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--cyan);background:var(--void-2)}
|
||||
td .st{font-family:var(--mono);font-size:.7rem;letter-spacing:.06em;padding:.05rem .4rem;border-radius:.25rem;border:1px solid var(--line);white-space:nowrap}
|
||||
.st.ok{color:var(--good);border-color:rgba(127,224,167,.4)}
|
||||
.st.open{color:var(--amber);border-color:rgba(232,180,74,.4)}
|
||||
.st.prop{color:var(--magenta);border-color:rgba(232,121,249,.4)}
|
||||
.facts{border:1px solid var(--line);border-radius:.55rem;background:var(--card);
|
||||
padding:1.1rem 1.3rem;margin:2.2rem 0;font-size:.92rem}
|
||||
.facts dl{display:grid;grid-template-columns:max-content 1fr;gap:.35rem 1.2rem;margin:0}
|
||||
.facts dt{font-family:var(--mono);font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-dim);padding-top:.15rem}
|
||||
.facts dd{margin:0}
|
||||
ol{padding-left:1.3rem} ol li{margin:.6rem 0}
|
||||
ul{padding-left:1.2rem} ul li{margin:.5rem 0}
|
||||
em{color:#fff}
|
||||
.traps p{margin:1.1rem 0}
|
||||
.traps b,strong{color:#fff}
|
||||
footer{margin:5rem 0 4rem;border-top:1px solid var(--line);padding-top:1.4rem;
|
||||
color:var(--ink-dim);font-size:.88rem;font-family:var(--mono)}
|
||||
a{color:var(--cyan)}
|
||||
:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
|
||||
.s21-ring{position:absolute;inset:0;pointer-events:none;mix-blend-mode:screen;opacity:.5;
|
||||
background:conic-gradient(from 0deg at 50% 50%, transparent 0deg, rgba(63,186,245,.28) 24deg, transparent 60deg)}
|
||||
.s22-glow{position:absolute;width:24%;aspect-ratio:1;border-radius:50%;pointer-events:none;
|
||||
mix-blend-mode:screen;opacity:0;background:radial-gradient(circle,rgba(63,186,245,.5),transparent 65%)}
|
||||
.s23-pulse{position:absolute;left:34%;top:44%;width:26%;aspect-ratio:1;border-radius:50%;pointer-events:none;
|
||||
mix-blend-mode:screen;opacity:.25;background:radial-gradient(circle,rgba(232,121,249,.55),transparent 62%)}
|
||||
.refs li{margin:.45rem 0;font-size:.92rem}
|
||||
.outro{max-width:64ch;margin:0 auto;padding:2vh 24px 14vh;text-align:center;border:0}
|
||||
.outro p{font-family:var(--mono);font-size:.7rem;letter-spacing:.12em;color:var(--ink-dim);line-height:1.9;margin:0}
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
html{scroll-behavior:auto}
|
||||
.s21-ring,.s23-pulse{display:none}
|
||||
.s22-glow{opacity:.35}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="bar"></div>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<header class="masthead">
|
||||
<nav class="chapnav" aria-label="Site"><a href="/">← The Teaching Lab</a></nav>
|
||||
<p class="eyebrow">Chapter 02 · The Mirror · Dossier 005</p>
|
||||
<h1>The cache was the easy part.<br><em>Now watch what it quietly throws away.</em></h1>
|
||||
<p class="lede">A pull-through registry, taken apart: what the deployed version actually
|
||||
guarantees, the retention behaviour that evicts digest-pinned content, and how each lesson
|
||||
translates to production. Every claim carries an evidence label - including the corrections
|
||||
to my own first draft. Scroll, and the machinery comes apart.</p>
|
||||
<dl class="mastfacts">
|
||||
<div><dt>Registry</dt><dd>zot v2.1.17</dd></div>
|
||||
<div><dt>Upstreams</dt><dd>5, on-demand</dd></div>
|
||||
<div><dt>Platform</dt><dd>Talos / containerd</dd></div>
|
||||
<div><dt>Open tests</dt><dd>6</dd></div>
|
||||
</dl>
|
||||
<p class="scrollcue" aria-hidden="true">scroll<span></span></p>
|
||||
</header>
|
||||
|
||||
<div class="facts">
|
||||
<dl>
|
||||
<dt>Registry</dt><dd>zot v2.1.17 (digest-pinned image; latest upstream release at last check: v2.1.20)</dd>
|
||||
<dt>Platform</dt><dd>Talos Linux v1.13.4, containerd 2.2.4, single-node Kubernetes</dd>
|
||||
<dt>Upstreams</dt><dd>5 (Docker Hub, GHCR, Quay, registry.k8s.io, NGC), all on-demand pull-through</dd>
|
||||
<dt>Storage</dt><dd>50Gi local PVC; dedupe on, GC on (1h delay / 24h interval); no retention config</dd>
|
||||
<dt>Auth state</dt><dd>anonymous read enabled; per-node pull credentials staged, not yet enforced</dd>
|
||||
<dt>Fallback</dt><dd>origin fallback ON (platform default; deliberately not disabled)</dd>
|
||||
<dt>Reviewed</dt><dd>2026-08-25: live cluster configuration read; upstream docs and v2.1.17 source checked; live runtime tests listed separately in the open register</dd>
|
||||
<dt>Open items</dt><dd>pull-aware digest-entry retention (needs v2.1.19+; blanket alternatives are capacity-heavy), auth canary negative test, upstream namespace prefixes, disconnected-serve drill</dd>
|
||||
</dl>
|
||||
<p style="margin:.9rem 0 0;font-size:.8rem;color:var(--ink-dim);font-family:var(--mono)">
|
||||
Evidence labels used below: <span class="st ok">lab</span> reproduced here with a dated receipt ·
|
||||
<span class="st ok">config</span> present in the inspected config ·
|
||||
<span class="st ok">source</span> established from the deployed version's source ·
|
||||
<span class="st ok">docs</span> stated in version-appropriate documentation ·
|
||||
<span class="st open">reported</span> described in an unresolved upstream issue ·
|
||||
<span class="st prop">proposed</span> not deployed · <span class="st open">open</span> not yet run</p>
|
||||
</div>
|
||||
|
||||
<nav class="toc" aria-label="Section index">
|
||||
<p class="kt">Index</p>
|
||||
<ol>
|
||||
<li><a href="#incident"><b>Section 01</b><span>The build that "hung"</span></a></li>
|
||||
<li><a href="#anatomy"><b>Section 02</b><span>What the mirror does</span></a></li>
|
||||
<li><a href="#routing"><b>Section 03</b><span>Routing and five upstreams</span></a></li>
|
||||
<li><a href="#bootstrap"><b>Section 04</b><span>Bootstrap, fallback, failure</span></a></li>
|
||||
<li><a href="#digests"><b>Section 05</b><span>Digests and supply chain</span></a></li>
|
||||
<li><a href="#retention"><b>Section 06</b><span>Retention and the digest trap</span></a></li>
|
||||
<li><a href="#auth"><b>Section 07</b><span>The authentication migration</span></a></li>
|
||||
<li><a href="#observability"><b>Section 08</b><span>Observability that means something</span></a></li>
|
||||
<li><a href="#production"><b>Section 09</b><span>The production translation</span></a></li>
|
||||
<li><a href="#verified"><b>Section 10</b><span>Verified, open, references</span></a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h2 id="incident"><span class="n" aria-hidden="true">01</span>The build that "hung"</h2>
|
||||
|
||||
<div class="scene" id="s20"><div class="inner">
|
||||
<img src="__M20__" alt="An armoured way-station machine exploded into parts: hexagonal hull, roof plate, a shelf of glowing bricks, intake pods and an output nozzle - the mirror as a physical machine.">
|
||||
<div class="cap">The mirror as a machine: upstream intakes, one cache shelf, one serving nozzle.</div>
|
||||
</div></div>
|
||||
|
||||
<p>I added zot after a CI pull hit Docker Hub's anonymous rate limit and the retry wrapper
|
||||
hid the 429. At the time of the incident, this site's build-stage pull was anonymous.</p>
|
||||
|
||||
<p>Docker Hub does not go silent when it limits you. It answers with HTTP 429 and the error
|
||||
code <code>toomanyrequests</code>. Under the
|
||||
<a href="https://docs.docker.com/docker-hub/usage/pulls/">current published limits</a>,
|
||||
unauthenticated users get 100 pulls per six hours per IPv4 address (or IPv6 /64); a
|
||||
single-platform image counts as one pull, a multi-architecture image counts once per
|
||||
architecture pulled, version checks do not count, and a <code>HEAD</code> request can read
|
||||
the rate-limit headers without consuming a pull. A separate abuse limiter covers all
|
||||
request types with its own 429 form. The <em>silence</em> in this incident was
|
||||
manufactured on my side, by retries swallowing the answer.</p>
|
||||
|
||||
<p>The swallower, recovered from the build script's git history, was a double retry stack:
|
||||
an outer shell wrapper allowing four attempts, wrapped around
|
||||
<code>buildah build --retry 3</code>. Buildah's flag counts <em>retries</em> - one initial
|
||||
try plus up to three more, applying to registry push/pull operations. Four outer attempts,
|
||||
each containing up to four inner attempts: <strong>up to sixteen requests</strong> for one
|
||||
repeatedly failing registry operation (a build with several pulls can produce more), each
|
||||
billed against the limit it was trying to outlast. Retry amplification turns a throttle
|
||||
into an outage and hides the evidence while doing it.</p>
|
||||
|
||||
<div class="pane-k">Receipt · the retry stack, as committed to the repo</div>
|
||||
<pre class="pane">retry() {
|
||||
local n=0 max=4
|
||||
until "$@"; do
|
||||
n=$((n+1)); [ "$n" -ge "$max" ] && { echo ">> failed after ${max} attempts" >&2; return 1; }
|
||||
echo ">> attempt ${n} failed, retrying in $((n*8))s..." >&2; sleep $((n*8))
|
||||
done
|
||||
}
|
||||
retry buildah build --retry 3 --retry-delay 5s ...
|
||||
<span class="cm"># --retry 3 = one attempt + three retries; the wrapper multiplies that by four.</span>
|
||||
<span class="cm"># A 429 answered every one of them, and the backoffs read as a hang.</span></pre>
|
||||
|
||||
<p>Pinning the base image by digest stopped the tag re-resolution but not the round-trip:
|
||||
an ephemeral runner with a cold store still fetches the manifest behind that digest every
|
||||
build. The repeated origin pulls justified a shared cache. This page takes it apart,
|
||||
including the parts that turned out not to work the way I first believed.</p>
|
||||
|
||||
<div class="note"><span class="k">FIELD NOTE</span><br>The mirror did not fix the red X in
|
||||
CI. That red was a separate upstream bug in the CI system's log-finalise step - cosmetic,
|
||||
tolerated, documented. Two problems, one symptom; diagnose them separately.</div>
|
||||
|
||||
<h2 id="anatomy"><span class="n" aria-hidden="true">02</span>What the mirror actually does</h2>
|
||||
|
||||
<p>The mirror is zot: one registry, one 50Gi cache volume, digest-pinned like everything
|
||||
it serves. Five public registries front it in <strong>on-demand pull-through</strong> mode -
|
||||
a miss fetches and caches, a hit serves from the shelf. zot also offers polled mirroring
|
||||
and pre-seeding; Docker Hub is on-demand only, per its documentation.</p>
|
||||
|
||||
<p>The storage block, from the live config:</p>
|
||||
|
||||
<pre>"storage": {
|
||||
"rootDirectory": "/var/lib/registry",
|
||||
"commit": true,
|
||||
"dedupe": true,
|
||||
"gc": true,
|
||||
"gcDelay": "1h",
|
||||
"gcInterval": "24h"
|
||||
}</pre>
|
||||
|
||||
<p><code>commit</code> asks zot to commit writes to disk immediately instead of relying on
|
||||
buffered flushing; it narrows the buffered-write window, while end-to-end power-loss
|
||||
durability still depends on the filesystem, volume and disk. <code>dedupe</code> uses hard
|
||||
links on local filesystem storage (remote backends implement it differently), saving
|
||||
capacity when five upstreams ship the same base layers under different names - at the cost
|
||||
of a startup reconciliation pass that matters operationally when toggled on existing data.
|
||||
And <code>gc</code> with its two timers looks innocuous here; section 06 is about why it is
|
||||
the most consequential block on this page.</p>
|
||||
|
||||
<div class="diagram" role="region" tabindex="0" aria-label="System context diagram"><span class="k">Diagram A · system context</span>
|
||||
<svg viewBox="0 0 860 400" role="img" aria-labelledby="da-t da-d">
|
||||
<title id="da-t">System context: consumers, the mirror, its storage, five upstreams, and the trust boundaries between them</title>
|
||||
<desc id="da-d">CI and Kubernetes nodes pull from the zot mirror over the LAN. The mirror stores content on a 50Gi volume and syncs on demand from five upstream registries across the internet boundary. Anonymous read is currently allowed inbound; outbound upstream credentials live in one mounted secret. The origin-fallback path from nodes directly to upstreams is shown dashed.</desc>
|
||||
<defs><marker id="da-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#3fbaf5"/></marker>
|
||||
<marker id="da-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#e879f9"/></marker></defs>
|
||||
<g font-family="ui-monospace,Menlo,monospace" font-size="12" text-anchor="middle">
|
||||
<rect x="12" y="60" width="150" height="52" rx="8" fill="none" stroke="#3fbaf5"/><text x="87" y="82" fill="#dfe6f0">CI (buildah)</text><text x="87" y="99" fill="#98a2b4">bastion host</text>
|
||||
<rect x="12" y="150" width="150" height="52" rx="8" fill="none" stroke="#3fbaf5"/><text x="87" y="172" fill="#dfe6f0">k8s nodes</text><text x="87" y="189" fill="#98a2b4">containerd + local store</text>
|
||||
<rect x="300" y="95" width="180" height="72" rx="8" fill="none" stroke="#dfe6f0"/><text x="390" y="122" fill="#dfe6f0">zot v2.1.17</text><text x="390" y="139" fill="#98a2b4">anon read (today)</text><text x="390" y="156" fill="#98a2b4">sync: onDemand x5</text>
|
||||
<rect x="322" y="205" width="136" height="46" rx="8" fill="none" stroke="#98a2b4"/><text x="390" y="224" fill="#dfe6f0">50Gi PVC</text><text x="390" y="241" fill="#98a2b4">dedupe + GC 1h/24h</text>
|
||||
<line x1="390" y1="167" x2="390" y2="200" stroke="#98a2b4"/>
|
||||
<rect x="660" y="14" width="186" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="753" y="39" fill="#dfe6f0">docker.io</text>
|
||||
<rect x="660" y="82" width="186" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="753" y="107" fill="#dfe6f0">ghcr.io</text>
|
||||
<rect x="660" y="150" width="186" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="753" y="175" fill="#dfe6f0">quay.io</text>
|
||||
<rect x="660" y="218" width="186" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="753" y="243" fill="#dfe6f0">registry.k8s.io</text>
|
||||
<rect x="660" y="286" width="186" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="753" y="311" fill="#dfe6f0">nvcr.io</text>
|
||||
<line x1="162" y1="86" x2="295" y2="115" stroke="#3fbaf5" marker-end="url(#da-a)"/>
|
||||
<line x1="162" y1="176" x2="295" y2="145" stroke="#3fbaf5" marker-end="url(#da-a)"/>
|
||||
<line x1="480" y1="110" x2="655" y2="40" stroke="#3fbaf5" stroke-dasharray="5 4" marker-end="url(#da-a)"/>
|
||||
<line x1="480" y1="120" x2="655" y2="100" stroke="#3fbaf5" stroke-dasharray="5 4" marker-end="url(#da-a)"/>
|
||||
<line x1="480" y1="131" x2="655" y2="168" stroke="#3fbaf5" stroke-dasharray="5 4" marker-end="url(#da-a)"/>
|
||||
<line x1="480" y1="142" x2="655" y2="235" stroke="#3fbaf5" stroke-dasharray="5 4" marker-end="url(#da-a)"/>
|
||||
<line x1="480" y1="153" x2="655" y2="300" stroke="#3fbaf5" stroke-dasharray="5 4" marker-end="url(#da-a)"/>
|
||||
<path d="M162 200 Q 390 330 660 315" fill="none" stroke="#e879f9" stroke-dasharray="2 6" marker-end="url(#da-m)"/>
|
||||
<text x="360" y="322" fill="#e879f9">origin fallback (dashed = only when the mirror cannot serve)</text>
|
||||
<line x1="580" y1="8" x2="580" y2="345" stroke="#1c2434"/>
|
||||
<text x="580" y="368" fill="#98a2b4">internet boundary - outbound creds in ONE mounted secret</text>
|
||||
<text x="230" y="45" fill="#98a2b4">LAN - anonymous read today; per-node creds staged</text>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="legend">solid = normal pull path · dashed cyan = on-demand sync on miss · dashed magenta = origin fallback</p>
|
||||
<p class="why">One cache, two consumer classes; the two credential domains never mix.</p></div>
|
||||
|
||||
<p>Consumers point at it via the platform's machine-level registry config:</p>
|
||||
|
||||
<pre>machine:
|
||||
registries:
|
||||
mirrors:
|
||||
docker.io:
|
||||
endpoints:
|
||||
- https://zot.bztmon.org
|
||||
ghcr.io:
|
||||
endpoints:
|
||||
- https://zot.bztmon.org</pre>
|
||||
|
||||
<p>The mirror is the only listed endpoint, yet not a hard dependency: Talos tries
|
||||
endpoints in order "and by default the last implicit endpoint is the original upstream
|
||||
registry", unless <code>skipFallback: true</code>. An early version listed the upstream
|
||||
explicitly as endpoint two; it was removed as redundant - origin fallback here is a Talos
|
||||
default, not an endpoint this project maintains.</p>
|
||||
|
||||
<p>One asymmetry decides section 07's rollout order. Talos's
|
||||
<a href="https://docs.siderolabs.com/talos/v1.13/reference/configuration/v1alpha1/config/">v1alpha1
|
||||
machine-config reference</a> states for registry auth: "changes to the registry auth will
|
||||
not be picked up by the CRI containerd plugin without a reboot" - matching what this lab
|
||||
saw on v1.13.4. Mirror <em>endpoint</em> changes applied live here (same versions; the
|
||||
docs are silent on that half, so it stays a lab observation, not a cross-version
|
||||
guarantee). Secrets the registry consumes from a mount rotate with a pod restart. Put each
|
||||
credential on the side that can move.</p>
|
||||
|
||||
<p>One pod, one PVC, one node is an accepted failure domain here, recorded as such;
|
||||
section 09 covers what changes when the requirements do.</p>
|
||||
|
||||
<h2 id="routing"><span class="n" aria-hidden="true">03</span>Request routing and five upstreams</h2>
|
||||
|
||||
<p>Five registries feed one endpoint: when a node asks the mirror for
|
||||
<code>pause:3.10</code>, how does it know which origin that means? Three facts:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>containerd names the origin</strong> - mirror requests carry it as a query
|
||||
parameter: <code>/v2/pause/manifests/3.10?ns=registry.k8s.io</code> (documented).</li>
|
||||
<li><strong>zot v2.1.17 ignores it</strong> - no <code>ns</code> handling exists in the
|
||||
deployed request path; using it for upstream selection is open feature request zot 4187.
|
||||
The URL path alone selects the local repository.</li>
|
||||
<li><strong>This deployment has no per-upstream prefixes</strong> - the live sync config
|
||||
gives all five upstreams <code>prefix: "**"</code> and no <code>destination</code>. On a
|
||||
miss, zot tries upstreams in config order until one has the path; the docs' own
|
||||
multi-registry example instead gives each a distinct <code>destination</code>.</li>
|
||||
</ol>
|
||||
|
||||
<div class="tablewrap" role="region" tabindex="0" aria-label="Upstream mapping table"><table>
|
||||
<caption class="mono" style="text-align:left;color:var(--ink-dim);font-size:.78rem;padding-bottom:.4rem">Upstream map - observed configuration</caption>
|
||||
<tr><th scope="col">Origin</th><th scope="col">Local namespace</th><th scope="col">Outbound auth</th><th scope="col">Sync mode</th><th scope="col">Digest preserved</th><th scope="col">On miss</th><th scope="col">Status</th></tr>
|
||||
<tr><td>docker.io</td><td rowspan="5">flat (no prefix - collision-ambiguous by construction)</td><td>Hub login (rate-limit lift)</td><td>onDemand</td><td>yes</td><td rowspan="5">upstreams tried in config order; first that resolves the path wins</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>ghcr.io</td><td>none</td><td>onDemand</td><td>yes</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>quay.io</td><td>none</td><td>onDemand</td><td>yes</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>registry.k8s.io</td><td>none</td><td>onDemand</td><td>yes</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>nvcr.io</td><td>$oauthtoken + key</td><td>onDemand</td><td>yes</td><td><span class="st ok">observed</span></td></tr>
|
||||
</table></div>
|
||||
|
||||
<p>The flat namespace has not bitten because the origins use largely disjoint paths -
|
||||
Hub's official images live under <code>library/</code> (the implicit prefix behind bare
|
||||
names like <code>alpine</code>), NGC under <code>nvidia/</code>. Largely disjoint is a
|
||||
probability: an organisation existing on both GHCR and Quay would collide, and config order
|
||||
would pick the winner. Accepted risk, recorded; not a design.</p>
|
||||
|
||||
<div class="note warn"><span class="k">PROPOSED - NOT YET DEPLOYED</span><br>The safer
|
||||
shape, documented on both halves: a distinct <code>destination</code> prefix per upstream
|
||||
in zot (<code>/docker</code>, <code>/ghcr</code>, ...), with each Talos mirror pointed at
|
||||
the prefixed path via <code>overridePath: true</code> (suppressing the automatic
|
||||
<code>/v2</code> append). The requested path then names the origin and collisions become
|
||||
impossible. Migration cost: every cached repository changes local path, so the cache
|
||||
re-warms.</div>
|
||||
|
||||
<div class="diagram" role="region" tabindex="0" aria-label="Request flow diagram"><span class="k">Diagram B · request flow</span>
|
||||
<svg viewBox="0 0 860 330" role="img" aria-labelledby="db-t db-d">
|
||||
<title id="db-t">Request flow from a node through containerd to the mirror, with hit, miss, revalidation and fallback branches</title>
|
||||
<desc id="db-d">A pull begins at the node's local content store. On local miss, containerd asks the mirror. A cached digest request is served from mirror storage without upstream contact. A tag request is revalidated against the upstream even when cached, on the deployed version. A storage miss triggers on-demand sync from the matching upstream. If the mirror cannot serve, containerd falls back to the origin registry.</desc>
|
||||
<defs><marker id="db-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#3fbaf5"/></marker>
|
||||
<marker id="db-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#e879f9"/></marker>
|
||||
<marker id="db-g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#7fe0a7"/></marker></defs>
|
||||
<g font-family="ui-monospace,Menlo,monospace" font-size="12" text-anchor="middle">
|
||||
<rect x="10" y="30" width="170" height="46" rx="8" fill="none" stroke="#3fbaf5"/><text x="95" y="49" fill="#dfe6f0">node content store</text><text x="95" y="66" fill="#98a2b4">hit = no network at all</text>
|
||||
<rect x="10" y="130" width="170" height="40" rx="8" fill="none" stroke="#3fbaf5"/><text x="95" y="155" fill="#dfe6f0">containerd -> mirror</text>
|
||||
<rect x="300" y="40" width="200" height="44" rx="8" fill="none" stroke="#7fe0a7"/><text x="400" y="58" fill="#dfe6f0">digest request, cached</text><text x="400" y="75" fill="#7fe0a7">served locally (verified)</text>
|
||||
<rect x="300" y="120" width="200" height="44" rx="8" fill="none" stroke="#e8b44a"/><text x="400" y="138" fill="#dfe6f0">tag request, cached</text><text x="400" y="155" fill="#e8b44a">still revalidates upstream</text>
|
||||
<rect x="300" y="200" width="200" height="44" rx="8" fill="none" stroke="#3fbaf5"/><text x="400" y="218" fill="#dfe6f0">storage miss</text><text x="400" y="235" fill="#98a2b4">on-demand sync + cache</text>
|
||||
<rect x="640" y="120" width="200" height="44" rx="8" fill="none" stroke="#98a2b4"/><text x="740" y="138" fill="#dfe6f0">origin registry</text><text x="740" y="155" fill="#98a2b4">rate limits live here</text>
|
||||
<line x1="95" y1="76" x2="95" y2="125" stroke="#3fbaf5" marker-end="url(#db-a)"/><text x="150" y="105" fill="#98a2b4">local miss</text>
|
||||
<line x1="180" y1="140" x2="295" y2="66" stroke="#7fe0a7" marker-end="url(#db-g)"/>
|
||||
<line x1="180" y1="150" x2="295" y2="142" stroke="#e8b44a" marker-end="url(#db-a)"/>
|
||||
<line x1="180" y1="160" x2="295" y2="218" stroke="#3fbaf5" marker-end="url(#db-a)"/>
|
||||
<line x1="500" y1="142" x2="635" y2="142" stroke="#e8b44a" stroke-dasharray="5 4" marker-end="url(#db-a)"/>
|
||||
<line x1="500" y1="222" x2="640" y2="158" stroke="#3fbaf5" stroke-dasharray="5 4" marker-end="url(#db-a)"/>
|
||||
<path d="M95 170 Q 95 300 400 300 L 660 300 Q 740 300 740 168" fill="none" stroke="#e879f9" stroke-dasharray="2 6" marker-end="url(#db-m)"/>
|
||||
<text x="400" y="290" fill="#e879f9">containerd fallback: mirror endpoints exhausted -> origin direct</text>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="legend">green = fully local · amber = the surprising branch on v2.1.17 · magenta = fallback</p>
|
||||
<p class="why">The amber branch: a warm cache does not take the network out of the story.</p></div>
|
||||
|
||||
<h2 id="bootstrap"><span class="n" aria-hidden="true">04</span>Bootstrap, fallback and failure modes</h2>
|
||||
|
||||
<div class="scene" id="s21"><div class="inner">
|
||||
<img src="__M21__" alt="Six machine parts arranged in a ring around an empty centre; one of them is a quarter-scale replica of the large way-station machine, representing the mirror's own image passing through the mirror.">
|
||||
<div class="s21-ring" id="ring"></div>
|
||||
<div class="cap">The dependency ring: one of the orbiting parts is the mirror itself - its own image is served through the thing it is.</div>
|
||||
</div></div>
|
||||
|
||||
<p>Four circular dependencies live in this design; each got a different answer.</p>
|
||||
|
||||
<h3 id="loop1">Loop 1 - the mirror's own image comes through the mirror</h3>
|
||||
<p>zot runs as a container whose image lives on a registry zot mirrors. When the hosting
|
||||
node boots, it asks the mirror - which is not running, because the node is booting. The
|
||||
loop breaks on the documented fallback: mirror endpoints exhaust, containerd falls through
|
||||
to the origin. Keeping <code>skipFallback</code> unset is the load-bearing choice.</p>
|
||||
|
||||
<div class="note"><span class="k">FIELD NOTE · LAB, 2026-06</span><br>Exercised by
|
||||
accident: a config change shipped with <code>preserveDigest</code> enabled but without its
|
||||
mandatory partner <code>http.compat</code> - a pairing the registry refuses to start
|
||||
without. The mirror crashlooped; the fleet fell through to upstream and nothing
|
||||
user-visible broke. Since then, every config change runs the registry's own
|
||||
<code>verify</code> in a throwaway pod before merging.</div>
|
||||
|
||||
<h3 id="loop2">Loop 2 - the recovery tooling ignores the mirror</h3>
|
||||
<p>The fleet's rescue tooling does not pull through the mirror: its image is cached on the
|
||||
operations host, outside the cluster, so it remains available while zot or its cluster is
|
||||
down.</p>
|
||||
|
||||
<h3 id="loop3">Loop 3 - authentication cannot flip everywhere at once</h3>
|
||||
<p>Enforcing auth needs every node to carry credentials that only apply after a reboot, so
|
||||
anonymous read must survive until the last node is proven - and the proof must be designed
|
||||
not to lie. Section 07 covers it.</p>
|
||||
|
||||
<h3 id="loop4">Loop 4 - the mirror's host is also its customer</h3>
|
||||
<p>The node hosting the mirror boots its own workloads through it, including the tunnel
|
||||
serving this page - so restarting that node carries a blast radius beyond the mirror, and
|
||||
its reboot runbook lists every public-facing dependant and the order they return.</p>
|
||||
|
||||
<div class="diagram" role="region" tabindex="0" aria-label="Bootstrap paths diagram"><span class="k">Diagram D · two bootstrap worlds</span>
|
||||
<svg viewBox="0 0 860 300" role="img" aria-labelledby="dd-t dd-d">
|
||||
<title id="dd-t">Connected fail-open bootstrap versus disconnected preseeded bootstrap, as separate paths</title>
|
||||
<desc id="dd-d">Left: the connected lab path - node boots, mirror miss, implicit fallback to origin, mirror comes up afterwards. Right: the disconnected path - no origin exists; the registry image and release set must be preseeded onto the host or imported from disk before anything else can start.</desc>
|
||||
<g font-family="ui-monospace,Menlo,monospace" font-size="12" text-anchor="middle">
|
||||
<text x="215" y="28" fill="#3fbaf5">CONNECTED LAB - FAIL-OPEN (observed)</text>
|
||||
<rect x="60" y="50" width="310" height="38" rx="8" fill="none" stroke="#3fbaf5"/><text x="215" y="74" fill="#dfe6f0">node boots, asks mirror: down</text>
|
||||
<rect x="60" y="112" width="310" height="38" rx="8" fill="none" stroke="#e879f9"/><text x="215" y="136" fill="#dfe6f0">implicit fallback -> origin serves boot images</text>
|
||||
<rect x="60" y="174" width="310" height="38" rx="8" fill="none" stroke="#3fbaf5"/><text x="215" y="198" fill="#dfe6f0">mirror starts (its image came via fallback)</text>
|
||||
<rect x="60" y="236" width="310" height="38" rx="8" fill="none" stroke="#7fe0a7"/><text x="215" y="260" fill="#dfe6f0">estate converges back onto the mirror</text>
|
||||
<line x1="215" y1="88" x2="215" y2="108" stroke="#98a2b4"/><line x1="215" y1="150" x2="215" y2="170" stroke="#98a2b4"/><line x1="215" y1="212" x2="215" y2="232" stroke="#98a2b4"/>
|
||||
<text x="645" y="28" fill="#e8b44a">DISCONNECTED SITE - PRESEEDED (design, not deployed here)</text>
|
||||
<rect x="490" y="50" width="310" height="38" rx="8" fill="none" stroke="#e8b44a"/><text x="645" y="74" fill="#dfe6f0">no origin exists - fallback is not a plan</text>
|
||||
<rect x="490" y="112" width="310" height="38" rx="8" fill="none" stroke="#e8b44a"/><text x="645" y="136" fill="#dfe6f0">registry image preseeded / imported from disk</text>
|
||||
<rect x="490" y="174" width="310" height="38" rx="8" fill="none" stroke="#e8b44a"/><text x="645" y="198" fill="#dfe6f0">release set, trust roots, creds staged locally</text>
|
||||
<rect x="490" y="236" width="310" height="38" rx="8" fill="none" stroke="#7fe0a7"/><text x="645" y="260" fill="#dfe6f0">site serves itself; syncs on a schedule</text>
|
||||
<line x1="645" y1="88" x2="645" y2="108" stroke="#98a2b4"/><line x1="645" y1="150" x2="645" y2="170" stroke="#98a2b4"/><line x1="645" y1="212" x2="645" y2="232" stroke="#98a2b4"/>
|
||||
<line x1="430" y1="40" x2="430" y2="285" stroke="#1c2434"/>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="legend">left = this lab, observed · right = the airgap translation, a design requirement not a deployed claim</p>
|
||||
<p class="why">Loop 1 assumes an upstream to fall through to; the airgap deletes that
|
||||
assumption and forces Loop 2's discipline onto the mirror itself.</p></div>
|
||||
|
||||
<p><strong>Fallback is a policy decision.</strong> This lab keeps it on for bootstrap
|
||||
survivability and availability through mirror outages; the cost is that pulls can bypass
|
||||
the mirror unnoticed and land on origin rate limits. A regulated or disconnected estate
|
||||
makes the opposite call - <code>skipFallback: true</code>, egress restriction, preseeded
|
||||
release stock, a tested recovery path - because there the bypass is the failure mode.
|
||||
Failure classes differ in symptom, path and consequence:</p>
|
||||
|
||||
<div class="tablewrap" role="region" tabindex="0" aria-label="Failure mode matrix"><table>
|
||||
<caption class="mono" style="text-align:left;color:var(--ink-dim);font-size:.78rem;padding-bottom:.4rem">Failure modes - symptom, path, fallback and consequence</caption>
|
||||
<tr><th scope="col">Failure</th><th scope="col">User-visible symptom</th><th scope="col">Request path</th><th scope="col">Fallback?</th><th scope="col">Detection</th><th scope="col">Security consequence</th></tr>
|
||||
<tr><td>Mirror pod down</td><td>public pulls may continue transparently; private images, origin limits, DNS or TLS issues can slow or fail them</td><td>node -> origin</td><td>after the mirror endpoint fails; elapsed time differs by failure class (refused vs DNS vs TLS vs blackhole)</td><td>mirror probes red; origin egress rises</td><td>policy/audit bypass while down</td></tr>
|
||||
<tr><td>Mirror up, sync wedged</td><td>pulls hang up to sync timeout</td><td>node -> mirror (blocked)</td><td>only after timeout</td><td>manifest probe stalls while /v2/ still answers</td><td>availability, not integrity</td></tr>
|
||||
<tr><td>Upstream 429</td><td>miss/tag pulls fail or crawl</td><td>mirror -> origin refused</td><td>fallback also reaches the origin and may hit a pull or abuse limit - the quota bucket depends on the node's identity and source IP, not necessarily zot's</td><td>zot logs; retry storms amplify</td><td>self-inflicted denial of service via retries</td></tr>
|
||||
<tr><td>Upstream down, digest cached</td><td>none expected</td><td>mirror serves locally</td><td>not needed</td><td>-</td><td>none (source-verified short-circuit; disconnected drill still open)</td></tr>
|
||||
<tr><td>Upstream down, tag cached</td><td>reported on v2.1.17: failure or waiting until timeout, with content possibly served after it</td><td>mirror revalidates the tag upstream first</td><td>origin also down</td><td>the test matrix in section 08</td><td>availability surprise for anyone assuming cached means offline-safe (upstream-reported; not yet reproduced here)</td></tr>
|
||||
<tr><td>PVC full</td><td>new pulls fail, cached OK-ish</td><td>mirror 5xx on writes</td><td>yes for missing content</td><td>capacity metrics (proposed)</td><td>availability; GC pressure</td></tr>
|
||||
<tr><td>Digest entry GC-evicted</td><td>silent re-fetch on next pull</td><td>mirror -> origin re-sync</td><td>n/a</td><td>upstream egress for "cached" content</td><td>rate-limit exposure returns (section 06)</td></tr>
|
||||
<tr><td>Node auth wrong (post-flip)</td><td>ImagePullBackOff</td><td>mirror 401; fallback unreliable on 401</td><td>unreliable</td><td>the canary gate (section 07)</td><td>the outage the gate exists to prevent</td></tr>
|
||||
</table></div>
|
||||
|
||||
<h2 id="digests"><span class="n" aria-hidden="true">05</span>Digests, manifests and the supply chain</h2>
|
||||
|
||||
<p>Half the mistakes on this page came from conflating these objects:</p>
|
||||
|
||||
<ul>
|
||||
<li>A <strong>tag</strong> is a mutable pointer; zero or many tags can reference one
|
||||
manifest.</li>
|
||||
<li>A <strong>manifest digest</strong> identifies exact manifest bytes; a multi-platform
|
||||
<strong>index</strong>, each platform manifest, and every layer and config blob carry their
|
||||
own.</li>
|
||||
<li><code>name:tag@digest</code> is client-side grammar, not wire protocol: the OCI
|
||||
distribution spec takes a tag <em>or</em> a digest in the URL. Clients resolve the digest
|
||||
and ignore the tag (Kubernetes documents this), so digest-pinned pulls are immune to tag
|
||||
moves. This page once mislearned the combined form as a cache-miss bug; retesting on
|
||||
v2.1.17 showed the historical zot rejection no longer reproduces.</li>
|
||||
<li>Digest pinning provides <strong>content integrity</strong> - not provenance, not
|
||||
authorisation, not availability, and not freedom from network fetches on a cold store.</li>
|
||||
</ul>
|
||||
|
||||
<p>For a mirror, digests carry one sharp operational rule: zot converts Docker-schema
|
||||
manifests to OCI by default, and conversion changes the digest - breaking every digest pin
|
||||
and signature downstream. The pairing that prevents it (<code>preserveDigest: true</code>
|
||||
per upstream, with <code>http.compat: ["docker2s2"]</code>) is mandatory in both
|
||||
directions: the registry refuses to start with one and not the other (the Loop 1 field
|
||||
note is this rule being learned). This deployment runs both on all five upstreams.</p>
|
||||
|
||||
<p>Digest preservation and artefact completeness are different properties. The docs tie
|
||||
<code>preserveDigest</code>/<code>compat</code> to keeping mirrored manifest bytes and
|
||||
media types - and therefore signature <em>validity</em> - aligned with upstream. Whether
|
||||
signature, SBOM and attestation objects <em>arrive</em> is separate: OCI 1.1 referrers
|
||||
ride the <code>subject</code> field and their own API, legacy Cosign signatures ride
|
||||
tag-schema conventions, and both depend on origin support and this version's sync
|
||||
behaviour. This deployment has not verified a complete referrer graph for any origin and
|
||||
performs no cryptographic signature verification; disconnected verification would also
|
||||
need locally held trust material. Both gaps sit in the open register. Production estates
|
||||
promote releases by digest and decide per policy whether referrers travel too; this lab
|
||||
runs only the digest half.</p>
|
||||
|
||||
<h2 id="retention"><span class="n" aria-hidden="true">06</span>Retention, GC and the digest-only trap</h2>
|
||||
|
||||
<div class="scene" id="s23"><div class="inner">
|
||||
<img src="__M23__" alt="A stuck intake pod ringed in magenta with three glowing cargo bricks queued behind it - cached content waiting on a jammed process.">
|
||||
<div class="s23-pulse" id="pulse"></div>
|
||||
<div class="cap">Cached bricks queued behind a jammed intake: availability problems here are quiet, not loud.</div>
|
||||
</div></div>
|
||||
|
||||
<p>This section corrects the largest error in this page's first edition, which said "no
|
||||
retention config means keep everything". True for tags; false for everything else - and
|
||||
"everything else" includes the content this mirror exists to hold. The semantics, from
|
||||
upstream documentation and the deployed version's source:</p>
|
||||
|
||||
<ul>
|
||||
<li>With no retention configuration, <strong>all tags are retained</strong> and
|
||||
<strong>all untagged manifests are deleted</strong> by GC (unless referenced by an index
|
||||
or artefact). Timing is two-stage: the manifest ages past <code>gcDelay</code> (1h here),
|
||||
then deletion happens at the next sweep (<code>gcInterval</code>, 24h here) - the survival
|
||||
window depends on where creation falls relative to that sweep, not a fixed one-hour fuse.</li>
|
||||
<li><strong>Digest-only pull-through entries are stored as untagged manifests.</strong>
|
||||
That is the upstream fix's own problem statement: pull an image through the mirror by
|
||||
digest, and the cache entry has no tag to protect it.</li>
|
||||
</ul>
|
||||
|
||||
<p>Combined with section 04's revalidation behaviour: <em>tag</em> pulls are cached but
|
||||
contact upstream anyway; <em>digest</em> pulls serve locally but their entries are
|
||||
GC-eligible between sweeps - a digest-pinned fleet caches exactly the entry class GC may
|
||||
delete. On v2.1.17 this mirror is a rate-limit shield and a latency win, <strong>not yet a
|
||||
disconnection shelf</strong>; the first edition implied otherwise and was wrong.</p>
|
||||
|
||||
<div class="diagram" role="region" tabindex="0" aria-label="Storage and retention diagram"><span class="k">Diagram C · what keeps an object alive</span>
|
||||
<svg viewBox="0 0 860 360" role="img" aria-labelledby="dc-t dc-d">
|
||||
<title id="dc-t">The OCI object graph and which references protect content from garbage collection</title>
|
||||
<desc id="dc-d">A tag points to an index; the index references platform manifests; manifests reference layers and a config blob. Referrers attach to a manifest via a subject field. A separate digest-only manifest sits with no tag pointing at it; it is eligible for garbage collection after the delay on the deployed version. Retention rules that keep tags do not protect the untagged manifest.</desc>
|
||||
<defs><marker id="dc-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#3fbaf5"/></marker>
|
||||
<marker id="dc-b" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#98a2b4"/></marker></defs>
|
||||
<g font-family="ui-monospace,Menlo,monospace" font-size="12" text-anchor="middle">
|
||||
<rect x="30" y="30" width="150" height="40" rx="8" fill="none" stroke="#7fe0a7"/><text x="105" y="55" fill="#dfe6f0">tag: v1.2</text>
|
||||
<rect x="240" y="30" width="170" height="40" rx="8" fill="none" stroke="#3fbaf5"/><text x="325" y="49" fill="#dfe6f0">index (multi-arch)</text><text x="325" y="64" fill="#98a2b4">own digest</text>
|
||||
<rect x="470" y="10" width="170" height="40" rx="8" fill="none" stroke="#3fbaf5"/><text x="555" y="35" fill="#dfe6f0">manifest amd64</text>
|
||||
<rect x="470" y="62" width="170" height="40" rx="8" fill="none" stroke="#3fbaf5"/><text x="555" y="87" fill="#dfe6f0">manifest arm64</text>
|
||||
<rect x="700" y="10" width="140" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="770" y="35" fill="#dfe6f0">layers + config</text>
|
||||
<rect x="700" y="62" width="140" height="40" rx="8" fill="none" stroke="#98a2b4"/><text x="770" y="87" fill="#dfe6f0">layers + config</text>
|
||||
<rect x="470" y="130" width="170" height="40" rx="8" fill="none" stroke="#e879f9" stroke-dasharray="4 3"/><text x="555" y="149" fill="#dfe6f0">referrer (sig/SBOM)</text><text x="555" y="164" fill="#e879f9">subject -> manifest</text>
|
||||
<line x1="180" y1="50" x2="235" y2="50" stroke="#7fe0a7" marker-end="url(#dc-a)"/>
|
||||
<line x1="410" y1="42" x2="465" y2="30" stroke="#3fbaf5" marker-end="url(#dc-a)"/>
|
||||
<line x1="410" y1="58" x2="465" y2="80" stroke="#3fbaf5" marker-end="url(#dc-a)"/>
|
||||
<line x1="640" y1="30" x2="695" y2="30" stroke="#98a2b4" marker-end="url(#dc-b)"/>
|
||||
<line x1="640" y1="82" x2="695" y2="82" stroke="#98a2b4" marker-end="url(#dc-b)"/>
|
||||
<line x1="555" y1="130" x2="555" y2="106" stroke="#e879f9" stroke-dasharray="4 3" marker-end="url(#dc-a)"/>
|
||||
<text x="105" y="120" fill="#7fe0a7">a tag is a keep-alive</text>
|
||||
<rect x="30" y="215" width="280" height="52" rx="8" fill="none" stroke="#e8b44a"/><text x="170" y="236" fill="#dfe6f0">digest-only cached manifest</text><text x="170" y="253" fill="#e8b44a">NO tag references it</text>
|
||||
<rect x="380" y="215" width="220" height="52" rx="8" fill="none" stroke="#ff8fa3"/><text x="490" y="236" fill="#dfe6f0">GC after gcDelay (1h here)</text><text x="490" y="253" fill="#ff8fa3">deleteUntagged default: true</text>
|
||||
<line x1="310" y1="241" x2="375" y2="241" stroke="#ff8fa3" marker-end="url(#dc-a)"/>
|
||||
<text x="430" y="310" fill="#98a2b4">keepTags protects TAGS in its repo. On v2.1.17/18 the amber box has one blanket lever: deleteUntagged:false,</text>
|
||||
<text x="430" y="330" fill="#98a2b4">which keeps ALL untagged manifests (capacity trade). Selective pull-aware keepUntagged ships in v2.1.19.</text>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="legend">green = protected by a tag · amber/red = the eviction path · dashed magenta = referrers (separate lifecycle)</p>
|
||||
<p class="why">GC evaluates reference reachability: an untagged digest-only manifest can be
|
||||
deleted even while its blobs remain present, and node-local image stores can mask the
|
||||
eviction for days.</p></div>
|
||||
|
||||
<p>Retention policy semantics - two nearby systems use opposite matching rules:</p>
|
||||
|
||||
<ul>
|
||||
<li>Retention policies match per repository, <strong>first match wins</strong> - order the
|
||||
list most-specific-first. A repository matching no policy keeps its tags.</li>
|
||||
<li>Within a matched repository, one <code>keepTags</code> rule inverts the default:
|
||||
non-matching tags in <em>that repository</em> become deletable. The inversion is scoped to
|
||||
the repository, not global - the first edition of this page overstated it.</li>
|
||||
<li>Untagged manifests are governed separately (<code>deleteUntagged</code>, default true),
|
||||
and on the deployed version no retention rule can protect them. Pull-aware
|
||||
<code>keepUntagged</code> exists upstream from v2.1.19.</li>
|
||||
<li>Authorisation policies (section 07) use <strong>longest match</strong>, not first
|
||||
match. Two adjacent config blocks, two opposite precedence rules. Label which one you are
|
||||
reasoning about.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Mitigations, ranked:</strong> upgrade to v2.1.19+ for pull-aware
|
||||
<code>keepUntagged</code> (the designed fix, schema to be validated against the actual
|
||||
binary before rollout); until then, widen <code>gcDelay</code>/<code>retention.delay</code>
|
||||
(the upstream maintainer's interim suggestion), or set <code>deleteUntagged: false</code>,
|
||||
which protects every untagged manifest at the cost of unbounded cache growth. Disabling GC
|
||||
entirely swaps eviction for storage exhaustion with no reclaim path.</p>
|
||||
|
||||
<div class="note warn"><span class="k">SAFE VALIDATION - PROPOSED, NOT YET RUN</span><br>
|
||||
The eviction claim is documented upstream and consistent with this config; it has not been
|
||||
reproduced here. The reproduction, in a <em>disposable</em> instance only: record version
|
||||
and sanitised config; pull by digest; confirm the stored manifest is untagged; shorten GC
|
||||
timers; observe the manifest across a sweep; then block upstream and re-pull from a clean
|
||||
runtime store, recording serve vs re-fetch. Two upstream warnings: the retention
|
||||
verification tool executes orphan-blob GC for real even in dry-run, and local storage
|
||||
requires the registry stopped. Never point retention experiments at live storage.</div>
|
||||
|
||||
<h2 id="auth"><span class="n" aria-hidden="true">07</span>The authentication migration</h2>
|
||||
|
||||
<div class="scene" id="s22"><div class="inner">
|
||||
<img src="__M22__" alt="Four parts left to right: a waiting keyed cartridge, a key wedge, a tall gate frame, and a cartridge beyond the gate with its keyway lit - per-node credentials proven at a checkpoint.">
|
||||
<div class="s22-glow" style="left:16%;top:30%" data-band="0"></div>
|
||||
<div class="s22-glow" style="left:44%;top:28%" data-band="1"></div>
|
||||
<div class="s22-glow" style="left:70%;top:26%" data-band="2"></div>
|
||||
<div class="cap">The proving gate: a node counts as migrated when an authenticated pull succeeds where an anonymous one cannot.</div>
|
||||
</div></div>
|
||||
|
||||
<p>Target state: the mirror refuses anonymous pulls. Current state: anonymous read on,
|
||||
per-node credentials staged inert in machine configs until each reboot. The sequencing
|
||||
problem has one central hazard: <strong>while anonymous read is on, an ordinary pull says
|
||||
nothing about auth</strong> - a node whose credentials never applied is served as an
|
||||
anonymous reader and false-passes the gate. The canary must live in a repository that
|
||||
denies anonymous read, so success can only mean an authenticated pull.</p>
|
||||
|
||||
<p>The design is upstream-supported: authorisation resolves per-repository policies by
|
||||
<strong>longest match</strong> (<code>**</code> is the default for anything unmatched), and
|
||||
maintainer guidance confirms anonymous and authenticated access evaluate independently. So
|
||||
a <code>canary/**</code> entry granting named identities read, with no
|
||||
<code>anonymousPolicy</code>, denies anonymous on that path while the glob keeps the rest
|
||||
of the shelf open. The staged policy:</p>
|
||||
|
||||
<pre>"accessControl": {
|
||||
"repositories": {
|
||||
"**": {
|
||||
"anonymousPolicy": ["read"],
|
||||
"policies": [
|
||||
{ "users": ["zot-push"], "actions": ["read", "create", "update"] },
|
||||
{ "users": ["zot-pull"], "actions": ["read"] }
|
||||
]
|
||||
},
|
||||
"canary/**": {
|
||||
"policies": [
|
||||
{ "users": ["zot-pull", "zot-push"], "actions": ["read"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}</pre>
|
||||
|
||||
<div class="note"><span class="k">CLIENT CAVEAT · DOCUMENTED</span><br>Mixed
|
||||
anonymous/authenticated policies trigger a Docker-client-specific workaround present in
|
||||
this exact version: <code>/v2/</code> returns 401 to Docker user agents so the Docker CLI
|
||||
sends credentials, meaning anonymous <em>docker</em> users must log in even for anonymous
|
||||
repos. Podman and containerd are unaffected. This estate pulls with containerd, podman and
|
||||
buildah, so the caveat is noted rather than felt.</div>
|
||||
|
||||
<p>The gate, per node, and what each step proves:</p>
|
||||
|
||||
<ol>
|
||||
<li>Stage credentials in the node's machine config (inert; documented as requiring reboot).</li>
|
||||
<li>Reboot the node at a planned window.</li>
|
||||
<li><strong>Identity check:</strong> pull the protected canary through the node's own
|
||||
runtime. Success = authenticated (anonymous cannot); 401 = back to step 1. The expected
|
||||
split - 401 for missing/wrong credentials, 403 for a valid identity lacking the action -
|
||||
sits in the open register, since the docs state the 403 case only for OIDC identities.</li>
|
||||
<li><strong>Interception check, separately:</strong> the canary is a direct mirror
|
||||
reference, so it says nothing about whether <code>docker.io/...</code> references route
|
||||
through the mirror. That needs an original-upstream reference pulled on a node with a cold
|
||||
runtime store, correlated with the mirror's request log - node-local content would satisfy
|
||||
the pull without any network and fake a pass.</li>
|
||||
<li>Anonymous read comes off the glob only when every node passes both checks. Rollback is
|
||||
the previous config commit, named before the flip.</li>
|
||||
</ol>
|
||||
|
||||
<div class="diagram" role="region" tabindex="0" aria-label="Authentication migration diagram"><span class="k">Diagram E · the migration ladder</span>
|
||||
<svg viewBox="0 0 860 210" role="img" aria-labelledby="de-t de-d">
|
||||
<title id="de-t">The authentication migration ladder with its per-node proof gate and rollback point</title>
|
||||
<desc id="de-d">Five stages: anonymous baseline, credentials staged inert, node reboot, the two-part proof - authenticated canary pull plus logged mirror interception - and the final anonymous-off flip with a named rollback commit. A failing node loops from the proof back to staging.</desc>
|
||||
<defs><marker id="de-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#3fbaf5"/></marker>
|
||||
<marker id="de-m" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" fill="#e879f9"/></marker></defs>
|
||||
<g font-family="ui-monospace,Menlo,monospace" font-size="11" text-anchor="middle">
|
||||
<rect x="10" y="70" width="140" height="48" rx="8" fill="none" stroke="#98a2b4"/><text x="80" y="90" fill="#dfe6f0">anon read ON</text><text x="80" y="106" fill="#98a2b4">baseline (today)</text>
|
||||
<rect x="180" y="70" width="140" height="48" rx="8" fill="none" stroke="#3fbaf5"/><text x="250" y="90" fill="#dfe6f0">creds staged</text><text x="250" y="106" fill="#98a2b4">inert in config</text>
|
||||
<rect x="350" y="70" width="140" height="48" rx="8" fill="none" stroke="#3fbaf5"/><text x="420" y="90" fill="#dfe6f0">node reboots</text><text x="420" y="106" fill="#98a2b4">auth goes live</text>
|
||||
<rect x="520" y="70" width="160" height="48" rx="8" fill="none" stroke="#e879f9"/><text x="600" y="86" fill="#dfe6f0">two-part proof</text><text x="600" y="100" fill="#e879f9">canary + logged</text><text x="600" y="113" fill="#e879f9">interception</text>
|
||||
<rect x="710" y="70" width="140" height="48" rx="8" fill="none" stroke="#7fe0a7"/><text x="780" y="90" fill="#dfe6f0">anon OFF</text><text x="780" y="106" fill="#98a2b4">rollback = prior commit</text>
|
||||
<line x1="150" y1="94" x2="175" y2="94" stroke="#3fbaf5" marker-end="url(#de-a)"/>
|
||||
<line x1="320" y1="94" x2="345" y2="94" stroke="#3fbaf5" marker-end="url(#de-a)"/>
|
||||
<line x1="490" y1="94" x2="515" y2="94" stroke="#3fbaf5" marker-end="url(#de-a)"/>
|
||||
<line x1="680" y1="94" x2="705" y2="94" stroke="#3fbaf5" marker-end="url(#de-a)"/>
|
||||
<path d="M600 118 Q 600 175 480 175 L 320 175 Q 250 175 250 122" fill="none" stroke="#e879f9" stroke-dasharray="4 4" marker-end="url(#de-m)"/>
|
||||
<text x="440" y="196" fill="#e879f9">401: node loops back - it does not count</text>
|
||||
</g>
|
||||
</svg>
|
||||
<p class="legend">the flip is per-fleet; the proof is per-node - one unproven node under a fleet-wide flip is an outage wearing a green tick</p>
|
||||
<p class="why">Identity and interception fail independently; each has a false-pass mode
|
||||
the other cannot detect.</p></div>
|
||||
|
||||
<p>Five trust domains never share material: node pull credentials (machine config,
|
||||
reboot-bound), the push credential (used by the operator-run build host for registry
|
||||
logins, not embedded in CI configuration), the mirror's upstream credentials (one mounted
|
||||
secret, pod-restart-bound), TLS trust, and human admin access (SSO in front of the UI).
|
||||
The push credential carries this page's oldest incident: in July 2026 its bcrypt hash and
|
||||
plaintext drifted apart in the secrets manager, rotation became impossible, and pushes
|
||||
stayed dead for twelve days - three applications shipped the day the rebuilt flow landed.
|
||||
Today the hash and plaintext are separate entries in the same secrets manager, rotated as
|
||||
a pair by a human; the registry consumes only the derived htpasswd file, each plaintext
|
||||
reaches only its consumer, and the sync identity is read-only so automation cannot
|
||||
half-rotate the pair. Residual cost, stated: one secrets-manager project holds both
|
||||
halves, so its compromise yields verifier and credential together - versioned,
|
||||
consumer-scoped secret objects remain a possible refinement.</p>
|
||||
|
||||
<h2 id="observability"><span class="n" aria-hidden="true">08</span>Observability that means something</h2>
|
||||
|
||||
<p>The worst incident in this mirror's life: an in-flight sync wedged, every pull from
|
||||
that upstream hung, and the health endpoint returned 200 throughout:</p>
|
||||
|
||||
<div class="pane-k">Replay · observed 2026-07-07 · the stalled sync</div>
|
||||
<pre class="pane">"image already demanded, waiting on channel"
|
||||
$ kubectl -n zot rollout restart deploy zot <span class="cm"><- recovery action (not a root cause)</span>
|
||||
<span class="cm"># probe an actual manifest, not the process's opinion of itself:</span>
|
||||
$ curl -sfS --max-time 10 -o /dev/null -w '%{http_code}\n' \
|
||||
https://zot.bztmon.org/v2/library/busybox/manifests/latest \
|
||||
-H 'Accept: application/vnd.oci.image.index.v1+json'
|
||||
<span class="good">200</span> <span class="cm"><- THIS is "the mirror works"</span></pre>
|
||||
|
||||
<p>The deployed source refines that story: the log line is coalescing by design -
|
||||
concurrent requests join the first sync, which runs on a detached background context with
|
||||
a three-hour default timeout and survives client disconnects. Waiters blocking on a
|
||||
stalled sync until that timeout matches what we saw; the restart was recovery, and the
|
||||
root cause was never isolated. Two knobs this config leaves unset - sync
|
||||
<code>maxRetries</code> (disabled by default upstream) and a tighter
|
||||
<code>syncTimeout</code> - are the first change on recurrence.</p>
|
||||
|
||||
<p>From the running version's source: <code>/livez</code>, <code>/readyz</code> and
|
||||
<code>/startupz</code> exist as health endpoints (absent from this version's docs),
|
||||
alongside the spec's <code>/v2/</code>. The Prometheus metrics extension exists upstream -
|
||||
series names include <code>zot_http_requests_total</code>,
|
||||
<code>zot_repo_storage_bytes</code>, <code>zot_repo_downloads_total</code> and
|
||||
<code>zot_storage_lock_latency_seconds</code> - and is <strong>not enabled here</strong>;
|
||||
wiring it, with scrape auth, is on the open list.</p>
|
||||
|
||||
<p>The layered checks, one question each:</p>
|
||||
|
||||
<div class="tablewrap" role="region" tabindex="0" aria-label="Layered operational checks"><table>
|
||||
<caption class="mono" style="text-align:left;color:var(--ink-dim);font-size:.78rem;padding-bottom:.4rem">Layered checks - one question each</caption>
|
||||
<tr><th scope="col">Check</th><th scope="col">Question it answers</th><th scope="col">Status here</th></tr>
|
||||
<tr><td><code>/livez</code> / <code>/readyz</code></td><td>is the process up / initialised</td><td><span class="st ok">available</span></td></tr>
|
||||
<tr><td>authenticated manifest GET of a local-only canary</td><td>can the right identity read real content from local storage</td><td><span class="st prop">proposed</span></td></tr>
|
||||
<tr><td>anonymous GET of the canary expecting 401</td><td>is the protection actually protecting</td><td><span class="st prop">proposed</span></td></tr>
|
||||
<tr><td>original-reference pull on a cold node + mirror log line</td><td>is the runtime actually routed through the mirror</td><td><span class="st prop">proposed</span></td></tr>
|
||||
<tr><td>digest pull with upstream blocked (disposable env)</td><td>does "cached" mean "served locally"</td><td><span class="st open">open - section 06</span></td></tr>
|
||||
<tr><td>PVC usage + growth, GC activity, sync latency</td><td>when does capacity or eviction become the story</td><td><span class="st prop">needs metrics ext</span></td></tr>
|
||||
<tr><td>certificate expiry, credential age</td><td>what breaks on a schedule</td><td><span class="st prop">proposed</span></td></tr>
|
||||
</table></div>
|
||||
|
||||
<p>The caching test matrix (digest rows: source-verified and consistent with behaviour
|
||||
here; disconnected rows: not yet run in this lab):</p>
|
||||
|
||||
<div class="tablewrap" role="region" tabindex="0" aria-label="Cache behaviour test matrix"><table>
|
||||
<caption class="mono" style="text-align:left;color:var(--ink-dim);font-size:.78rem;padding-bottom:.4rem">Cache behaviour - each row is an experiment</caption>
|
||||
<tr><th scope="col">Scenario</th><th scope="col">Expected on v2.1.17</th><th scope="col">What proves it</th></tr>
|
||||
<tr><td>cached tag, upstream reachable</td><td>served; upstream contacted anyway (revalidation)</td><td>zot log shows upstream request; latency includes round-trip</td></tr>
|
||||
<tr><td>cached tag, upstream unreachable</td><td>fails or stalls until timeout, then local fallback path</td><td>pull timing vs sync timeout; error text</td></tr>
|
||||
<tr><td>cached digest, upstream reachable</td><td>served locally, no upstream contact</td><td>absence of upstream request in zot logs during the pull</td></tr>
|
||||
<tr><td>cached digest, upstream unreachable</td><td>served locally - IF the entry survived GC</td><td>the section-06 disposable-instance drill</td></tr>
|
||||
<tr><td>cold node, warm mirror</td><td>mirror serves; node store fills</td><td>mirror log + no origin egress</td></tr>
|
||||
<tr><td>warm node, evicted mirror entry</td><td>pull succeeds from node store - masking the eviction</td><td>this is the false-pass: only mirror-side inspection reveals it</td></tr>
|
||||
<tr><td>multi-arch index + child manifests</td><td>index and per-platform manifests are separate cache entries</td><td>per-digest existence checks on the mirror</td></tr>
|
||||
</table></div>
|
||||
|
||||
<h2 id="production"><span class="n" aria-hidden="true">09</span>The production translation</h2>
|
||||
|
||||
<div class="scene" id="s24"><div class="inner">
|
||||
<img src="__M24__" alt="A monolithic vault with a wall of glowing bricks visible through its door, attended by four smaller self-sufficient way-stations - a central release source and site-local mirrors.">
|
||||
<div class="cap">A central curated source and site-local registries: each site holds its own shelf, so a cut WAN idles nothing.</div>
|
||||
</div></div>
|
||||
|
||||
<p>The first edition of this section asserted what "production" does; production chooses
|
||||
from patterns against requirements:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Single registry, accepted failure domain</strong> - this lab's shape.
|
||||
Legitimate wherever a mirror outage degrades to origin pulls (fallback on) or a paused
|
||||
deploy window (fallback off), with the cost accepted in writing.</li>
|
||||
<li><strong>Sync-based HA</strong> - documented active/standby or active/active pairs of
|
||||
<em>independent</em> instances, each with its own storage, mirroring each other behind a
|
||||
load balancer. The caveat: the synchronisation window between polls bounds what failover
|
||||
can lose.</li>
|
||||
<li><strong>Scale-out clustering</strong> - instances shard repositories by hash and proxy
|
||||
to the owner, classically over shared S3-compatible storage. Horizontal scale for load;
|
||||
the shared storage becomes the availability story - a different property from the HA pair.</li>
|
||||
<li><strong>Edge estates</strong> - a central source promotes releases by digest; each
|
||||
site runs a local registry holding its release set on a schedule, so a severed WAN idles
|
||||
nothing at pull time. A disconnection claim is valid only when content, required
|
||||
referrers, trust roots, credentials and the registry's own bootstrap image are already
|
||||
local (diagram D's right-hand column, per site).</li>
|
||||
</ul>
|
||||
|
||||
<p>Whatever the topology: recovery time and point objectives get numbers before an
|
||||
incident supplies them; backups are restore-tested copies off the failure domain (array
|
||||
snapshots and RAID protect against disk loss, not against the array, the site or operator
|
||||
error); cache capacity is planned from the section 06 retention policy; and the section 04
|
||||
circular dependencies get drawn per site, because each exists at every scale - only the
|
||||
cost of ignoring them changes.</p>
|
||||
|
||||
<h2 id="verified"><span class="n" aria-hidden="true">10</span>Verified, open, and where the claims come from</h2>
|
||||
|
||||
<div class="scene" id="s25"><div class="inner">
|
||||
<img src="__M25__" alt="A wide flat node slab in an otherwise empty void; a single luminous brick hovers midway in its descent toward the slab - one small transfer of bytes as proof of life.">
|
||||
<div class="cap">Proof of life is one authenticated byte transfer through the real path - not a status page.</div>
|
||||
</div></div>
|
||||
|
||||
<p>When a node powers on, the fleet's admission check performs an authenticated manifest
|
||||
pull through the node's own runtime before the node counts as a member: a process-health
|
||||
response validates neither routing nor authorisation. Most of this page condenses into
|
||||
that one command.</p>
|
||||
|
||||
<div class="tablewrap" role="region" tabindex="0" aria-label="Architecture decision table"><table>
|
||||
<caption class="mono" style="text-align:left;color:var(--ink-dim);font-size:.78rem;padding-bottom:.4rem">Architecture decisions</caption>
|
||||
<tr><th scope="col">Decision</th><th scope="col">Chosen</th><th scope="col">Why</th><th scope="col">Trade-off</th><th scope="col">Rollback / alternative</th><th scope="col">Status</th></tr>
|
||||
<tr><td>Origin fallback</td><td>ON (default kept)</td><td>bootstrap survivability, Loop 1</td><td>silent mirror bypass possible</td><td>skipFallback:true + preseed (airgap shape)</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>Sync mode</td><td>onDemand, all upstreams</td><td>cache follows real usage; Hub-safe</td><td>tag pulls revalidate upstream; digest entries untagged</td><td>polled sync for a curated release set</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>Digest preservation</td><td>preserveDigest + docker2s2, x5</td><td>digest pins + signatures survive mirroring</td><td>mandatory config pairing (crashloop scar)</td><td>none - non-negotiable for digest-pinned estates</td><td><span class="st ok">observed</span></td></tr>
|
||||
<tr><td>Namespace layout</td><td>flat (no destinations)</td><td>simplicity at build time</td><td>ordered-trial upstream selection; theoretical collisions</td><td>per-origin destinations + overridePath</td><td><span class="st prop">redesign proposed</span></td></tr>
|
||||
<tr><td>Retention</td><td>none configured</td><td>predates understanding the untagged rule</td><td>digest-only entries evict within hours</td><td>v2.1.19+ keepUntagged; interim wider gcDelay</td><td><span class="st open">correction owed</span></td></tr>
|
||||
<tr><td>Auth</td><td>anonymous read until per-node proof</td><td>containerd 401-fallback unreliability makes big-bang flips dangerous</td><td>window with no pull auth</td><td>staged flip w/ canary gate + named rollback</td><td><span class="st open">in flight</span></td></tr>
|
||||
<tr><td>Metrics</td><td>not enabled</td><td>minimal first deployment</td><td>capacity/eviction invisible</td><td>metrics extension + scrape auth</td><td><span class="st prop">proposed</span></td></tr>
|
||||
</table></div>
|
||||
|
||||
<h3 id="open">Open verification register</h3>
|
||||
<ol>
|
||||
<li>Digest-entry eviction reproduction in a disposable instance (section 06 procedure).</li>
|
||||
<li>Canary repo negative test: anonymous 401 on <code>canary/**</code> while anonymous
|
||||
read elsewhere still succeeds; observed 401-vs-403 boundaries recorded.</li>
|
||||
<li>Mirror interception proof: original-reference pull on a cold node correlated with the
|
||||
mirror's request log.</li>
|
||||
<li>Disconnected-serve drill: cached digest pull with upstream blocked, disposable
|
||||
environment first.</li>
|
||||
<li>Upgrade evaluation: v2.1.19/v2.1.20 for <code>keepUntagged</code>, with the retention
|
||||
config written and reviewed before the upgrade, not after.</li>
|
||||
<li>Namespace redesign migration plan (per-origin destinations + overridePath), including
|
||||
cache re-warm cost.</li>
|
||||
</ol>
|
||||
|
||||
<h3 id="refs">Verified against</h3>
|
||||
<ul class="refs">
|
||||
<li>zot documentation (versioned): <a href="https://zotregistry.dev/v2.1.18/articles/retention/">retention</a> · <a href="https://zotregistry.dev/v2.1.18/articles/storage/">storage</a> · <a href="https://zotregistry.dev/v2.1.18/articles/mirroring/">mirroring</a> · <a href="https://zotregistry.dev/v2.1.18/articles/authn-authz/">authn/authz</a> · <a href="https://zotregistry.dev/v2.1.18/articles/high-availability/">high availability</a> · <a href="https://zotregistry.dev/v2.1.18/articles/scaleout/">scale-out</a></li>
|
||||
<li>zot upstream issues/PRs: <a href="https://github.com/project-zot/zot/issues/4148">4148 digest-only eviction</a> · <a href="https://github.com/project-zot/zot/pull/4191">4191 keepUntagged (v2.1.19)</a> · <a href="https://github.com/project-zot/zot/issues/4128">4128 tag revalidation</a> · <a href="https://github.com/project-zot/zot/issues/4187">4187 containerd ns param</a> · <a href="https://github.com/project-zot/zot/issues/2928">2928 mixed anonymous auth</a> · <a href="https://github.com/project-zot/zot/issues/2505">2505 demand coalescing</a></li>
|
||||
<li>Talos: <a href="https://docs.siderolabs.com/talos/v1.13/configure-your-talos-cluster/images-container-runtime/pull-through-cache">pull-through registry cache (v1.13)</a> · containerd: <a href="https://github.com/containerd/containerd/blob/main/docs/hosts.md">registry hosts and the ns parameter</a></li>
|
||||
<li>OCI: <a href="https://github.com/opencontainers/distribution-spec/blob/main/spec.md">distribution spec</a> · <a href="https://opencontainers.org/posts/blog/2024-03-13-image-and-distribution-1-1/">image/distribution 1.1 (referrers)</a> · signing: <a href="https://docs.sigstore.dev/cosign/verifying/verify/">cosign verify</a></li>
|
||||
<li>Docker Hub <a href="https://docs.docker.com/docker-hub/usage/">usage and limits</a> · buildah <a href="https://github.com/containers/buildah/blob/main/docs/buildah-build.1.md">build --retry semantics</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
<footer class="outro">
|
||||
<p>© 2026 Jonathon Wright · <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>
|
||||
· hand-built static, shipped via GitOps · <a href="https://www.bztmon.com/">bztmon.com</a></p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="__MIRROR_JS__"></script>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Scene prompt - Course 00: terminal surround (M5 - DOM bezel owns the text)
|
||||
|
||||
The CRT terminal itself is DOM-drawn; this hero only DRESSES the scene around it. The
|
||||
composition rule is therefore inverted from every other scene: the centre must stay EMPTY.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Even studio lighting, no vignette, no lens flare,
|
||||
no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no words, no code,
|
||||
no glyphs, no labels, no arrows, no callout lines, no UI elements anywhere in the image -
|
||||
every surface marking must be an abstract geometric pattern only. Every component is fully
|
||||
separated from its neighbours by generous gaps of pure void - nothing touching, nothing
|
||||
overlapping, and no beams, cables, or connectors spanning between the separated parts.
|
||||
|
||||
The subject: six dark angular machine fragments - broken armour plates, chamfered metal
|
||||
shards, and slim engineering brackets with glowing cyan edge seams and occasional magenta
|
||||
accents - floating in a loose ring near the OUTER EDGES of the frame, as if a machine
|
||||
casing has just blown apart outward. The exact centre of the frame is a large empty
|
||||
rectangular region of pure void, roughly half the frame wide and half the frame tall,
|
||||
with absolutely nothing inside it and nothing crossing into it. The fragments are
|
||||
distributed around that empty centre: top-left, top-right, left, right, bottom-left,
|
||||
bottom-right. Each fragment floats alone, fully surrounded by void, angled as if caught
|
||||
mid-explosion, drifting away from the empty centre.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Deviation from the master block:** "subject perfectly centered" is replaced by the
|
||||
empty-centre rule above - the DOM CRT bezel occupies the centre rect and owns all text.
|
||||
30-degree iso camera is dropped too: fragments are free-floating, no ground plane.
|
||||
- **Slice plan:** ~6 polygons, one per fragment. At rest the fragments sit at their hero
|
||||
positions framing the terminal; on scroll they fly OUTWARD past the frame edge while the
|
||||
DOM terminal does its own shard-shatter (P1b). Outward travel past the hero layout is
|
||||
SAFE in this scene only: the fragments never overlap in the hero, so no slice carries a
|
||||
hidden bite. The clamp rule still applies to every other scene.
|
||||
- **Registration:** the bezel's text rect anchors to the DOM grid, never to pixels in this
|
||||
image. The empty-centre rect in the hero just has to be COMFORTABLY larger than the
|
||||
bezel at every width (360-1440px check at P1b).
|
||||
- **Legend mapping:** none - Course 00 has no legend; the terminal copy carries the scene.
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Scene prompt - Course II scene A: the cluster (decide / run split)
|
||||
|
||||
v2 diagram: CONTROL PLANE slab above NODE 1/2/3 - "decide above, run below". Four parts.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: a small fleet of four machines floating in the void, mid-explosion.
|
||||
|
||||
At the top: one wide, flat command slab of dark brushed metal, clearly the authority of
|
||||
the scene - a strong cyan glowing seam around its edge and a fine glowing lattice pattern
|
||||
across its top face, hovering alone with a large gap of void beneath it.
|
||||
|
||||
Below it: a row of three identical compact worker blocks, evenly spaced with generous
|
||||
void gaps between them - chunky dark metal cubes, each with a subtle cyan edge glow and
|
||||
a single soft magenta accent line, visibly simpler and plainer than the command slab
|
||||
above. All three share the same size and design, reading as interchangeable units.
|
||||
|
||||
The four machines are aligned as one formation pulled apart for inspection: one commander
|
||||
above, three identical workers below.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 4 polygons - control plane, node 1, node 2, node 3. Collapse pulls the
|
||||
three nodes up and inward toward the slab (formation closes); scrub opens it to the
|
||||
hero layout. Travel clamps at the hero - no overshoot.
|
||||
- **Legend mapping (v2 terms carried to DOM):** control plane "decides - holds truth,
|
||||
schedules, reconciles" / nodes "run pods" / the decide-vs-run split line. Terms:
|
||||
declarative intent, reconciliation loop, desired vs actual state.
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Scene prompt - Course II scene B: the control plane (four processes and a filing cabinet)
|
||||
|
||||
v2 diagram: kube-apiserver (the only door), etcd (consensus truth), scheduler
|
||||
(filter -> score -> bind), controller-manager (the loops). Five parts.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: five distinct machines floating apart in the void, the internals of one
|
||||
control room pulled apart for inspection.
|
||||
|
||||
Front and centre: a tall imposing gateway block - a monolithic dark metal doorframe with
|
||||
a single bright cyan glowing vertical slot down its middle, clearly the only entrance to
|
||||
the scene, the most commanding object in the frame.
|
||||
|
||||
Behind and below the gateway: a heavy squat vault - a dark armoured strongbox with
|
||||
layered horizontal plates like sealed drawers and a deep cyan glow escaping from its
|
||||
seams, visibly the most protected object in the scene.
|
||||
|
||||
To the left of the gateway: a precise instrument block - a slim dark machine with a
|
||||
tilted glass top face showing an abstract geometric sorting pattern of cyan facets,
|
||||
elegant and analytical.
|
||||
|
||||
To the right of the gateway: a compact engine block with several small circular recesses
|
||||
on its face, each recess holding a glowing magenta ring, like a bank of idling rotary
|
||||
mechanisms.
|
||||
|
||||
Above the gateway, small and off to one side: a little drifting cluster of three tiny
|
||||
identical dark chips with faint cyan edges, minor satellites of the bigger machines.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 5 polygons - apiserver gate, etcd vault, scheduler instrument,
|
||||
controller-manager ring bank, the chip cluster (spawned pods motif). Collapse draws
|
||||
everything in toward the gate (everything watches the api-server, nothing talks
|
||||
sideways); scrub opens to the hero. Clamp at hero.
|
||||
- **Legend mapping (v2 terms):** kube-apiserver "the only door - authn, authz, admission,
|
||||
persistence" / etcd "consensus truth - quorum or nothing" / scheduler
|
||||
"filter -> score -> bind" / controller-manager "the reconciliation loops" / chips =
|
||||
Deployment -> ReplicaSet -> pods lineage.
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Scene prompt - Course III scene A: the node (where intent becomes a process)
|
||||
|
||||
v2 diagram: kubelet -> containerd/CRI-O -> CNI -> kube-proxy, pod lands here.
|
||||
"kubelet drives, runtime executes, cni wires, proxy routes." Five parts.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: five machines arranged in a descending diagonal chain from upper left to
|
||||
lower right, one workstation pulled apart into its stations, each floating alone with
|
||||
generous void between them.
|
||||
|
||||
First, upper left: a compact command module - a dark angular unit with a bright cyan
|
||||
glowing visor strip across its front, alert and directive, the driver of the chain.
|
||||
|
||||
Second: a powerful engine block - the largest unit, dark layered metal with visible
|
||||
stacked internal plates glowing faint cyan at their edges, an industrial machine built
|
||||
to assemble and launch things.
|
||||
|
||||
Third: a wiring hub - a dark ring-shaped unit with evenly spaced glowing cyan ports
|
||||
around its rim and one bright magenta port, clearly the connector of the group.
|
||||
|
||||
Fourth, lower right: a routing prism - a faceted dark crystal wedge with thin magenta
|
||||
light tracing its edges, sharp and directional.
|
||||
|
||||
Fifth, floating just above the chain's midpoint: a small sealed capsule - a smooth dark
|
||||
pill-shaped pod with a soft white-cyan glow, the payload this whole chain exists to run,
|
||||
descending toward the engine block but not touching anything.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 5 polygons - kubelet visor module, containerd engine, CNI ring hub,
|
||||
kube-proxy prism, the pod capsule. Collapse packs the chain into one tight machine
|
||||
(capsule docked); scrub opens the diagonal and lets the capsule hover. Clamp at hero.
|
||||
- **Legend mapping (v2 terms):** kubelet "node agent - the only thing that starts your
|
||||
container" / containerd/CRI-O "pulls images, runs containers (CRI)" / CNI plugin
|
||||
"a real IP per pod" / kube-proxy or eBPF "makes Services route" / pod "lands here".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Scene prompt - Course III scene B: the pod (one IP, shared fate)
|
||||
|
||||
v2 diagram: the jacket around containers - app, sidecar, initContainer, volumes, probes,
|
||||
one shared network namespace. Six parts.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: one sealed capsule blown open into its six pieces, all floating apart in
|
||||
the void, mid-explosion.
|
||||
|
||||
The two halves of the capsule shell, upper left and upper right: smooth semi-transparent
|
||||
frosted-glass shells with a soft cyan rim glow, clearly two halves of one enclosure that
|
||||
has been opened, each floating alone.
|
||||
|
||||
Centre: the main core - an upright glowing cylinder of dark metal and glass with a
|
||||
strong cyan light column inside it, the heart of the scene and its brightest object.
|
||||
|
||||
Beside the core, slightly smaller: a companion cylinder of the same design language but
|
||||
with a magenta light column, clearly a partner unit rather than a twin.
|
||||
|
||||
Below the core: a flat gate wedge - a low dark chamfered block with a single amber-white
|
||||
glowing edge, positioned underneath everything like a foundation step.
|
||||
|
||||
To the lower right: a short stack of three thin crystalline discs with faint cyan edge
|
||||
light, neatly aligned but not touching, storage-like and passive.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 6 polygons - shell half L, shell half R, app core, sidecar cylinder,
|
||||
initContainer gate wedge, volume disc stack. Collapse closes the shell around the
|
||||
contents (the jacket); scrub blows it open to the hero. Clamp at hero.
|
||||
- **Probes:** NOT in the image - startup/readiness/liveness are legend entries + a small
|
||||
DOM accent on the core, keeping the part count sliceable. The exit-137 field note
|
||||
stays with this scene.
|
||||
- **Legend mapping (v2 terms):** shared network namespace "one IP" / app container "your
|
||||
process" / sidecar "proxy, logs, reload" / initContainer "runs first, to completion -
|
||||
the gatekeeper" / volumes "shared mounts" / probes "startup -> readiness -> liveness".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Scene prompt - Course IV: the traffic (pods are cattle, Services are the brand)
|
||||
|
||||
v2 diagram: client -> LB -> Ingress -> Service VIP -> ready pod in the pool, not-ready
|
||||
pod silently removed. "Readiness decides membership - traffic follows." Six parts.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: six objects arranged in a gentle descending arc from upper left to lower
|
||||
right, a delivery route pulled apart into its checkpoints, each floating alone in void.
|
||||
|
||||
First, upper left: a small smooth luminous white-cyan orb, simple and organic against
|
||||
all the machinery, clearly the visitor of the scene.
|
||||
|
||||
Second: a wide flat splitter wedge of dark metal, its leading edge glowing cyan and its
|
||||
trailing edge fanning into several subtle facets, built to divide incoming flow.
|
||||
|
||||
Third: a tall thin archway frame of dark metal - an open rectangular gate with a cyan
|
||||
glowing inner rim and nothing inside the opening.
|
||||
|
||||
Fourth, centre of the scene: a floating faceted prism of dark glass with a strong steady
|
||||
cyan core glow, the calm fixed point of the whole composition.
|
||||
|
||||
Fifth, lower right: a sealed pod capsule - a smooth dark pill shape with a healthy bright
|
||||
cyan glow and crisp edge light, solid and present.
|
||||
|
||||
Sixth, drifting below and away from the arc: an identical pod capsule but dim - its glow
|
||||
faded to a weak grey-magenta ember, tilted, clearly falling out of the formation.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 6 polygons - client orb, LB wedge, Ingress arch, Service prism, ready
|
||||
pod, not-ready pod. Collapse packs the arc into a tight column behind the prism; scrub
|
||||
opens the route. The not-ready pod's travel is the teaching beat: it exits the
|
||||
formation as the legend hits "readiness decides membership". Clamp at hero.
|
||||
- **Legend mapping (v2 terms):** client / LoadBalancer "at the edge" / Ingress or Gateway
|
||||
API "host- and path-routing" / Service VIP "the stable name in front of the churn" /
|
||||
pod ready "in the pool" / pod not ready "silently removed". Field note: "the network
|
||||
is broken after a deploy is usually readiness telling the truth".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Scene prompt - Course V: OpenShift (a ring of opinions around the same core)
|
||||
|
||||
v2 diagram: the preserve-3d orbit - KUBERNETES CORE circled by SCC / Routes / Operators /
|
||||
oc + console / MachineConfig / SNO. Seven parts.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: one radiant core with six distinct satellite modules spaced evenly around
|
||||
it in a wide ring, every object floating alone with generous void between all of them.
|
||||
|
||||
Centre: a glowing reactor core - a faceted geodesic sphere of dark glass panels with
|
||||
intense cyan light burning through the seams, the unmistakable heart of the system.
|
||||
|
||||
Around it, six satellites, clockwise from the top, each a different silhouette but the
|
||||
same material language of dark metal with neon edge light:
|
||||
|
||||
A raised angular shield plate with a strong magenta rim glow, defensive and imposing.
|
||||
|
||||
A miniature gate arch, a thick open doorframe with a cyan inner glow.
|
||||
|
||||
A cluster of three interlocking rings, gear-like, with soft cyan edges, mid-rotation.
|
||||
|
||||
A slim handheld slab like a thin console tile, its face lit with a faint abstract cyan
|
||||
lattice.
|
||||
|
||||
A neat stack of three rigid rectangular plates with amber-white edge seams, immutable
|
||||
and orderly.
|
||||
|
||||
A single compact rugged box - small, dense, self-contained, with one bright cyan seam,
|
||||
clearly an entire machine shrunk into one unit.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 7 polygons - core + 6 satellites. Collapse pulls the ring radially
|
||||
into the core (the distribution wraps the engine); scrub expands to the hero orbit.
|
||||
Radial in-out only - no lateral orbiting motion in v3 (that was v2's spin; slices
|
||||
cannot orbit without exposing bites). Clamp at hero.
|
||||
- **Legend mapping (v2 terms):** Kubernetes core "same engine underneath" / SCC shield
|
||||
"admission: what a pod may BE - restricted-v2, random non-root UID" / Routes arch
|
||||
"L7 exposure, pre-Ingress" / Operators rings "OLM - the platform runs itself" /
|
||||
oc + console tile "kubectl, extended" / MachineConfig stack "immutable OS, declared
|
||||
changes" / SNO box "the whole cluster on one edge box".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with
|
||||
`bztmon-site/scripts/dewatermark.mjs` before slicing.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Scene prompt - Course VI: GitOps (repo -> reconciler -> cluster, vault apart)
|
||||
|
||||
Delivery arc scene A. Six parts; the vault deliberately floats APART from the chain -
|
||||
that separation is the teaching point, keep it in curation.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: a descending diagonal chain of machines from upper left to lower right,
|
||||
each floating alone in void, plus one outlier above.
|
||||
|
||||
First, upper left: a large faceted crystal slab of dark glass, its surface etched with
|
||||
a fine branching lattice pattern glowing soft cyan, like frozen forks of a river - a
|
||||
monument of recorded history.
|
||||
|
||||
Second: a compact engine block with two counter-rotating rings recessed into its face,
|
||||
one ring glowing cyan and the other magenta, caught mid-comparison - the busiest, most
|
||||
mechanical object in the scene.
|
||||
|
||||
Third: a neat stack of three thin rigid rectangular sheets with faint cyan edge seams,
|
||||
crisp and freshly minted, hovering in perfect alignment.
|
||||
|
||||
Fourth, lower right: a wide commanding slab of dark brushed metal with a strong cyan
|
||||
glowing seam around its edge - the authority the chain serves.
|
||||
|
||||
Fifth, drifting below and away from the fourth slab, tilted and dim: a small broken
|
||||
shard with a weak grey-magenta ember glow, clearly falling out of the composition.
|
||||
|
||||
Sixth, floating high above the whole chain and clearly apart from it: a small heavily
|
||||
armoured vault cube - layered dark plates, one thin amber-white glowing seam - sealed,
|
||||
dense, deliberately distant from everything else.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 6 polygons - repo crystal, reconciler engine, rendered-sheet stack,
|
||||
cluster slab, drift shard, secret vault. Collapse packs the chain into the engine;
|
||||
the VAULT keeps its distance even collapsed (never absorbed into the pipeline - the
|
||||
Move 3 visual argument). Drift shard exits as the legend hits selfHeal. Clamp at hero.
|
||||
- **Legend mapping:** repo "desired state - the deploy button is a commit" / engine
|
||||
"ArgoCD: compare, diff, sync" / sheets "rendered manifests" / slab "actual state -
|
||||
Course II's cluster" / shard "drift - reverted by selfHeal" / vault "the one thing
|
||||
git never holds - logical reference in git, value in the vault".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip before slicing (ffmpeg delogo).
|
||||
@@ -0,0 +1,58 @@
|
||||
# Scene prompt - Course VII: the image supply chain (registry -> mirror -> node; tag vs digest)
|
||||
|
||||
Delivery arc scene B. Six parts; the two seal plates must read as GHOST vs ENGRAVED -
|
||||
that contrast carries the whole thesis. The image cube is Course I's stack in tight
|
||||
formation (visual callback).
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: a left-to-right journey of four machines floating alone in void, with two
|
||||
contrasting plates floating above the journey.
|
||||
|
||||
First, far left: a compact stack of four thin square slabs in tight vertical formation
|
||||
with narrow void gaps - dark metal, cyan edge seams, one faint magenta accent slab -
|
||||
reading as one shipped unit.
|
||||
|
||||
Second: a tall slender registry tower - dark metal with vertical ranks of small
|
||||
recessed cells, each cell holding a faint cool white-cyan glow, an archive of many
|
||||
sealed compartments.
|
||||
|
||||
Third: a squat wide way-station block - heavy, low, cache-like, with a broad warm cyan
|
||||
glowing intake recess on its upstream face and thick armoured sides.
|
||||
|
||||
Fourth, far right: a dark cluster core - a compact commanding block with a strong cyan
|
||||
glowing seam, the destination of the journey.
|
||||
|
||||
Above the journey, two flat plates floating side by side with clear void between them:
|
||||
the left plate a GHOST - semi-transparent frosted glass, edges softly dissolving, a
|
||||
faint magenta ribbon motif barely visible on its face; the right plate ENGRAVED - dense
|
||||
dark crystal, crisp hard edges, a deep precise geometric seal pattern cut into its face
|
||||
glowing steady cyan.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 6 polygons - image stack, registry tower, mirror way-station, cluster
|
||||
core, ghost plate (tag), engraved plate (digest). Collapse draws the journey into the
|
||||
mirror block (everything local); the two plates collapse toward each other but never
|
||||
merge. Clamp at hero.
|
||||
- **Legend mapping:** image stack "Course I's layers, shipped" / tower "upstream managed
|
||||
registry (ACR as the worked example)" / way-station "the pull-through mirror (zot) -
|
||||
the fleet pulls once" / core "the node pulls local" / ghost plate "tag - a promise
|
||||
that can move" / engraved plate "digest - a fact that cannot".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip before slicing (ffmpeg delogo).
|
||||
@@ -0,0 +1,58 @@
|
||||
# Scene prompt - Course VIII: the Helm press (chart + contexts -> renders, schema gate)
|
||||
|
||||
Delivery arc scene C. Seven parts; the rejected sheet stopped BEHIND the gate is the
|
||||
Move 3 argument - curate hard for it. The press face must show clearly EMPTY voids
|
||||
(the "holes" a template leaves for values).
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: an exploded pressing-works arranged around one central machine, every
|
||||
piece floating alone in void.
|
||||
|
||||
Centre: a large engraved press plate - a thick dark slab held at the isometric angle,
|
||||
its upper face carrying an elegant abstract lattice of rectangular RECESSED VOIDS,
|
||||
clearly empty sockets waiting to be filled, their inner walls glowing faint cyan.
|
||||
|
||||
Feeding in from the left, in a loose arc: three compact faceted crystals of glowing
|
||||
cyan glass - small, dense, jewel-like, one subtly larger than the other two, each
|
||||
floating alone.
|
||||
|
||||
Fanned out to the right: three thin rigid rendered sheets, identical in outline and
|
||||
framework pattern, but each carrying a different small accent motif glowing a different
|
||||
hue (one cyan, one white, one magenta) - same shape, different filling.
|
||||
|
||||
Below the press, off-centre: a low chamfered gate wedge of dark metal with a single
|
||||
amber-white glowing edge, calm and final.
|
||||
|
||||
Behind the gate wedge, stopped short of it, tilted and dim: one more sheet - grey,
|
||||
unlit, visibly incomplete, its framework pattern broken by dark gaps.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Slice plan:** 7 polygons - press plate, three values crystals, rendered-sheet fan
|
||||
(three sheets as ONE slice if cuts get tight - judge at slicing), schema gate wedge,
|
||||
rejected sheet. Collapse draws crystals + sheets into the press (one machine at
|
||||
rest); the rejected sheet collapses BEHIND the gate and never crosses it. Clamp at
|
||||
hero.
|
||||
- **Legend mapping:** press "the chart - a function with holes" / crystals "per-cluster
|
||||
values contexts - the cluster's voice" / sheets "renders: same shape, different
|
||||
numbers - review the render, not just the template" / gate "values.schema.json +
|
||||
required - absence fails at render time" / rejected sheet "the render that refuses -
|
||||
the failure you want".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip before slicing (ffmpeg delogo).
|
||||
@@ -0,0 +1,58 @@
|
||||
# Scene prompt - Appendix: the dependency ledger (optional STILL - three supporting cast)
|
||||
|
||||
This page is DOM-only by design; this hero is OPTIONAL dressing. It is a STILL - never
|
||||
scrubbed, never sliced - so composition matters more than separation mechanics, but the
|
||||
house contract stays so it sits beside the arc scenes as one family. The three objects
|
||||
deliberately ECHO silhouettes from the arc heroes: the etched crystal slab (Course VI's
|
||||
repo), the armoured vault cube (Course VI's secret store), the squat way-station block
|
||||
(Course VII's mirror). Curate for family resemblance.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, subject perfectly
|
||||
centered with generous empty margins on all sides. Even studio lighting, no vignette,
|
||||
no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements
|
||||
anywhere in the image - every surface marking must be an abstract geometric pattern
|
||||
only. Every component is fully separated from its neighbours by generous gaps of pure
|
||||
void - nothing touching, nothing overlapping, and no beams, cables, or connectors
|
||||
spanning between the separated parts.
|
||||
|
||||
The subject: a calm museum-piece portrait of three machines in a loose triangular
|
||||
arrangement, each floating alone in void with generous space between them - three
|
||||
supporting pillars at rest, presented for inspection rather than caught mid-explosion.
|
||||
|
||||
Upper left: a faceted crystal slab of dark glass, its surface etched with a fine
|
||||
branching lattice pattern glowing soft cyan - frozen forks of recorded history.
|
||||
|
||||
Upper right: a small heavily armoured vault cube - layered dark plates, dense and
|
||||
sealed, with one thin amber-white glowing seam around its middle.
|
||||
|
||||
Lower centre, slightly larger than the other two: a squat wide way-station block -
|
||||
heavy, low, cache-like, with a broad warm cyan glowing intake recess on one face and
|
||||
thick armoured sides.
|
||||
|
||||
The three sit in quiet balance, none touching, none dominant, a matched set from the
|
||||
same machine world.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Usage:** page header still for the dependency-ledger appendix - rendered ONCE,
|
||||
static, no slice manifest, no scrub. WebP <=250KB (smaller than scene budget - it is
|
||||
dressing, not a set-piece). If it never generates, the page ships DOM-only as
|
||||
designed - this prompt is a nice-to-have, not a gate.
|
||||
- **Family check at curation:** the three silhouettes should read as the SAME objects
|
||||
from the Course VI and VII heroes (repo crystal, vault cube, mirror way-station) -
|
||||
reject candidates that invent new shapes, even pretty ones. The teaching point is
|
||||
recognition: "you have met these three before."
|
||||
- **Legend mapping (alt text):** crystal "GitHub - where the desired state lives" /
|
||||
vault "Azure Key Vault - where the secret values live" / way-station "zot - where
|
||||
the fleet pulls from".
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip before use (ffmpeg delogo).
|
||||
@@ -0,0 +1,51 @@
|
||||
# Scene prompt - Intro: the establishing shot (masthead backdrop)
|
||||
|
||||
The one image the site opens on, behind the title. It is NOT a course scene: nothing gets sliced
|
||||
here, so the usual "generous void gaps between parts" rule relaxes. What it must do is read at a
|
||||
GLANCE as "a machine world seen from a distance", sit far enough back that white title text stays
|
||||
legible over it, and use the same materials as every course so the page feels like one object.
|
||||
|
||||
Composition rule for this one: the CENTRE-TOP THIRD stays dark and quiet (the headline lives
|
||||
there). Weight the interest low and wide.
|
||||
Generate ~4 at 16:9, curate with the reject checklist, save to disk for pickup.
|
||||
|
||||
---
|
||||
|
||||
Dark technical exploded-view illustration in a cinematic sci-fi engineering style.
|
||||
Solid near-black deep void background, hex #070b14 - completely clean: no stars, no grid,
|
||||
no texture, no particles behind or around the objects. Matte dark metal-and-glass surfaces
|
||||
with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta #e879f9 accents,
|
||||
cool white rim highlights. Camera: fixed 30-degree isometric angle, even studio lighting, no
|
||||
vignette, no lens flare, no depth-of-field blur. ABSOLUTELY NO text, no letters, no numbers, no
|
||||
words, no code, no glyphs, no labels, no arrows, no callout lines, no UI elements anywhere in the
|
||||
image - every surface marking must be an abstract geometric pattern only.
|
||||
|
||||
The subject: a wide establishing view of an entire machine world, seen from far away and slightly
|
||||
above, as if the whole facility has been taken apart and left floating in the dark. Across the
|
||||
lower two thirds of the frame, a loose archipelago of small dark machine forms drifts in the void
|
||||
with generous space between them: flat stacked slabs, a few chamfered cubes, a ringed cylinder, a
|
||||
faceted geodesic sphere lit from within, an open doorframe arch, a low armoured vault box, and
|
||||
several thin plates tumbling at angles. Each object is small in frame - none dominant - each
|
||||
picked out by fine cyan edge light with occasional magenta accents, and each fully surrounded by
|
||||
void. Scale falls away toward the horizon so the nearer objects are larger and the far ones are
|
||||
tiny points of cyan light. The upper third of the frame is almost entirely empty void, with only
|
||||
one or two faint distant shapes near the edges.
|
||||
|
||||
---
|
||||
|
||||
## Build-side notes (not part of the prompt)
|
||||
|
||||
- **Usage:** masthead backdrop for the intro. NOT sliced - it renders as one image behind the
|
||||
title block, scroll-parallaxed and faded out as the index arrives (motion class = parallax, so
|
||||
it is disabled entirely under reduced-motion per house-motion-system).
|
||||
- **Curation gate specific to this image:** hold a light-grey rectangle over the centre-top third;
|
||||
if the title would be hard to read there, reject the candidate. Busy-centre compositions fail
|
||||
here even if they look great alone.
|
||||
- **Family check:** the objects should be recognisable relatives of the course heroes (slabs =
|
||||
Course I, cubes = Course II workers, sphere = Course V core, arch = Routes, vault = the secret
|
||||
store). A viewer who scrolls the whole site should feel they have just toured this picture.
|
||||
- **Processing:** WebP, 1600px wide, target <=180KB (it is a backdrop, quality can sit lower than
|
||||
a scene hero since text overlays it and it is partly faded in use).
|
||||
- **Watermark:** Gemini sparkle bottom-right - strip with ffmpeg delogo before use.
|
||||
- **If it never gets generated:** the intro entrance already ships and works without art; this is
|
||||
additive, not a blocker.
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Segment the delivery-arc heroes into per-part polygons.
|
||||
|
||||
Phase 'debug': label bright components on the void, save numbered overlays for eyeball
|
||||
assignment. Phase 'emit': given a component->part mapping, write the scene manifest
|
||||
(percent-coordinate hulls + collapse offsets toward the scene anchor).
|
||||
"""
|
||||
import json, sys, pathlib
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from scipy import ndimage
|
||||
from scipy.spatial import ConvexHull
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
W = 1376 # working width
|
||||
|
||||
def load(name):
|
||||
im = Image.open(ROOT / "assets/raw" / name).convert("RGB")
|
||||
r = W / im.width
|
||||
im2 = im.resize((W, int(im.height * r)))
|
||||
a = np.asarray(im2).astype(np.float32)
|
||||
lum = a @ np.array([0.299, 0.587, 0.114], dtype=np.float32)
|
||||
return im2, lum
|
||||
|
||||
def components(lum, thresh=26, dilate=6, min_area=350):
|
||||
mask = lum > thresh
|
||||
mask = ndimage.binary_dilation(mask, iterations=dilate)
|
||||
lab, n = ndimage.label(mask)
|
||||
out = []
|
||||
for i in range(1, n + 1):
|
||||
ys, xs = np.nonzero(lab == i)
|
||||
if len(xs) < min_area:
|
||||
continue
|
||||
out.append(dict(id=len(out) + 1, xs=xs, ys=ys,
|
||||
cx=float(xs.mean()), cy=float(ys.mean()),
|
||||
bbox=(int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())),
|
||||
area=int(len(xs))))
|
||||
return out
|
||||
|
||||
def debug(name, tag):
|
||||
im, lum = load(name)
|
||||
comps = components(lum)
|
||||
d = ImageDraw.Draw(im)
|
||||
for c in comps:
|
||||
x0, y0, x1, y1 = c["bbox"]
|
||||
d.rectangle([x0, y0, x1, y1], outline=(63, 186, 245), width=2)
|
||||
d.text((c["cx"] - 8, c["cy"] - 10), str(c["id"]), fill=(255, 80, 80))
|
||||
out = pathlib.Path(sys.argv[3]) / f"debug-{tag}.png"
|
||||
im.save(out)
|
||||
h = im.height
|
||||
print(f"== {tag} ({len(comps)} components, {W}x{h})")
|
||||
for c in comps:
|
||||
print(f" #{c['id']:2d} centroid=({c['cx']/W*100:5.1f}%,{c['cy']/h*100:5.1f}%) "
|
||||
f"bbox%=({c['bbox'][0]/W*100:.0f},{c['bbox'][1]/h*100:.0f},"
|
||||
f"{c['bbox'][2]/W*100:.0f},{c['bbox'][3]/h*100:.0f}) area={c['area']}")
|
||||
|
||||
def emit(name, tag, spec):
|
||||
im, lum = load(name)
|
||||
comps = {c["id"]: c for c in components(lum)}
|
||||
h = im.height
|
||||
parts = []
|
||||
for part in spec["parts"]:
|
||||
xs = np.concatenate([comps[i]["xs"] for i in part["comps"]])
|
||||
ys = np.concatenate([comps[i]["ys"] for i in part["comps"]])
|
||||
if "clip" in part:
|
||||
x0, y0, x1, y1 = part["clip"]
|
||||
W2, H2 = W, lum.shape[0]
|
||||
keep = (xs >= x0*W2) & (xs < x1*W2) & (ys >= y0*H2) & (ys < y1*H2)
|
||||
xs, ys = xs[keep], ys[keep]
|
||||
pts = np.stack([xs, ys], 1).astype(np.float64)
|
||||
hull = ConvexHull(pts)
|
||||
poly = pts[hull.vertices]
|
||||
# pad the hull outward from its centroid by ~1.2% of width (glow safety)
|
||||
c = poly.mean(0)
|
||||
v = poly - c
|
||||
poly = c + v * (1 + (W * 0.012) / (np.abs(v).max(1, keepdims=True) + 1e-6))
|
||||
# simplify: keep every k-th vertex to <= 18 points
|
||||
k = max(1, len(poly) // 18)
|
||||
poly = poly[::k]
|
||||
parts.append(dict(
|
||||
name=part["name"], title=part["title"], blurb=part["blurb"],
|
||||
z=part.get("z", 0),
|
||||
points=[[round(float(x) / W * 100, 2), round(float(y) / h * 100, 2)] for x, y in poly],
|
||||
cx=round(float(np.mean(xs)) / W * 100, 2), cy=round(float(np.mean(ys)) / h * 100, 2)))
|
||||
if "anchor_xy" in spec:
|
||||
ax, ay = spec["anchor_xy"]
|
||||
else:
|
||||
ax, ay = next((p["cx"], p["cy"]) for p in parts if p["name"] == spec["anchor"])
|
||||
for p in parts:
|
||||
nest = spec.get("nest_overrides", {}).get(p["name"], spec.get("nest", 0.72))
|
||||
p["dx"] = round((ax - p["cx"]) * nest, 2)
|
||||
p["dy"] = round((ay - p["cy"]) * nest, 2)
|
||||
del p["cx"]; del p["cy"]
|
||||
man = dict(scene=tag, source=f"assets/raw/{name}", parts=parts)
|
||||
if "stagger" in spec:
|
||||
man["stagger"] = spec["stagger"]
|
||||
if spec.get("reverse"):
|
||||
man["reverse"] = True
|
||||
for kk in ("motion","bow","orbit"):
|
||||
if kk in spec: man[kk]=spec[kk]
|
||||
out = ROOT / "pilot" / f"manifest-{tag}.json"
|
||||
out.write_text(json.dumps(man, indent=1))
|
||||
print(f"manifest-{tag}.json: {len(parts)} parts ->",
|
||||
", ".join(f"{p['name']}({p['dx']},{p['dy']})" for p in parts))
|
||||
|
||||
if __name__ == "__main__":
|
||||
mode, scenes_json, outdir = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
scenes = json.loads(pathlib.Path(scenes_json).read_text()) if mode == "emit" else None
|
||||
heroes = [("course-00-surround.jpeg", "surround"),
|
||||
("course-III-node.jpeg", "node"),
|
||||
("course-III-pod.jpeg", "pod"),
|
||||
("course-IV-traffic.jpeg", "traffic"),
|
||||
("course-V-orbit.jpeg", "orbit"),
|
||||
("course-II-cluster.jpeg", "cluster"),
|
||||
("course-VI-gitops.jpeg", "gitops"),
|
||||
("course-VII-supply-chain.jpeg", "supply"),
|
||||
("course-VIII-helm-press.jpeg", "helm")]
|
||||
for name, tag in heroes:
|
||||
if mode == "debug":
|
||||
debug(name, tag)
|
||||
else:
|
||||
emit(name, tag, scenes[tag])
|
||||