Repository navigation
feat(dev): reusable kind dev stack — make dev-kind, with OIDC and gateway routing - #69
Merged
Merged
Conversation
Seven slices with disjoint file sets for the Kubernetes flavour of `make dev`: script and cluster config, pins in versions.env, dev values, Alloy and Gitea workloads, gateway and mock OIDC, Makefile targets, docs. Constraints are the four decisions signed today (Calico, versions.env pins Renovate-excluded, Gateway API + NGF, values-declared OIDC) and the design review's corrections, each re-verified against the code. No slice runs a kind cluster; the live bring-up and the OIDC walk are the orchestrator's, in section 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
Adds dev-kind, dev-kind-reload, dev-kind-seed, dev-kind-status and
dev-kind-down after the existing dev-* block, per
docs/plans/2026-09-14-kind-dev-stack.md §K6. dev-kind depends on
preflight-k8s, docker-build-local and docker-build-simulator so a cold
run always builds both local images from this tree before bringing the
cluster up (matching the e2e-k8s target's prerequisite pattern);
dev-kind-reload depends only on docker-build-local since reload only
rebuilds the app image. All five delegate to scripts/dev-kind.sh's
verbs (up/reload/seed/status/down) — that script, its manifests and
its pins are other slices' files and are referenced here only by the
names §1 of the plan fixes, not created.
Red run first (scripts/repocheck/devkind_makefile_test.go): before the
targets existed, mkTargetLine("dev-kind") failed with
`target "dev-kind" not found in Makefile` and 6 more specs failed on
missing .PHONY membership, help comments, dependencies and recipe
verbs (go test ./scripts/repocheck/ -count=1 -run TestRepocheck: 53
passed, 7 failed). All seven are green after the Makefile change, and
the full repocheck suite, make help and make -n dev-kind-down match
the plan's acceptance checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
Slice K2 of docs/plans/2026-09-14-kind-dev-stack.md. KIND_NODE_IMAGE,
CALICO_VERSION and NGF_CHART_VERSION move from Go literals/constants in
e2e/k8s into deploy/versions.env, each commented, so scripts/dev-kind.sh
(a later slice) can read the exact pins the e2e suite installs instead
of copying or grepping them out of route_conformance_test.go.
- main_test.go: kindNodeImage() keeps E2E_K8S_NODE_IMAGE first, then
reads KIND_NODE_IMAGE via readVersionsEnvValue (log.Fatalf on error —
this runs in TestMain before any cluster exists); installCNI reads
CALICO_VERSION the same way. The calicoManifest const is gone.
- route_conformance_test.go: the ngfChartVersion const is gone;
installGatewayController reads NGF_CHART_VERSION. The NGF-selection
rationale comment stays, with its kindest/node:v1.31.4 mention
repointed at "the KIND_NODE_IMAGE pin" and a new sentence explaining
where the version itself now lives and why.
- renovate.json: a packageRules entry disables kindest/node entirely
(no matchUpdateTypes, so a digest bump is excluded too) — Renovate's
custom.regex manager would otherwise treat KIND_NODE_IMAGE as an
ordinary image:tag pin and propose digest/bump PRs for a kind node
image, which is a reviewed decision (kind/helm-kind-action support,
above NGF 2.6.0's and CNPG 0.29.0's kubeVersion floors), not routine.
- scripts/repocheck/kind_pins_test.go (new): red-first spec covering
all four files.
Red run (scripts/repocheck/kind_pins_test.go, before main_test.go/
route_conformance_test.go/renovate.json were edited):
[FAILED] e2e/k8s/main_test.go reads KIND_NODE_IMAGE and CALICO_VERSION ...
Expected <string>: ...kindNodeImage()... not to contain substring
<string>: kindest/node:
[FAILED] e2e/k8s/route_conformance_test.go reads NGF_CHART_VERSION ...
Expected <string>: ...not to contain substring
<string>: ngfChartVersion = "
[FAILED] renovate.json disables updates to kindest/node entirely
Expected <bool>: false to be true
Ran 57 of 57 Specs -- 54 Passed | 3 Failed
(deploy/versions.env's own lines were written first in this slice, so
that assertion was already green on the first run; the three above are
the literals-and-config red run this slice exists to close.)
Verified: go test ./scripts/repocheck/ -count=1 green (all 57 specs,
including the existing renovate.json spec); go vet -tags e2ek8s ./e2e/k8s/
clean; make lint 0 issues; make guards green (all ten, including
check-docker/check-gateway-pin/check-chartvalues-pin); go build ./...
clean; grep of the three new keys in deploy/versions.env shows bare
alphanumeric/.:/- values, and `make lint`/`make guards` parsing the
Makefile's `include deploy/versions.env` succeeded, confirming the
unquoted values stay shell- and make-safe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
Slice K7 of the kind dev stack plan (docs/plans/2026-09-14-kind-dev-stack.md). Adds kind-test-environment-plan.md §11, covering what make dev-kind shares with the e2e/k8s suite (Calico, NGF, CNPG, the deploy/versions.env pins) and what it deliberately does not (single node, one namespace, no teardown on failure), the one-CoreDNS-rewrite routing/DNS design and why a single rule is correct rather than a shortcut, the OIDC walk together with the dialGuard boundary that makes the SSO settings page read-only against a chart-declared issuer, the reload build-annotation mechanics, where the manifests live and why, and the agents-do-not-run-kind rule. Updates the status header's pin sentence and §9 item 1 to reflect that CALICO_VERSION is now a deploy/versions.env variable rather than a Go constant. Also updates dev-guide.md (a "Kubernetes flavour" section after Mode B, five new Dev stack target-table rows, and a fix to the stale sentence implying OIDC is compose-only), README.md's command table, e2e/k8s/README.md's setup list and Known limits to point at the versions.env pin locations, AGENTS.md's Commands line and image table (kindest/node, projectcalico/calico, and a note that dev/kind/*.yaml restates the Gitea and mock-oauth2 pins), the project-status.md document-map row, and one CHANGELOG.md Unreleased line. This slice has no runtime control of its own; the checks are the existing guards. Red run before this commit (all pass unmodified, confirming the guards do not need this slice's changes to stay green — the point is that they still pass *with* the new docs content and cross-references): make check-docs-drift -> check-docs-drift: OK (21 pages match) make check-docs-version -> check-docs-version: OK go test ./scripts/repocheck/ -count=1 -> ok shepherd/scripts/repocheck Post-change: `make guards` all ten checks green; `go test ./scripts/repocheck/ -count=1` green; every `make dev-kind*` command quoted in the docs (dev-guide.md, README.md, kind-test-environment-plan.md, AGENTS.md, CHANGELOG.md, e2e/k8s/README.md, project-status.md) is one of the five real Makefile targets (dev-kind, -reload, -seed, -status, -down); `git status --porcelain site/` is empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
Slice K4 ("workloads") of the reusable kind dev stack plan
(docs/plans/2026-09-14-kind-dev-stack.md). Adds the plain manifests the
forthcoming scripts/dev-kind.sh applies:
- dev/kind/alloy.yaml: three Deployments (alloy-metrics, alloy-logs,
alloy-staging), one per dev/alloy-*.alloy fixture, each mounting a
same-named ConfigMap read-only at /etc/alloy and running compose's exact
args. The image is the literal placeholder __ALLOY_IMAGE__, substituted
by the script from ALLOY_IMAGE (deploy/versions.env) with sed at apply
time, so the pin is never restated outside Renovate's files (plan §1).
- dev/kind/gitea.yaml: a 1Gi RWO PVC (C5 — the dev seed is check-then-skip
on the gitea-demo credential row, so an emptyDir would leave it pointing
at a repo that no longer exists after any restart), a Deployment with
Recreate strategy and compose's env/image/ports verbatim, and a Service
exposing 3000/2222. Admin bootstrap stays the script's job (kubectl exec),
matching compose's gitea-init one-shot.
- scripts/repocheck/devkind_workloads_test.go: red-run first (see below),
then a spec parsing both manifests (yaml.v3, multi-document) and
dev/docker-compose.dev.yaml, checking the shapes the plan pins down —
Deployment count and naming, the placeholder image and absence of any
real grafana/alloy reference, ConfigMap mount identity, and the Gitea
PVC/Deployment/Service shape with env and image equal to compose's
key-for-key.
Red run (scripts/repocheck/devkind_workloads_test.go against the tree
before dev/kind/ existed):
open .../dev/kind/alloy.yaml: no such file or directory
(same shape for dev/kind/gitea.yaml) — 13 of 66 specs failed, all in this
new file; every other repocheck spec was unaffected. After adding the
manifests, `go test ./scripts/repocheck/ -count=1` is green (66/66).
Verified statically only, per the plan's rule that no slice creates, uses
or deletes a kind cluster: go vet and golangci-lint (--allow-parallel-runners,
0 issues — make lint's own golangci-lint invocation collided with another
agent's concurrent run on this shared laptop, an expected condition per the
plan) are clean on the new file, and `make lint`'s guards target (docs,
docker, gateway/chart pins, dist) passed. A `kubectl create --dry-run=client
--validate=false` check on the sed-substituted manifests could not be run
here: kubectl's client-side dry-run still needs a reachable API server for
discovery/RESTMapping even with --validate=false, and no cluster context is
available or permitted in this isolated worktree — left for the
orchestrator's live gate (plan §4/§5), where a real kube context exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
K1 slice of docs/plans/2026-09-14-kind-dev-stack.md: the entry point for the reusable kind dev stack (`make dev-kind`, wired up by a later slice). scripts/dev-kind.sh implements up/reload/seed/status/down exactly as plan section K1 specifies: it sources deploy/versions.env and fails loudly if KIND_NODE_IMAGE, CALICO_VERSION, NGF_CHART_VERSION, CNPG_CHART_VERSION, GATEWAY_API_VERSION, GATEWAY_API_CHANNEL or ALLOY_IMAGE is missing (the first three land in the pins slice, K2); every kubectl/helm call goes through the kc()/hm() wrappers so the kind-shepherd-dev context is always explicit, never ambient. `up` creates the cluster from dev/kind/cluster.yaml, installs Calico, waits for nodes+CoreDNS, loads both local images, installs the Gateway API CRDs and NGF pinned to NodePort 30080/listener 80, installs CNPG, applies the Gateway and verifies — at the consumed layer, not just the declared values — that the provisioned data-plane Service actually got nodePort 30080, inserts the CoreDNS *.localtest.me rewrite idempotently, creates the Secret and three Alloy ConfigMaps, applies the workload manifests other slices own (gateway/gitea/oidc/routes/alloy, the last rendered through the __ALLOY_IMAGE__ placeholder), bootstraps the Gitea admin tolerating "already exists", then runs the real Helm chart with a fresh podAnnotations.dev-build so Helm rolls the pods after the migrate hook instead of racing it (plan C3). `reload` reuses the same install path with a new build id; `down` is `kind delete cluster --name`. dev/kind/cluster.yaml is the one-node counterpart to the e2e suite's three-node testdata/kind-cluster.yaml: disableDefaultCNI, the same 10.244.0.0/16 pod subnet (documented why, condensed from that file), and the containerPort 30080 -> hostPort 80 mapping that makes every dev-stack hostname a plain http://*.localtest.me URL. Red run (scripts/repocheck/devkind_script_test.go, before either file existed): every one of the 10 new specs failed identically — `open scripts/dev-kind.sh: no such file or directory` — via readRepoFile, os.Stat and loadYAML. After adding the two files, `go test ./scripts/repocheck/ -count=1` is green (63 specs), `bash -n scripts/dev-kind.sh` and `shellcheck scripts/dev-kind.sh` are clean, and `make lint` (guards + golangci-lint) reports 0 issues. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
dev/kind/values.yaml is the Helm values file scripts/dev-kind.sh (a different slice) will install the `shepherd` release with, on top of deploy/helm/shepherd's own defaults. It uses only keys values.schema.json already knows about, so no chart change ships alongside it: - image / simulator.image: registry "", tag local, pullPolicy Never — the pods can only start from what `kind load docker-image` put on the node. - existingSecret: shepherd-dev-env, the Secret another slice creates from dev/shepherd.dev.env plus the OIDC client secret (plan §1) — zero copies of the compose env file. - cnpg.enabled with 1 instance / 1Gi storage: the chart provisions Postgres instead of pointing at one that already exists. - route.enabled for shepherd.localtest.me, attached to the `dev` Gateway another slice declares in the same namespace. - config.server.base_url and config.oidc.*: the mock-oauth2-server issuer (http://oidc.localtest.me/default), provider generic (sub subject claim, no Graph lookup — mock-oauth2 has no Graph API), and the redirect URL. The client secret rides in existingSecret, never here. - simulator stays enabled (the chart's own default since v0.0.1) so its default-deny NetworkPolicy is exercised for real once Calico is the CNI (plan D1). scripts/repocheck/devkind_values_test.go is the red-run-first guard: it shells out to the real `helm template` (not a hand-parsed approximation) with this file layered on deploy/helm/shepherd and asserts the rendered Deployment/shepherd, Deployment/shepherd-simulator, HTTPRoute/shepherd, Cluster/shepherd-db and ConfigMap/shepherd shapes the plan's K3 section lists, that no Secret/shepherd-secrets renders (existingSecret must be the only Secret in play), and that `helm lint --strict` exits 0. Red run, 2026-09-14 (dev/kind/values.yaml temporarily removed to capture it): every assertion failed via `helm template shepherd deploy/helm/shepherd -f dev/kind/values.yaml --namespace shepherd-dev` returning: Error: open dev/kind/values.yaml: no such file or directory All seven specs are green with the file restored; `helm lint --strict`, `make helm-lint`, `go vet ./scripts/repocheck/...`, and `make lint` (guards + golangci-lint) are all clean. templates/NOTES.txt printing https for an http route hostname is a known, separately-tracked cosmetic issue (plan §6) — not touched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
…PRoutes
Slice K5 ("gateway-oidc") of docs/plans/2026-09-14-kind-dev-stack.md: the
three manifests scripts/dev-kind.sh applies after NGF and CoreDNS are in
place.
- dev/kind/gateway.yaml: the Gateway `dev` (gatewayClassName nginx, one
HTTP:80 listener). The header explains NGF's per-Gateway data-plane
Service, why the script pins its NodePort to 30080 via the NGF chart
values, and that the single CoreDNS rewrite targets that Service so a
pod and the host browser resolve every *.localtest.me hostname
identically (D3).
- dev/kind/oidc.yaml: Deployment/Service `oidc` running navikt
mock-oauth2-server, with SERVER_PORT and JSON_CONFIG copied verbatim
from dev/docker-compose.dev.yaml's services.oidc (D2, C7) and a
readiness probe against /default/.well-known/openid-configuration --
the same issuer path dev/kind/values.yaml declares. The image tag stays
pinned here rather than deploy/versions.env, matching the decision that
dev-only fixture pins should not bill e2e-k8s. No runAsNonRoot is set:
the image's default user isn't verified, and guessing wrong would only
fail live, where nothing here could catch it -- security context stays
hygiene, not a gate (C9).
- dev/kind/routes.yaml: the two hand-written HTTPRoutes, oidc.localtest.me
-> oidc:8090 and gitea.localtest.me -> gitea:3000, both parented to
`dev` with a PathPrefix "/" match. The shepherd route is deliberately
absent -- it's the chart's own (K3).
- scripts/repocheck/devkind_gateway_test.go: red-first Ginkgo spec parsing
the three manifests with yaml.v3 (no cluster needed, per the plan's
guidance to avoid kubectl-dependent checks in repocheck) and asserting
the Gateway shape, both routes' hostnames/backends/parentRefs, the oidc
image against compose's, JSON_CONFIG parsing with interactiveLogin
true, the oidc Service port, and that no shepherd.localtest.me hostname
appears in this slice's files.
Red run, 2026-09-14: with the three manifests moved aside, every assertion
failed with `open <repo>/dev/kind/gateway.yaml: no such file or
directory` (and the equivalent for oidc.yaml / routes.yaml), matching the
plan's stated red run. `go test ./scripts/repocheck/ -count=1` is green
with the files restored; `golangci-lint run ./scripts/repocheck/...`
reports 0 issues.
`kubectl create --dry-run=client --validate=false` on these three files
could not be verified here: in every configuration tried (no context, and
stale kind contexts with a cached but expired discovery cache) kubectl's
RESTMapper needs a live, reachable API server to resolve GVK->GVR, even
for built-in Deployment/Service kinds and even with --validate=false --
contradicting the plan's "no cluster" expectation for that check. This
repo has no live kind cluster in this session and agents are not to
create one; deferred to the orchestrator's live gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
Two review findings against docs/kind-test-environment-plan.md §11: - The OIDC walk example had the wrong first barrier. Handler.TestSettings calls validateIssuer before fetchDiscovery, so pasting http://oidc.localtest.me/default into Test connection is refused by the https-only rule (settings.go), never by dialGuard. Reworded the example to two steps: the https rule refuses the http input, and only an https input reaches dialGuard, which then refuses because the mock OIDC provider resolves to the NGF ClusterIP. Added a pointer telling the orchestrator to correct plan §5 step 10 the same way before the live walk (that file is out of this slice's scope). - The DNS-rewrite bullet misdescribed the mechanism and contradicted the paragraph right after it: the browser never touches CoreDNS, so "both paths cross the same rewrite" was false, and the per-hostname-drift argument built on it doesn't hold. Rewrote the bullet: the rewrite lets in-cluster clients resolve *.localtest.me at all (the public wildcard's 127.0.0.1 answer is a pod's own loopback), and pod/browser issuer strings agree because both reach the same Gateway with the same Host header, which mock-oauth2-server derives the issuer from. Replaced the readiness-probe citation (oidc.yaml doesn't exist yet) with a reference to §5 step 7's live curl, the only step that actually proves the pod-seen string. Also folded in three cheap non-blocking fixes: dropped the incorrect checksum/config claim and tightened the reload section to Helm's rendered-manifest comparison; noted dev-kind-reload kind-loads both shepherd:local and shepherd-simulator:local, not just the app image; and qualified "only one such cluster" to "only one shepherd-dev cluster" so it doesn't read as a kind limitation. Verified: make check-docs-drift (21 pages), make check-docs-version, go test ./scripts/repocheck/, and make guards (10/10) all green. Diff touches only docs/kind-test-environment-plan.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
…stop mangling the CoreDNS rewrite regex
Two blocking review findings on the K1 script slice:
1. apply_secret_and_configmaps combined --from-env-file with
--from-literal in one `kubectl create secret` call. kubectl v1.36.4
rejects that deterministically ("from-env-file cannot be combined
with from-file or from-literal"), so every `up` would die at this
step after Calico/NGF/CNPG/Gateway were already installed.
Reproduced offline (client-side dry-run, no cluster). Plan C2's
prescribed command has the same invalid combination and is wrong
for the same reason — this fix does not touch the plan doc (out of
this slice's file scope), only the script that actually ships.
Fixed by appending the OIDC client secret to the env-file's lines
via process substitution, so there is only ever one env source.
Verified with --dry-run=client: the rendered Secret carries all
7 dev/shepherd.dev.env keys plus SHEPHERD_OIDC_CLIENT_SECRET.
2. apply_coredns_rewrite had two bugs that canceled each other out:
`awk -v line="$rewrite_line"` unescapes backslashes in its value
(verified on macOS awk; gawk does the same and warns), so the
Corefile line actually written was the any-char regex
`(.*).localtest.me ...` rather than the §1 contract line
`(.*)\.localtest\.me ...`. The idempotency guard
(`grep -q 'localtest\.me'`) searches for the literal dotted text,
which the *correct*, backslash-escaped line does not contain — the
guard only ever passed because bug (1) was stripping the
backslashes it depends on. Fixing the awk bug alone would have made
every re-run of `up` append a duplicate rewrite line (reproduced in
a stubbed harness). Fixed by passing the line through ENVIRON
instead of -v (single-quoted in bash so backslashes survive), and
matching the guard on the rule text itself
(`rewrite name regex .*localtest`) rather than the dotted hostname
substring. Verified offline across two simulated runs: exactly the
§1 line, and no duplicate on the second run.
New repocheck specs guard both regressions (red-first: 3 new specs
fail against the pre-fix script — 63 passed/3 failed — then pass
after the fix, 66/66). Also tightened bareKubectlHelmRE to catch a
bare kubectl/helm call after a pipe, `;`, `&`, or `$(...)`/`(...)`,
not just one that starts its own line (non-blocking review remark).
Cheap non-blocking fixes folded in: cmd_status's curl checks now
print "unreachable" on failure instead of exiting silently (bare `&&
echo` was the function's last command, so a down service made
`status` exit 1 with no output), and apply_coredns_rewrite now waits
for `rollout status deploy/coredns` after the restart so CoreDNS is
actually serving before oidc/gitea/shepherd start resolving names
against it.
dev/kind/cluster.yaml needed no change — the review verified it
against the K1 spec (single node, disableDefaultCNI, 30080->80
mapping) and raised no finding there.
Verification: bash -n, shellcheck 0.11.0, gofmt, go vet, golangci-lint
(0 issues), make guards — all clean. go test ./scripts/repocheck/...
66/66 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
The label-selected Service can briefly lag Gateway condition=Programmed=True - e2e/k8s/route_conformance_test.go's waitProvisionedServiceName documents this exact race and retries every 2s for the same reason. find_ngf_service was a one-shot check run immediately after `kc wait --for=condition=Programmed gateway/dev`, so a cold `up` could die non-deterministically at this step right after Calico/NGF/CNPG were already installed - the same failure shape as the round-1 blocking findings. find_ngf_service now polls the same label-selected query for up to ~2m, sleeping 2s between attempts, and only emits the existing "expected exactly one Service ... found N" error and exits 1 once the deadline passes with the count still not exactly one. Red-first: added a repocheck spec asserting find_ngf_service's body matches \bsleep\b, confirmed it fails against the pre-fix one-shot function (66 Passed | 1 Failed, with the exact "must retry (sleep) instead of checking the Service count once" message), then made the fix and reran to 67/67. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
The dev stack's values file is the one combination a developer actually installs, and no ci/ file covers it; the plan's cross-slice seam (§3) adds this render at integration so a chart change that breaks the dev stack fails helm-lint, not the next make dev-kind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
The chart's own HTTPRoute never resolved through NGINX Gateway Fabric: the
Service hardcodes appProtocol kubernetes.io/h2c and NGF refuses to proxy an
HTTP route to an h2c upstream ("UnsupportedProtocol"), so every request to
shepherd.localtest.me answered 500 from nginx. The conformance suite routes
to the receiver tier, not to Shepherd's Service, which is why nothing had
caught it. appProtocol is now a value: the h2c default is unchanged for
gateways that honour the hint, and the dev values clear it (Shepherd serves
HTTP/1.1 equally well). The chart spec was red on "values don't meet the
schema" and green after; the generated values reference is regenerated.
The three Alloy agents were applied before the chart, resolved the
`shepherd` Service on their initial load, exited, and crash-looped through
five restarts each until the backoff happened to line up with the Service
appearing. Alloy's remotecfg exits on a failed initial load, so the agents
now go on after install_shepherd, with a wait on their Deployments. The
ordering spec was red against the previous cmd_up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
The first OIDC login on the kind dev stack reached the mock provider on its Service address, exchanged the code, and then failed with "fetching keys: address is not a public internet address". Discovery already used the per-source client (a chart-declared issuer may be private; an admin-supplied one is guarded), but the go-oidc Provider built from that document — and so every JWKS fetch for the rest of its life — used the guarded client regardless of source. The provider now takes the same client that fetched the document, the runtime keeps it, and the code exchange goes through it too, so the token endpoint the document named is guarded or not by the same rule as the rest. The new test builds a provider from a loopback issuer both ways: for a chart-declared source the key fetch reaches the server (verification then fails on the empty JWKS, which is the point), for an admin-supplied source it is still refused at the key fetch. It failed to compile against the old two-argument constructor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
Section 11 gains the three findings the cluster showed that no static check could (the chart route's h2c hint, the chart-declared issuer blocked at its key fetch, the agents applied before the chart), each now fixed on this branch, plus the sandbox-window-versus-scrape-interval observation for the ledger. The changelog gets the two product fixes as user-facing entries beside the dev-tooling line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
…the chart The second cold bring-up still crash-looped all three agents, now on "unauthenticated": they started after the chart, but before the seed that creates the static agent token their configs authenticate with, and Alloy's remotecfg exits on a failed initial load. cmd_up now runs the seed before the agents; the ordering spec asserts both orderings and was red against the previous sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw
procoduck
added a commit
that referenced
this pull request
Sep 15, 2026
… bundle (#73) * chore(release): prepare v0.7.0 — chart 0.11.0, changelog, rebuilt SPA bundle The chart moves a minor because a template changed: the Service's appProtocol became the service.appProtocol value (#69). Its default renders exactly what 0.10.2 did, so there is no UPGRADING.md section. The changelog closes Unreleased as v0.7.0 with an upgrade preamble; the docs, README, landing page and bug template quote the new pair, and the Kubernetes page no longer says Shepherd is 0.3.x. The embedded bundle is rebuilt from main at f82957e so the release binary carries the walkthrough fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw * chore(release): fold the two header fixes into the v0.7.0 preamble Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw --------- Co-authored-by: Andre Staats <staats.andre@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
procoduck
added a commit
that referenced
this pull request
Sep 15, 2026
…lans (#76) The verified-baseline table now cites the runs on the v0.7.0 merge commit (CI 34973037472 on PR #73, e2e-k8s 34973037471, compose e2e 34971974892, web/test-ui from PR #69's run 34945312703, release 34974782542 with scan-published green for both images). The pending chart-bump follow-up is closed by chart 0.11.0. docs/plans/2026-09-14-walkthrough-fixes.md and 2026-09-14-kind-dev-stack.md move to docs/archive/plans/ per the policy in docs/archive/README.md, each with a closing note that names what shipped (PR #67 / #69, v0.7.0) and what the ledger still carries. Every citation — changelog, ledger, the kind suite plan, and the Go test comments in repocheck, mgmtapi and the self-monitoring wizard — now points at the archived path. docs/plans/ is empty between releases; the three doc maps say so. Claude-Session: https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw Co-authored-by: Andre Staats <staats.andre@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Kubernetes flavour of
make dev: a single-node kind clustershepherd-devrunning the real chart with Calico, CloudNativePG, Gateway API + NGINX Gateway Fabric, the existing dev seed, three live Alloy agents fromdev/*.alloy, Gitea on a PVC, and the navikt mock OIDC provider — everything on host port 80 under*.localtest.me, with one CoreDNS rewrite so pods and the browser agree on the issuer string.make dev-kind/dev-kind-reload/dev-kind-seed/dev-kind-status/dev-kind-down.Plan with the four signed decisions and the design review's corrections:
docs/plans/2026-09-14-kind-dev-stack.md. Section 11 ofdocs/kind-test-environment-plan.mddocuments the stack.Built as one Fable plan, seven Sonnet slices in parallel worktrees with disjoint file sets, each Opus-reviewed with a fix loop; then integrated and proven live three times from a deleted cluster.
What the live bring-ups found, all fixed here with red runs
route.enablednever resolved through NGF: the Service hardcodedappProtocol: kubernetes.io/h2c, which NGF refuses (UnsupportedProtocol, 500 on every request). New chart valueservice.appProtocol, default unchanged; the dev values clear it.internal/auth).remotecfgexits on a failed initial load, so the agents must be applied after the chart and after the seed that mints their token.Live proof (third cold run, all pods 0 restarts): plan §5 steps 1–13 including Gateway
Programmed, NodePort 30080 at the consumed layer, identical issuer strings from host and pod, seed idempotent,reloadrolling through the migrate hook,downleaving nothing. OIDC walk via Playwright, 23/23: PKCE+nonce+state, app admin from the groups claim, org admin scoped without the Admin menu, reader as viewer, no-groups user on an empty overview, SSO page read-only with the chart banner. Sandbox runs capture under Calico (21 series; a 30 s run against a 30 s scrape interval can land on 0 — noted in the docs).Pins:
KIND_NODE_IMAGE,CALICO_VERSION,NGF_CHART_VERSIONmove todeploy/versions.env, read by the suite and the script;kindest/nodeis Renovate-excluded. This PR therefore triggerse2e-k8s, which is the proof the suite still installs the same stack.Static gate:
make lint0 issues; repocheck (5 new spec files) green;go vet -tags e2ek8s;make helm-lintnow renders the dev values too; docs guards green.internal/spa/distuntouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01BPMsuQQBnedAsJ8tJMH3rw