@intentius/chant-lexicon-k8s 0.30.0 → 0.32.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/dist/api/classify.d.ts +65 -0
- package/dist/api/classify.d.ts.map +1 -0
- package/dist/api/connect.d.ts +58 -0
- package/dist/api/connect.d.ts.map +1 -0
- package/dist/api/fake-cluster.d.ts +55 -0
- package/dist/api/fake-cluster.d.ts.map +1 -0
- package/dist/api/operation-surface.d.ts +64 -0
- package/dist/api/operation-surface.d.ts.map +1 -0
- package/dist/api/owner-chain.d.ts +46 -0
- package/dist/api/owner-chain.d.ts.map +1 -0
- package/dist/api/sweep-types.d.ts +17 -0
- package/dist/api/sweep-types.d.ts.map +1 -0
- package/dist/codegen/generate-operations.d.ts +29 -0
- package/dist/codegen/generate-operations.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/crd/parser.d.ts.map +1 -1
- package/dist/crd/types.d.ts +7 -0
- package/dist/crd/types.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +86 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +146 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +58 -26
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/export-resources.d.ts +31 -1
- package/dist/export-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/kube/apply.d.ts +26 -0
- package/dist/kube/apply.d.ts.map +1 -0
- package/dist/kube/connect.d.ts +30 -0
- package/dist/kube/connect.d.ts.map +1 -0
- package/dist/kube/delete.d.ts +27 -0
- package/dist/kube/delete.d.ts.map +1 -0
- package/dist/kube/describe.d.ts +19 -0
- package/dist/kube/describe.d.ts.map +1 -0
- package/dist/kube/events.d.ts +34 -0
- package/dist/kube/events.d.ts.map +1 -0
- package/dist/kube/flags.d.ts +40 -0
- package/dist/kube/flags.d.ts.map +1 -0
- package/dist/kube/get.d.ts +21 -0
- package/dist/kube/get.d.ts.map +1 -0
- package/dist/kube/group.d.ts +19 -0
- package/dist/kube/group.d.ts.map +1 -0
- package/dist/kube/logs.d.ts +16 -0
- package/dist/kube/logs.d.ts.map +1 -0
- package/dist/kube/project.d.ts +59 -0
- package/dist/kube/project.d.ts.map +1 -0
- package/dist/kube/render.d.ts +75 -0
- package/dist/kube/render.d.ts.map +1 -0
- package/dist/kube/source.d.ts +19 -0
- package/dist/kube/source.d.ts.map +1 -0
- package/dist/kube/target.d.ts +52 -0
- package/dist/kube/target.d.ts.map +1 -0
- package/dist/kube/testing.d.ts +21 -0
- package/dist/kube/testing.d.ts.map +1 -0
- package/dist/kube/top.d.ts +23 -0
- package/dist/kube/top.d.ts.map +1 -0
- package/dist/kube/verdict.d.ts +45 -0
- package/dist/kube/verdict.d.ts.map +1 -0
- package/dist/kube/wait.d.ts +23 -0
- package/dist/kube/wait.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +7 -4
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +120 -2
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts +30 -3
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/spec/parse.d.ts +42 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/api/classify.test.ts +133 -0
- package/src/api/classify.ts +144 -0
- package/src/api/connect.ts +104 -0
- package/src/api/fake-cluster.ts +218 -0
- package/src/api/operation-surface.test.ts +116 -0
- package/src/api/operation-surface.ts +129 -0
- package/src/api/owner-chain.test.ts +171 -0
- package/src/api/owner-chain.ts +144 -0
- package/src/api/sweep-types.ts +36 -0
- package/src/codegen/generate-operations.ts +56 -0
- package/src/codegen/generate.ts +9 -0
- package/src/config.ts +17 -0
- package/src/crd/parser.ts +8 -0
- package/src/crd/types.ts +7 -0
- package/src/deep-observe-hooks.ts +125 -0
- package/src/deep-observe.test.ts +668 -0
- package/src/deep-observe.ts +299 -0
- package/src/describe-resources.test.ts +619 -191
- package/src/describe-resources.ts +218 -117
- package/src/export-resources-io.test.ts +76 -51
- package/src/export-resources.ts +47 -35
- package/src/generated/operations.json +2156 -0
- package/src/kube/apply.test.ts +105 -0
- package/src/kube/apply.ts +101 -0
- package/src/kube/connect.ts +54 -0
- package/src/kube/delete.test.ts +123 -0
- package/src/kube/delete.ts +174 -0
- package/src/kube/describe.test.ts +106 -0
- package/src/kube/describe.ts +195 -0
- package/src/kube/events.test.ts +115 -0
- package/src/kube/events.ts +160 -0
- package/src/kube/flags.test.ts +65 -0
- package/src/kube/flags.ts +114 -0
- package/src/kube/get.test.ts +195 -0
- package/src/kube/get.ts +190 -0
- package/src/kube/group.test.ts +33 -0
- package/src/kube/group.ts +125 -0
- package/src/kube/logs.test.ts +126 -0
- package/src/kube/logs.ts +96 -0
- package/src/kube/project.test.ts +57 -0
- package/src/kube/project.ts +106 -0
- package/src/kube/render.test.ts +142 -0
- package/src/kube/render.ts +252 -0
- package/src/kube/source.test.ts +138 -0
- package/src/kube/source.ts +139 -0
- package/src/kube/target.test.ts +67 -0
- package/src/kube/target.ts +89 -0
- package/src/kube/testing.ts +42 -0
- package/src/kube/top.test.ts +149 -0
- package/src/kube/top.ts +120 -0
- package/src/kube/verdict.test.ts +48 -0
- package/src/kube/verdict.ts +77 -0
- package/src/kube/wait.test.ts +125 -0
- package/src/kube/wait.ts +160 -0
- package/src/lifecycle-integration.test.ts +132 -92
- package/src/op/activities/index.ts +7 -3
- package/src/op/activities/kubectl.test.ts +420 -0
- package/src/op/activities/kubectl.ts +379 -13
- package/src/op/activities/wait-for-ready.test.ts +94 -0
- package/src/op/activities/wait-for-ready.ts +66 -15
- package/src/plugin.test.ts +58 -1
- package/src/plugin.ts +29 -0
- package/src/spec/parse.ts +93 -1
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
|
|
20
|
+
import type { K8sClient, K8sObject } from "@intentius/chant-k8s-client";
|
|
21
|
+
import {
|
|
22
|
+
classifyOwnerChain,
|
|
23
|
+
DEFAULT_MAX_OWNER_CHAIN_DEPTH,
|
|
24
|
+
type OwnerChainNode,
|
|
25
|
+
type OwnerChainVerdict,
|
|
26
|
+
} from "@intentius/chant/owner-chain";
|
|
27
|
+
|
|
28
|
+
interface RawOwnerRef {
|
|
29
|
+
apiVersion?: string;
|
|
30
|
+
kind?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
uid?: string;
|
|
33
|
+
controller?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ownerRefs(obj: K8sObject): RawOwnerRef[] {
|
|
37
|
+
const refs = obj.metadata?.ownerReferences;
|
|
38
|
+
return Array.isArray(refs) ? (refs as RawOwnerRef[]) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The reference Kubernetes' own garbage collector treats as the controlling
|
|
43
|
+
* parent: the entry with `controller: true`, else the first. An object with
|
|
44
|
+
* several owner references (rare — usually a shared, non-controller owner
|
|
45
|
+
* alongside the controlling one) is walked through its controller, matching
|
|
46
|
+
* what actually recreates the object were it deleted.
|
|
47
|
+
*/
|
|
48
|
+
function controllingRef(obj: K8sObject): RawOwnerRef | undefined {
|
|
49
|
+
const refs = ownerRefs(obj);
|
|
50
|
+
return refs.find((r) => r.controller === true) ?? refs[0];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The client surface this module needs — a single object read, addressed by
|
|
54
|
+
* an owner reference's own coordinates. */
|
|
55
|
+
export type OwnerChainReader = Pick<K8sClient, "readIfPresent">;
|
|
56
|
+
|
|
57
|
+
export interface ResolveOwnerChainOptions {
|
|
58
|
+
/** uid → declared chant entity name, from this observation's own resolved entities. */
|
|
59
|
+
declaredByUid: ReadonlyMap<string, string>;
|
|
60
|
+
/** Reads each intermediate owner. */
|
|
61
|
+
reader: OwnerChainReader;
|
|
62
|
+
/**
|
|
63
|
+
* Namespace the starting object lives in. Owner references are same-namespace
|
|
64
|
+
* only — a namespaced object cannot be owned by an object in another
|
|
65
|
+
* namespace, the same rule Kubernetes' own garbage collector enforces —so
|
|
66
|
+
* every hop is read in this namespace.
|
|
67
|
+
*/
|
|
68
|
+
namespace: string | undefined;
|
|
69
|
+
maxDepth?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Walk `obj`'s owner-reference chain up to a declared entity, a foreign root,
|
|
74
|
+
* or a bound (#1077). Fetches at most `maxDepth + 1` objects (the starting
|
|
75
|
+
* object plus up to `maxDepth` ancestors) — the same bound `classifyOwnerChain`
|
|
76
|
+
* enforces when interpreting the result, so nothing is fetched that the
|
|
77
|
+
* interpretation would not have used anyway.
|
|
78
|
+
*/
|
|
79
|
+
export async function resolveK8sOwnerChain(
|
|
80
|
+
obj: K8sObject,
|
|
81
|
+
options: ResolveOwnerChainOptions,
|
|
82
|
+
): Promise<OwnerChainVerdict> {
|
|
83
|
+
const startUid = obj.metadata?.uid;
|
|
84
|
+
if (!startUid) return { root: "unknown" };
|
|
85
|
+
|
|
86
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_OWNER_CHAIN_DEPTH;
|
|
87
|
+
const nodes = new Map<string, OwnerChainNode>();
|
|
88
|
+
const visited = new Set<string>();
|
|
89
|
+
|
|
90
|
+
let uid: string | undefined = startUid;
|
|
91
|
+
let object: K8sObject | undefined = obj;
|
|
92
|
+
|
|
93
|
+
for (let depth = 0; uid !== undefined && depth <= maxDepth; depth++) {
|
|
94
|
+
if (visited.has(uid)) break; // cycle — classifyOwnerChain reads it off `nodes`
|
|
95
|
+
visited.add(uid);
|
|
96
|
+
|
|
97
|
+
const declaredEntity = options.declaredByUid.get(uid);
|
|
98
|
+
if (declaredEntity) {
|
|
99
|
+
nodes.set(uid, { declaredEntity });
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (!object) {
|
|
103
|
+
nodes.set(uid, { ownerUnreadable: true });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ref = controllingRef(object);
|
|
108
|
+
if (!ref?.uid || !ref.kind || !ref.apiVersion || !ref.name) {
|
|
109
|
+
nodes.set(uid, {}); // no further owner — a real, live root
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
nodes.set(uid, { ownerId: ref.uid });
|
|
113
|
+
|
|
114
|
+
if (visited.has(ref.uid)) {
|
|
115
|
+
// About to cycle back to an already-visited node — let the loop's own
|
|
116
|
+
// check classify it next iteration rather than issuing a wasted read.
|
|
117
|
+
uid = ref.uid;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (options.declaredByUid.has(ref.uid)) {
|
|
121
|
+
// Already known to be a declared entity from this observation's own
|
|
122
|
+
// resolved set — no need to read it, the next iteration resolves it
|
|
123
|
+
// from `declaredByUid` directly.
|
|
124
|
+
uid = ref.uid;
|
|
125
|
+
object = undefined;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
object = await options.reader.readIfPresent({
|
|
131
|
+
apiVersion: ref.apiVersion,
|
|
132
|
+
kind: ref.kind,
|
|
133
|
+
name: ref.name,
|
|
134
|
+
namespace: options.namespace,
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
object = undefined;
|
|
138
|
+
}
|
|
139
|
+
if (!object) nodes.set(ref.uid, { ownerUnreadable: true });
|
|
140
|
+
uid = ref.uid;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return classifyOwnerChain(startUid, nodes, maxDepth);
|
|
144
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 a Temporal worker loading the apply
|
|
14
|
+
* activity should not.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_IMPORT_TYPES: readonly string[] = [
|
|
17
|
+
"K8s::Apps::Deployment",
|
|
18
|
+
"K8s::Apps::StatefulSet",
|
|
19
|
+
"K8s::Apps::DaemonSet",
|
|
20
|
+
"K8s::Apps::ReplicaSet",
|
|
21
|
+
"K8s::Core::Service",
|
|
22
|
+
"K8s::Core::ConfigMap",
|
|
23
|
+
"K8s::Core::Secret",
|
|
24
|
+
"K8s::Core::Namespace",
|
|
25
|
+
"K8s::Core::Pod",
|
|
26
|
+
"K8s::Core::PersistentVolumeClaim",
|
|
27
|
+
"K8s::Core::ServiceAccount",
|
|
28
|
+
"K8s::Batch::Job",
|
|
29
|
+
"K8s::Batch::CronJob",
|
|
30
|
+
"K8s::Networking::Ingress",
|
|
31
|
+
"K8s::Networking::NetworkPolicy",
|
|
32
|
+
"K8s::Rbac::Role",
|
|
33
|
+
"K8s::Rbac::RoleBinding",
|
|
34
|
+
"K8s::Rbac::ClusterRole",
|
|
35
|
+
"K8s::Rbac::ClusterRoleBinding",
|
|
36
|
+
];
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
|
|
23
|
+
import type { K8sParseResult } from "../spec/parse";
|
|
24
|
+
import { gvkToApiVersion } from "../spec/parse";
|
|
25
|
+
import { pluralizeKind, type K8sOperationDescriptor, type K8sOperationTable } from "../api/operation-surface";
|
|
26
|
+
|
|
27
|
+
export type { K8sOperationDescriptor, K8sOperationTable };
|
|
28
|
+
|
|
29
|
+
/** Build the operation table from the same parsed results the types come from. */
|
|
30
|
+
export function buildOperationTable(results: K8sParseResult[]): K8sOperationTable {
|
|
31
|
+
const table: K8sOperationTable = {};
|
|
32
|
+
for (const result of results) {
|
|
33
|
+
if (result.isProperty) continue;
|
|
34
|
+
const entityType = result.resource.typeName;
|
|
35
|
+
// A later result for the same type wins nothing — the first parse of a
|
|
36
|
+
// preferred version is canonical, matching the registry's own precedence.
|
|
37
|
+
if (table[entityType]) continue;
|
|
38
|
+
table[entityType] = {
|
|
39
|
+
entityType,
|
|
40
|
+
apiVersion: gvkToApiVersion(result.gvk),
|
|
41
|
+
kind: result.gvk.kind,
|
|
42
|
+
plural: result.operation?.plural ?? pluralizeKind(result.gvk.kind),
|
|
43
|
+
scope: result.operation?.scope ?? "Namespaced",
|
|
44
|
+
verbs: result.operation?.verbs ?? [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return table;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Serialize the table, key-sorted so regeneration produces a stable diff. */
|
|
51
|
+
export function generateOperationsJSON(results: K8sParseResult[]): string {
|
|
52
|
+
const table = buildOperationTable(results);
|
|
53
|
+
const sorted: K8sOperationTable = {};
|
|
54
|
+
for (const key of Object.keys(table).sort()) sorted[key] = table[key];
|
|
55
|
+
return `${JSON.stringify(sorted, null, 2)}\n`;
|
|
56
|
+
}
|
package/src/codegen/generate.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { loadMultipleCRDs } from "../crd/loader";
|
|
|
17
17
|
import { CRD_SOURCES } from "../crd/crd-sources";
|
|
18
18
|
import { NamingStrategy, propertyTypeName, extractDefName } from "./naming";
|
|
19
19
|
import { generateLexiconJSON } from "./generate-lexicon";
|
|
20
|
+
import { generateOperationsJSON } from "./generate-operations";
|
|
20
21
|
import { generateTypeScriptDeclarations } from "./generate-typescript";
|
|
21
22
|
import {
|
|
22
23
|
generateRuntimeIndex as coreGenerateRuntimeIndex,
|
|
@@ -98,6 +99,13 @@ export async function generate(opts: K8sGenerateOptions = {}): Promise<GenerateR
|
|
|
98
99
|
generateRuntimeIndex: (results, naming) => {
|
|
99
100
|
return generateRuntimeIndex(results, naming as NamingStrategy);
|
|
100
101
|
},
|
|
102
|
+
|
|
103
|
+
// chant #1074 — the operation surface, out of the same results the types
|
|
104
|
+
// and the registry come out of, so the live client cannot address a kind
|
|
105
|
+
// differently from how the declarable surface names it.
|
|
106
|
+
generateExtraArtifacts: (results) => ({
|
|
107
|
+
"operations.json": generateOperationsJSON(results),
|
|
108
|
+
}),
|
|
101
109
|
};
|
|
102
110
|
|
|
103
111
|
return generatePipeline(config, opts);
|
|
@@ -114,6 +122,7 @@ export function writeGeneratedFiles(result: GenerateResult, baseDir: string): vo
|
|
|
114
122
|
"index.d.ts": result.typesDTS,
|
|
115
123
|
"index.ts": result.indexTS,
|
|
116
124
|
"runtime.ts": `/**\n * Runtime factory constructors — re-exported from core.\n */\nexport { createResource, createProperty } from "@intentius/chant/runtime";\n`,
|
|
125
|
+
...(result.extraArtifacts ?? {}),
|
|
117
126
|
},
|
|
118
127
|
});
|
|
119
128
|
}
|
package/src/config.ts
CHANGED
|
@@ -60,4 +60,21 @@ export interface K8sClusterProfile {
|
|
|
60
60
|
export interface K8sChantConfig {
|
|
61
61
|
/** Named environment → cluster bindings, keyed by environment name. */
|
|
62
62
|
profiles?: Record<string, K8sClusterProfile>;
|
|
63
|
+
/**
|
|
64
|
+
* Exec credential-plugin commands chant may execute (chant #1074).
|
|
65
|
+
*
|
|
66
|
+
* On EKS, AKS and GKE, kubeconfig authentication is a subprocess:
|
|
67
|
+
* `aws eks get-token`, `kubelogin`, `gke-gcloud-auth-plugin`. Those three
|
|
68
|
+
* plus `kubectl` are allowed by default. Anything else the kubeconfig names
|
|
69
|
+
* is refused, because an exec plugin is an arbitrary binary named in a file
|
|
70
|
+
* chant did not write. Setting this **replaces** the default list.
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* k8s: {
|
|
74
|
+
* profiles: { prod: { context: "prod-eks" } },
|
|
75
|
+
* execCredentialPlugins: ["aws", "my-org-oidc-helper"],
|
|
76
|
+
* } satisfies K8sChantConfig
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
execCredentialPlugins?: string[];
|
|
63
80
|
}
|
package/src/crd/parser.ts
CHANGED
|
@@ -124,6 +124,14 @@ export function parseCRDSpec(spec: CRDSpec): K8sParseResult[] {
|
|
|
124
124
|
propertyTypes: status.propertyType ? [...propertyTypes, status.propertyType] : propertyTypes,
|
|
125
125
|
enums: [],
|
|
126
126
|
gvk,
|
|
127
|
+
// chant #1074 — the CRD declares its own plural and scope, so the
|
|
128
|
+
// operation surface for a custom resource comes from the same document its
|
|
129
|
+
// types do, exactly as the OpenAPI `paths` supply them for built-in kinds.
|
|
130
|
+
operation: {
|
|
131
|
+
plural: spec.names.plural,
|
|
132
|
+
scope: spec.scope ?? "Namespaced",
|
|
133
|
+
verbs: ["delete", "get", "list", "patch", "post", "put", "watch"],
|
|
134
|
+
},
|
|
127
135
|
});
|
|
128
136
|
|
|
129
137
|
return results;
|
package/src/crd/types.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface CRDSource {
|
|
|
34
34
|
export interface CRDSpec {
|
|
35
35
|
/** API group (e.g. "cert-manager.io") */
|
|
36
36
|
group: string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether instances are namespaced. Declared by the CRD itself, so it is the
|
|
39
|
+
* authoritative source for a custom resource's scope — the equivalent of what
|
|
40
|
+
* the OpenAPI `paths` say for built-in kinds (chant #1074). Defaults to
|
|
41
|
+
* `Namespaced`, matching the API server's own default.
|
|
42
|
+
*/
|
|
43
|
+
scope?: "Namespaced" | "Cluster";
|
|
37
44
|
/** Name variants for the CRD */
|
|
38
45
|
names: {
|
|
39
46
|
kind: string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The k8s lexicon's *static* deep-observation noise rules (#1076, epic #1073).
|
|
3
|
+
*
|
|
4
|
+
* Split out from `./deep-observe.ts` for one reason: this file must be safe
|
|
5
|
+
* to import from `plugin.ts` at module load time, because
|
|
6
|
+
* `LexiconPlugin.deepNormalizationHooks` is plain data core reads to
|
|
7
|
+
* normalize the *declared* tree — the half of the contract that runs whether
|
|
8
|
+
* or not a cluster is ever touched (`lifecycle diff` without `--live`,
|
|
9
|
+
* `chant build`, tests that only exercise normalization). `./deep-observe.ts`
|
|
10
|
+
* itself imports `@intentius/chant-k8s-client` for the live read, and chant
|
|
11
|
+
* #1074 made that package's reachability from the build path a structural
|
|
12
|
+
* property (`examples/k8s-client-boundary.test.ts`) rather than a lint rule —
|
|
13
|
+
* so nothing this file exports may pull that package in, directly or
|
|
14
|
+
* transitively. It imports nothing but the core contract's own types.
|
|
15
|
+
*
|
|
16
|
+
* What lives here is deliberately the *entityType-keyed, resource-agnostic*
|
|
17
|
+
* half of the rules: which fields the API server always populates regardless
|
|
18
|
+
* of what a manager wrote (`status`, `metadata.uid`, …), which fields
|
|
19
|
+
* Kubernetes defaults when a manifest is silent about them, and which arrays
|
|
20
|
+
* are sets addressed by a well-known identity (containers by name, ports by
|
|
21
|
+
* containerPort+protocol). None of that needs a live object in hand.
|
|
22
|
+
*
|
|
23
|
+
* What does *not* live here is the managed-fields prune — whether one
|
|
24
|
+
* specific field on one specific live object is chant-owned, foreign-owned,
|
|
25
|
+
* or contested. That is inherently per-object (it depends on *that* object's
|
|
26
|
+
* `metadata.managedFields`, which the declared tree never carries and which
|
|
27
|
+
* differs between two Deployments of the same type), so it cannot be
|
|
28
|
+
* expressed as a fixed rule keyed only by entity type and path — the shape
|
|
29
|
+
* every other hook in this file takes. `./deep-observe.ts` computes it once
|
|
30
|
+
* per resource and layers it on top of the rules below.
|
|
31
|
+
*
|
|
32
|
+
* The *entity-type-agnostic* half of these rules — which fields every
|
|
33
|
+
* Kubernetes API object carries regardless of kind, and the well-known
|
|
34
|
+
* list-map-key ordering conventions (containers/env/volumes/ports) — lives in
|
|
35
|
+
* `@intentius/chant/managed-fields` (chant #1087), because a GCP Config
|
|
36
|
+
* Connector custom resource is a Kubernetes object too and needs the exact
|
|
37
|
+
* same rules without depending on this lexicon's package. What stays here is
|
|
38
|
+
* only what's genuinely k8s-*lexicon*-specific: {@link K8S_SERVICE_DEFAULTS},
|
|
39
|
+
* keyed by chant's own k8s entityType catalog.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import type { DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
|
|
43
|
+
import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, k8sListMapOrderKey } from "@intentius/chant/managed-fields";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Kubernetes-defaulted fields, per entity type, as index-erased property
|
|
47
|
+
* paths. Subtracted only where source never declared the property
|
|
48
|
+
* (`side === "live" && counterpart === "absent"`) — cdk-real-drift's default
|
|
49
|
+
* subtraction, same convention as AWS/Azure/Temporal's tables.
|
|
50
|
+
*
|
|
51
|
+
* Sparse and evidence-based rather than derived from the generated schema:
|
|
52
|
+
* the k8s OpenAPI spec this lexicon's codegen consumes
|
|
53
|
+
* (`lexicons/k8s/src/spec/parse.ts`) does not carry a `default` value for
|
|
54
|
+
* these fields the way ARM's schema sometimes does, so "per discovery" is not
|
|
55
|
+
* actually expressible today. Widening this table is additive and needs no
|
|
56
|
+
* contract change.
|
|
57
|
+
*
|
|
58
|
+
* `spec.strategy` is listed whole, not as `spec.strategy.type`, for the same
|
|
59
|
+
* reason Temporal's `TEMPORAL_SCHEDULE_DEFAULTS` lists `state` whole: pruning
|
|
60
|
+
* only the leaf would still recurse into the object, and a nested default the
|
|
61
|
+
* table does not separately name (`rollingUpdate.maxSurge`/`maxUnavailable`,
|
|
62
|
+
* both `"25%"` when `spec.strategy` is omitted entirely) would leave behind an
|
|
63
|
+
* empty `strategy: {}` — a value distinct from no `strategy` key at all, and
|
|
64
|
+
* itself a spurious "undeclared" finding. Matching the whole node first, before
|
|
65
|
+
* its children are ever visited, drops the wrapper outright when every field
|
|
66
|
+
* under it is exactly at its default.
|
|
67
|
+
*/
|
|
68
|
+
export const K8S_SERVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
|
|
69
|
+
"K8s::Apps::Deployment": {
|
|
70
|
+
"spec.strategy": { type: "RollingUpdate", rollingUpdate: { maxSurge: "25%", maxUnavailable: "25%" } },
|
|
71
|
+
"spec.revisionHistoryLimit": 10,
|
|
72
|
+
"spec.progressDeadlineSeconds": 600,
|
|
73
|
+
"spec.template.spec.dnsPolicy": "ClusterFirst",
|
|
74
|
+
"spec.template.spec.restartPolicy": "Always",
|
|
75
|
+
"spec.template.spec.terminationGracePeriodSeconds": 30,
|
|
76
|
+
"spec.template.spec.schedulerName": "default-scheduler",
|
|
77
|
+
},
|
|
78
|
+
"K8s::Core::Service": {
|
|
79
|
+
"spec.sessionAffinity": "None",
|
|
80
|
+
"spec.type": "ClusterIP",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a natural identity field. */
|
|
85
|
+
function canonicalJson(value: unknown): string {
|
|
86
|
+
return (
|
|
87
|
+
JSON.stringify(value, (_k, v: unknown) =>
|
|
88
|
+
v && typeof v === "object" && !Array.isArray(v)
|
|
89
|
+
? Object.fromEntries(Object.entries(v as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
90
|
+
: v,
|
|
91
|
+
) ?? ""
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The k8s lexicon's static noise rules: the generic Kubernetes object
|
|
97
|
+
* envelope (unconditional, by pattern, `@intentius/chant/managed-fields`) and
|
|
98
|
+
* Kubernetes-defaulted fields (gated on `counterpart === "absent"`), plus the
|
|
99
|
+
* array orderings the acceptance criteria name — `x-kubernetes-patch-merge-
|
|
100
|
+
* key`/`list-map-keys` conventions the generated surface does not currently
|
|
101
|
+
* carry (see the module doc), so these are the "else named-by-name
|
|
102
|
+
* conventions" the issue calls for: containers by `name`, `env` by `name`,
|
|
103
|
+
* `volumes` by `name`, container/service `ports` by `containerPort`/`port` +
|
|
104
|
+
* `protocol` — the same conventions `@intentius/chant/managed-fields`'s
|
|
105
|
+
* `k8sListMapOrderKey` implements, reused verbatim.
|
|
106
|
+
*
|
|
107
|
+
* This is the object `k8sPlugin.deepNormalizationHooks` is. It is also what
|
|
108
|
+
* `./deep-observe.ts` layers its per-resource managed-fields prune on top of,
|
|
109
|
+
* so the two normalization passes (the reader's own, and core's later
|
|
110
|
+
* re-normalization of both the declared and the already-normalized live tree
|
|
111
|
+
* — see `packages/core/src/lifecycle/deep-observe.ts`) apply the identical
|
|
112
|
+
* entityType-keyed rules either way.
|
|
113
|
+
*/
|
|
114
|
+
export const k8sDeepNormalizationHooks: DeepNormalizationHooks = {
|
|
115
|
+
prune(node: DeepNode): boolean {
|
|
116
|
+
if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern)) return true;
|
|
117
|
+
|
|
118
|
+
if (node.side !== "live" || node.counterpart !== "absent") return false;
|
|
119
|
+
const defaults = K8S_SERVICE_DEFAULTS[node.entityType];
|
|
120
|
+
if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern)) return false;
|
|
121
|
+
return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
orderKey: k8sListMapOrderKey,
|
|
125
|
+
};
|