the complete menu: aperitif CRT terminal + courses III node/pod, IV traffic, V orbit - five animation personalities, clip-split and reverse slice modes

This commit is contained in:
2026-08-17 16:06:11 +10:00
parent a201af9d29
commit b12e435003
20 changed files with 3663 additions and 9 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

+5 -1
View File
@@ -11,12 +11,16 @@ here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd -- "$here" cd -- "$here"
for f in pilot/arc.tpl.html pilot/manifest-docker.json pilot/manifest-cluster.json \ 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-gitops.json pilot/manifest-supply.json \
pilot/manifest-helm.json gsap.min.js st.min.js \ 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-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/course-VIII-helm-press.webp assets/dist/appendix-dependency-ledger.webp \
assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \ assets/dist/delivery-flow.svg assets/dist/docker-layers.webp \
assets/dist/course-II-cluster.webp; do 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; do
[[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; } [[ -f "$f" ]] || { printf 'missing source: %s\n' "$f" >&2; exit 1; }
done done
+240 -6
View File
@@ -10,8 +10,13 @@
<nav class="toc" aria-label="index"> <nav class="toc" aria-label="index">
<p class="kt">Index</p> <p class="kt">Index</p>
<ol> <ol>
<li><a href="#c0"><b>Course 00</b><span>Aperitif - three commands, and what actually just happened</span></a></li>
<li><a href="#c1"><b>Course I</b><span>Docker - an image is a stack of frozen diffs</span></a></li> <li><a href="#c1"><b>Course I</b><span>Docker - an image is a stack of frozen diffs</span></a></li>
<li><a href="#c2"><b>Course II</b><span>Kubernetes - a cluster is a promise, not a place</span></a></li> <li><a href="#c2"><b>Course II</b><span>Kubernetes - a cluster is a promise, not a place</span></a></li>
<li><a href="#c3a"><b>Course III</b><span>The node - where intent becomes a process</span></a></li>
<li><a href="#c3b"><b>Course III</b><span>The pod - one IP, shared fate</span></a></li>
<li><a href="#c4"><b>Course IV</b><span>The traffic - pods are cattle, Services are the brand</span></a></li>
<li><a href="#c5"><b>Course V</b><span>OpenShift - Kubernetes with opinions, and a security guard</span></a></li>
<li><a href="#c6"><b>Course VI</b><span>GitOps - nobody deploys anything; the cluster syncs itself</span></a></li> <li><a href="#c6"><b>Course VI</b><span>GitOps - nobody deploys anything; the cluster syncs itself</span></a></li>
<li><a href="#c7"><b>Course VII</b><span>The image supply chain - a tag is a promise, a digest is a fact</span></a></li> <li><a href="#c7"><b>Course VII</b><span>The image supply chain - a tag is a promise, a digest is a fact</span></a></li>
<li><a href="#c8"><b>Course VIII</b><span>Helm - a chart is a function; the values file is the cluster speaking</span></a></li> <li><a href="#c8"><b>Course VIII</b><span>Helm - a chart is a function; the values file is the cluster speaking</span></a></li>
@@ -26,13 +31,50 @@
<p>The Exploded Cluster teaches how modern container platforms work by taking them apart - <p>The Exploded Cluster teaches how modern container platforms work by taking them apart -
literally. Each course is one machine drawn as a single exploded illustration, sliced into its literally. Each course is one machine drawn as a single exploded illustration, sliced into its
real components and wired to your scroll, so the architecture moves while the words explain it. real components and wired to your scroll, so the architecture moves while the words explain it.
Start at Course I with what an image actually is; finish knowing how a change travels from a Start with the aperitif&#39;s three terminal commands; finish knowing how a change travels from a
git commit to a running, secret-fed, digest-pinned workload on a fleet. Every claim comes from git commit to a running, secret-fed, digest-pinned workload on a fleet. Every claim comes from
running these systems for real, and the library at the end links only to official running these systems for real, and the library at the end links only to official
documentation.</p> documentation.</p>
</section> </section>
<!-- ============ COURSE 00 ============ -->
<section class="course" id="c0" data-scene="surround" aria-labelledby="c0h">
<header class="chead">
<p class="eyebrow">Course 00 &middot; Aperitif</p>
<h2 id="c0h" class="thesis">Three commands.<br><em>What actually just happened?</em></h2>
<p class="lede">The whole ceremony of shipping software fits in four lines of terminal. They
work on your first day and stay mysterious for years. Scroll - the shell comes off first.</p>
</header>
<div class="pin">
<div class="viewport solo">
<figure class="scene">
<img class="flat" src="__HERO_SURROUND__"
alt="Six dark armour fragments with neon seams framing a large empty centre - the
casing of a machine caught the instant before it comes apart.">
<div class="crt" role="img" aria-label="A terminal transcript: docker build, docker push,
kubectl apply, kubectl get pods showing Running - then the question: but what
actually just happened?">
<p class="line">$ docker build -t shop/api:1.4.2 .</p>
<p class="line ok">=&gt; exported image sha256:9f2c41...</p>
<p class="line">$ docker push shop/api:1.4.2</p>
<p class="line">$ kubectl apply -f deploy.yaml</p>
<p class="line ok">deployment.apps/api created</p>
<p class="line">$ kubectl get pods</p>
<p class="line ok">api-7d4b9c6-x2k4f&nbsp;&nbsp;1/1&nbsp;&nbsp;Running</p>
<p class="line q">...but what actually just happened?</p>
</div>
</figure>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<p>An image got <b>built</b> - of what, exactly? <b>Pushed</b> - to where, and what travelled?
<b>Applied</b> - which is not the same as launched. <b>Running</b> - according to whom?
Every course below takes one of those words apart. The armour is already loose.</p>
</div>
</section>
<!-- ============ COURSE I ============ --> <!-- ============ COURSE I ============ -->
<section class="course" id="c1" data-scene="docker" aria-labelledby="c1h"> <section class="course" id="c1" data-scene="docker" aria-labelledby="c1h">
<header class="chead"> <header class="chead">
@@ -126,6 +168,171 @@
</div> </div>
</section> </section>
<!-- ============ COURSE III - THE NODE ============ -->
<section class="course" id="c3a" data-scene="node" aria-labelledby="c3ah">
<header class="chead">
<p class="eyebrow">Course III &middot; The node</p>
<h2 id="c3ah" class="thesis">Where intent<br><em>becomes a process.</em></h2>
<p class="lede">Everything so far was decision. This is the machine where a pod stops being
paperwork and starts being a process - and the chain assembles link by link as you scroll.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_NODE__"
alt="A diagonal chain of node machinery: a visor-lit kubelet module, a layered
container-runtime engine, a ported CNI ring, a magenta routing prism - and a
small glowing pod capsule descending toward the engine.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>One component starts containers. One.</h3>
<p>Every node runs a <b>kubelet</b> - and in the entire system it is the only component that
actually starts your container. It speaks CRI to <b>containerd</b> or CRI-O, which pulls the
image (through the mirror of Course VII) and launches it. A <b>CNI plugin</b> hands the pod
a real IP, and <b>kube-proxy</b> - or an eBPF datapath replacing it - makes Service
addresses route to real pods. The kubelet drives, the runtime executes, the CNI wires, the
proxy routes.</p>
<p class="sayit">The kubelet is the only component that actually starts your container.</p>
<p class="fieldnote"><b>Field note.</b> Node NotReady? Check kubelet-to-API-server
connectivity before anything else - a node that cannot phone home is presumed lost.</p>
<p class="closeline">The control plane never touches your workload. It writes intent; the
kubelet reads it and makes it flesh. Authority and execution meet exactly here, nowhere
else.</p>
<p class="reads"><b>Pre-reads:</b> C-II
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/architecture/">cluster architecture</a> &middot;
<a href="https://kubernetes.io/docs/concepts/architecture/#node-components">node components</a></p>
</div>
</section>
<!-- ============ COURSE III - THE POD ============ -->
<section class="course" id="c3b" data-scene="pod" aria-labelledby="c3bh">
<header class="chead">
<p class="eyebrow">Course III &middot; The pod</p>
<h2 id="c3bh" class="thesis">One IP,<br><em>shared fate.</em></h2>
<p class="lede">A pod is not a container - it is a jacket around one or more. Scroll, and the
capsule opens like a clamshell: shells apart, contents on display.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_POD__"
alt="A pod capsule blown open: two frosted shell halves floating apart, a cyan app
container column standing on an amber-lit init gate, a smaller magenta sidecar
beside it, and a stack of translucent volume discs.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>The jacket, not the container</h3>
<p>Everything inside the jacket shares a <b>network namespace</b>: one IP, localhost between
friends, volumes mountable by all. <b>initContainers</b> run first, in order, to completion -
gatekeepers under everything. <b>Sidecars</b> ride along with their own containers and their
own jobs: proxy, logs, reload.</p>
<h3>Three probes, three different jobs</h3>
<p><b>startup</b> owns warm-up, <b>readiness</b> gates traffic, <b>liveness</b> restarts the
truly hung. Confusing them is how healthy pods get executed - a slow start killed by an
impatient liveness probe looks exactly like a crash.</p>
<p class="sayit">containerPort is documentation - the app still has to bind the port itself.</p>
<p class="fieldnote"><b>Field note.</b> Exit 137 with reason Error is the kubelet's probe
kill; OOMKilled is memory. Same exit code, different crimes - read the reason.</p>
<p class="closeline">The pod is the smallest schedulable unit - the jacket, never the
container. Once that distinction lands, half of Kubernetes networking stops being
mysterious.</p>
<p class="reads"><b>Pre-reads:</b> C-III's node scene above
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/workloads/pods/">pods</a> &middot;
<a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/">the three probes</a></p>
</div>
</section>
<!-- ============ COURSE IV ============ -->
<section class="course" id="c4" data-scene="traffic" aria-labelledby="c4h">
<header class="chead">
<p class="eyebrow">Course IV &middot; The traffic</p>
<h2 id="c4h" class="thesis">Pods are cattle.<br><em>Services are the brand.</em></h2>
<p class="lede">Pods die, respawn and change addresses - and traffic still arrives. Scroll,
and the delivery route assembles checkpoint by checkpoint; watch what happens to the pod
that stops answering.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_TRAFFIC__"
alt="A delivery route in the void: a glowing client orb, a fanned load-balancer
wedge, an open ingress doorframe, a Service prism with a bright core, a lit
ready pod - and a dark unlit pod fallen out of the line.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>The stable name in front of the churn</h3>
<p>A <b>Service</b> is the fixed point: a ClusterIP inside, a LoadBalancer at the edge,
Ingress or the Gateway API doing host- and path-routing above. Clients hold the name; the
pods behind it come and go without anyone being told.</p>
<h3>Readiness decides membership</h3>
<p>A pod failing its readiness probe <b>silently leaves the pool</b>. No error, no event at
the client - traffic simply stops arriving. That is the feature: broken instances remove
themselves. It is also the first place to look when traffic "disappears".</p>
<p class="sayit">Never talk to a pod; talk to a Service.</p>
<p class="fieldnote"><b>Field note.</b> "The network is broken" after a deploy is usually
readiness telling the truth about your app - not the network lying about your packets.</p>
<p class="closeline">The dark pod in the scene is not an error state - it is the system
working. Membership is earned per probe, continuously, forever.</p>
<p class="reads"><b>Pre-reads:</b> C-III's pod scene (readiness lives there)
&ensp;<b>Further:</b> <a href="https://kubernetes.io/docs/concepts/services-networking/">Services and networking</a></p>
</div>
</section>
<!-- ============ COURSE V ============ -->
<section class="course" id="c5" data-scene="orbit" aria-labelledby="c5h">
<header class="chead">
<p class="eyebrow">Course V &middot; OpenShift</p>
<h2 id="c5h" class="thesis">Kubernetes with opinions -<br><em>and a security guard.</em></h2>
<p class="lede">OpenShift is a distribution of Kubernetes: same engine, opinionated chassis.
Scroll, and the opinions bloom outward from the core they orbit.</p>
</header>
<div class="pin">
<div class="viewport">
<figure class="scene">
<img class="flat" src="__HERO_ORBIT__"
alt="A glowing geodesic core ringed by six satellites: a magenta admission shield,
an open route arch, interlocking operator rings, a handheld console, an
amber-lit stack of machine-config plates and a compact single-node box.">
</figure>
<ol class="legend"></ol>
</div>
<div class="bench"></div>
</div>
<div class="prose">
<h3>The doorman interviews every pod</h3>
<p>The <b>SCC</b> is admission deciding what a pod may BE. The default, restricted-v2, runs
your container as a <b>random non-root UID</b> - your image has to cope. Workloads that
genuinely need privilege get a dedicated ServiceAccount bound to a minimal custom SCC,
never the stock one.</p>
<h3>The platform runs itself</h3>
<p><b>Routes</b> predate Ingress and still rule here. <b>Operators</b> run the platform's own
machinery. The OS underneath is immutable - changed by <b>MachineConfig</b>, never by SSH.
And <b>SNO</b> puts the entire promise on one box at the edge. At fleet scale the labels
from Course VI decide which of these boxes runs what.</p>
<p class="sayit">On OpenShift, admission is the interview - the SCC is the dress code.</p>
<p class="fieldnote"><b>Field note.</b> Deployment stuck at 0/1 with no pod at all? The
refusal happened above scheduling - read the ReplicaSet events. The error lives a level
up.</p>
<p class="closeline">Everything in the ring is opinion wrapped around the same core you
already know. Learn the core once; the opinions are just house rules.</p>
<p class="reads"><b>Pre-reads:</b> C-II
&ensp;<b>Further:</b> <a href="https://docs.redhat.com/en/documentation/openshift_container_platform">OpenShift documentation</a> &middot;
<a href="https://www.redhat.com/en/technologies/cloud-computing/openshift">Red Hat OpenShift</a></p>
</div>
</section>
<!-- ============ COURSE VI ============ --> <!-- ============ COURSE VI ============ -->
<section class="course" id="c6" data-scene="gitops" aria-labelledby="c6h"> <section class="course" id="c6" data-scene="gitops" aria-labelledby="c6h">
<header class="chead"> <header class="chead">
@@ -516,6 +723,18 @@ b{color:var(--ink)} .mono{font-family:var(--mono);font-size:.9em;color:var(--cya
.legend .t{font-size:.92rem;font-weight:600;margin:0 0 .22rem;letter-spacing:-.01em} .legend .t{font-size:.92rem;font-weight:600;margin:0 0 .22rem;letter-spacing:-.01em}
.legend .b{font-size:.76rem;line-height:1.5;color:var(--dim);margin:0} .legend .b{font-size:.76rem;line-height:1.5;color:var(--dim);margin:0}
.viewport.solo{grid-template-columns:1fr;max-width:980px}
.crt{position:absolute;inset:22% 26%;display:flex;flex-direction:column;justify-content:center;
gap:.35rem;padding:clamp(10px,2.5vw,26px);border:1px solid var(--line);border-radius:10px;
background:rgba(5,8,16,0.88);box-shadow:0 0 40px rgba(63,186,245,0.07) inset}
.crt .line{font-family:var(--mono);font-size:clamp(.55rem,1.35vw,.85rem);line-height:1.5;
color:var(--ink);margin:0;white-space:nowrap;overflow:hidden}
.crt .line.ok{color:var(--dim)}
.crt .line.q{color:var(--magenta);margin-top:.5rem}
body.fx .crt .line{opacity:0;transform:translateY(4px);transition:opacity .3s ease,transform .3s ease}
body.fx .crt .line.on{opacity:1;transform:none}
@media (max-width:700px){.crt{inset:16% 12%}}
.bench{width:min(720px,100%);margin:16px auto 0;text-align:center} .bench{width:min(720px,100%);margin:16px auto 0;text-align:center}
.bench input{width:100%;accent-color:var(--cyan)} .bench input{width:100%;accent-color:var(--cyan)}
.bench .hud{font-family:var(--mono);font-size:.66rem;letter-spacing:.06em;color:var(--dim);margin:.45rem 0 0} .bench .hud{font-family:var(--mono);font-size:.66rem;letter-spacing:.06em;color:var(--dim);margin:.45rem 0 0}
@@ -562,7 +781,7 @@ body:not(.fx) .bench{display:none}
<script> <script>
(() => { (() => {
"use strict"; "use strict";
const SCENES = { docker: __MAN_DOCKER__, cluster: __MAN_CLUSTER__, gitops: __MAN_GITOPS__, supply: __MAN_SUPPLY__, helm: __MAN_HELM__ }; const SCENES = { surround: __MAN_SURROUND__, docker: __MAN_DOCKER__, cluster: __MAN_CLUSTER__, node: __MAN_NODE__, pod: __MAN_POD__, traffic: __MAN_TRAFFIC__, orbit: __MAN_ORBIT__, gitops: __MAN_GITOPS__, supply: __MAN_SUPPLY__, helm: __MAN_HELM__ };
const RM = matchMedia("(prefers-reduced-motion: reduce)").matches; const RM = matchMedia("(prefers-reduced-motion: reduce)").matches;
const units = []; const units = [];
@@ -571,7 +790,7 @@ body:not(.fx) .bench{display:none}
const scene = sec.querySelector(".scene"); const scene = sec.querySelector(".scene");
const flat = scene.querySelector(".flat"); const flat = scene.querySelector(".flat");
const legend = sec.querySelector(".legend"); const legend = sec.querySelector(".legend");
man.parts.forEach(p => { if (legend) man.parts.forEach(p => {
const li = document.createElement("li"); const li = document.createElement("li");
li.innerHTML = '<p class="t">' + p.title + '</p><p class="b">' + p.blurb + '</p>'; li.innerHTML = '<p class="t">' + p.title + '</p><p class="b">' + p.blurb + '</p>';
legend.appendChild(li); legend.appendChild(li);
@@ -594,15 +813,18 @@ body:not(.fx) .bench{display:none}
return d; return d;
}); });
u.flat.style.visibility = "hidden"; u.flat.style.visibility = "hidden";
const lis = [...u.legend.children]; const lis = u.legend ? [...u.legend.children] : [];
const n = lis.length; const n = lis.length;
u.apply = t => { u.apply = t => {
// stagger > 0 = cascade: parts complete their travel one after another in list order // stagger > 0 = cascade: parts complete their travel one after another in list order
const sg = u.man.stagger || 0; const sg = u.man.stagger || 0;
u.man.parts.forEach((p, i) => { u.man.parts.forEach((p, i) => {
const pt = sg ? Math.min(1, Math.max(0, t * (1 + sg * (n - 1)) - sg * i)) : t; const k = u.man.parts.length;
const pt = sg ? Math.min(1, Math.max(0, t * (1 + sg * (k - 1)) - sg * i)) : t;
// reverse scenes sit assembled at rest and FLY APART as the scrub advances
const e = u.man.reverse ? (1 - pt) : pt;
u.slabs[i].style.transform = u.slabs[i].style.transform =
"translate3d(" + (p.dx * (1 - pt)).toFixed(3) + "%," + (p.dy * (1 - pt)).toFixed(3) + "%,0)"; "translate3d(" + (p.dx * (1 - e)).toFixed(3) + "%," + (p.dy * (1 - e)).toFixed(3) + "%,0)";
}); });
u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")"; u.scene.style.transform = "scale(" + (0.95 + 0.05 * t).toFixed(4) + ")";
// the side list lights in reading order, top to bottom, as the scrub advances // the side list lights in reading order, top to bottom, as the scrub advances
@@ -654,6 +876,18 @@ body:not(.fx) .bench{display:none}
}); });
if (!RM) ScrollTrigger.refresh(); if (!RM) ScrollTrigger.refresh();
}; };
// the aperitif terminal types its transcript once, when it first enters view
document.querySelectorAll(".crt").forEach(crt => {
const lines = [...crt.querySelectorAll(".line")];
if (RM) { lines.forEach(l => l.classList.add("on")); return; }
const io = new IntersectionObserver(es => {
if (!es.some(x => x.isIntersecting)) return;
io.disconnect();
lines.forEach((l, i) => setTimeout(() => l.classList.add("on"), 350 * i + 200));
}, { threshold: 0.4 });
io.observe(crt);
});
Promise.all(units.map(u => u.flat.decode ? u.flat.decode().catch(() => {}) : Promise.resolve())) Promise.all(units.map(u => u.flat.decode ? u.flat.decode().catch(() => {}) : Promise.resolve()))
.then(() => requestAnimationFrame(() => requestAnimationFrame(boot))); .then(() => requestAnimationFrame(() => requestAnimationFrame(boot)));
})(); })();
+10
View File
@@ -20,6 +20,11 @@ const i = tpl.lastIndexOf("<script>");
const j = tpl.lastIndexOf("</script>"); const j = tpl.lastIndexOf("</script>");
if (i < 0 || j < i) throw new Error("app script block not found"); if (i < 0 || j < i) throw new Error("app script block not found");
let app = tpl.slice(i + 8, j) 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_DOCKER__", man("docker"))
.replace("__MAN_CLUSTER__", man("cluster")) .replace("__MAN_CLUSTER__", man("cluster"))
.replace("__MAN_GITOPS__", man("gitops")) .replace("__MAN_GITOPS__", man("gitops"))
@@ -28,6 +33,11 @@ let app = tpl.slice(i + 8, j)
let body = tpl.slice(0, i) + tpl.slice(j + 9); let body = tpl.slice(0, i) + tpl.slice(j + 9);
const heroes = { const heroes = {
__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_DOCKER__: "docker-layers.webp",
__HERO_CLUSTER__: "course-II-cluster.webp", __HERO_CLUSTER__: "course-II-cluster.webp",
__HERO_GITOPS__: "course-VI-gitops.webp", __HERO_GITOPS__: "course-VI-gitops.webp",
+10
View File
@@ -16,11 +16,21 @@ def man(tag):
separators=(",", ":")) separators=(",", ":"))
out = (ROOT / "pilot/arc.tpl.html").read_text() \ out = (ROOT / "pilot/arc.tpl.html").read_text() \
.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_DOCKER__", man("docker")) \
.replace("__MAN_CLUSTER__", man("cluster")) \ .replace("__MAN_CLUSTER__", man("cluster")) \
.replace("__MAN_GITOPS__", man("gitops")) \ .replace("__MAN_GITOPS__", man("gitops")) \
.replace("__MAN_SUPPLY__", man("supply")) \ .replace("__MAN_SUPPLY__", man("supply")) \
.replace("__MAN_HELM__", man("helm")) \ .replace("__MAN_HELM__", man("helm")) \
.replace("__HERO_SURROUND__", b64("course-00-surround.webp")) \
.replace("__HERO_NODE__", b64("course-III-node.webp")) \
.replace("__HERO_POD__", b64("course-III-pod.webp")) \
.replace("__HERO_TRAFFIC__", b64("course-IV-traffic.webp")) \
.replace("__HERO_ORBIT__", b64("course-V-orbit.webp")) \
.replace("__HERO_DOCKER__", b64("docker-layers.webp")) \ .replace("__HERO_DOCKER__", b64("docker-layers.webp")) \
.replace("__HERO_CLUSTER__", b64("course-II-cluster.webp")) \ .replace("__HERO_CLUSTER__", b64("course-II-cluster.webp")) \
.replace("__HERO_GITOPS__", b64("course-VI-gitops.webp")) \ .replace("__HERO_GITOPS__", b64("course-VI-gitops.webp")) \
+601
View File
@@ -0,0 +1,601 @@
{
"scene": "node",
"source": "assets/raw/course-III-node.jpeg",
"parts": [
{
"name": "kubelet",
"title": "kubelet",
"blurb": "The node agent - the only component in the whole system that actually starts your container.",
"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": "Speaks CRI: pulls images, launches containers. The engine the kubelet drives.",
"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. The wiring of the whole machine.",
"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
}
+832
View File
@@ -0,0 +1,832 @@
{
"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. restricted-v2 runs you as a random non-root UID - your image has to cope.",
"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": "OLM - the platform runs itself. The rings never stop turning.",
"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": "kubectl, extended - 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": "The OS underneath is immutable - changed by declaration, never by SSH.",
"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
}
]
}
+598
View File
@@ -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": "initContainer",
"blurb": "Runs first, to completion - the gatekeeper underneath everything.",
"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": "Shared mounts - the only place anything survives the writable layer's death.",
"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
}
]
}
+679
View File
@@ -0,0 +1,679 @@
{
"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
}
+671
View File
@@ -0,0 +1,671 @@
{
"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: silently removed. 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
}
+16 -1
View File
@@ -62,6 +62,11 @@ def emit(name, tag, spec):
for part in spec["parts"]: for part in spec["parts"]:
xs = np.concatenate([comps[i]["xs"] for i in part["comps"]]) xs = np.concatenate([comps[i]["xs"] for i in part["comps"]])
ys = np.concatenate([comps[i]["ys"] 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) pts = np.stack([xs, ys], 1).astype(np.float64)
hull = ConvexHull(pts) hull = ConvexHull(pts)
poly = pts[hull.vertices] poly = pts[hull.vertices]
@@ -77,6 +82,9 @@ def emit(name, tag, spec):
z=part.get("z", 0), z=part.get("z", 0),
points=[[round(float(x) / W * 100, 2), round(float(y) / h * 100, 2)] for x, y in poly], 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))) 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"]) ax, ay = next((p["cx"], p["cy"]) for p in parts if p["name"] == spec["anchor"])
for p in parts: for p in parts:
nest = spec.get("nest_overrides", {}).get(p["name"], spec.get("nest", 0.72)) nest = spec.get("nest_overrides", {}).get(p["name"], spec.get("nest", 0.72))
@@ -86,6 +94,8 @@ def emit(name, tag, spec):
man = dict(scene=tag, source=f"assets/raw/{name}", parts=parts) man = dict(scene=tag, source=f"assets/raw/{name}", parts=parts)
if "stagger" in spec: if "stagger" in spec:
man["stagger"] = spec["stagger"] man["stagger"] = spec["stagger"]
if spec.get("reverse"):
man["reverse"] = True
out = ROOT / "pilot" / f"manifest-{tag}.json" out = ROOT / "pilot" / f"manifest-{tag}.json"
out.write_text(json.dumps(man, indent=1)) out.write_text(json.dumps(man, indent=1))
print(f"manifest-{tag}.json: {len(parts)} parts ->", print(f"manifest-{tag}.json: {len(parts)} parts ->",
@@ -94,7 +104,12 @@ def emit(name, tag, spec):
if __name__ == "__main__": if __name__ == "__main__":
mode, scenes_json, outdir = sys.argv[1], sys.argv[2], sys.argv[3] 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 scenes = json.loads(pathlib.Path(scenes_json).read_text()) if mode == "emit" else None
heroes = [("course-II-cluster.jpeg", "cluster"), 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-VI-gitops.jpeg", "gitops"),
("course-VII-supply-chain.jpeg", "supply"), ("course-VII-supply-chain.jpeg", "supply"),
("course-VIII-helm-press.jpeg", "helm")] ("course-VIII-helm-press.jpeg", "helm")]