@intentius/chant-lexicon-k8s 0.124.31 → 0.124.32
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/dist/actions/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- package/src/serializer.ts +2 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving a live Kubernetes object's `ownerReferences` chain up to a
|
|
3
|
+
* declared, chant-observed entity (chant #1077).
|
|
4
|
+
*
|
|
5
|
+
* There is no server-side "who ultimately owns this object" query — Kubernetes'
|
|
6
|
+
* own garbage collector does not have one either; it walks the same chain one
|
|
7
|
+
* hop at a time, reading each intermediate owner. A Pod's `ownerReferences`
|
|
8
|
+
* names its ReplicaSet, which chant never declared; the ReplicaSet's own
|
|
9
|
+
* `ownerReferences` names the Deployment, which is declared. Reaching that
|
|
10
|
+
* declared entity takes reading the ReplicaSet in between.
|
|
11
|
+
*
|
|
12
|
+
* This module's only job is assembling that chain — fetching each hop through
|
|
13
|
+
* the typed client, bounded and cycle-guarded so a corrupt or adversarial
|
|
14
|
+
* chain cannot hang an observation. The bounded/cycle-safe *interpretation* of
|
|
15
|
+
* the assembled chain (declared / unowned / foreign / unknown) is core's
|
|
16
|
+
* `classifyOwnerChain` (`@intentius/chant/owner-chain`) — the issue's own
|
|
17
|
+
* division of labor: core owns the category, the lexicon supplies the chain.
|
|
18
|
+
*/
|
|
19
|
+
import { classifyOwnerChain, DEFAULT_MAX_OWNER_CHAIN_DEPTH, } from "@intentius/chant/owner-chain";
|
|
20
|
+
function ownerRefs(obj) {
|
|
21
|
+
const refs = obj.metadata?.ownerReferences;
|
|
22
|
+
return Array.isArray(refs) ? refs : [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The reference Kubernetes' own garbage collector treats as the controlling
|
|
26
|
+
* parent: the entry with `controller: true`, else the first. An object with
|
|
27
|
+
* several owner references (rare — usually a shared, non-controller owner
|
|
28
|
+
* alongside the controlling one) is walked through its controller, matching
|
|
29
|
+
* what actually recreates the object were it deleted.
|
|
30
|
+
*/
|
|
31
|
+
function controllingRef(obj) {
|
|
32
|
+
const refs = ownerRefs(obj);
|
|
33
|
+
return refs.find((r) => r.controller === true) ?? refs[0];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Walk `obj`'s owner-reference chain up to a declared entity, a foreign root,
|
|
37
|
+
* or a bound (#1077). Fetches at most `maxDepth + 1` objects (the starting
|
|
38
|
+
* object plus up to `maxDepth` ancestors) — the same bound `classifyOwnerChain`
|
|
39
|
+
* enforces when interpreting the result, so nothing is fetched that the
|
|
40
|
+
* interpretation would not have used anyway.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveK8sOwnerChain(obj, options) {
|
|
43
|
+
const startUid = obj.metadata?.uid;
|
|
44
|
+
if (!startUid)
|
|
45
|
+
return { root: "unknown" };
|
|
46
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_OWNER_CHAIN_DEPTH;
|
|
47
|
+
const nodes = new Map();
|
|
48
|
+
const visited = new Set();
|
|
49
|
+
let uid = startUid;
|
|
50
|
+
let object = obj;
|
|
51
|
+
for (let depth = 0; uid !== undefined && depth <= maxDepth; depth++) {
|
|
52
|
+
if (visited.has(uid))
|
|
53
|
+
break; // cycle — classifyOwnerChain reads it off `nodes`
|
|
54
|
+
visited.add(uid);
|
|
55
|
+
const declaredEntity = options.declaredByUid.get(uid);
|
|
56
|
+
if (declaredEntity) {
|
|
57
|
+
nodes.set(uid, { declaredEntity });
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (!object) {
|
|
61
|
+
nodes.set(uid, { ownerUnreadable: true });
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
const ref = controllingRef(object);
|
|
65
|
+
if (!ref?.uid || !ref.kind || !ref.apiVersion || !ref.name) {
|
|
66
|
+
nodes.set(uid, {}); // no further owner — a real, live root
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
nodes.set(uid, { ownerId: ref.uid });
|
|
70
|
+
if (visited.has(ref.uid)) {
|
|
71
|
+
// About to cycle back to an already-visited node — let the loop's own
|
|
72
|
+
// check classify it next iteration rather than issuing a wasted read.
|
|
73
|
+
uid = ref.uid;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (options.declaredByUid.has(ref.uid)) {
|
|
77
|
+
// Already known to be a declared entity from this observation's own
|
|
78
|
+
// resolved set — no need to read it, the next iteration resolves it
|
|
79
|
+
// from `declaredByUid` directly.
|
|
80
|
+
uid = ref.uid;
|
|
81
|
+
object = undefined;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
object = await options.reader.readIfPresent({
|
|
86
|
+
apiVersion: ref.apiVersion,
|
|
87
|
+
kind: ref.kind,
|
|
88
|
+
name: ref.name,
|
|
89
|
+
namespace: options.namespace,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
object = undefined;
|
|
94
|
+
}
|
|
95
|
+
if (!object)
|
|
96
|
+
nodes.set(ref.uid, { ownerUnreadable: true });
|
|
97
|
+
uid = ref.uid;
|
|
98
|
+
}
|
|
99
|
+
return classifyOwnerChain(startUid, nodes, maxDepth);
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kinds chant sweeps when nothing narrows the question.
|
|
3
|
+
*
|
|
4
|
+
* A *product* decision — what a bare `chant import` should pull back, and
|
|
5
|
+
* (chant #1075) which kinds an ownership-scoped prune has to look at to notice
|
|
6
|
+
* that a whole kind was removed from source. Not an addressing limit: since
|
|
7
|
+
* chant #1074 removed `KUBECTL_RESOURCE`, a `--selector type=<entity type>`
|
|
8
|
+
* import can name any of the ~180 types the generated operation surface
|
|
9
|
+
* carries, CRDs included.
|
|
10
|
+
*
|
|
11
|
+
* It lives in its own module, with no imports of its own, because both
|
|
12
|
+
* consumers reach it from different directions — `../export-resources.ts`
|
|
13
|
+
* pulls in the whole import parser, and the apply activity should not.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_IMPORT_TYPES = [
|
|
16
|
+
"K8s::Apps::Deployment",
|
|
17
|
+
"K8s::Apps::StatefulSet",
|
|
18
|
+
"K8s::Apps::DaemonSet",
|
|
19
|
+
"K8s::Apps::ReplicaSet",
|
|
20
|
+
"K8s::Core::Service",
|
|
21
|
+
"K8s::Core::ConfigMap",
|
|
22
|
+
"K8s::Core::Secret",
|
|
23
|
+
"K8s::Core::Namespace",
|
|
24
|
+
"K8s::Core::Pod",
|
|
25
|
+
"K8s::Core::PersistentVolumeClaim",
|
|
26
|
+
"K8s::Core::ServiceAccount",
|
|
27
|
+
"K8s::Batch::Job",
|
|
28
|
+
"K8s::Batch::CronJob",
|
|
29
|
+
"K8s::Networking::Ingress",
|
|
30
|
+
"K8s::Networking::NetworkPolicy",
|
|
31
|
+
"K8s::Rbac::Role",
|
|
32
|
+
"K8s::Rbac::RoleBinding",
|
|
33
|
+
"K8s::Rbac::ClusterRole",
|
|
34
|
+
"K8s::Rbac::ClusterRoleBinding",
|
|
35
|
+
];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this lexicon's entity types are worth to a behaviour engine (#2382).
|
|
3
|
+
*
|
|
4
|
+
* Contributed as rows through the plugin's `behaviourKinds` field; the
|
|
5
|
+
* vocabulary, the resolution and the prediction are
|
|
6
|
+
* `packages/core/src/behaviour-kinds.ts`. These rows were a section of one
|
|
7
|
+
* cross-lexicon table in one lexicon until #2382 moved each substrate's
|
|
8
|
+
* rows to the lexicon that owns the types — this file can say what
|
|
9
|
+
* `K8s::Apps::Deployment` means because it is the file that defines it.
|
|
10
|
+
*
|
|
11
|
+
* A table rather than a heuristic over type names: a heuristic would map
|
|
12
|
+
* `K8s::Rbac::RoleBinding` to storage and price a join table.
|
|
13
|
+
*/
|
|
14
|
+
/** Types an engine prices, with the declared property its size is read from. */
|
|
15
|
+
const MAPPED = {
|
|
16
|
+
// No `sizeProp` on the five workload kinds. A workload's size is its pod
|
|
17
|
+
// template's container resource requests — an object, per container, over an
|
|
18
|
+
// array — and this contract's `size` is one string in the provider's own
|
|
19
|
+
// vocabulary. Kubernetes has no such string for a workload. Naming
|
|
20
|
+
// `spec.template.spec.containers` here, as the first version did, advertised
|
|
21
|
+
// a column that resolved to an array and therefore to nothing, on every
|
|
22
|
+
// Kubernetes workload in the repository.
|
|
23
|
+
"K8s::Apps::Deployment": { kind: "compute" },
|
|
24
|
+
"K8s::Apps::StatefulSet": { kind: "compute" },
|
|
25
|
+
"K8s::Apps::DaemonSet": { kind: "compute" },
|
|
26
|
+
"K8s::Core::Pod": { kind: "compute" },
|
|
27
|
+
"K8s::Batch::Job": { kind: "serverless" },
|
|
28
|
+
"K8s::Batch::CronJob": { kind: "serverless" },
|
|
29
|
+
// These two do state a size, as a single string in Kubernetes' own
|
|
30
|
+
// vocabulary: a quantity (`20Gi`) and a service type (`LoadBalancer`).
|
|
31
|
+
"K8s::Core::PersistentVolumeClaim": { kind: "block-store", sizeProp: "spec.resources.requests.storage", sizeType: "string" },
|
|
32
|
+
"K8s::Core::Service": { kind: "load-balancer", sizeProp: "spec.type", sizeType: "string" },
|
|
33
|
+
"K8s::Networking::Ingress": { kind: "load-balancer" },
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Types considered and declared unsendable, each with the reason it carries no
|
|
37
|
+
* rate. The fifth family core's contributor guidance names is this lexicon's
|
|
38
|
+
* own: a controller's request is not the thing it creates. A `Certificate`
|
|
39
|
+
* costs nothing; the issuer's work and the Secret it writes are elsewhere in
|
|
40
|
+
* the graph or outside the cluster entirely.
|
|
41
|
+
*/
|
|
42
|
+
const UNMAPPED = {
|
|
43
|
+
"K8s::Rbac::Role": "an RBAC role is a grant; the workloads it admits are what cost and saturate",
|
|
44
|
+
"K8s::Rbac::RoleBinding": "an RBAC binding is a grant; the workloads it admits are what cost and saturate",
|
|
45
|
+
"K8s::Rbac::ClusterRole": "the cluster-scoped twin of an RBAC role, and a grant for the same reason",
|
|
46
|
+
"K8s::Rbac::ClusterRoleBinding": "the cluster-scoped twin of an RBAC binding, and a grant for the same reason",
|
|
47
|
+
"K8s::Core::ServiceAccount": "an identity, not a workload",
|
|
48
|
+
"K8s::Core::Namespace": "a boundary the workloads sit inside, and the workloads carry the figures",
|
|
49
|
+
"K8s::Networking::IngressClass": "names which controller handles an Ingress; the controller and the load balancer it creates carry the figures",
|
|
50
|
+
"K8s::Networking::NetworkPolicy": "a filter on edges that already exist, not a thing at either end of one",
|
|
51
|
+
"K8s::Storage::StorageClass": "the kind of disk a claim may ask for; the PersistentVolumeClaim carries the figures",
|
|
52
|
+
"K8s::Autoscaling::HorizontalPodAutoscaler": "an instruction about how many replicas to run; the workload it scales carries the figures, and a prediction is at one stated traffic level rather than across a scaling range",
|
|
53
|
+
"K8s::HorizontalPodAutoscaler": "an instruction about how many replicas to run; the workload it scales carries the figures, and a prediction is at one stated traffic level rather than across a scaling range",
|
|
54
|
+
"K8s::Policy::PodDisruptionBudget": "a constraint on voluntary eviction. It shapes what a lost zone does to a workload and has no rate of its own; expressing that constraint to an engine is a resilience input this contract has no field for",
|
|
55
|
+
"K8s::Core::LimitRange": "a default and a ceiling for workloads in a namespace; the workloads carry the figures",
|
|
56
|
+
"K8s::Core::ResourceQuota": "a ceiling on a namespace's total requests; the workloads under it carry the figures",
|
|
57
|
+
// ── A controller's request is not the thing it creates ───────────
|
|
58
|
+
"K8s::Argo::Application": "a request to Argo CD to reconcile a source; whatever it creates is priced where that lands",
|
|
59
|
+
"K8s::Flux::GitRepository": "a source Flux polls; the workloads it reconciles carry the figures",
|
|
60
|
+
"K8s::Flux::Kustomization": "a request to Flux to apply a source; whatever it creates is priced where that lands",
|
|
61
|
+
"K8s::CertManager::Certificate": "a request to an issuer. The certificate costs nothing; the issuer's work and the Secret it writes are elsewhere",
|
|
62
|
+
"K8s::CertManager::ClusterIssuer": "the issuer a certificate request names; it holds no capacity and serves no traffic",
|
|
63
|
+
"K8s::ExternalSecrets::ExternalSecret": "a request to copy a value from an external store into a Secret; neither end is a rate",
|
|
64
|
+
"K8s::ExternalSecrets::ClusterSecretStore": "names where secrets are fetched from; it holds no capacity and serves no traffic",
|
|
65
|
+
"K8s::Monitoring::ServiceMonitor": "tells Prometheus what to scrape; Prometheus is a workload elsewhere in the graph and carries the figures",
|
|
66
|
+
"K8s::Monitoring::PrometheusRule": "alerting and recording rules evaluated by Prometheus, which carries the figures",
|
|
67
|
+
"K8s::Traefik::IngressRoute": "routing handled by the Traefik workload, which carries the figures",
|
|
68
|
+
"K8s::Calico::FelixConfiguration": "tuning for the CNI agent on every node; the nodes carry the figures",
|
|
69
|
+
"K8s::GKE::BackendConfig": "tuning for a Google load balancer an Ingress creates; the gcp substrate is not modelled here, so neither end is priced here",
|
|
70
|
+
"K8s::NetworkingGKE::ManagedCertificate": "a certificate Google issues for an Ingress; the gcp substrate is not modelled here",
|
|
71
|
+
"K8s::NetworkingGKEBeta::FrontendConfig": "tuning for a Google load balancer an Ingress creates; the gcp substrate is not modelled here",
|
|
72
|
+
// ── Configuration and secrets: real, declared, carrying no rate ──
|
|
73
|
+
"K8s::Core::ConfigMap": "configuration the workloads read; it has no rate and no saturation axis of its own",
|
|
74
|
+
"K8s::Core::Secret": "configuration the workloads read; it has no rate and no saturation axis of its own",
|
|
75
|
+
};
|
|
76
|
+
/** This lexicon's answer for its own types. */
|
|
77
|
+
export const k8sBehaviourKinds = {
|
|
78
|
+
provider: "kubernetes",
|
|
79
|
+
prefixes: ["K8s::"],
|
|
80
|
+
mapped: MAPPED,
|
|
81
|
+
unmapped: UNMAPPED,
|
|
82
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation generation for the Kubernetes lexicon.
|
|
3
|
+
*
|
|
4
|
+
* Generates Starlight MDX pages for K8s entities using the core docs pipeline.
|
|
5
|
+
*/
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
|
|
9
|
+
/**
|
|
10
|
+
* Extract service name from K8s type: "K8s::Apps::Deployment" → "Apps"
|
|
11
|
+
*/
|
|
12
|
+
function serviceFromType(resourceType) {
|
|
13
|
+
const parts = resourceType.split("::");
|
|
14
|
+
return parts.length >= 2 ? parts[1] : "Core";
|
|
15
|
+
}
|
|
16
|
+
const overview = `The **Kubernetes** lexicon provides typed constructors for Kubernetes resource
|
|
17
|
+
manifests. It covers Deployments, Services, ConfigMaps, StatefulSets, Jobs,
|
|
18
|
+
Ingress, RBAC, and 147 resources and 50 property types.
|
|
19
|
+
|
|
20
|
+
New? Start with the [Getting Started](/chant/lexicons/k8s/getting-started/) guide.
|
|
21
|
+
|
|
22
|
+
Install it with:
|
|
23
|
+
|
|
24
|
+
\`\`\`bash
|
|
25
|
+
npm install --save-dev @intentius/chant-lexicon-k8s
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
\`\`\`typescript
|
|
31
|
+
import { Deployment, Service, Container, Probe } from "@intentius/chant-lexicon-k8s";
|
|
32
|
+
|
|
33
|
+
export const deployment = new Deployment({
|
|
34
|
+
metadata: { name: "my-app", labels: { "app.kubernetes.io/name": "my-app" } },
|
|
35
|
+
spec: {
|
|
36
|
+
replicas: 2,
|
|
37
|
+
selector: { matchLabels: { "app.kubernetes.io/name": "my-app" } },
|
|
38
|
+
template: {
|
|
39
|
+
metadata: { labels: { "app.kubernetes.io/name": "my-app" } },
|
|
40
|
+
spec: {
|
|
41
|
+
containers: [
|
|
42
|
+
new Container({
|
|
43
|
+
name: "app",
|
|
44
|
+
image: "my-app:1.0",
|
|
45
|
+
ports: [{ containerPort: 8080, name: "http" }],
|
|
46
|
+
livenessProbe: new Probe({ httpGet: { path: "/healthz", port: 8080 } }),
|
|
47
|
+
readinessProbe: new Probe({ httpGet: { path: "/readyz", port: 8080 } }),
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const service = new Service({
|
|
56
|
+
metadata: { name: "my-app" },
|
|
57
|
+
spec: {
|
|
58
|
+
selector: { "app.kubernetes.io/name": "my-app" },
|
|
59
|
+
ports: [{ port: 80, targetPort: 8080, name: "http" }],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
The lexicon provides **147 resource types** (Deployment, Service, ConfigMap, StatefulSet, and more), **50 property types** (Container, Probe, Volume, SecurityContext, etc.), and composites (WebApp, StatefulApp, CronWorkload, AutoscaledService, WorkerPool, NamespaceEnv, NodeAgent) for common patterns.
|
|
65
|
+
`;
|
|
66
|
+
const outputFormat = `The Kubernetes lexicon serializes resources into **multi-document YAML** with
|
|
67
|
+
\`---\` separators between resources. Each resource gets the standard K8s
|
|
68
|
+
structure: \`apiVersion\`, \`kind\`, \`metadata\`, and \`spec\`.
|
|
69
|
+
|
|
70
|
+
## Building
|
|
71
|
+
|
|
72
|
+
Run \`chant build\` to produce Kubernetes manifests from your declarations:
|
|
73
|
+
|
|
74
|
+
\`\`\`bash
|
|
75
|
+
chant build src/ --output dist/manifests.yaml
|
|
76
|
+
# Writes dist/manifests.yaml
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
The generated file includes:
|
|
80
|
+
|
|
81
|
+
- Multi-document YAML with \`---\` separators
|
|
82
|
+
- Correct \`apiVersion\` and \`kind\` for each resource
|
|
83
|
+
- \`metadata.name\` auto-generated from export names (camelCase → kebab-case)
|
|
84
|
+
- Default labels and annotations injected from \`defaultLabels()\`/\`defaultAnnotations()\`
|
|
85
|
+
|
|
86
|
+
The lexicon also materializes [effect receipts](/chant/concepts/effect-receipts/) as
|
|
87
|
+
ConfigMaps named \`chant-receipt.<stack>.<env>.<effect>\` in \`k8s.receipts.namespace\`
|
|
88
|
+
(\`default\` when unset), holding the expectation under \`data.expectation\`. A receipt is
|
|
89
|
+
never a document in the manifest stream: the rows ride a trailing
|
|
90
|
+
\`# chant:effect-receipts\` comment, which \`kubectl apply\` ignores, because the
|
|
91
|
+
\`effect()\` step is a receipt's sole writer.
|
|
92
|
+
|
|
93
|
+
## Key conversions
|
|
94
|
+
|
|
95
|
+
| Chant (TypeScript) | YAML output | Rule |
|
|
96
|
+
|--------------------|-------------|------|
|
|
97
|
+
| \`export const myApp = new Deployment({...})\` | \`metadata.name: my-app\` | Export name → kebab-case |
|
|
98
|
+
| \`new Container({...})\` | Inline container spec | Property types expanded inline |
|
|
99
|
+
| \`defaultLabels({...})\` | Merged into all resources | Project-wide label injection |
|
|
100
|
+
|
|
101
|
+
## Applying
|
|
102
|
+
|
|
103
|
+
The output is standard Kubernetes YAML. Apply with kubectl:
|
|
104
|
+
|
|
105
|
+
\`\`\`bash
|
|
106
|
+
# Dry run first
|
|
107
|
+
kubectl apply -f dist/manifests.yaml --dry-run=server
|
|
108
|
+
|
|
109
|
+
# Apply
|
|
110
|
+
kubectl apply -f dist/manifests.yaml
|
|
111
|
+
|
|
112
|
+
# Diff before applying
|
|
113
|
+
kubectl diff -f dist/manifests.yaml
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Compatibility
|
|
117
|
+
|
|
118
|
+
The output is compatible with:
|
|
119
|
+
- kubectl apply/diff
|
|
120
|
+
- Helm (as raw manifests)
|
|
121
|
+
- ArgoCD / Flux GitOps controllers
|
|
122
|
+
- Kustomize (as a base)
|
|
123
|
+
- Any tool that processes Kubernetes YAML`;
|
|
124
|
+
/**
|
|
125
|
+
* Generate documentation for the Kubernetes lexicon.
|
|
126
|
+
*/
|
|
127
|
+
export async function generateDocs(opts) {
|
|
128
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
129
|
+
const config = {
|
|
130
|
+
name: "k8s",
|
|
131
|
+
displayName: "Kubernetes",
|
|
132
|
+
description: "Typed constructors for Kubernetes resource manifests",
|
|
133
|
+
distDir: join(pkgDir, "dist"),
|
|
134
|
+
outDir: join(pkgDir, "docs"),
|
|
135
|
+
overview,
|
|
136
|
+
outputFormat,
|
|
137
|
+
serviceFromType,
|
|
138
|
+
suppressPages: ["pseudo-parameters"],
|
|
139
|
+
basePath: "/chant/lexicons/k8s/",
|
|
140
|
+
};
|
|
141
|
+
const result = await docsPipeline(config);
|
|
142
|
+
writeDocsSite(config, result);
|
|
143
|
+
if (opts?.verbose) {
|
|
144
|
+
console.error(`Generated docs: ${result.stats.resources} resources, ${result.stats.properties} properties, ${result.stats.services} services, ${result.stats.rules} rules`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for Kubernetes lexicon generation.
|
|
4
|
+
*/
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { generate, writeGeneratedFiles } from "./generate.js";
|
|
8
|
+
import { K8S_SCHEMA_VERSION } from "../spec/fetch.js";
|
|
9
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
10
|
+
async function main() {
|
|
11
|
+
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
12
|
+
const force = process.argv.includes("--force") || process.argv.includes("-f");
|
|
13
|
+
// Parse --schema-version <ref>
|
|
14
|
+
const versionIdx = process.argv.indexOf("--schema-version");
|
|
15
|
+
const schemaVersion = versionIdx !== -1 ? process.argv[versionIdx + 1] : undefined;
|
|
16
|
+
console.error(`Generating Kubernetes lexicon (schema: ${schemaVersion ?? K8S_SCHEMA_VERSION})...`);
|
|
17
|
+
const result = await generate({ verbose, force, schemaVersion });
|
|
18
|
+
writeGeneratedFiles(result, pkgDir);
|
|
19
|
+
console.error(`Generated ${result.resources} resources, ${result.properties} property types, ${result.enums} enums`);
|
|
20
|
+
if (result.warnings.length > 0) {
|
|
21
|
+
console.error(`${result.warnings.length} warnings:`);
|
|
22
|
+
for (const w of result.warnings) {
|
|
23
|
+
console.error(` ${w.file}: ${w.error}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
main().catch((err) => {
|
|
28
|
+
console.error("Generation failed:", err);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexicon JSON generator — produces lexicon-k8s.json with metadata
|
|
3
|
+
* for all Kubernetes resource and property types.
|
|
4
|
+
*/
|
|
5
|
+
import { k8sShortName, gvkToApiVersion } from "../spec/parse.js";
|
|
6
|
+
import { buildRegistry, serializeRegistry, } from "@intentius/chant/codegen/generate-registry";
|
|
7
|
+
/**
|
|
8
|
+
* Generate the lexicon-k8s.json content.
|
|
9
|
+
*/
|
|
10
|
+
export function generateLexiconJSON(results, naming) {
|
|
11
|
+
const registryResources = results.map((r) => ({
|
|
12
|
+
typeName: r.resource.typeName,
|
|
13
|
+
attributes: r.resource.attributes,
|
|
14
|
+
properties: r.resource.properties,
|
|
15
|
+
propertyTypes: r.propertyTypes.map((pt) => ({ name: pt.name, specType: pt.defType })),
|
|
16
|
+
}));
|
|
17
|
+
const entries = buildRegistry(registryResources, naming, {
|
|
18
|
+
shortName: k8sShortName,
|
|
19
|
+
buildEntry: (resource, _tsName, _attrs, propConstraints) => {
|
|
20
|
+
const r = results.find((res) => res.resource.typeName === resource.typeName);
|
|
21
|
+
const entry = {
|
|
22
|
+
resourceType: resource.typeName,
|
|
23
|
+
kind: (r?.isProperty ? "property" : "resource"),
|
|
24
|
+
lexicon: "k8s",
|
|
25
|
+
};
|
|
26
|
+
if (r && !r.isProperty) {
|
|
27
|
+
entry.apiVersion = gvkToApiVersion(r.gvk);
|
|
28
|
+
entry.gvkKind = r.gvk.kind;
|
|
29
|
+
}
|
|
30
|
+
if (r?.resource.deprecatedProperties?.length) {
|
|
31
|
+
entry.deprecatedProperties = r.resource.deprecatedProperties;
|
|
32
|
+
}
|
|
33
|
+
if (propConstraints && Object.keys(propConstraints).length > 0) {
|
|
34
|
+
entry.constraints = propConstraints;
|
|
35
|
+
}
|
|
36
|
+
if (r?.specSchema) {
|
|
37
|
+
entry.specSchema = r.specSchema;
|
|
38
|
+
}
|
|
39
|
+
return entry;
|
|
40
|
+
},
|
|
41
|
+
buildPropertyEntry: (resourceType, propertyType) => ({
|
|
42
|
+
resourceType: `${resourceType}.${propertyType}`,
|
|
43
|
+
kind: "property",
|
|
44
|
+
lexicon: "k8s",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
return serializeWithCompactSchemas(entries);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pretty-print the registry the way every lexicon does, but keep each
|
|
51
|
+
* `specSchema` on one line (chant #1372). A CRD schema nests deeply — an
|
|
52
|
+
* ApplicationSet or RayCluster embeds a whole PodTemplateSpec — and two-space
|
|
53
|
+
* indentation on that tree quadruples the file for no reader's benefit.
|
|
54
|
+
* Compact, the schemas add ~0.8 MB to the registry; pretty, ~3.9 MB.
|
|
55
|
+
*/
|
|
56
|
+
export function serializeWithCompactSchemas(entries) {
|
|
57
|
+
const compact = new Map();
|
|
58
|
+
const stripped = {};
|
|
59
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
60
|
+
if (!entry.specSchema) {
|
|
61
|
+
stripped[name] = entry;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const { specSchema, ...rest } = entry;
|
|
65
|
+
const token = `__chant_spec_schema_${compact.size}__`;
|
|
66
|
+
compact.set(token, JSON.stringify(specSchema));
|
|
67
|
+
stripped[name] = { ...rest, specSchema: token };
|
|
68
|
+
}
|
|
69
|
+
let out = serializeRegistry(stripped);
|
|
70
|
+
for (const [token, json] of compact) {
|
|
71
|
+
out = out.replace(`"${token}"`, json);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Associative-list key table, derived from the spec rather than hardcoded
|
|
3
|
+
* (chant #1441).
|
|
4
|
+
*
|
|
5
|
+
* The Kubernetes OpenAPI spec states, per property, how the API server merges
|
|
6
|
+
* a list: `x-kubernetes-list-type: map` plus `x-kubernetes-list-map-keys`
|
|
7
|
+
* naming the fields that jointly identify one element. chant's drift engine
|
|
8
|
+
* needs exactly that to compare `containers` by name rather than by index.
|
|
9
|
+
* Before this, `k8sListMapOrderKey` (core's `managed-fields.ts`) hardcoded six
|
|
10
|
+
* properties; the spec annotates 32.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the table is keyed by property name
|
|
13
|
+
*
|
|
14
|
+
* The consumer is `DeepNormalizationHooks.orderKey`, which receives an
|
|
15
|
+
* index-erased `pattern` (`spec.template.spec.containers`) and keys off its
|
|
16
|
+
* LAST segment. A by-name table matches that lookup exactly and needs no
|
|
17
|
+
* resolution of the definition graph at generation time.
|
|
18
|
+
*
|
|
19
|
+
* The cost is that two definitions declaring the same property name with
|
|
20
|
+
* different keys collapse into one entry. That is real but small, and it is
|
|
21
|
+
* represented rather than lost: each name maps to a LIST of candidate key
|
|
22
|
+
* sets, and the consumer picks the first whose fields are all present on the
|
|
23
|
+
* element. In v1.36.2 exactly one property needs it — `ports`, keyed
|
|
24
|
+
* `(containerPort, protocol)` on a container and `(port, protocol)` on a
|
|
25
|
+
* Service — which is the same dual shape the hardcoded implementation already
|
|
26
|
+
* special-cased by hand.
|
|
27
|
+
*
|
|
28
|
+
* ## Two sources
|
|
29
|
+
*
|
|
30
|
+
* The whole spec is read directly, because chant emits types for 257 of
|
|
31
|
+
* roughly a thousand definitions and the merge semantics of a definition are
|
|
32
|
+
* worth having whether or not it becomes a type. Reading only the parsed
|
|
33
|
+
* results finds 18 of the 32 properties and misses `ServicePort.port`.
|
|
34
|
+
*
|
|
35
|
+
* Parsed results are read too, because CRDs arrive that way — an operator CRD
|
|
36
|
+
* that declares these extensions contributes its keys for free.
|
|
37
|
+
*/
|
|
38
|
+
function everyProperty(results) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const r of results) {
|
|
41
|
+
out.push(...r.resource.properties);
|
|
42
|
+
for (const pt of r.propertyTypes)
|
|
43
|
+
out.push(...pt.properties);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** The `(name, keys)` pairs carried on parsed properties — core types and every CRD. */
|
|
48
|
+
export function parsedListMapKeyPairs(results) {
|
|
49
|
+
const pairs = [];
|
|
50
|
+
for (const p of everyProperty(results)) {
|
|
51
|
+
if (p.listType === "map" && p.listMapKeys?.length)
|
|
52
|
+
pairs.push([p.name, p.listMapKeys]);
|
|
53
|
+
}
|
|
54
|
+
return pairs;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fold `(name, keys)` pairs into a by-name table.
|
|
58
|
+
*
|
|
59
|
+
* Candidate sets are deduplicated by content and ordered longest-first, so a
|
|
60
|
+
* more specific key set is tried before a shorter one whose fields a longer
|
|
61
|
+
* element would also satisfy.
|
|
62
|
+
*/
|
|
63
|
+
export function buildListMapKeyTable(...sources) {
|
|
64
|
+
const byName = new Map();
|
|
65
|
+
for (const pairs of sources) {
|
|
66
|
+
for (const [name, rawKeys] of pairs) {
|
|
67
|
+
const keys = [...rawKeys].sort();
|
|
68
|
+
const seen = byName.get(name) ?? new Map();
|
|
69
|
+
seen.set(keys.join(" "), keys);
|
|
70
|
+
byName.set(name, seen);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const table = {};
|
|
74
|
+
for (const name of [...byName.keys()].sort()) {
|
|
75
|
+
const variants = byName.get(name);
|
|
76
|
+
if (!variants)
|
|
77
|
+
continue;
|
|
78
|
+
const sets = [...variants.values()];
|
|
79
|
+
sets.sort((a, b) => b.length - a.length || a.join().localeCompare(b.join()));
|
|
80
|
+
table[name] = sets;
|
|
81
|
+
}
|
|
82
|
+
return table;
|
|
83
|
+
}
|
|
84
|
+
/** Serialize the `list-map-keys.json` generated artifact. */
|
|
85
|
+
export function generateListMapKeysJSON(specPairs, results) {
|
|
86
|
+
const table = buildListMapKeyTable(specPairs, parsedListMapKeyPairs(results));
|
|
87
|
+
return `${JSON.stringify(table, null, 2)}\n`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated operation surface — chant #1074.
|
|
3
|
+
*
|
|
4
|
+
* `describeResources` used to reach the cluster through a hand-written
|
|
5
|
+
* `entityType → kubectl resource` map with twenty entries in it. Every one of
|
|
6
|
+
* the other ~180 generated resource types, and every CRD, fell off the end of
|
|
7
|
+
* it. The map was hand-maintained precisely because nothing derived it, and
|
|
8
|
+
* nothing derived it because the codegen pass that produces the types never
|
|
9
|
+
* emitted the addressing half.
|
|
10
|
+
*
|
|
11
|
+
* It does now. This artifact is written by the same `generate()` run that
|
|
12
|
+
* writes `lexicon-k8s.json` and `index.d.ts`, out of the same parsed results,
|
|
13
|
+
* so a resource that has a declarable class necessarily has an operation entry
|
|
14
|
+
* with the same apiVersion and kind. `operation-surface.test.ts` asserts that
|
|
15
|
+
* correspondence rather than trusting it.
|
|
16
|
+
*
|
|
17
|
+
* What it is not: an authority on what a given cluster serves. `plural` and
|
|
18
|
+
* `scope` are what the schema says; the live client confirms both against the
|
|
19
|
+
* cluster's own discovery before addressing anything, because a cluster can
|
|
20
|
+
* serve a different version of a CRD than the one chant generated from.
|
|
21
|
+
*/
|
|
22
|
+
import { gvkToApiVersion } from "../spec/parse.js";
|
|
23
|
+
import { pluralizeKind } from "../api/operation-surface.js";
|
|
24
|
+
/** Build the operation table from the same parsed results the types come from. */
|
|
25
|
+
export function buildOperationTable(results) {
|
|
26
|
+
const table = {};
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
if (result.isProperty)
|
|
29
|
+
continue;
|
|
30
|
+
const entityType = result.resource.typeName;
|
|
31
|
+
// A later result for the same type wins nothing — the first parse of a
|
|
32
|
+
// preferred version is canonical, matching the registry's own precedence.
|
|
33
|
+
if (table[entityType])
|
|
34
|
+
continue;
|
|
35
|
+
table[entityType] = {
|
|
36
|
+
entityType,
|
|
37
|
+
apiVersion: gvkToApiVersion(result.gvk),
|
|
38
|
+
kind: result.gvk.kind,
|
|
39
|
+
plural: result.operation?.plural ?? pluralizeKind(result.gvk.kind),
|
|
40
|
+
scope: result.operation?.scope ?? "Namespaced",
|
|
41
|
+
verbs: result.operation?.verbs ?? [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return table;
|
|
45
|
+
}
|
|
46
|
+
/** Serialize the table, key-sorted so regeneration produces a stable diff. */
|
|
47
|
+
export function generateOperationsJSON(results) {
|
|
48
|
+
const table = buildOperationTable(results);
|
|
49
|
+
const sorted = {};
|
|
50
|
+
for (const key of Object.keys(table).sort())
|
|
51
|
+
sorted[key] = table[key];
|
|
52
|
+
return `${JSON.stringify(sorted, null, 2)}\n`;
|
|
53
|
+
}
|