@intentius/chant-lexicon-k8s 0.38.0 → 0.41.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/codegen/generate-list-map-keys.d.ts +53 -0
- package/dist/codegen/generate-list-map-keys.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts.map +1 -1
- package/dist/composites/adot-collector.d.ts +7 -1
- package/dist/composites/adot-collector.d.ts.map +1 -1
- package/dist/composites/agic-ingress.d.ts +3 -1
- package/dist/composites/agic-ingress.d.ts.map +1 -1
- package/dist/composites/aks-external-dns-agent.d.ts +6 -1
- package/dist/composites/aks-external-dns-agent.d.ts.map +1 -1
- package/dist/composites/alb-ingress.d.ts +3 -1
- package/dist/composites/alb-ingress.d.ts.map +1 -1
- package/dist/composites/autoscaled-service.d.ts +6 -1
- package/dist/composites/autoscaled-service.d.ts.map +1 -1
- package/dist/composites/azure-disk-storage-class.d.ts +3 -1
- package/dist/composites/azure-disk-storage-class.d.ts.map +1 -1
- package/dist/composites/azure-file-storage-class.d.ts +3 -1
- package/dist/composites/azure-file-storage-class.d.ts.map +1 -1
- package/dist/composites/azure-monitor-collector.d.ts +7 -1
- package/dist/composites/azure-monitor-collector.d.ts.map +1 -1
- package/dist/composites/batch-job.d.ts +1 -1
- package/dist/composites/batch-job.d.ts.map +1 -1
- package/dist/composites/cockroachdb-cluster.d.ts +13 -1
- package/dist/composites/cockroachdb-cluster.d.ts.map +1 -1
- package/dist/composites/config-connector-context.d.ts +3 -1
- package/dist/composites/config-connector-context.d.ts.map +1 -1
- package/dist/composites/configured-app.d.ts +1 -1
- package/dist/composites/configured-app.d.ts.map +1 -1
- package/dist/composites/cron-workload.d.ts +6 -1
- package/dist/composites/cron-workload.d.ts.map +1 -1
- package/dist/composites/ebs-storage-class.d.ts +3 -1
- package/dist/composites/ebs-storage-class.d.ts.map +1 -1
- package/dist/composites/efs-storage-class.d.ts +3 -1
- package/dist/composites/efs-storage-class.d.ts.map +1 -1
- package/dist/composites/external-dns-agent.d.ts +6 -1
- package/dist/composites/external-dns-agent.d.ts.map +1 -1
- package/dist/composites/filestore-storage-class.d.ts +3 -1
- package/dist/composites/filestore-storage-class.d.ts.map +1 -1
- package/dist/composites/fluent-bit-agent.d.ts +7 -1
- package/dist/composites/fluent-bit-agent.d.ts.map +1 -1
- package/dist/composites/gce-ingress.d.ts +3 -1
- package/dist/composites/gce-ingress.d.ts.map +1 -1
- package/dist/composites/gce-pd-storage-class.d.ts +3 -1
- package/dist/composites/gce-pd-storage-class.d.ts.map +1 -1
- package/dist/composites/gke-external-dns-agent.d.ts +6 -1
- package/dist/composites/gke-external-dns-agent.d.ts.map +1 -1
- package/dist/composites/gke-fluent-bit-agent.d.ts +7 -1
- package/dist/composites/gke-fluent-bit-agent.d.ts.map +1 -1
- package/dist/composites/gke-gateway.d.ts +4 -1
- package/dist/composites/gke-gateway.d.ts.map +1 -1
- package/dist/composites/gke-otel-collector.d.ts +7 -1
- package/dist/composites/gke-otel-collector.d.ts.map +1 -1
- package/dist/composites/irsa-service-account.d.ts +1 -1
- package/dist/composites/irsa-service-account.d.ts.map +1 -1
- package/dist/composites/metrics-server.d.ts +10 -1
- package/dist/composites/metrics-server.d.ts.map +1 -1
- package/dist/composites/monitored-service.d.ts +1 -1
- package/dist/composites/monitored-service.d.ts.map +1 -1
- package/dist/composites/namespace-env.d.ts +1 -1
- package/dist/composites/namespace-env.d.ts.map +1 -1
- package/dist/composites/network-isolated-app.d.ts +5 -1
- package/dist/composites/network-isolated-app.d.ts.map +1 -1
- package/dist/composites/node-agent.d.ts +1 -1
- package/dist/composites/node-agent.d.ts.map +1 -1
- package/dist/composites/ray-cluster.d.ts +10 -1
- package/dist/composites/ray-cluster.d.ts.map +1 -1
- package/dist/composites/ray-job.d.ts +8 -1
- package/dist/composites/ray-job.d.ts.map +1 -1
- package/dist/composites/ray-service.d.ts +10 -1
- package/dist/composites/ray-service.d.ts.map +1 -1
- package/dist/composites/secure-ingress.d.ts +1 -1
- package/dist/composites/secure-ingress.d.ts.map +1 -1
- package/dist/composites/sidecar-app.d.ts +4 -1
- package/dist/composites/sidecar-app.d.ts.map +1 -1
- package/dist/composites/stateful-app.d.ts +1 -1
- package/dist/composites/stateful-app.d.ts.map +1 -1
- package/dist/composites/web-app.d.ts +1 -1
- package/dist/composites/web-app.d.ts.map +1 -1
- package/dist/composites/worker-pool.d.ts +1 -1
- package/dist/composites/worker-pool.d.ts.map +1 -1
- package/dist/composites/workload-identity-sa.d.ts +1 -1
- package/dist/composites/workload-identity-sa.d.ts.map +1 -1
- package/dist/composites/workload-identity-service-account.d.ts +1 -1
- package/dist/composites/workload-identity-service-account.d.ts.map +1 -1
- package/dist/config-schema.d.ts +40 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +15 -18
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +1 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/spec/parse.d.ts +17 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/codegen/generate-list-map-keys.test.ts +112 -0
- package/src/codegen/generate-list-map-keys.ts +99 -0
- package/src/codegen/generate.ts +9 -1
- package/src/composites/adot-collector.ts +1 -1
- package/src/composites/agic-ingress.ts +1 -1
- package/src/composites/aks-external-dns-agent.ts +1 -1
- package/src/composites/alb-ingress.ts +1 -1
- package/src/composites/autoscaled-service.ts +1 -1
- package/src/composites/azure-disk-storage-class.ts +1 -1
- package/src/composites/azure-file-storage-class.ts +1 -1
- package/src/composites/azure-monitor-collector.ts +1 -1
- package/src/composites/batch-job.ts +1 -1
- package/src/composites/cockroachdb-cluster.ts +1 -1
- package/src/composites/config-connector-context.ts +1 -1
- package/src/composites/configured-app.ts +1 -1
- package/src/composites/cron-workload.ts +1 -1
- package/src/composites/ebs-storage-class.ts +1 -1
- package/src/composites/efs-storage-class.ts +1 -1
- package/src/composites/external-dns-agent.ts +1 -1
- package/src/composites/filestore-storage-class.ts +1 -1
- package/src/composites/fluent-bit-agent.ts +1 -1
- package/src/composites/gce-ingress.ts +1 -1
- package/src/composites/gce-pd-storage-class.ts +1 -1
- package/src/composites/gke-external-dns-agent.ts +1 -1
- package/src/composites/gke-fluent-bit-agent.ts +1 -1
- package/src/composites/gke-gateway.ts +1 -1
- package/src/composites/gke-otel-collector.ts +1 -1
- package/src/composites/irsa-service-account.ts +1 -1
- package/src/composites/metrics-server.ts +1 -1
- package/src/composites/monitored-service.ts +1 -1
- package/src/composites/namespace-env.ts +1 -1
- package/src/composites/network-isolated-app.ts +1 -1
- package/src/composites/node-agent.ts +1 -1
- package/src/composites/ray-cluster.ts +1 -1
- package/src/composites/ray-job.ts +1 -1
- package/src/composites/ray-service.ts +1 -1
- package/src/composites/secure-ingress.ts +1 -1
- package/src/composites/sidecar-app.ts +1 -1
- package/src/composites/stateful-app.ts +1 -1
- package/src/composites/web-app.ts +1 -1
- package/src/composites/worker-pool.ts +1 -1
- package/src/composites/workload-identity-sa.ts +1 -1
- package/src/composites/workload-identity-service-account.ts +1 -1
- package/src/config-schema.ts +46 -0
- package/src/config.ts +25 -1
- package/src/deep-observe-hooks.ts +90 -10
- package/src/deep-observe.ts +7 -0
- package/src/describe-resources.test.ts +138 -1
- package/src/describe-resources.ts +98 -0
- package/src/generated/list-map-keys.json +166 -0
- package/src/list-map-order-key.test.ts +119 -0
- package/src/plugin.ts +2 -0
- package/src/spec/parse.test.ts +42 -0
- package/src/spec/parse.ts +51 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
{
|
|
2
|
+
"addresses": [
|
|
3
|
+
[
|
|
4
|
+
"type"
|
|
5
|
+
]
|
|
6
|
+
],
|
|
7
|
+
"allocatedResourcesStatus": [
|
|
8
|
+
[
|
|
9
|
+
"name"
|
|
10
|
+
]
|
|
11
|
+
],
|
|
12
|
+
"claims": [
|
|
13
|
+
[
|
|
14
|
+
"name"
|
|
15
|
+
]
|
|
16
|
+
],
|
|
17
|
+
"conditions": [
|
|
18
|
+
[
|
|
19
|
+
"type"
|
|
20
|
+
]
|
|
21
|
+
],
|
|
22
|
+
"containers": [
|
|
23
|
+
[
|
|
24
|
+
"name"
|
|
25
|
+
]
|
|
26
|
+
],
|
|
27
|
+
"devices": [
|
|
28
|
+
[
|
|
29
|
+
"device",
|
|
30
|
+
"driver",
|
|
31
|
+
"pool",
|
|
32
|
+
"shareID"
|
|
33
|
+
]
|
|
34
|
+
],
|
|
35
|
+
"drivers": [
|
|
36
|
+
[
|
|
37
|
+
"name"
|
|
38
|
+
]
|
|
39
|
+
],
|
|
40
|
+
"env": [
|
|
41
|
+
[
|
|
42
|
+
"name"
|
|
43
|
+
]
|
|
44
|
+
],
|
|
45
|
+
"ephemeralContainers": [
|
|
46
|
+
[
|
|
47
|
+
"name"
|
|
48
|
+
]
|
|
49
|
+
],
|
|
50
|
+
"hostAliases": [
|
|
51
|
+
[
|
|
52
|
+
"ip"
|
|
53
|
+
]
|
|
54
|
+
],
|
|
55
|
+
"imagePullSecrets": [
|
|
56
|
+
[
|
|
57
|
+
"name"
|
|
58
|
+
]
|
|
59
|
+
],
|
|
60
|
+
"initContainers": [
|
|
61
|
+
[
|
|
62
|
+
"name"
|
|
63
|
+
]
|
|
64
|
+
],
|
|
65
|
+
"matchConditions": [
|
|
66
|
+
[
|
|
67
|
+
"name"
|
|
68
|
+
]
|
|
69
|
+
],
|
|
70
|
+
"ownerReferences": [
|
|
71
|
+
[
|
|
72
|
+
"uid"
|
|
73
|
+
]
|
|
74
|
+
],
|
|
75
|
+
"podGroupTemplates": [
|
|
76
|
+
[
|
|
77
|
+
"name"
|
|
78
|
+
]
|
|
79
|
+
],
|
|
80
|
+
"podIPs": [
|
|
81
|
+
[
|
|
82
|
+
"ip"
|
|
83
|
+
]
|
|
84
|
+
],
|
|
85
|
+
"ports": [
|
|
86
|
+
[
|
|
87
|
+
"containerPort",
|
|
88
|
+
"protocol"
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"port",
|
|
92
|
+
"protocol"
|
|
93
|
+
]
|
|
94
|
+
],
|
|
95
|
+
"reservedFor": [
|
|
96
|
+
[
|
|
97
|
+
"uid"
|
|
98
|
+
]
|
|
99
|
+
],
|
|
100
|
+
"resourceClaimStatuses": [
|
|
101
|
+
[
|
|
102
|
+
"name"
|
|
103
|
+
]
|
|
104
|
+
],
|
|
105
|
+
"resourceClaims": [
|
|
106
|
+
[
|
|
107
|
+
"name"
|
|
108
|
+
]
|
|
109
|
+
],
|
|
110
|
+
"resources": [
|
|
111
|
+
[
|
|
112
|
+
"resourceID"
|
|
113
|
+
]
|
|
114
|
+
],
|
|
115
|
+
"schedulingGates": [
|
|
116
|
+
[
|
|
117
|
+
"name"
|
|
118
|
+
]
|
|
119
|
+
],
|
|
120
|
+
"secrets": [
|
|
121
|
+
[
|
|
122
|
+
"name"
|
|
123
|
+
]
|
|
124
|
+
],
|
|
125
|
+
"storageVersions": [
|
|
126
|
+
[
|
|
127
|
+
"apiServerID"
|
|
128
|
+
]
|
|
129
|
+
],
|
|
130
|
+
"topologySpreadConstraints": [
|
|
131
|
+
[
|
|
132
|
+
"topologyKey",
|
|
133
|
+
"whenUnsatisfiable"
|
|
134
|
+
]
|
|
135
|
+
],
|
|
136
|
+
"variables": [
|
|
137
|
+
[
|
|
138
|
+
"name"
|
|
139
|
+
]
|
|
140
|
+
],
|
|
141
|
+
"volumeDevices": [
|
|
142
|
+
[
|
|
143
|
+
"devicePath"
|
|
144
|
+
]
|
|
145
|
+
],
|
|
146
|
+
"volumeMounts": [
|
|
147
|
+
[
|
|
148
|
+
"mountPath"
|
|
149
|
+
]
|
|
150
|
+
],
|
|
151
|
+
"volumes": [
|
|
152
|
+
[
|
|
153
|
+
"name"
|
|
154
|
+
]
|
|
155
|
+
],
|
|
156
|
+
"webhooks": [
|
|
157
|
+
[
|
|
158
|
+
"name"
|
|
159
|
+
]
|
|
160
|
+
],
|
|
161
|
+
"x-kubernetes-validations": [
|
|
162
|
+
[
|
|
163
|
+
"rule"
|
|
164
|
+
]
|
|
165
|
+
]
|
|
166
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { schemaListMapOrderKey } from "./deep-observe-hooks";
|
|
3
|
+
import { k8sListMapOrderKey } from "@intentius/chant/managed-fields";
|
|
4
|
+
import type { DeepArrayElement } from "@intentius/chant/lexicon";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* chant #1441 — list elements are identified by the key the SPEC declares,
|
|
8
|
+
* with the hand-written conventions kept as fallback.
|
|
9
|
+
*
|
|
10
|
+
* The behavioural claim under test: two observations of the same object whose
|
|
11
|
+
* associative lists arrive in different orders must canonicalize to the same
|
|
12
|
+
* sequence, so ordering alone is never reported as drift.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function el(pattern: string, element: unknown, index = 0): DeepArrayElement {
|
|
16
|
+
return { entityType: "K8s::Apps::Deployment", pattern, path: pattern, element, index, side: "live" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Sort a list the way core's normalization does, using the hook's key. */
|
|
20
|
+
function canonicalOrder(pattern: string, elements: unknown[]): unknown[] {
|
|
21
|
+
return elements
|
|
22
|
+
.map((element, index) => ({ element, key: schemaListMapOrderKey(el(pattern, element, index)) }))
|
|
23
|
+
.sort((a, b) => (a.key ?? "").localeCompare(b.key ?? ""))
|
|
24
|
+
.map((e) => e.element);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("conditions — the case the hardcoded table missed", () => {
|
|
28
|
+
const ready = { type: "Ready", status: "True" };
|
|
29
|
+
const progressing = { type: "Progressing", status: "True" };
|
|
30
|
+
|
|
31
|
+
test("is identified by type", () => {
|
|
32
|
+
expect(schemaListMapOrderKey(el("status.conditions", ready))).toBe("Ready");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("was NOT identified before this change", () => {
|
|
36
|
+
// Guards the premise: without the spec-derived table, `conditions` fell
|
|
37
|
+
// through to `undefined`, which leaves the array order alone — so two
|
|
38
|
+
// orderings compared positionally and read as drift.
|
|
39
|
+
expect(k8sListMapOrderKey(el("status.conditions", ready))).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("two orderings of the same conditions canonicalize identically", () => {
|
|
43
|
+
expect(canonicalOrder("status.conditions", [ready, progressing])).toEqual(
|
|
44
|
+
canonicalOrder("status.conditions", [progressing, ready]),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("ports — one name, two key shapes", () => {
|
|
50
|
+
test("a container port is keyed by containerPort and protocol", () => {
|
|
51
|
+
// Byte-identical to what the hardcoded implementation produced, because
|
|
52
|
+
// this string is also the element's address in drift output.
|
|
53
|
+
const element = { containerPort: 8080, protocol: "TCP" };
|
|
54
|
+
expect(schemaListMapOrderKey(el("spec.template.spec.containers.ports", element))).toBe("08080/TCP");
|
|
55
|
+
expect(schemaListMapOrderKey(el("spec.template.spec.containers.ports", element))).toBe(
|
|
56
|
+
k8sListMapOrderKey(el("spec.template.spec.containers.ports", element)),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a Service port is keyed by port and protocol", () => {
|
|
61
|
+
expect(schemaListMapOrderKey(el("spec.ports", { port: 443, protocol: "TCP" }))).toBe("00443/TCP");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("numeric key fields order numerically, not lexicographically", () => {
|
|
65
|
+
const ordered = canonicalOrder("spec.ports", [
|
|
66
|
+
{ port: 443, protocol: "TCP" },
|
|
67
|
+
{ port: 80, protocol: "TCP" },
|
|
68
|
+
]);
|
|
69
|
+
expect(ordered).toEqual([
|
|
70
|
+
{ port: 80, protocol: "TCP" },
|
|
71
|
+
{ port: 443, protocol: "TCP" },
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("fallback", () => {
|
|
77
|
+
test("a list the spec does not annotate still gets the hand-written key", () => {
|
|
78
|
+
// `containers` is in both the generated table and the hardcoded one; the
|
|
79
|
+
// point is that removing an element's spec-declared fields does not strand
|
|
80
|
+
// it — it lands on the by-name convention.
|
|
81
|
+
const key = schemaListMapOrderKey(el("spec.template.spec.containers", { name: "app", image: "nginx" }));
|
|
82
|
+
expect(key).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("an unknown list is left unordered, exactly as before", () => {
|
|
86
|
+
expect(schemaListMapOrderKey(el("spec.somethingUnknown", { a: 1 }))).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a non-object element is left to the fallback rather than crashing", () => {
|
|
90
|
+
expect(() => schemaListMapOrderKey(el("spec.template.spec.containers", "not-an-object"))).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("an element missing its declared key fields falls through", () => {
|
|
94
|
+
// A conditions entry with no `type` cannot be identified by the spec key.
|
|
95
|
+
// It must not produce `type=undefined`, which would collide across
|
|
96
|
+
// elements and silently merge distinct entries.
|
|
97
|
+
const key = schemaListMapOrderKey(el("status.conditions", { status: "True" }));
|
|
98
|
+
expect(key ?? "").not.toContain("undefined");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("compatibility with the hardcoded table", () => {
|
|
103
|
+
// The order key is the element's address in drift output, so every property
|
|
104
|
+
// the hand-written table already covered must keep producing the identical
|
|
105
|
+
// string. A change here silently rewrites existing drift paths.
|
|
106
|
+
const cases: Array<[string, unknown]> = [
|
|
107
|
+
["spec.template.spec.containers", { name: "app", image: "nginx" }],
|
|
108
|
+
["spec.template.spec.initContainers", { name: "init" }],
|
|
109
|
+
["spec.template.spec.ephemeralContainers", { name: "debug" }],
|
|
110
|
+
["spec.template.spec.containers.env", { name: "PORT", value: "8080" }],
|
|
111
|
+
["spec.template.spec.volumes", { name: "data" }],
|
|
112
|
+
["spec.template.spec.containers.ports", { containerPort: 80, protocol: "TCP" }],
|
|
113
|
+
["spec.ports", { port: 443, protocol: "TCP" }],
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
test.each(cases)("%s produces the same key as before", (pattern, element) => {
|
|
117
|
+
expect(schemaListMapOrderKey(el(pattern, element))).toBe(k8sListMapOrderKey(el(pattern, element)));
|
|
118
|
+
});
|
|
119
|
+
});
|
package/src/plugin.ts
CHANGED
|
@@ -27,10 +27,12 @@ import { K8sParser } from "./import/parser";
|
|
|
27
27
|
import { K8sGenerator } from "./import/generator";
|
|
28
28
|
import { k8sDeepNormalizationHooks } from "./deep-observe-hooks";
|
|
29
29
|
import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
30
|
+
import { k8sConfigSchema } from "./config-schema";
|
|
30
31
|
|
|
31
32
|
export const k8sPlugin: LexiconPlugin = {
|
|
32
33
|
name: "k8s",
|
|
33
34
|
ownershipChannel: { keys: LABEL_OWNERSHIP_KEYS, reads: ["describeResources", "observeResourcesDeep", "exportResources"] },
|
|
35
|
+
configSchema: k8sConfigSchema,
|
|
34
36
|
auditCatalog: () => k8sAuditCatalog,
|
|
35
37
|
serializer: k8sSerializer,
|
|
36
38
|
// Self-upgrade: where the pinned Kubernetes schema version lives + its upstream (#685).
|
package/src/spec/parse.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
gvkToApiVersion,
|
|
5
5
|
k8sShortName,
|
|
6
6
|
k8sServiceName,
|
|
7
|
+
specListMapKeyPairs,
|
|
7
8
|
} from "./parse";
|
|
8
9
|
|
|
9
10
|
describe("gvkToTypeName", () => {
|
|
@@ -100,3 +101,44 @@ describe("k8sServiceName", () => {
|
|
|
100
101
|
expect(typeof name).toBe("string");
|
|
101
102
|
});
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* chant #1441 — the merge semantics the API server publishes, read off every
|
|
107
|
+
* definition rather than only the ones chant emits a type for.
|
|
108
|
+
*/
|
|
109
|
+
describe("specListMapKeyPairs", () => {
|
|
110
|
+
const spec = JSON.stringify({
|
|
111
|
+
definitions: {
|
|
112
|
+
"io.k8s.api.core.v1.PodSpec": {
|
|
113
|
+
properties: {
|
|
114
|
+
containers: { type: "array", "x-kubernetes-list-type": "map", "x-kubernetes-list-map-keys": ["name"] },
|
|
115
|
+
// atomic and set lists carry no identity key
|
|
116
|
+
tolerations: { type: "array", "x-kubernetes-list-type": "atomic" },
|
|
117
|
+
finalizers: { type: "array", "x-kubernetes-list-type": "set" },
|
|
118
|
+
nodeName: { type: "string" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"io.k8s.api.core.v1.ServiceSpec": {
|
|
122
|
+
properties: {
|
|
123
|
+
ports: { type: "array", "x-kubernetes-list-type": "map", "x-kubernetes-list-map-keys": ["port", "protocol"] },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
"io.k8s.api.core.v1.NoProperties": {},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("collects only map-typed lists that name their keys", () => {
|
|
131
|
+
expect(specListMapKeyPairs(spec)).toEqual([
|
|
132
|
+
["containers", ["name"]],
|
|
133
|
+
["ports", ["port", "protocol"]],
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("accepts a Buffer, as the fetch path supplies", () => {
|
|
138
|
+
expect(specListMapKeyPairs(Buffer.from(spec, "utf-8"))).toHaveLength(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a spec with no definitions yields nothing rather than throwing", () => {
|
|
142
|
+
expect(specListMapKeyPairs(JSON.stringify({}))).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/spec/parse.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface ParsedProperty {
|
|
|
25
25
|
description?: string;
|
|
26
26
|
enum?: string[];
|
|
27
27
|
constraints: PropertyConstraints;
|
|
28
|
+
/** `x-kubernetes-list-type` — `map`, `set` or `atomic`. Absent when the spec does not say (chant #1441). */
|
|
29
|
+
listType?: string;
|
|
30
|
+
/** `x-kubernetes-list-map-keys` — the fields that jointly identify one element. Present only alongside `listType: "map"`. */
|
|
31
|
+
listMapKeys?: string[];
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
export interface ParsedPropertyType {
|
|
@@ -104,6 +108,16 @@ interface SwaggerDefinition {
|
|
|
104
108
|
"x-kubernetes-group-version-kind"?: GroupVersionKind[];
|
|
105
109
|
"x-kubernetes-int-or-string"?: boolean;
|
|
106
110
|
"x-kubernetes-preserve-unknown-fields"?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* How the API server merges this list: `map` (associative, identified by
|
|
113
|
+
* {@link SwaggerDefinition["x-kubernetes-list-map-keys"]}), `set` (unordered,
|
|
114
|
+
* elements are their own identity), or `atomic` (replaced wholesale). chant
|
|
115
|
+
* #1441 — carried through so the drift engine can identify list elements
|
|
116
|
+
* semantically instead of positionally.
|
|
117
|
+
*/
|
|
118
|
+
"x-kubernetes-list-type"?: string;
|
|
119
|
+
/** The property names that jointly identify one element of an associative list. */
|
|
120
|
+
"x-kubernetes-list-map-keys"?: string[];
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
interface SwaggerProperty extends SwaggerDefinition {
|
|
@@ -227,6 +241,33 @@ export function parseK8sSwagger(data: string | Buffer): K8sParseResult[] {
|
|
|
227
241
|
}
|
|
228
242
|
|
|
229
243
|
/** Stable key for a GVK, used to join the `paths` pass onto the `definitions` pass. */
|
|
244
|
+
/**
|
|
245
|
+
* Every `(property name, list-map keys)` pair the spec declares, across ALL
|
|
246
|
+
* definitions (chant #1441).
|
|
247
|
+
*
|
|
248
|
+
* Deliberately independent of {@link parseK8sSwagger}'s resource/property-type
|
|
249
|
+
* extraction. That extraction keeps 257 resources and 320 property types out
|
|
250
|
+
* of roughly a thousand definitions, so collecting merge keys from its output
|
|
251
|
+
* alone finds 18 of the 32 properties the spec annotates — and misses
|
|
252
|
+
* `ServicePort.port`, whose absence would silently demote every Service port
|
|
253
|
+
* list to the hand-written fallback. A definition is worth reading for its
|
|
254
|
+
* merge semantics whether or not chant emits a type for it.
|
|
255
|
+
*/
|
|
256
|
+
export function specListMapKeyPairs(data: string | Buffer): Array<[string, string[]]> {
|
|
257
|
+
const spec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8")) as SwaggerSpec;
|
|
258
|
+
const pairs: Array<[string, string[]]> = [];
|
|
259
|
+
|
|
260
|
+
for (const def of Object.values(spec.definitions ?? {})) {
|
|
261
|
+
for (const [name, prop] of Object.entries(def.properties ?? {})) {
|
|
262
|
+
if (prop["x-kubernetes-list-type"] !== "map") continue;
|
|
263
|
+
const keys = prop["x-kubernetes-list-map-keys"];
|
|
264
|
+
if (keys?.length) pairs.push([name, [...keys]]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return pairs;
|
|
269
|
+
}
|
|
270
|
+
|
|
230
271
|
export function gvkKey(gvk: GroupVersionKind): string {
|
|
231
272
|
return `${gvk.group}|${gvk.version}|${gvk.kind}`;
|
|
232
273
|
}
|
|
@@ -442,14 +483,22 @@ function parseProperties(
|
|
|
442
483
|
|
|
443
484
|
for (const [name, prop] of Object.entries(properties)) {
|
|
444
485
|
const tsType = resolvePropertyType(prop, definitions);
|
|
445
|
-
|
|
486
|
+
const parsed: ParsedProperty = {
|
|
446
487
|
name,
|
|
447
488
|
tsType,
|
|
448
489
|
required: requiredSet.has(name),
|
|
449
490
|
description: prop.description,
|
|
450
491
|
enum: prop.enum,
|
|
451
492
|
constraints: coreExtractConstraints(prop as JsonSchemaProperty),
|
|
452
|
-
}
|
|
493
|
+
};
|
|
494
|
+
// chant #1441 — the merge semantics the API server itself uses. Kept only
|
|
495
|
+
// when present; a spec that says nothing leaves both fields absent rather
|
|
496
|
+
// than asserting a default the schema did not state.
|
|
497
|
+
const listType = prop["x-kubernetes-list-type"];
|
|
498
|
+
if (listType) parsed.listType = listType;
|
|
499
|
+
const listMapKeys = prop["x-kubernetes-list-map-keys"];
|
|
500
|
+
if (listMapKeys?.length) parsed.listMapKeys = [...listMapKeys];
|
|
501
|
+
result.push(parsed);
|
|
453
502
|
}
|
|
454
503
|
|
|
455
504
|
return result;
|