@intentius/behold 0.7.0 → 0.8.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 (76) hide show
  1. package/AGENTS.md +9 -2
  2. package/README.md +12 -2
  3. package/demos.json +8 -0
  4. package/dist/cli.js +750 -132
  5. package/example-argo-estate/README.md +46 -0
  6. package/example-argo-estate/app-a/chant.config.ts +10 -0
  7. package/example-argo-estate/app-a/manifests/app.yaml +54 -0
  8. package/example-argo-estate/app-a/package.json +13 -0
  9. package/example-argo-estate/app-a/src/app.ts +35 -0
  10. package/example-argo-estate/app-a/tsconfig.json +1 -0
  11. package/example-argo-estate/app-b/chant.config.ts +9 -0
  12. package/example-argo-estate/app-b/manifests/app.yaml +54 -0
  13. package/example-argo-estate/app-b/package.json +13 -0
  14. package/example-argo-estate/app-b/src/app.ts +34 -0
  15. package/example-argo-estate/app-b/tsconfig.json +1 -0
  16. package/example-argo-estate/control-plane/chant.config.ts +14 -0
  17. package/example-argo-estate/control-plane/package.json +13 -0
  18. package/example-argo-estate/control-plane/src/argo.ts +69 -0
  19. package/example-argo-estate/control-plane/tsconfig.json +1 -0
  20. package/example-argo-estate/package-lock.json +1949 -0
  21. package/example-argo-estate/package.json +10 -0
  22. package/example-flux-estate/README.md +13 -8
  23. package/example-flux-estate/app-a/package.json +2 -2
  24. package/example-flux-estate/app-b/package.json +2 -2
  25. package/example-flux-estate/control-plane/package.json +2 -2
  26. package/example-flux-estate/control-plane/src/flux.ts +6 -0
  27. package/example-flux-estate/package-lock.json +17 -17
  28. package/example-k8s/README.md +16 -2
  29. package/example-k8s/base/deployment.yaml +21 -0
  30. package/example-k8s/base/kustomization.yaml +3 -0
  31. package/example-k8s/base/service.yaml +12 -0
  32. package/example-k8s/chant.config.ts +10 -0
  33. package/example-k8s/overlays/dev/kustomization.yaml +6 -0
  34. package/example-k8s/package-lock.json +18 -18
  35. package/example-k8s/package.json +3 -3
  36. package/example-writes/package-lock.json +14 -14
  37. package/example-writes/package.json +3 -3
  38. package/package.json +3 -2
  39. package/web/app.js +350 -22
  40. package/web/icons/cncf/argo.svg +1 -0
  41. package/web/icons/cncf/flux.svg +1 -0
  42. package/web/icons/cncf/helm.svg +1 -0
  43. package/web/icons/k8s/c-role.svg +1 -0
  44. package/web/icons/k8s/cm.svg +1 -0
  45. package/web/icons/k8s/crb.svg +1 -0
  46. package/web/icons/k8s/crd.svg +1 -0
  47. package/web/icons/k8s/cronjob.svg +1 -0
  48. package/web/icons/k8s/deploy.svg +1 -0
  49. package/web/icons/k8s/ds.svg +1 -0
  50. package/web/icons/k8s/ep.svg +1 -0
  51. package/web/icons/k8s/group.svg +1 -0
  52. package/web/icons/k8s/hpa.svg +1 -0
  53. package/web/icons/k8s/ing.svg +1 -0
  54. package/web/icons/k8s/job.svg +1 -0
  55. package/web/icons/k8s/limits.svg +1 -0
  56. package/web/icons/k8s/netpol.svg +1 -0
  57. package/web/icons/k8s/ns.svg +1 -0
  58. package/web/icons/k8s/pod.svg +1 -0
  59. package/web/icons/k8s/psp.svg +1 -0
  60. package/web/icons/k8s/pv.svg +1 -0
  61. package/web/icons/k8s/pvc.svg +1 -0
  62. package/web/icons/k8s/quota.svg +1 -0
  63. package/web/icons/k8s/rb.svg +1 -0
  64. package/web/icons/k8s/role.svg +1 -0
  65. package/web/icons/k8s/rs.svg +1 -0
  66. package/web/icons/k8s/sa.svg +1 -0
  67. package/web/icons/k8s/sc.svg +1 -0
  68. package/web/icons/k8s/secret.svg +1 -0
  69. package/web/icons/k8s/sts.svg +1 -0
  70. package/web/icons/k8s/svc.svg +1 -0
  71. package/web/icons/k8s/user.svg +1 -0
  72. package/web/icons/k8s/vol.svg +1 -0
  73. package/web/index.html +192 -84
  74. package/web/layout-store.js +118 -0
  75. package/web/layout-store.test.js +163 -0
  76. package/web/theme.js +58 -6
