kubepath

Before the material ships

Two findings, left in the manifests on purpose.

Both turned up while reading the repository. Neither is fixed: a brief was asked for, not a patch. They are written up here with diffs so the material ships with them acknowledged rather than quietly wrong — and because the first one is a better lesson than anything that could replace it.

Say the word and the manifests get corrected instead.

FINDING 1 breaks a documented step infrastructure/apps/kustomization.yaml

Two apps are in the tree and in no kustomization

What it is

infrastructure/apps/kustomization.yaml lists eight apps under resources:. The repository contains ten. deployer/ and mail-in/ are absent from the list.

So kubectl apply -k infrastructure/apps deploys neither of them, and exits 0. Kustomize is not silent because it is broken — it is silent because a directory that is not named in resources: is not part of the build. There is nothing for it to warn about.

What it breaks

The wait-loop in infrastructure/README.md polls for mail-in to become ready. It was never created, so the loop does not fail — it waits, on a Deployment that does not exist, until whatever timeout it has expires or someone interrupts it. A first-time bootstrap therefore ends in a hang with no error message, which is the hardest failure shape to diagnose because nothing anywhere reports a problem.

deployer is worse in a quieter way. Nothing waits for it, so bootstrap appears to succeed completely. The absence only shows up later, when the first deployment does not happen and there is no controller to have not happened it — which sends the investigation to the deployer’s logs, of which there are none, because there is no pod.

The fix

infrastructure/apps/kustomization.yaml
resources:  - ../../apps/api  - ../../apps/web  - ../../apps/worker  - ../../apps/builder  - ../../apps/deployer  - ../../apps/forge  - ../../apps/mail-in  - ../../apps/analytics  - ../../apps/batteries  - ../../apps/enterprise
how to prove it, before and after
# prove it before and after — this is the check that should be in CI:
$ diff <(kubectl kustomize infrastructure/apps | grep -c '^kind:') \
       <(ls -d apps/*/ | wc -l)

# or, more usefully, name what is missing:
$ comm -13 \
    <(kubectl kustomize infrastructure/apps \
        | yq -r 'select(.kind=="Deployment").metadata.name' | sort -u) \
    <(ls -1 apps/ | sort)
deployer
mail-in

Why it is worth classroom time

Do not fix this before the class sees it. It is the best example in the repository of what "no Argo, no Flux, no reconciler" actually costs, and it is far more convincing as a discovered fact than as a bullet on a slide.

The argument runs: a reconciler continuously compares desired state to actual state, so an app in the repo and not in the cluster is drift, and drift is reported on the next pass — within a minute, in a dashboard, without anyone looking. With kubectl apply -k there is no continuous comparison, so "in the repo" and "in the cluster" are two independent facts that nobody is checking against each other. This erratum is not a typo that slipped through review. It is the predicted consequence of the architecture, arriving on schedule.

The follow-on exercise is better than the finding: ask a class to write the CI check that would have caught it. They will discover that "every directory under apps/ appears in a kustomization" is a three-line shell script, that nobody wrote it, and that this is the general shape of the work a reconciler was doing for free.

Check yourself

1 question. One attempt each is recorded; the explanation is the point, not the score.

Two app directories are absent from resources:. Why does kubectl apply -k exit 0?

FINDING 2 documentation drift infrastructure/README.md

The README’s layout section predates four directories

What it is

The layout section in infrastructure/README.md was written before batteries/, deployer/, docs/ and enterprise/ existed. It documents the tree as it was, and nothing has updated it since.

On its own this is ordinary staleness. What makes it worth listing is the company it keeps: the same README contains the wait-loop that expects mail-in. So this file both describes a tree that is missing four directories and depends on an app that the kustomization does not deploy.

What it breaks

Nothing, mechanically. A stale tree in a README does not fail a command.

It costs a newcomer’s first hour, in the specific way that matters most: they read the layout, form a model of the repository, and then meet four directories the model does not account for. The natural conclusion is that those directories are new, experimental, or not part of the real system — which is exactly wrong for deployer/, which is load-bearing. A stale map is worse than no map, because no map makes you look.

The fix

infrastructure/README.md — the layout section
## Layout    apps/      analytics/     ClickHouse, shared, 500 DBs each      api/      batteries/     the six platform services      builder/       BuildKit rootless, one Job per build      deployer/      applies tenant manifests; write-only RBAC      enterprise/    single-tenant deployments      forge/         Forgejo, SSH :2222      mail-in/       inbound mail; NOT in the kustomization — see #1      web/      worker/    docs/            operator documentation    infrastructure/    platform/        traefik, cert-manager, cnpg, gvisor
how to prove it, before and after
# the layout section is derivable, so derive it:
$ ls -1d apps/*/ platform/*/ | sed 's|/$||'

# and make CI reject a README whose tree does not match the filesystem.
# a generated section that is verified is a section that stays true;
# a hand-written one is a section that is true on the day it is written.

Why it is worth classroom time

The narrow lesson is that a hand-maintained directory listing has a half-life. The broader one, and the one worth the classroom minute, is that this README holds two things of very different natures: a tree, which is derivable from the filesystem and should be generated, and a wait-loop, which encodes operational knowledge and cannot be.

Generate the first and CI will keep it honest. Leave the second hand-written but move what it asserts into a test — because the wait-loop is where finding #1 hides, and a wait-loop that hangs is a test that never runs.

Check yourself

1 question. One attempt each is recorded; the explanation is the point, not the score.

The README’s layout section is a stale directory tree. Why is that worse than having no tree at all?

← Hop 10, where finding 1 comes from