@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 +9 -0
- package/dist/cli.js +24 -9
- package/example-flux-estate/README.md +41 -0
- package/example-flux-estate/app-a/chant.config.ts +18 -0
- package/example-flux-estate/app-a/manifests/app.yaml +56 -0
- package/example-flux-estate/app-a/package.json +13 -0
- package/example-flux-estate/app-a/src/app.ts +35 -0
- package/example-flux-estate/app-a/tsconfig.json +1 -0
- package/example-flux-estate/app-b/chant.config.ts +20 -0
- package/example-flux-estate/app-b/manifests/app.yaml +54 -0
- package/example-flux-estate/app-b/package.json +13 -0
- package/example-flux-estate/app-b/src/app.ts +37 -0
- package/example-flux-estate/app-b/tsconfig.json +1 -0
- package/example-flux-estate/control-plane/chant.config.ts +19 -0
- package/example-flux-estate/control-plane/package.json +13 -0
- package/example-flux-estate/control-plane/src/flux.ts +51 -0
- package/example-flux-estate/control-plane/tsconfig.json +1 -0
- package/example-flux-estate/package-lock.json +1949 -0
- package/example-flux-estate/package.json +10 -0
- package/example-flux-estate/scripts/estate-down.sh +19 -0
- package/example-flux-estate/scripts/estate-up.sh +61 -0
- package/example-k8s/package-lock.json +43 -40
- package/example-k8s/package.json +4 -4
- package/example-writes/package-lock.json +18 -16
- package/example-writes/package.json +3 -3
- package/package.json +2 -1
- package/web/app.js +10 -0
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
|
-
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
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 <
|
|
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))
|
|
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
|
|
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"] }
|