@intentius/behold 0.5.0 → 0.7.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.
package/demos.json CHANGED
@@ -17,6 +17,15 @@
17
17
  "setup": "bash scripts/local/local-up.sh",
18
18
  "serve": { "local": true, "env": "local" }
19
19
  },
20
+ {
21
+ "name": "flux-estate",
22
+ "description": "A GitOps estate: a Flux control plane (GitRepository + Kustomizations) plus two app projects, served as one composed estate — cross-stack sourceRef edges, estate-wide drift, and the namespace-mismatch note on purpose (app-b uses targetNamespace). k3d + Flux, no cloud. scripts/estate-down.sh removes the cluster.",
23
+ "source": "bundled",
24
+ "dir": "example-flux-estate",
25
+ "requires": ["docker", "k3d", "kubectl"],
26
+ "setup": "bash scripts/estate-up.sh",
27
+ "serve": { "env": "local", "dirs": ["control-plane", "app-a", "app-b"] }
28
+ },
20
29
  {
21
30
  "name": "fountain",
22
31
  "description": "The mature estate: self-hosted fountain (a real app) on a throwaway k3d cluster — tiers (try the ha tier picker), seams, drift, runtime Pods. Clones INTENTIUS/fountain-ops; ~5 minutes, mostly image pulls. `just down` in the clone removes the cluster.",
package/dist/cli.js CHANGED
@@ -1972,6 +1972,12 @@ import { join as join6 } from "node:path";
1972
1972
  function overlayBoxTitle(name) {
1973
1973
  return `overlay ${name}`;
1974
1974
  }
1975
+ var ROOT_ANNOTATION = "chant.intentius.io/kustomize-root";
1976
+ function annotatedRoot(node) {
1977
+ const metadata2 = node.attrs?.metadata;
1978
+ const v = metadata2?.annotations?.[ROOT_ANNOTATION];
1979
+ return typeof v === "string" && v ? v : void 0;
1980
+ }
1975
1981
  function dirOf2(node) {
1976
1982
  const file = node.sourceLoc?.file;
1977
1983
  if (typeof file !== "string") return "";
@@ -2002,12 +2008,15 @@ function projectKustomizeLogical(ir, sourceRoots, exists = existsSync5) {
2002
2008
  };
2003
2009
  const rootCache = /* @__PURE__ */ new Map();
2004
2010
  for (const n of k8s) {
2005
- const dir = dirOf2(n);
2006
- if (dir === "") continue;
2007
- if (!rootCache.has(dir)) rootCache.set(dir, kustomizationRoot(bases, dir, exists));
2008
- const root = rootCache.get(dir);
2011
+ let root = annotatedRoot(n);
2012
+ if (root === void 0) {
2013
+ const dir = dirOf2(n);
2014
+ if (dir === "") continue;
2015
+ if (!rootCache.has(dir)) rootCache.set(dir, kustomizationRoot(bases, dir, exists));
2016
+ root = rootCache.get(dir);
2017
+ }
2009
2018
  if (root === void 0) continue;
2010
- const name = root.split("/").pop() || root;
2019
+ const name = root.replace(/\/+$/, "").split("/").pop() || root;
2011
2020
  child(overlayBoxTitle(name), n.id);
2012
2021
  }
2013
2022
  return { ir: { nodes: [], edges: [], groups: {} }, byContainer };
@@ -2593,7 +2602,7 @@ function namespaceMismatchNote(nodes) {
2593
2602
  const k8s = nodes.filter((n) => n?.lexicon === "k8s");
2594
2603
  if (k8s.length === 0) return void 0;
2595
2604
  const pending = k8s.filter((n) => n.attrs?._status === "accent" && !CLUSTER_SCOPED.has(n.kind ?? ""));
2596
- if (pending.length < 3) return void 0;
2605
+ if (pending.length < 2) return void 0;
2597
2606
  if (pending.some((n) => typeof n.attrs?.metadata?.namespace === "string" && n.attrs.metadata.namespace)) return void 0;
2598
2607
  if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
2599
2608
  return `${pending.length} pending k8s objects declare no metadata.namespace \u2014 if a controller stamps it at apply time (e.g. Flux's targetNamespace), the live read looked in "default", not where they run`;
@@ -2984,16 +2993,19 @@ function nodeDiff(json, nodeId) {
2984
2993
  if (drift) return { category: "drifted", changes: drift.changes ?? [] };
2985
2994
  const unobserved = r.unobserved?.find((u) => u.name === nodeId);
2986
2995
  if (unobserved) {
2996
+ const queried = unobserved.queried ?? r.queried?.[nodeId];
2987
2997
  return {
2988
2998
  category: "unobserved",
2989
2999
  changes: [],
2990
3000
  unobservedReason: unobserved.reason,
2991
- ...unobserved.detail ? { unobservedDetail: unobserved.detail } : {}
3001
+ ...unobserved.detail ? { unobservedDetail: unobserved.detail } : {},
3002
+ ...queried ? { queried } : {}
2992
3003
  };
2993
3004
  }
2994
3005
  const runtimeChild = r.runtimeChildren?.find((rc) => rc.name === nodeId);
2995
3006
  if (runtimeChild) return { category: "runtime", changes: [], runtimeOwner: runtimeChild.owner };
2996
- if (r.missing?.includes(nodeId)) return { category: "missing", changes: [] };
3007
+ if (r.missing?.includes(nodeId))
3008
+ return { category: "missing", changes: [], ...r.queried?.[nodeId] ? { queried: r.queried[nodeId] } : {} };
2997
3009
  if (r.orphan?.includes(nodeId)) return { category: "orphan", changes: [] };
2998
3010
  if (r.disappeared?.includes(nodeId)) return { category: "disappeared", changes: [] };
2999
3011
  if (r.newlyObserved?.includes(nodeId)) return { category: "newlyObserved", changes: [] };
@@ -4742,6 +4754,8 @@ function loadDemoRegistry(pkgRoot) {
4742
4754
  }
4743
4755
  if (!Array.isArray(d.requires) || d.requires.some((r) => typeof r !== "string")) return false;
4744
4756
  if (!d.serve || typeof d.serve !== "object") return false;
4757
+ if (d.serve.dirs !== void 0 && (!Array.isArray(d.serve.dirs) || d.serve.dirs.some((x) => typeof x !== "string") || !d.serve.dirs.length))
4758
+ return false;
4745
4759
  return true;
4746
4760
  });
4747
4761
  }
@@ -5173,7 +5187,8 @@ async function runDemo(rest) {
5173
5187
  }
5174
5188
  process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
5175
5189
  `);
5176
- const serveArgs = ["serve", target, "--port", String(port)];
5190
+ const serveDirs = entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join13(target, d)) : [target];
5191
+ const serveArgs = ["serve", ...serveDirs, "--port", String(port)];
5177
5192
  if (entry.serve.local) serveArgs.push("--local");
5178
5193
  if (entry.serve.env) serveArgs.push("--env", entry.serve.env);
5179
5194
  await run3(serveArgs);
@@ -0,0 +1,41 @@
1
+ # flux-estate — behold's GitOps estate demo (#211)
2
+
3
+ Three chant projects served as one composed estate:
4
+
5
+ - **control-plane/** — the Flux machinery: a `GitRepository` pointing at
6
+ behold's own public repo and one `Kustomization` per app. The reconcilers
7
+ deploy the apps; this project never applies a workload itself.
8
+ - **app-a/** — a small web workload with explicit `metadata.namespace` on
9
+ every object: the fully-resolved half.
10
+ - **app-b/** — the same workload with **no** namespace anywhere: the control
11
+ plane's `targetNamespace` stamps it at apply time. Idiomatic Flux, and the
12
+ deliberate exhibit of behold's namespace-mismatch note (behold#192) — a
13
+ plain live read scopes to `default`, finds nothing, and behold explains the
14
+ all-pending paint instead of letting it read as "not deployed".
15
+
16
+ ```sh
17
+ npx @intentius/behold demo flux-estate
18
+ ```
19
+
20
+ That stands up a throwaway k3d cluster (`behold-flux-demo`), installs Flux's
21
+ controllers straight from the pinned release manifest (no `flux` CLI needed),
22
+ applies the control plane, waits for the reconcilers to deploy both apps from
23
+ this repo, and serves the three projects composed. What to look at:
24
+
25
+ - The **estate**: per-project boundary boxes, and the `sourceRef` edges wiring
26
+ Kustomization → GitRepository across stacks.
27
+ - The **overlay** (env `local`): app-a green, the control plane's CRs green,
28
+ app-b pending **with the note explaining why**.
29
+ - The **runtime tier** on the control plane alone
30
+ (`behold serve control-plane --env local`, zoom: runtime): the workloads
31
+ Flux deployed, attributed back to the declaring Kustomization via its
32
+ labels (chant#1549).
33
+
34
+ The app `manifests/` are committed `chant build` output — Flux syncs them
35
+ from GitHub `main`, so changes to an app's source need a rebuild
36
+ (`npm run build` in the app) and a merge before the cluster follows. To
37
+ verify an unmerged branch: `BEHOLD_FLUX_REF=<branch> bash scripts/estate-up.sh`
38
+ patches the live `GitRepository` (the declared source stays `main`).
39
+
40
+ `bash scripts/estate-down.sh` deletes the cluster and restores your previous
41
+ kubectl context.
@@ -0,0 +1,18 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
3
+
4
+ // App project A. Deployed by the control plane's Flux Kustomization, never by
5
+ // this project — its committed manifests/ (chant build output) are what Flux
6
+ // syncs from the repo. behold serves it as part of the estate; the live
7
+ // overlay reads the cluster through the same binding as every other member.
8
+ export default {
9
+ lexicons: ["k8s"],
10
+ sourceDir: "src",
11
+ environments: ["local"],
12
+ ownership: { stack: "app-a", env: "local" },
13
+ k8s: {
14
+ profiles: {
15
+ local: { context: "k3d-behold-flux-demo" },
16
+ },
17
+ } satisfies K8sChantConfig,
18
+ } satisfies ChantConfig;
@@ -0,0 +1,56 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: app-a
5
+ namespace: app-a
6
+ labels:
7
+ app.kubernetes.io/managed-by: chant
8
+ chant.intentius.io/stack: app-a
9
+ chant.intentius.io/env: local
10
+ app: app-a
11
+ spec:
12
+ replicas: 1
13
+ selector:
14
+ matchLabels:
15
+ app: app-a
16
+ template:
17
+ metadata:
18
+ labels:
19
+ app: app-a
20
+ spec:
21
+ containers:
22
+ - name: web
23
+ image: nginxinc/nginx-unprivileged:1.27-alpine
24
+ ports:
25
+ - containerPort: 8080
26
+ securityContext:
27
+ runAsNonRoot: true
28
+ runAsUser: 101
29
+ allowPrivilegeEscalation: false
30
+ capabilities:
31
+ drop:
32
+ - ALL
33
+ resources:
34
+ requests:
35
+ cpu: '25m'
36
+ memory: '32Mi'
37
+ limits:
38
+ cpu: '100m'
39
+ memory: '64Mi'
40
+
41
+ ---
42
+ apiVersion: v1
43
+ kind: Service
44
+ metadata:
45
+ name: app-a
46
+ namespace: app-a
47
+ labels:
48
+ app.kubernetes.io/managed-by: chant
49
+ chant.intentius.io/stack: app-a
50
+ chant.intentius.io/env: local
51
+ spec:
52
+ selector:
53
+ app: app-a
54
+ ports:
55
+ - port: 80
56
+ targetPort: 8080
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "behold-example-flux-app-a",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "App project A: declares its workload WITH explicit namespaces \u2014 the fully-resolved half of the estate.",
6
+ "scripts": {
7
+ "build": "chant build src -o manifests/app.yaml --format yaml"
8
+ },
9
+ "dependencies": {
10
+ "@intentius/chant": "^0.44.2",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.2"
12
+ }
13
+ }
@@ -0,0 +1,35 @@
1
+ // App A: one small web workload, namespace declared EXPLICITLY on every
2
+ // object. Contrast app-b, which leaves namespace to the control plane's
3
+ // targetNamespace — the two apps together show both halves of the Flux
4
+ // namespace story (behold#192).
5
+ import { Deployment, Service } from "@intentius/chant-lexicon-k8s";
6
+
7
+ const labels = { app: "app-a" };
8
+ const image = "nginxinc/nginx-unprivileged:1.27-alpine";
9
+
10
+ export const deployment = new Deployment({
11
+ metadata: { name: "app-a", namespace: "app-a", labels },
12
+ spec: {
13
+ replicas: 1,
14
+ selector: { matchLabels: labels },
15
+ template: {
16
+ metadata: { labels },
17
+ spec: {
18
+ containers: [
19
+ {
20
+ name: "web",
21
+ image,
22
+ ports: [{ containerPort: 8080 }],
23
+ securityContext: { runAsNonRoot: true, runAsUser: 101, allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } },
24
+ resources: { requests: { cpu: "25m", memory: "32Mi" }, limits: { cpu: "100m", memory: "64Mi" } },
25
+ },
26
+ ],
27
+ },
28
+ },
29
+ },
30
+ });
31
+
32
+ export const service = new Service({
33
+ metadata: { name: "app-a", namespace: "app-a" },
34
+ spec: { selector: labels, ports: [{ port: 80, targetPort: 8080 }] },
35
+ });
@@ -0,0 +1 @@
1
+ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*", "ops/**/*", "chant.config.ts"] }
@@ -0,0 +1,20 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
3
+
4
+ // App project B — the targetNamespace half of the estate: its objects declare
5
+ // no metadata.namespace (see src/app.ts), so the control plane's
6
+ // Kustomization stamps `app-b` at apply time. That is idiomatic Flux, AND it
7
+ // means a plain live read scopes to `default` and finds nothing — behold's
8
+ // namespace-mismatch note (behold#192) names exactly this, on purpose, in
9
+ // this demo.
10
+ export default {
11
+ lexicons: ["k8s"],
12
+ sourceDir: "src",
13
+ environments: ["local"],
14
+ ownership: { stack: "app-b", env: "local" },
15
+ k8s: {
16
+ profiles: {
17
+ local: { context: "k3d-behold-flux-demo" },
18
+ },
19
+ } satisfies K8sChantConfig,
20
+ } satisfies ChantConfig;
@@ -0,0 +1,54 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: app-b
5
+ labels:
6
+ app.kubernetes.io/managed-by: chant
7
+ chant.intentius.io/stack: app-b
8
+ chant.intentius.io/env: local
9
+ app: app-b
10
+ spec:
11
+ replicas: 1
12
+ selector:
13
+ matchLabels:
14
+ app: app-b
15
+ template:
16
+ metadata:
17
+ labels:
18
+ app: app-b
19
+ spec:
20
+ containers:
21
+ - name: web
22
+ image: nginxinc/nginx-unprivileged:1.27-alpine
23
+ ports:
24
+ - containerPort: 8080
25
+ securityContext:
26
+ runAsNonRoot: true
27
+ runAsUser: 101
28
+ allowPrivilegeEscalation: false
29
+ capabilities:
30
+ drop:
31
+ - ALL
32
+ resources:
33
+ requests:
34
+ cpu: '25m'
35
+ memory: '32Mi'
36
+ limits:
37
+ cpu: '100m'
38
+ memory: '64Mi'
39
+
40
+ ---
41
+ apiVersion: v1
42
+ kind: Service
43
+ metadata:
44
+ name: app-b
45
+ labels:
46
+ app.kubernetes.io/managed-by: chant
47
+ chant.intentius.io/stack: app-b
48
+ chant.intentius.io/env: local
49
+ spec:
50
+ selector:
51
+ app: app-b
52
+ ports:
53
+ - port: 80
54
+ targetPort: 8080
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "behold-example-flux-app-b",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "App project B: declares NO metadata.namespace \u2014 Flux's targetNamespace stamps it at apply time (the behold#192 shape).",
6
+ "scripts": {
7
+ "build": "chant build src -o manifests/app.yaml --format yaml"
8
+ },
9
+ "dependencies": {
10
+ "@intentius/chant": "^0.44.2",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.2"
12
+ }
13
+ }
@@ -0,0 +1,37 @@
1
+ // App B: same small workload as app-a, with NO metadata.namespace anywhere —
2
+ // the control plane's Kustomization stamps targetNamespace: app-b at apply
3
+ // time. Idiomatic Flux (that's what the field is for), and the deliberate
4
+ // exhibit of behold's namespace-mismatch note (behold#192): a plain live read
5
+ // scopes to `default`, finds nothing, and behold explains the all-pending
6
+ // paint instead of letting it read as "not deployed".
7
+ import { Deployment, Service } from "@intentius/chant-lexicon-k8s";
8
+
9
+ const labels = { app: "app-b" };
10
+ const image = "nginxinc/nginx-unprivileged:1.27-alpine";
11
+
12
+ export const deployment = new Deployment({
13
+ metadata: { name: "app-b", labels },
14
+ spec: {
15
+ replicas: 1,
16
+ selector: { matchLabels: labels },
17
+ template: {
18
+ metadata: { labels },
19
+ spec: {
20
+ containers: [
21
+ {
22
+ name: "web",
23
+ image,
24
+ ports: [{ containerPort: 8080 }],
25
+ securityContext: { runAsNonRoot: true, runAsUser: 101, allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } },
26
+ resources: { requests: { cpu: "25m", memory: "32Mi" }, limits: { cpu: "100m", memory: "64Mi" } },
27
+ },
28
+ ],
29
+ },
30
+ },
31
+ },
32
+ });
33
+
34
+ export const service = new Service({
35
+ metadata: { name: "app-b" },
36
+ spec: { selector: labels, ports: [{ port: 80, targetPort: 8080 }] },
37
+ });
@@ -0,0 +1 @@
1
+ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*", "ops/**/*", "chant.config.ts"] }
@@ -0,0 +1,19 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+ import type { K8sChantConfig } from "@intentius/chant-lexicon-k8s";
3
+
4
+ // The estate's control plane: it declares the Flux machinery (GitRepository +
5
+ // one Kustomization per app), and the reconcilers deploy the app projects —
6
+ // this project never applies a workload itself. Bound to the demo's own
7
+ // cluster (scripts/estate-up.sh creates it) so a live read can never observe
8
+ // whatever kubectl context happens to be ambient (chant#1100).
9
+ export default {
10
+ lexicons: ["k8s"],
11
+ sourceDir: "src",
12
+ environments: ["local"],
13
+ ownership: { stack: "flux-control-plane", env: "local" },
14
+ k8s: {
15
+ profiles: {
16
+ local: { context: "k3d-behold-flux-demo" },
17
+ },
18
+ } satisfies K8sChantConfig,
19
+ } satisfies ChantConfig;
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "behold-example-flux-control-plane",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "The estate's control plane: the Flux CRs (GitRepository + Kustomizations) that deploy the app projects.",
6
+ "scripts": {
7
+ "build": "chant build src -o dist/control-plane.yaml --format yaml"
8
+ },
9
+ "dependencies": {
10
+ "@intentius/chant": "^0.44.2",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.2"
12
+ }
13
+ }
@@ -0,0 +1,51 @@
1
+ // The control plane, GitOps-style: Flux reconcilers deploy the app projects
2
+ // FROM THIS REPO. The GitRepository points at behold's own public repo; each
3
+ // Kustomization applies one app project's committed manifests into that app's
4
+ // namespace via targetNamespace — which is why app-b's objects declare no
5
+ // metadata.namespace (see ../../app-b/src/app.ts): Flux stamps it at apply
6
+ // time. That is the normal Flux estate shape, and exactly the shape behold's
7
+ // namespace-mismatch note (behold#192) exists to explain.
8
+ //
9
+ // scripts/estate-up.sh applies the BUILT form of this project (chant build →
10
+ // dist/control-plane.yaml) once, after installing Flux; from then on the
11
+ // reconcilers own the apps.
12
+ import { Namespace, GitRepository, Kustomization } from "@intentius/chant-lexicon-k8s";
13
+
14
+ // The app namespaces are control-plane concerns: a Kustomization's
15
+ // targetNamespace must already exist — Flux does not create it.
16
+ export const nsAppA = new Namespace({ metadata: { name: "app-a" } });
17
+ export const nsAppB = new Namespace({ metadata: { name: "app-b" } });
18
+
19
+ export const source = new GitRepository({
20
+ metadata: { name: "behold", namespace: "flux-system" },
21
+ spec: {
22
+ interval: "1m",
23
+ url: "https://github.com/INTENTIUS/behold",
24
+ // main is where the committed manifests live once merged. Verifying a
25
+ // not-yet-merged branch: estate-up.sh patches the LIVE object's ref when
26
+ // BEHOLD_FLUX_REF is set — the declared source stays main.
27
+ ref: { branch: "main" },
28
+ },
29
+ });
30
+
31
+ export const appA = new Kustomization({
32
+ metadata: { name: "app-a", namespace: "flux-system" },
33
+ spec: {
34
+ interval: "1m",
35
+ prune: true,
36
+ targetNamespace: "app-a",
37
+ sourceRef: { kind: "GitRepository", name: "behold" },
38
+ path: "./example-flux-estate/app-a/manifests",
39
+ },
40
+ });
41
+
42
+ export const appB = new Kustomization({
43
+ metadata: { name: "app-b", namespace: "flux-system" },
44
+ spec: {
45
+ interval: "1m",
46
+ prune: true,
47
+ targetNamespace: "app-b",
48
+ sourceRef: { kind: "GitRepository", name: "behold" },
49
+ path: "./example-flux-estate/app-b/manifests",
50
+ },
51
+ });
@@ -0,0 +1 @@
1
+ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*", "ops/**/*", "chant.config.ts"] }