@@ -0,0 +1,46 @@
1
+ # argo-estate — behold's Argo CD estate demo (#235)
2
+
3
+ Three chant projects served as one composed estate, the Argo mirror of
4
+ [example-flux-estate](../example-flux-estate):
5
+
6
+ - **control-plane/** — the Argo machinery: an `AppProject` fencing which repo
7
+ and which namespaces the estate may deploy, and one `Application` per app
8
+ syncing that app's committed manifests from behold's own public repo. Argo's
9
+ controller does the applying; this project never applies a workload itself.
10
+ - **app-a/**, **app-b/** — small web workloads in namespaces `app-a` and
11
+ `app-b`, the namespaces the Applications name as their sync destinations.
12
+
13
+ ```sh
14
+ npx @intentius/behold demo argo-estate
15
+ ```
16
+
17
+ Unlike flux-estate this one is **declared only** — no cluster, no Argo install,
18
+ no Docker. It copies out, `npm install`s, and serves. What to look at:
19
+
20
+ - The **estate**: per-project boundary boxes, and the `project` edges joining
21
+ both Applications to the AppProject they name (behold#222) — the join chant
22
+ lints as ARGO002, so an estate whose lint passes cannot be drawn wrong.
23
+ The entity graph carries them at the attributes tier (`?detail=3`).
24
+ - The **logical lens** on the control plane alone
25
+ (`behold serve control-plane`, zoom: logical): `namespace app-a` and
26
+ `namespace app-b` drawn as boxes even though the control plane declares no
27
+ `Namespace` object anywhere. They come from each Application's
28
+ `spec.destination.namespace` — a namespace Argo will own objects in is a
29
+ namespace the estate is committed to. Serve all three members and the apps'
30
+ Deployments and Services sit inside those boxes.
31
+
32
+ That last one is the Argo/Flux difference worth noticing: flux-estate's control
33
+ plane must declare the app namespaces itself (a Kustomization's
34
+ `targetNamespace` must already exist), while here `CreateNamespace=true` means
35
+ Argo makes them — so the box exists on the strength of the destination alone.
36
+
37
+ Argo's ordering is the `argocd.argoproj.io/sync-wave` annotation, so unlike
38
+ Flux's `dependsOn` (behold#223) there is no edge to draw for it: app-a is wave
39
+ 0, app-b wave 1, and the picture says nothing about it.
40
+
41
+ The app `manifests/` are committed `chant build` output — the path each
42
+ Application syncs. Changing an app's source means a rebuild (`npm run build` in
43
+ the app). `chant build` in the control plane warns ARGO005 on both
44
+ Applications: `source.path` is repo-relative (that's what Argo resolves it
45
+ against), not relative to the build root, so it doesn't resolve locally. Fine
46
+ here, and the check's own message says so.
@@ -0,0 +1,10 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+
3
+ // App project A. Argo syncs its committed manifests/ (chant build output) from
4
+ // the repo; this project applies nothing itself. Declared-only demo, so no
5
+ // kube context is bound — see ../control-plane/chant.config.ts.
6
+ export default {
7
+ lexicons: ["k8s"],
8
+ sourceDir: "src",
9
+ ownership: { stack: "argo-app-a" },
10
+ } satisfies ChantConfig;
@@ -0,0 +1,54 @@
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: argo-app-a
9
+ app: app-a
10
+ spec:
11
+ replicas: 1
12
+ selector:
13
+ matchLabels:
14
+ app: app-a
15
+ template:
16
+ metadata:
17
+ labels:
18
+ app: app-a
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-a
45
+ namespace: app-a
46
+ labels:
47
+ app.kubernetes.io/managed-by: chant
48
+ chant.intentius.io/stack: argo-app-a
49
+ spec:
50
+ selector:
51
+ app: app-a
52
+ ports:
53
+ - port: 80
54
+ targetPort: 8080
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "behold-example-argo-app-a",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "App project A: a small web workload in namespace app-a, synced by the control plane's Argo Application.",
6
+ "scripts": {
7
+ "build": "chant build src -o manifests/app.yaml --format yaml"
8
+ },
9
+ "dependencies": {
10
+ "@intentius/chant": "^0.44.3",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.3"
12
+ }
13
+ }
@@ -0,0 +1,35 @@
1
+ // App A: one small web workload, namespace declared on every object — the same
2
+ // namespace the control plane's Application names as its sync destination.
3
+ // That agreement is the point: behold draws the namespace box from the
4
+ // Application's destination and fills it with these cards.
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,9 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+
3
+ // App project B — same shape as app-a, second sync wave. Declared-only demo,
4
+ // so no kube context is bound; see ../control-plane/chant.config.ts.
5
+ export default {
6
+ lexicons: ["k8s"],
7
+ sourceDir: "src",
8
+ ownership: { stack: "argo-app-b" },
9
+ } satisfies ChantConfig;
@@ -0,0 +1,54 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: app-b
5
+ namespace: app-b
6
+ labels:
7
+ app.kubernetes.io/managed-by: chant
8
+ chant.intentius.io/stack: argo-app-b
9
+ app: app-b
10
+ spec:
11
+ replicas: 2
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
+ namespace: app-b
46
+ labels:
47
+ app.kubernetes.io/managed-by: chant
48
+ chant.intentius.io/stack: argo-app-b
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-argo-app-b",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "App project B: the same workload in namespace app-b, synced one wave later.",
6
+ "scripts": {
7
+ "build": "chant build src -o manifests/app.yaml --format yaml"
8
+ },
9
+ "dependencies": {
10
+ "@intentius/chant": "^0.44.3",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.3"
12
+ }
13
+ }
@@ -0,0 +1,34 @@
1
+ // App B: the same workload in namespace app-b, the estate's second sync wave.
2
+ // Two apps rather than one so the AppProject has more than a single Application
3
+ // hanging off it — the fan-out is what an app-of-apps picture is for.
4
+ import { Deployment, Service } from "@intentius/chant-lexicon-k8s";
5
+
6
+ const labels = { app: "app-b" };
7
+ const image = "nginxinc/nginx-unprivileged:1.27-alpine";
8
+
9
+ export const deployment = new Deployment({
10
+ metadata: { name: "app-b", namespace: "app-b", labels },
11
+ spec: {
12
+ replicas: 2,
13
+ selector: { matchLabels: labels },
14
+ template: {
15
+ metadata: { labels },
16
+ spec: {
17
+ containers: [
18
+ {
19
+ name: "web",
20
+ image,
21
+ ports: [{ containerPort: 8080 }],
22
+ securityContext: { runAsNonRoot: true, runAsUser: 101, allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } },
23
+ resources: { requests: { cpu: "25m", memory: "32Mi" }, limits: { cpu: "100m", memory: "64Mi" } },
24
+ },
25
+ ],
26
+ },
27
+ },
28
+ },
29
+ });
30
+
31
+ export const service = new Service({
32
+ metadata: { name: "app-b", namespace: "app-b" },
33
+ spec: { selector: labels, ports: [{ port: 80, targetPort: 8080 }] },
34
+ });
@@ -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,14 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+
3
+ // The estate's control plane: it declares the Argo machinery (an AppProject +
4
+ // one Application per app), and Argo's controller deploys the app projects —
5
+ // this project never applies a workload itself.
6
+ //
7
+ // No k8s profile block, unlike the flux-estate demo's: argo-estate is the
8
+ // DECLARED story only. Nothing here binds a kube context because nothing here
9
+ // reads one — `behold demo argo-estate` needs no cluster and installs no Argo.
10
+ export default {
11
+ lexicons: ["k8s"],
12
+ sourceDir: "src",
13
+ ownership: { stack: "argo-control-plane" },
14
+ } satisfies ChantConfig;
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "behold-example-argo-control-plane",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "The estate's control plane: the Argo CD CRs (an AppProject + one Application per app project).",
6
+ "scripts": {
7
+ "build": "chant build src -o dist/control-plane.yaml --format yaml"
8
+ },
9
+ "dependencies": {
10
+ "@intentius/chant": "^0.44.3",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.3"
12
+ }
13
+ }
@@ -0,0 +1,69 @@
1
+ // The control plane, Argo-style: one AppProject that fences what the estate is
2
+ // allowed to deploy, and one Application per app project, each syncing that
3
+ // app's committed manifests from this repo. Argo's application controller does
4
+ // the applying — this project never applies a workload itself.
5
+ //
6
+ // Written with the raw declarables rather than the ArgoAppFor composite on
7
+ // purpose: the two fields the whole demo is about, `spec.project` and
8
+ // `spec.destination.namespace`, are the ones a stranger reading Argo YAML
9
+ // already knows by sight, and here they sit in the source verbatim. (For real
10
+ // estates the composite is fewer lines: ArgoAppFor("app-a", { repo, path,
11
+ // project, destination }) renders this same Application.)
12
+ import { AppProject, Application } from "@intentius/chant-lexicon-k8s";
13
+
14
+ const REPO = "https://github.com/INTENTIUS/behold";
15
+ // The in-cluster target every Argo install has. Naming it (rather than a
16
+ // registered cluster Secret) is what keeps this estate a one-cluster story.
17
+ const IN_CLUSTER = "https://kubernetes.default.svc";
18
+
19
+ // The fence: which repo the apps may sync from, and which namespaces Argo may
20
+ // own objects in. Both Applications name it in spec.project — the join behold
21
+ // draws (behold#222) and chant lints as ARGO002.
22
+ export const project = new AppProject({
23
+ metadata: { name: "estate", namespace: "argocd" },
24
+ spec: {
25
+ description: "behold's demo estate: two app projects, one cluster.",
26
+ sourceRepos: [REPO],
27
+ destinations: [
28
+ { server: IN_CLUSTER, namespace: "app-a" },
29
+ { server: IN_CLUSTER, namespace: "app-b" },
30
+ ],
31
+ // CreateNamespace=true below means Argo creates the destination namespaces,
32
+ // so the project has to permit that one cluster-scoped kind.
33
+ clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
34
+ },
35
+ });
36
+
37
+ export const appA = new Application({
38
+ metadata: {
39
+ name: "app-a",
40
+ namespace: "argocd",
41
+ // Argo's ordering idiom is a wave annotation, not a reference — so unlike
42
+ // Flux's dependsOn (behold#223) there is no edge to draw for it. app-a
43
+ // syncs first; app-b follows.
44
+ annotations: { "argocd.argoproj.io/sync-wave": "0" },
45
+ },
46
+ spec: {
47
+ project: "estate",
48
+ source: { repoURL: REPO, targetRevision: "main", path: "example-argo-estate/app-a/manifests" },
49
+ // The namespace Argo will own objects in. app-a's own objects declare it
50
+ // too, so the box behold draws holds the app's cards; declaring it here is
51
+ // what makes the box a fact about the CONTROL PLANE's graph as well.
52
+ destination: { server: IN_CLUSTER, namespace: "app-a" },
53
+ syncPolicy: { automated: { prune: true, selfHeal: true }, syncOptions: ["CreateNamespace=true"] },
54
+ },
55
+ });
56
+
57
+ export const appB = new Application({
58
+ metadata: {
59
+ name: "app-b",
60
+ namespace: "argocd",
61
+ annotations: { "argocd.argoproj.io/sync-wave": "1" },
62
+ },
63
+ spec: {
64
+ project: "estate",
65
+ source: { repoURL: REPO, targetRevision: "main", path: "example-argo-estate/app-b/manifests" },
66
+ destination: { server: IN_CLUSTER, namespace: "app-b" },
67
+ syncPolicy: { automated: { prune: true, selfHeal: true }, syncOptions: ["CreateNamespace=true"] },
68
+ },
69
+ });
@@ -0,0 +1 @@
1
+ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*", "ops/**/*", "chant.config.ts"] }