@intentius/behold 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +4 -0
  2. package/demos.json +39 -0
  3. package/dist/cli.js +147 -40
  4. package/example-flux-estate/README.md +41 -0
  5. package/example-flux-estate/app-a/chant.config.ts +18 -0
  6. package/example-flux-estate/app-a/manifests/app.yaml +56 -0
  7. package/example-flux-estate/app-a/package.json +13 -0
  8. package/example-flux-estate/app-a/src/app.ts +35 -0
  9. package/example-flux-estate/app-a/tsconfig.json +1 -0
  10. package/example-flux-estate/app-b/chant.config.ts +20 -0
  11. package/example-flux-estate/app-b/manifests/app.yaml +54 -0
  12. package/example-flux-estate/app-b/package.json +13 -0
  13. package/example-flux-estate/app-b/src/app.ts +37 -0
  14. package/example-flux-estate/app-b/tsconfig.json +1 -0
  15. package/example-flux-estate/control-plane/chant.config.ts +19 -0
  16. package/example-flux-estate/control-plane/package.json +13 -0
  17. package/example-flux-estate/control-plane/src/flux.ts +51 -0
  18. package/example-flux-estate/control-plane/tsconfig.json +1 -0
  19. package/example-flux-estate/package-lock.json +1949 -0
  20. package/example-flux-estate/package.json +10 -0
  21. package/example-flux-estate/scripts/estate-down.sh +19 -0
  22. package/example-flux-estate/scripts/estate-up.sh +61 -0
  23. package/example-k8s/README.md +120 -0
  24. package/example-k8s/chant.config.ts +21 -0
  25. package/example-k8s/ops/k3d-apply.op.ts +18 -0
  26. package/example-k8s/package-lock.json +2071 -0
  27. package/example-k8s/package.json +14 -0
  28. package/example-k8s/scripts/local/local-down.sh +23 -0
  29. package/example-k8s/scripts/local/local-up.sh +46 -0
  30. package/example-k8s/src/config.ts +5 -0
  31. package/example-k8s/src/web.ts +29 -0
  32. package/example-k8s/tsconfig.json +1 -0
  33. package/package.json +4 -1
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "behold-example-flux-estate",
3
+ "private": true,
4
+ "description": "A GitOps estate for behold's flux-estate demo: a Flux control plane + two app projects, served composed.",
5
+ "workspaces": [
6
+ "control-plane",
7
+ "app-a",
8
+ "app-b"
9
+ ]
10
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Tear the flux-estate demo down: delete the uniquely-named cluster and
3
+ # restore whatever kubectl context was current before estate-up.sh switched it.
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")/.."
6
+
7
+ CLUSTER=behold-flux-demo
8
+ STATE="scripts/.prev-context"
9
+
10
+ k3d cluster delete "$CLUSTER" 2>/dev/null || echo "flux-estate demo: no ${CLUSTER} cluster to remove."
11
+ if [ -f "$STATE" ]; then
12
+ prev="$(cat "$STATE")"
13
+ if [ -n "$prev" ] && kubectl config get-contexts -o name 2>/dev/null | grep -qx "$prev"; then
14
+ kubectl config use-context "$prev" >/dev/null
15
+ echo "flux-estate demo: restored kubectl context \"$prev\"."
16
+ fi
17
+ rm -f "$STATE"
18
+ fi
19
+ echo "flux-estate demo: cluster removed."
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # Stand up the flux-estate demo (#211): a throwaway single-node k3d cluster,
3
+ # Flux's controllers (applied straight from the pinned release manifest — no
4
+ # flux CLI needed), and the control plane's CRs. From there the reconcilers
5
+ # deploy the app projects out of behold's public repo.
6
+ #
7
+ # Safety, same contract as example-k8s: never touches any OTHER kubeconfig
8
+ # context. Whatever context is current is saved to .prev-context and restored
9
+ # by estate-down.sh; the cluster and its context are uniquely named
10
+ # (behold-flux-demo / k3d-behold-flux-demo).
11
+ #
12
+ # BEHOLD_FLUX_REF (optional): patch the LIVE GitRepository to sync a branch
13
+ # other than main — how a not-yet-merged manifests change is verified. The
14
+ # declared source always says main.
15
+ set -euo pipefail
16
+ cd "$(dirname "$0")/.."
17
+
18
+ CLUSTER=behold-flux-demo
19
+ CONTEXT="k3d-${CLUSTER}"
20
+ STATE="scripts/.prev-context"
21
+ FLUX_MANIFEST="https://github.com/fluxcd/flux2/releases/download/v2.4.0/install.yaml"
22
+
23
+ if ! docker info >/dev/null 2>&1; then
24
+ echo "flux-estate demo: Docker is not running — start Docker and re-run." >&2
25
+ exit 1
26
+ fi
27
+ for bin in k3d kubectl; do
28
+ if ! command -v "$bin" >/dev/null 2>&1; then
29
+ echo "flux-estate demo: $bin is not installed." >&2
30
+ exit 1
31
+ fi
32
+ done
33
+
34
+ if ! k3d cluster list 2>/dev/null | grep -q "^${CLUSTER} "; then
35
+ kubectl config current-context > "$STATE" 2>/dev/null || true
36
+ k3d cluster create "$CLUSTER" --wait
37
+ else
38
+ echo "flux-estate demo: cluster ${CLUSTER} already up — reusing."
39
+ fi
40
+
41
+ echo "flux-estate demo: installing Flux controllers (${FLUX_MANIFEST##*/download/})…"
42
+ kubectl --context "$CONTEXT" apply -f "$FLUX_MANIFEST" > /dev/null
43
+ kubectl --context "$CONTEXT" -n flux-system rollout status deploy/source-controller --timeout=180s
44
+ kubectl --context "$CONTEXT" -n flux-system rollout status deploy/kustomize-controller --timeout=180s
45
+
46
+ echo "flux-estate demo: building + applying the control plane's CRs…"
47
+ (cd control-plane && npx chant build src -o dist/control-plane.yaml --format yaml)
48
+ kubectl --context "$CONTEXT" apply -f control-plane/dist/control-plane.yaml
49
+
50
+ if [ -n "${BEHOLD_FLUX_REF:-}" ]; then
51
+ echo "flux-estate demo: syncing branch ${BEHOLD_FLUX_REF} instead of main (BEHOLD_FLUX_REF)"
52
+ kubectl --context "$CONTEXT" -n flux-system patch gitrepository behold --type merge \
53
+ -p "{\"spec\":{\"ref\":{\"branch\":\"${BEHOLD_FLUX_REF}\"}}}"
54
+ fi
55
+
56
+ echo "flux-estate demo: waiting for the reconcilers to deploy the apps…"
57
+ kubectl --context "$CONTEXT" -n flux-system wait --for=condition=Ready --timeout=180s gitrepository/behold
58
+ kubectl --context "$CONTEXT" -n flux-system wait --for=condition=Ready --timeout=180s kustomization/app-a kustomization/app-b
59
+ kubectl --context "$CONTEXT" -n app-a rollout status deploy/app-a --timeout=180s
60
+ kubectl --context "$CONTEXT" -n app-b rollout status deploy/app-b --timeout=180s
61
+ echo "flux-estate demo: up — Flux deployed both apps from the repo."
@@ -0,0 +1,120 @@
1
+ # The k3d turnkey demo — behold's Kubernetes counterpart to Loom-on-Floci
2
+
3
+ The smallest real thing on Kubernetes: one `nginx` Deployment + Service (+ a
4
+ PodDisruptionBudget), deployed from the browser with the **▶ Deploy
5
+ (k3d-apply)** header button, against a local, single-node
6
+ [k3d](https://k3d.io) cluster — no cloud account, no credentials.
7
+
8
+ ```
9
+ src/config.ts static config — app name, pinned image tag
10
+ src/web.ts WebApp composite → Deployment + Service + PodDisruptionBudget
11
+ ops/k3d-apply.op.ts ApplyOp "k3d-apply" — code → local k3d, server-side apply
12
+ chant.config.ts lexicons [k8s, temporal], k8s.profiles.local bound to k3d-behold-k3d-demo
13
+ scripts/local/ local-up.sh / local-down.sh — the k3d cluster's own lifecycle
14
+ ```
15
+
16
+ ## Run it
17
+
18
+ From the **behold** repo root:
19
+
20
+ ```sh
21
+ npm run demo:k8s
22
+ # → brings up a single-node k3d cluster ("behold-k3d-demo"),
23
+ # then serves this project at http://localhost:4600
24
+ ```
25
+
26
+ That one command does two things: `scripts/local/local-up.sh` creates the
27
+ cluster (idempotent — reuses one already up), then `behold serve --local`
28
+ points its live overlay at it. `--local`'s usual boot step (`chant emulator
29
+ up`) is a no-op here — chant's k8s lexicon has no local-emulator capability
30
+ the way Floci does for aws — so the cluster comes up via the same generic
31
+ substrate **Bring up** mechanism the Floci demo's `scripts/local/local-up.sh`
32
+ convention already uses (see `src/substrates.ts`), just run up front instead
33
+ of from a click. Ctrl-C stops `behold serve` and tears the cluster back down;
34
+ Docker or k3d missing degrades to the source graph with a clear message
35
+ instead of crashing.
36
+
37
+ 1. Open **http://localhost:4600**. The graph shows the Deployment, Service and
38
+ PodDisruptionBudget — **blue** (declared, not yet deployed).
39
+ 2. Click **▶ Deploy (k3d-apply)** in the header (or ⌘K → "Deploy: Sync"). The
40
+ now-line streams Build → Plan (a live
41
+ diff) → Apply: a Kubernetes **server-side apply**, field manager
42
+ `chant:behold-k3d-demo` (chant#1074/#1075) — deletes are **owned-only**, a
43
+ marker-scoped prune that only ever touches what chant itself applied.
44
+ 3. The three nodes flip **blue → green (managed)**. Zoom in on the Deployment
45
+ and its two Pods appear **nested underneath it** — the runtime tier
46
+ (chant#1077/behold#86): live children a Deployment's controller created,
47
+ never declared here, never classified as drift or an orphan.
48
+
49
+ ## The four things this proves live (epic #84)
50
+
51
+ behold's k8s parity (#85–#87) shipped against fixtures; this is it against a
52
+ real cluster.
53
+
54
+ **Declared, not yet deployed.** Before step 2, `/api/overlay` reports all
55
+ three nodes `_status: accent` (pending) — chant knows about them, the cluster
56
+ doesn't yet.
57
+
58
+ **Runtime children.** After apply, `/api/overlay`'s `ir.groups.byContainer`
59
+ nests the Deployment's Pods under it, each `_status: runtime` — a tier below
60
+ what's declared, sourced from the cluster's own `ownerReferences`, that
61
+ behold's zoom dial can descend into.
62
+
63
+ **Managed-fields drift.** Scale or label the Deployment out of band —
64
+ `kubectl scale deployment/web --replicas=3`, bypassing chant entirely — and
65
+ `/api/diff`'s `fieldDrift` reports `spec.replicas: { kind: "changed", declared:
66
+ 2, live: 3 }`: chant's own SSA-tracked field, now diverged from a competing
67
+ field manager (`kubectl`, visible in `kubectl get deploy/web -o json`'s
68
+ `metadata.managedFields`). Re-running `k3d-apply` at that point doesn't
69
+ silently overwrite it: chant's server-side apply refuses —
70
+
71
+ ```
72
+ k8s: server-side apply of apps/v1 Deployment web was refused — 1 field is owned by another field manager.
73
+
74
+ "kubectl" owns:
75
+ .spec.replicas
76
+
77
+ chant applied as field manager "chant:behold-k3d-demo". Taking these fields means the managers above
78
+ stop owning them, and will contest them again on their next apply.
79
+ ```
80
+
81
+ — naming the contested path and the competing manager, and leaving the field
82
+ alone rather than force-resolving it. (One honest gap found running this
83
+ live: a **new** out-of-band key with no chant-declared counterpart at all —
84
+ `kubectl label deployment/web team=platform`, which has no corresponding
85
+ `web.spec` field in `src/web.ts` — doesn't currently surface in `fieldDrift`,
86
+ only a value change to a field chant *does* declare does. Worth a follow-up
87
+ issue on the chant side; noted here rather than papered over.)
88
+
89
+ **Unobserved.** `chant.config.ts` binds environment `local` to kubectl context
90
+ `k3d-behold-k3d-demo` (chant#1100) — every read/apply checks that binding
91
+ against whatever context is actually ambient, and refuses rather than reading
92
+ the wrong cluster. Switch away from it for a moment —
93
+
94
+ ```sh
95
+ kubectl config use-context <anything-else>
96
+ ```
97
+
98
+ — and `/api/overlay` flips every node to `_status: neutral` with
99
+ `unobservedReason: "read-failed"`, Pods and all: an honest "did not look",
100
+ never a false "these are all gone." Switch back
101
+ (`kubectl config use-context k3d-behold-k3d-demo`) and the next refresh
102
+ recovers cleanly.
103
+
104
+ ## Cleanup
105
+
106
+ `Ctrl-C` on `npm run demo:k8s` runs `scripts/local/local-down.sh`
107
+ automatically: deletes the `behold-k3d-demo` cluster and restores whatever
108
+ kubectl context was current before `local-up.sh` ran. Safe to run by hand too:
109
+
110
+ ```sh
111
+ bash scripts/local/local-down.sh
112
+ ```
113
+
114
+ ## Note for chant#1179
115
+
116
+ This Op's `delete: "owned-only"` exercises the same typed, marker-scoped
117
+ prune path chant#1179 wants a live k3d E2E for. This demo isn't that test —
118
+ it's a manual walkthrough, not an automated assertion of "exactly the orphan
119
+ and nothing else was deleted" — but it does confirm the path runs against a
120
+ real cluster, not just `fakeCluster`.
@@ -0,0 +1,21 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
3
+
4
+ // Bound to the demo's own cluster (scripts/local/local-up.sh creates it, k3d
5
+ // names the context "k3d-behold-k3d-demo") rather than whatever kubectl
6
+ // context happens to be ambient — chant#1100. A declared binding is checked
7
+ // against the ambient context on every live read/apply; a mismatch refuses
8
+ // loudly instead of silently reading the wrong cluster. The walkthrough's
9
+ // "unobserved" step (README.md) is exactly that refusal, produced on purpose
10
+ // by switching away from this context for a moment.
11
+ export default {
12
+ lexicons: ["k8s", "temporal"],
13
+ sourceDir: "src",
14
+ environments: ["local"],
15
+ ownership: { stack: "behold-k3d-demo", env: "local" },
16
+ k8s: {
17
+ profiles: {
18
+ local: { context: "k3d-behold-k3d-demo" },
19
+ },
20
+ } satisfies K8sChantConfig,
21
+ } satisfies ChantConfig;
@@ -0,0 +1,18 @@
1
+ import { ApplyOp } from "@intentius/chant-lexicon-temporal";
2
+
3
+ // code → local k3d (chant#704), no cloud account. scripts/local/local-up.sh
4
+ // (run by `npm run demo:k8s` before behold starts serving) brings the cluster
5
+ // up; behold's Run button on this Op deploys the declared app to it —
6
+ // build → plan (live diff) → server-side apply, field manager
7
+ // "chant:behold-k3d-demo" (chant#1074/#1075). Deletes are owned-only: a
8
+ // marker-scoped prune that never touches anything chant didn't declare — the
9
+ // same path chant#1179 wants a live E2E for; this Op exercises it, but isn't
10
+ // that test.
11
+ const { op } = ApplyOp({
12
+ name: "k3d-apply",
13
+ env: "local",
14
+ target: "kubectl",
15
+ output: "app.yaml",
16
+ delete: "owned-only",
17
+ });
18
+ export default op;