@intentius/behold 0.5.0 → 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.
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
@@ -2593,7 +2593,7 @@ function namespaceMismatchNote(nodes) {
2593
2593
  const k8s = nodes.filter((n) => n?.lexicon === "k8s");
2594
2594
  if (k8s.length === 0) return void 0;
2595
2595
  const pending = k8s.filter((n) => n.attrs?._status === "accent" && !CLUSTER_SCOPED.has(n.kind ?? ""));
2596
- if (pending.length < 3) return void 0;
2596
+ if (pending.length < 2) return void 0;
2597
2597
  if (pending.some((n) => typeof n.attrs?.metadata?.namespace === "string" && n.attrs.metadata.namespace)) return void 0;
2598
2598
  if (!k8s.some((n) => n.attrs?._status === "good")) return void 0;
2599
2599
  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`;
@@ -4742,6 +4742,8 @@ function loadDemoRegistry(pkgRoot) {
4742
4742
  }
4743
4743
  if (!Array.isArray(d.requires) || d.requires.some((r) => typeof r !== "string")) return false;
4744
4744
  if (!d.serve || typeof d.serve !== "object") return false;
4745
+ if (d.serve.dirs !== void 0 && (!Array.isArray(d.serve.dirs) || d.serve.dirs.some((x) => typeof x !== "string") || !d.serve.dirs.length))
4746
+ return false;
4745
4747
  return true;
4746
4748
  });
4747
4749
  }
@@ -5173,7 +5175,8 @@ async function runDemo(rest) {
5173
5175
  }
5174
5176
  process.stdout.write(`behold demo ${entry.name} \u2192 serving. Blue = declared; Deploy turns it green.
5175
5177
  `);
5176
- const serveArgs = ["serve", target, "--port", String(port)];
5178
+ const serveDirs = entry.serve.dirs?.length ? entry.serve.dirs.map((d) => join13(target, d)) : [target];
5179
+ const serveArgs = ["serve", ...serveDirs, "--port", String(port)];
5177
5180
  if (entry.serve.local) serveArgs.push("--local");
5178
5181
  if (entry.serve.env) serveArgs.push("--env", entry.serve.env);
5179
5182
  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 — 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.41.20",
11
+ "@intentius/chant-lexicon-k8s": "^0.41.20"
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 — 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.41.20",
11
+ "@intentius/chant-lexicon-k8s": "^0.41.20"
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.41.20",
11
+ "@intentius/chant-lexicon-k8s": "^0.41.20"
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"] }