@intentius/chant-lexicon-k8s 0.58.0 → 0.60.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/fake-cluster.d.ts +7 -6
- package/dist/api/fake-cluster.d.ts.map +1 -1
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/composites/cron-schedule.d.ts +26 -0
- package/dist/composites/cron-schedule.d.ts.map +1 -0
- package/dist/composites/cron-workload.d.ts +1 -1
- package/dist/composites/cron-workload.d.ts.map +1 -1
- package/dist/composites/operator-stack.d.ts +1 -1
- package/dist/composites/operator-stack.d.ts.map +1 -1
- package/dist/config-schema.d.ts +3 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/effect-receipt-row.d.ts +164 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/lint/post-synth/wk8505.d.ts +3 -1
- package/dist/lint/post-synth/wk8505.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +1 -1
- package/dist/okf/rules/WK8505.md +2 -2
- package/dist/okf/types/Kustomization.md +1 -1
- package/dist/op/activities/index.d.ts +6 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +113 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/wk8505.ts +4 -2
- package/dist/serializer.d.ts.map +1 -1
- package/dist/subscribe-changes.d.ts +84 -0
- package/dist/subscribe-changes.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/api/fake-cluster.ts +17 -4
- package/src/codegen/docs.ts +7 -0
- package/src/composites/composites.test.ts +59 -1
- package/src/composites/cron-schedule.ts +47 -0
- package/src/composites/cron-workload.ts +4 -1
- package/src/composites/operator-stack.ts +3 -4
- package/src/config-schema.ts +5 -0
- package/src/config.ts +23 -0
- package/src/deep-observe.ts +26 -7
- package/src/describe-resources.ts +31 -7
- package/src/effect-receipt-row.test.ts +285 -0
- package/src/effect-receipt-row.ts +268 -0
- package/src/index.ts +22 -0
- package/src/lint/audit-catalog.ts +1 -1
- package/src/lint/post-synth/wk8505.ts +4 -2
- package/src/op/activities/index.ts +14 -0
- package/src/op/activities/kubectl.test.ts +38 -0
- package/src/op/activities/kubectl.ts +15 -0
- package/src/plugin.ts +15 -0
- package/src/receipt-store.test.ts +380 -0
- package/src/receipt-store.ts +290 -0
- package/src/serializer.ts +92 -1
- package/src/subscribe-changes.test.ts +368 -0
- package/src/subscribe-changes.ts +210 -0
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
import { operationFor } from "./api/operation-surface";
|
|
69
69
|
import { resolveK8sOwnerChain } from "./api/owner-chain";
|
|
70
70
|
import { gvkToTypeName } from "./spec/parse";
|
|
71
|
+
import { observeReceiptRows, receiptRowsFor } from "./receipt-store";
|
|
71
72
|
|
|
72
73
|
function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
|
|
73
74
|
const out: Record<string, unknown> = {};
|
|
@@ -357,11 +358,26 @@ export async function describeResources(
|
|
|
357
358
|
// looked for where it lives".
|
|
358
359
|
const queried: Record<string, string> = {};
|
|
359
360
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
// Effect receipt rows (#2074) are read by their own leg below: the applier
|
|
362
|
+
// never wrote them (#1832), and their declaration carries no props, so the
|
|
363
|
+
// generic sweep has no `metadata.name` to query by and would report a hole
|
|
364
|
+
// where the receipt leg has a real answer. Their addresses come from the
|
|
365
|
+
// build output's receipt comment, which is the serializer's one rendering of
|
|
366
|
+
// the derivation (./effect-receipt-row.ts).
|
|
367
|
+
const receiptRows = receiptRowsFor(options.entityNames, options.buildOutput);
|
|
368
|
+
|
|
369
|
+
const declared: Declared[] = [...options.entities]
|
|
370
|
+
.filter(([entityName]) => !receiptRows.has(entityName))
|
|
371
|
+
.map(([entityName, entity]) => ({
|
|
372
|
+
entityName,
|
|
373
|
+
entityType: entity.entityType,
|
|
374
|
+
props: entity.props,
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
// A whole-lexicon failure below is a hole for the receipts too: nobody
|
|
378
|
+
// looked at those either, and a connect that never happened proves nothing
|
|
379
|
+
// about a receipt's presence.
|
|
380
|
+
const everyName = [...declared.map((d) => d.entityName), ...receiptRows.keys()];
|
|
365
381
|
|
|
366
382
|
// Connect first. The binding check lives here, so a bound-but-mismatched
|
|
367
383
|
// context throws before any resource is read — core turns that into
|
|
@@ -374,7 +390,7 @@ export async function describeResources(
|
|
|
374
390
|
return observation(
|
|
375
391
|
{},
|
|
376
392
|
unobservedAll(
|
|
377
|
-
|
|
393
|
+
everyName,
|
|
378
394
|
"read-failed",
|
|
379
395
|
MISSING_CLIENT_DETAIL,
|
|
380
396
|
options.entities,
|
|
@@ -386,7 +402,7 @@ export async function describeResources(
|
|
|
386
402
|
return observation(
|
|
387
403
|
{},
|
|
388
404
|
unobservedAll(
|
|
389
|
-
|
|
405
|
+
everyName,
|
|
390
406
|
outcome.kind === "unobserved" ? outcome.reason : "read-failed",
|
|
391
407
|
outcome.kind === "unobserved" ? outcome.detail : undefined,
|
|
392
408
|
options.entities,
|
|
@@ -507,6 +523,14 @@ export async function describeResources(
|
|
|
507
523
|
|
|
508
524
|
await addRuntimeChildren(client, resources, unobserved, options.owned, declared);
|
|
509
525
|
|
|
526
|
+
// The receipt leg last, so its answers are the ones that stand for the
|
|
527
|
+
// receipt entities, because nothing above ever looked at one.
|
|
528
|
+
if (receiptRows.size > 0) {
|
|
529
|
+
const receiptObs = await observeReceiptRows(client, receiptRows);
|
|
530
|
+
Object.assign(resources, receiptObs.resources);
|
|
531
|
+
Object.assign(unobserved, receiptObs.unobserved);
|
|
532
|
+
}
|
|
533
|
+
|
|
510
534
|
return observation(resources, unobserved, queried);
|
|
511
535
|
}
|
|
512
536
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The k8s effect-receipt materialization row (#2074, epic #1703): the
|
|
3
|
+
* declaration, the ConfigMap address derivation, what the serializer renders,
|
|
4
|
+
* and the #1833 guards over the row.
|
|
5
|
+
*
|
|
6
|
+
* The shape mirrors lexicons/aws/src/effect-receipt-row.test.ts, which covers
|
|
7
|
+
* the SSM row, so the two rows can be read against each other.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from "vitest";
|
|
11
|
+
import * as ts from "typescript";
|
|
12
|
+
import { loadAll } from "js-yaml";
|
|
13
|
+
import {
|
|
14
|
+
EffectReceipt,
|
|
15
|
+
receiptConfigMapName,
|
|
16
|
+
receiptConfigMapRef,
|
|
17
|
+
receiptNamespaceFrom,
|
|
18
|
+
isEffectReceiptObject,
|
|
19
|
+
parseReceiptComment,
|
|
20
|
+
renderReceiptComment,
|
|
21
|
+
EFFECT_RECEIPTS_COMMENT_MARKER,
|
|
22
|
+
K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
23
|
+
RECEIPT_DATA_KEY,
|
|
24
|
+
RECEIPT_DEFAULT_NAMESPACE,
|
|
25
|
+
RECEIPT_LABEL_KEY,
|
|
26
|
+
RECEIPT_UNRESOLVED_VALUE_NOTE,
|
|
27
|
+
} from "./effect-receipt-row";
|
|
28
|
+
import { k8sSerializer } from "./serializer";
|
|
29
|
+
import {
|
|
30
|
+
EXISTENCE_EXPECTATION,
|
|
31
|
+
isEffectReceipt,
|
|
32
|
+
receiptExpectation,
|
|
33
|
+
EFFECT_RECEIPT_MARKER,
|
|
34
|
+
} from "@intentius/chant/effect-receipt";
|
|
35
|
+
import { receiptCheckInput } from "@intentius/chant/op/receipt-store";
|
|
36
|
+
import { coreReceiptChecks, RECEIPT_PLAIN_STORE_CHECK_ID } from "@intentius/chant/lint/receipt-checks";
|
|
37
|
+
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
38
|
+
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
39
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
40
|
+
import { cor022ReceiptLeafRule } from "@intentius/chant/lint/rules/cor022-receipt-leaf";
|
|
41
|
+
import type { LintContext } from "@intentius/chant/lint/rule";
|
|
42
|
+
|
|
43
|
+
/** A minimal deploy-time reference, for the placeholder-value case. */
|
|
44
|
+
const someRef = { [INTRINSIC_MARKER]: true as const, toJSON: () => ({ ref: "other" }) };
|
|
45
|
+
|
|
46
|
+
const ownership = { stack: "demo", env: "dev" };
|
|
47
|
+
|
|
48
|
+
function serializeReceipts(
|
|
49
|
+
receipts: Map<string, Declarable>,
|
|
50
|
+
marker?: { stack: string; env?: string },
|
|
51
|
+
config?: Record<string, unknown>,
|
|
52
|
+
): string {
|
|
53
|
+
const out = k8sSerializer.serialize(new Map(), [], {
|
|
54
|
+
...(marker ? { ownership: marker } : {}),
|
|
55
|
+
...(config ? { config } : {}),
|
|
56
|
+
receipts,
|
|
57
|
+
});
|
|
58
|
+
return typeof out === "string" ? out : out.primary;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function receiptRows(output: string) {
|
|
62
|
+
return parseReceiptComment(output);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("EffectReceipt (k8s materialization row)", () => {
|
|
66
|
+
it("declares under the k8s lexicon with the real resource kind, carrying the marker", () => {
|
|
67
|
+
const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
68
|
+
expect(r.lexicon).toBe("k8s");
|
|
69
|
+
expect(r.entityType).toBe(K8S_EFFECT_RECEIPT_ENTITY_TYPE);
|
|
70
|
+
expect(r.entityType).toBe("K8s::Core::ConfigMap");
|
|
71
|
+
expect(isEffectReceipt(r)).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("is accepted by the effect() step's receiptCheckInput, expectation stamped when static", () => {
|
|
75
|
+
const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { v: 1 } });
|
|
76
|
+
const input = receiptCheckInput(r);
|
|
77
|
+
expect(input.receipt.effect).toBe("db-seed");
|
|
78
|
+
expect(input.expectation).toBe(receiptExpectation(r));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("validates the effect as a name segment at declaration", () => {
|
|
82
|
+
expect(() => EffectReceipt("bad", { effect: "db/seed", flavor: "existence" })).toThrow(/DNS-1123/);
|
|
83
|
+
expect(() => EffectReceipt("bad", { effect: "DbSeed", flavor: "existence" })).toThrow(/DNS-1123/);
|
|
84
|
+
expect(() => EffectReceipt("bad", { effect: "", flavor: "existence" })).toThrow(/non-empty/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("receiptConfigMapName", () => {
|
|
89
|
+
it("derives chant-receipt.<stack>.<env>.<effect>", () => {
|
|
90
|
+
expect(receiptConfigMapName("demo", "dev", "db-seed")).toBe("chant-receipt.demo.dev.db-seed");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("refuses a segment that is not a DNS-1123 label, so the identity stays unambiguous", () => {
|
|
94
|
+
expect(() => receiptConfigMapName("a.b", "dev", "seed")).toThrow(/stack/);
|
|
95
|
+
expect(() => receiptConfigMapName("demo", "", "seed")).toThrow(/env/);
|
|
96
|
+
expect(() => receiptConfigMapName("demo", "dev", "Seed")).toThrow(/effect/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("refuses a name over Kubernetes' 253-character ceiling", () => {
|
|
100
|
+
expect(() => receiptConfigMapName("a".repeat(63), "b".repeat(63), "c".repeat(63))).not.toThrow();
|
|
101
|
+
expect(() => receiptConfigMapName("a".repeat(64), "dev", "seed")).toThrow(/DNS-1123/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("addresses the receipt in the project's namespace, `default` when none is set", () => {
|
|
105
|
+
expect(receiptConfigMapRef("demo", "dev", "db-seed")).toEqual({
|
|
106
|
+
name: "chant-receipt.demo.dev.db-seed",
|
|
107
|
+
namespace: RECEIPT_DEFAULT_NAMESPACE,
|
|
108
|
+
});
|
|
109
|
+
expect(receiptConfigMapRef("demo", "dev", "db-seed", "chant-system").namespace).toBe("chant-system");
|
|
110
|
+
expect(receiptNamespaceFrom({ k8s: { receipts: { namespace: "chant-system" } } })).toBe("chant-system");
|
|
111
|
+
expect(receiptNamespaceFrom(undefined)).toBe("default");
|
|
112
|
+
expect(receiptNamespaceFrom({ k8s: {} })).toBe("default");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("k8sSerializer receipt rows", () => {
|
|
117
|
+
it("renders each receipt as a ConfigMap row at the derived address, expectation under data", () => {
|
|
118
|
+
const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { v: 1 } });
|
|
119
|
+
const rows = receiptRows(serializeReceipts(new Map([["seeded", seeded]]), ownership));
|
|
120
|
+
expect(rows.seeded).toEqual({
|
|
121
|
+
kind: "ConfigMap",
|
|
122
|
+
namespace: "default",
|
|
123
|
+
name: "chant-receipt.demo.dev.db-seed",
|
|
124
|
+
data: { [RECEIPT_DATA_KEY]: receiptExpectation(seeded) },
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("takes the namespace from k8s.receipts.namespace", () => {
|
|
129
|
+
const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
130
|
+
const output = serializeReceipts(new Map([["seeded", seeded]]), ownership, {
|
|
131
|
+
k8s: { receipts: { namespace: "chant-system" } },
|
|
132
|
+
});
|
|
133
|
+
expect(receiptRows(output).seeded.namespace).toBe("chant-system");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("keeps the receipt out of the documents, the only thing an applier applies", () => {
|
|
137
|
+
const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
138
|
+
const output = serializeReceipts(new Map([["seeded", seeded]]), ownership);
|
|
139
|
+
const documents = loadAll(output).filter((d) => d && typeof d === "object");
|
|
140
|
+
expect(documents).toEqual([]);
|
|
141
|
+
expect(output).toContain(EFFECT_RECEIPTS_COMMENT_MARKER);
|
|
142
|
+
expect(receiptRows(output).seeded).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("leaves a real manifest applyable with the comment appended", () => {
|
|
146
|
+
const configMap = {
|
|
147
|
+
[DECLARABLE_MARKER]: true as const,
|
|
148
|
+
lexicon: "k8s",
|
|
149
|
+
entityType: "K8s::Core::ConfigMap",
|
|
150
|
+
props: { metadata: { name: "app-config" }, data: { a: "1" } },
|
|
151
|
+
};
|
|
152
|
+
const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
153
|
+
const out = k8sSerializer.serialize(
|
|
154
|
+
new Map<string, Declarable>([["appConfig", configMap as unknown as Declarable]]),
|
|
155
|
+
[],
|
|
156
|
+
{ ownership, receipts: new Map<string, Declarable>([["seeded", seeded]]) },
|
|
157
|
+
);
|
|
158
|
+
const output = typeof out === "string" ? out : out.primary;
|
|
159
|
+
const documents = loadAll(output).filter((d): d is Record<string, unknown> => !!d && typeof d === "object");
|
|
160
|
+
expect(documents).toHaveLength(1);
|
|
161
|
+
expect((documents[0].metadata as { name: string }).name).toBe("app-config");
|
|
162
|
+
expect(receiptRows(output).seeded.name).toBe("chant-receipt.demo.dev.db-seed");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("renders the existence expectation for an existence receipt", () => {
|
|
166
|
+
const r = EffectReceipt("booted", { effect: "bootstrap", flavor: "existence" });
|
|
167
|
+
const rows = receiptRows(serializeReceipts(new Map([["booted", r]]), ownership));
|
|
168
|
+
expect(rows.booted.data[RECEIPT_DATA_KEY]).toBe(EXISTENCE_EXPECTATION);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("renders the placeholder note, never a placeholder digest, for reference inputs", () => {
|
|
172
|
+
const r = EffectReceipt("wired", { effect: "wire-up", flavor: "hash", inputs: { target: someRef } });
|
|
173
|
+
const rows = receiptRows(serializeReceipts(new Map([["wired", r]]), ownership));
|
|
174
|
+
expect(rows.wired.data[RECEIPT_DATA_KEY]).toBe(RECEIPT_UNRESOLVED_VALUE_NOTE);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("errors when no ownership marker resolves", () => {
|
|
178
|
+
const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
179
|
+
expect(() => serializeReceipts(new Map([["seeded", r]]))).toThrow(/ownership/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("errors when ownership resolves no env, because the segment is explicit and never guessed", () => {
|
|
183
|
+
const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
184
|
+
expect(() => serializeReceipts(new Map([["seeded", r]]), { stack: "demo" })).toThrow(/env/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("emits no receipt block when the context carries no receipts", () => {
|
|
188
|
+
const out = k8sSerializer.serialize(new Map(), [], { ownership });
|
|
189
|
+
const output = typeof out === "string" ? out : out.primary;
|
|
190
|
+
expect(output).not.toContain(EFFECT_RECEIPTS_COMMENT_MARKER);
|
|
191
|
+
expect(parseReceiptComment(output)).toEqual({});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("round-trips the block deterministically, and reads an output that has none as none", () => {
|
|
195
|
+
const rows = {
|
|
196
|
+
b: { kind: "ConfigMap" as const, namespace: "default", name: "chant-receipt.demo.dev.b", data: { expectation: "x" } },
|
|
197
|
+
a: { kind: "ConfigMap" as const, namespace: "default", name: "chant-receipt.demo.dev.a", data: { expectation: "y" } },
|
|
198
|
+
};
|
|
199
|
+
const line = renderReceiptComment(rows);
|
|
200
|
+
expect(line.indexOf('"a"')).toBeLessThan(line.indexOf('"b"'));
|
|
201
|
+
expect(parseReceiptComment(`${line}\n`)).toEqual(rows);
|
|
202
|
+
expect(parseReceiptComment("apiVersion: v1\nkind: Namespace\n")).toEqual({});
|
|
203
|
+
expect(parseReceiptComment(`${EFFECT_RECEIPTS_COMMENT_MARKER}not json\n`)).toEqual({});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("the receipt label", () => {
|
|
208
|
+
it("recognizes a live receipt ConfigMap by its label alone", () => {
|
|
209
|
+
expect(isEffectReceiptObject({ [RECEIPT_LABEL_KEY]: "db-seed" })).toBe(true);
|
|
210
|
+
expect(isEffectReceiptObject({ [RECEIPT_LABEL_KEY]: "" })).toBe(false);
|
|
211
|
+
expect(isEffectReceiptObject({ "app.kubernetes.io/managed-by": "chant" })).toBe(false);
|
|
212
|
+
expect(isEffectReceiptObject(undefined)).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("#1833's plain-store guard over the k8s row", () => {
|
|
217
|
+
const check = coreReceiptChecks().find((c) => c.id === RECEIPT_PLAIN_STORE_CHECK_ID)!;
|
|
218
|
+
|
|
219
|
+
function runCheck(entities: Map<string, Declarable>) {
|
|
220
|
+
const ctx: PostSynthContext = {
|
|
221
|
+
outputs: new Map(),
|
|
222
|
+
entities,
|
|
223
|
+
buildResult: { outputs: new Map(), entities, warnings: [], errors: [], sourceFileCount: 0 },
|
|
224
|
+
};
|
|
225
|
+
return check.check(ctx);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
it("passes the factory's row, a plain ConfigMap", () => {
|
|
229
|
+
const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
230
|
+
expect(runCheck(new Map([["seeded", r]]))).toEqual([]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("fails a Secret-kind fixture, the same way it fails SSM SecureString", () => {
|
|
234
|
+
const sneaky = {
|
|
235
|
+
[DECLARABLE_MARKER]: true as const,
|
|
236
|
+
[EFFECT_RECEIPT_MARKER]: true as const,
|
|
237
|
+
lexicon: "k8s",
|
|
238
|
+
entityType: "K8s::Core::Secret",
|
|
239
|
+
name: "sneaky",
|
|
240
|
+
effect: "db-seed",
|
|
241
|
+
flavor: "existence" as const,
|
|
242
|
+
inputs: {},
|
|
243
|
+
};
|
|
244
|
+
const diagnostics = runCheck(new Map([["sneaky", sneaky as unknown as Declarable]]));
|
|
245
|
+
expect(diagnostics).toHaveLength(1);
|
|
246
|
+
expect(diagnostics[0].checkId).toBe(RECEIPT_PLAIN_STORE_CHECK_ID);
|
|
247
|
+
expect(diagnostics[0].severity).toBe("error");
|
|
248
|
+
expect(diagnostics[0].message).toMatch(/K8s::Core::Secret/);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("#1833's leaf rule over a ConfigMap receipt", () => {
|
|
253
|
+
/** The k8s source fixture: the row's own factory, referenced the way COR022
|
|
254
|
+
* refuses. Recognition is by factory name, so the rule fires here exactly as
|
|
255
|
+
* it fires on the aws SSM row. */
|
|
256
|
+
function lintContext(code: string): LintContext {
|
|
257
|
+
const sourceFile = ts.createSourceFile("infra.ts", code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
258
|
+
return { sourceFile, entities: [], filePath: "infra.ts", lexicon: "k8s" };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
it("fires when a ConfigMap derives a value from the receipt", () => {
|
|
262
|
+
const diags = cor022ReceiptLeafRule.check(
|
|
263
|
+
lintContext(`
|
|
264
|
+
import { EffectReceipt, ConfigMap } from "@intentius/chant-lexicon-k8s";
|
|
265
|
+
export const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
266
|
+
export const app = new ConfigMap({ data: { seededBy: seeded.effect } });
|
|
267
|
+
`),
|
|
268
|
+
);
|
|
269
|
+
expect(diags).toHaveLength(1);
|
|
270
|
+
expect(diags[0].ruleId).toBe("COR022");
|
|
271
|
+
expect(diags[0].severity).toBe("error");
|
|
272
|
+
expect(diags[0].message).toContain('"seeded" is an effect receipt');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("passes when the receipt is only handed to the effect() step whole", () => {
|
|
276
|
+
const diags = cor022ReceiptLeafRule.check(
|
|
277
|
+
lintContext(`
|
|
278
|
+
import { EffectReceipt } from "@intentius/chant-lexicon-k8s";
|
|
279
|
+
export const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
|
|
280
|
+
export const migrate = effect(seeded, [shell({ run: "./seed.sh" })]);
|
|
281
|
+
`),
|
|
282
|
+
);
|
|
283
|
+
expect(diags).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The k8s effect-receipt materialization row (#2074, epic #1703): an effect
|
|
3
|
+
* receipt stored as a core `ConfigMap`, named
|
|
4
|
+
* `chant-receipt.<stack>.<env>.<effect>`, holding the expectation under
|
|
5
|
+
* `data.expectation`.
|
|
6
|
+
*
|
|
7
|
+
* Core's `EffectReceipt` factory (#1831) declares a receipt under the `chant`
|
|
8
|
+
* pseudo-lexicon, which no serializer claims. This module is the k8s
|
|
9
|
+
* materialization, built to the same shape as the aws SSM row (#1835,
|
|
10
|
+
* `lexicons/aws/src/effect-receipt-row.ts`): the {@link EffectReceipt} factory
|
|
11
|
+
* here produces core's declaration under `lexicon: "k8s"`, so the build
|
|
12
|
+
* partitions it to the k8s serializer, #1832's write-exclusion seam withholds
|
|
13
|
+
* it from the apply-bound entity set, and the serializer renders it for
|
|
14
|
+
* visibility through `SerializeContext.receipts` (see ./serializer.ts).
|
|
15
|
+
*
|
|
16
|
+
* Where the aws row parks its rendered rows in the CloudFormation template's
|
|
17
|
+
* `Metadata` (deliberately outside `Resources`, the section an applier writes
|
|
18
|
+
* from), this row parks them in a YAML COMMENT at the end of the manifest
|
|
19
|
+
* stream. Kubernetes YAML has no metadata channel outside the documents
|
|
20
|
+
* themselves, and a document is exactly what an applier applies: `loadAll`
|
|
21
|
+
* yields no object for a comment, so the receipt block is structurally
|
|
22
|
+
* unreachable from `applyManifest` while still riding the one build output the
|
|
23
|
+
* observation leg is handed. See {@link EFFECT_RECEIPTS_COMMENT_MARKER}.
|
|
24
|
+
*
|
|
25
|
+
* ## Name and namespace
|
|
26
|
+
*
|
|
27
|
+
* Path identity (epic decision 4): the ConfigMap name derives from the SAME
|
|
28
|
+
* ownership-block fields that stamp markers, `ownership.stack` and an explicit
|
|
29
|
+
* `ownership.env`, plus the receipt's `effect`. The separator is `.`, which no
|
|
30
|
+
* segment may contain, so `stack=a-b env=c` and `stack=a env=b-c` cannot
|
|
31
|
+
* produce the same name. The result is a valid RFC 1123 DNS subdomain, which
|
|
32
|
+
* is what a ConfigMap name has to be.
|
|
33
|
+
*
|
|
34
|
+
* The namespace is the project's `k8s.receipts.namespace`, and `default` when
|
|
35
|
+
* the project sets none, which is where every other namespace-less k8s read
|
|
36
|
+
* and write in this lexicon already lands. It is never invented from the stack
|
|
37
|
+
* or the environment: a namespace chant guessed would be a namespace chant has
|
|
38
|
+
* to create, and the receipt row creates nothing but the receipt.
|
|
39
|
+
*
|
|
40
|
+
* ## Ownership, and why the prune leaves it alone
|
|
41
|
+
*
|
|
42
|
+
* Every receipt ConfigMap carries chant's ownership marker labels plus
|
|
43
|
+
* {@link RECEIPT_LABEL_KEY}. The marker is what makes `chant kube get` and the
|
|
44
|
+
* observation classify it as chant's rather than foreign. The receipt label is
|
|
45
|
+
* what keeps `delete: "owned-only"` from pruning it: the receipt is owned and
|
|
46
|
+
* is never in any apply set (the effect step is its sole writer), which is the
|
|
47
|
+
* exact shape the owned-only sweep deletes. `pruneOrphans`
|
|
48
|
+
* (./op/activities/kubectl.ts) reports it `retained` instead, the same
|
|
49
|
+
* treatment a generated-once Secret gets (./secret-labels.ts) and for the same
|
|
50
|
+
* reason: destroying it silently converts at-least-once into a re-run nobody
|
|
51
|
+
* asked for, or worse, into never.
|
|
52
|
+
*
|
|
53
|
+
* Plain store (#1833's COR023): a ConfigMap is the plain half of the
|
|
54
|
+
* ConfigMap/Secret pair, and the entityType alone is what the guard reads. A
|
|
55
|
+
* receipt value is a witness (an existence marker or a `sha256:` digest),
|
|
56
|
+
* never a secret; materializing one into a `K8s::Core::Secret` fails COR023.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
|
|
60
|
+
import {
|
|
61
|
+
EffectReceipt as CoreEffectReceipt,
|
|
62
|
+
EFFECT_RECEIPT_MARKER,
|
|
63
|
+
type EffectReceiptFlavor,
|
|
64
|
+
type EffectReceiptOptions,
|
|
65
|
+
} from "@intentius/chant/effect-receipt";
|
|
66
|
+
|
|
67
|
+
/** The entityType of the k8s materialization row, the real resource kind the
|
|
68
|
+
* receipt is stored as, which is what lint's plain-store guard checks. */
|
|
69
|
+
export const K8S_EFFECT_RECEIPT_ENTITY_TYPE = "K8s::Core::ConfigMap";
|
|
70
|
+
|
|
71
|
+
/** apiVersion/kind of the materialized row, for the client reads and writes. */
|
|
72
|
+
export const RECEIPT_CONFIGMAP_REF = { apiVersion: "v1", kind: "ConfigMap" } as const;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The line prefix the serializer renders receipt rows behind, and the
|
|
76
|
+
* observation leg reads them back from. A YAML comment: an applier's `loadAll`
|
|
77
|
+
* produces no document for it, so a receipt can never enter an apply set, and
|
|
78
|
+
* `kubectl apply -f` ignores it exactly as it ignores every other comment.
|
|
79
|
+
* The remainder of the line is one JSON object keyed by entity name.
|
|
80
|
+
*/
|
|
81
|
+
export const EFFECT_RECEIPTS_COMMENT_MARKER = "# chant:effect-receipts ";
|
|
82
|
+
|
|
83
|
+
/** First segment of every receipt ConfigMap name. */
|
|
84
|
+
export const RECEIPT_NAME_PREFIX = "chant-receipt";
|
|
85
|
+
|
|
86
|
+
/** The `data` key the expectation is stored under. One key, the same single
|
|
87
|
+
* value the aws row puts in the SSM parameter's `Value`. */
|
|
88
|
+
export const RECEIPT_DATA_KEY = "expectation";
|
|
89
|
+
|
|
90
|
+
/** The label a live receipt ConfigMap carries, valued with the effect it
|
|
91
|
+
* witnesses. Recognition for the observation leg and, more importantly, the
|
|
92
|
+
* exclusion the owned-only prune keys on. */
|
|
93
|
+
export const RECEIPT_LABEL_KEY = "chant.intentius.io/effect-receipt";
|
|
94
|
+
|
|
95
|
+
/** Where receipts live when the project names no namespace. */
|
|
96
|
+
export const RECEIPT_DEFAULT_NAMESPACE = "default";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The rendered value of a hash-flavor receipt that still carries reference
|
|
100
|
+
* inputs at synthesis. References resolve at plan and at run, never at
|
|
101
|
+
* synthesis (epic decision 5), so the row carries this note instead of a
|
|
102
|
+
* digest hashed over placeholders.
|
|
103
|
+
*/
|
|
104
|
+
export const RECEIPT_UNRESOLVED_VALUE_NOTE =
|
|
105
|
+
"unresolved at synthesis, reference inputs; the expectation resolves at plan and at run (chant #1703, decision 5)";
|
|
106
|
+
|
|
107
|
+
/** One segment of the receipt name. RFC 1123 DNS label, which is what each
|
|
108
|
+
* dot-separated piece of a ConfigMap name has to be, and the `.` separator is
|
|
109
|
+
* excluded from it so the identity stays unambiguous. */
|
|
110
|
+
const NAME_SEGMENT = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/;
|
|
111
|
+
|
|
112
|
+
/** Kubernetes' own ceiling on a DNS subdomain, which a ConfigMap name is. */
|
|
113
|
+
const MAX_NAME_LENGTH = 253;
|
|
114
|
+
|
|
115
|
+
function checkSegment(field: string, value: string): string {
|
|
116
|
+
if (value.length > 63 || !NAME_SEGMENT.test(value)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`receipt name: ${field} "${value}" is not a valid DNS-1123 label, ` +
|
|
119
|
+
`use lowercase letters, digits and "-" (the segment becomes one dot-separated piece of ` +
|
|
120
|
+
`${RECEIPT_NAME_PREFIX}.<stack>.<env>.<effect>, which must be a valid ConfigMap name)`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The ConfigMap name of one effect's receipt:
|
|
128
|
+
* `chant-receipt.<stack>.<env>.<effect>`, from the resolved ownership marker
|
|
129
|
+
* fields (epic decision 4). The single source of the name identity: the
|
|
130
|
+
* serializer's rendered row, the receipt store's reads and writes, and the
|
|
131
|
+
* observation leg all call this.
|
|
132
|
+
*/
|
|
133
|
+
export function receiptConfigMapName(stack: string, env: string, effect: string): string {
|
|
134
|
+
const name = `${RECEIPT_NAME_PREFIX}.${checkSegment("stack", stack)}.${checkSegment("env", env)}.${checkSegment("effect", effect)}`;
|
|
135
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`receipt name: "${name}" is ${name.length} characters, over Kubernetes' ${MAX_NAME_LENGTH}-character ` +
|
|
138
|
+
`limit for a ConfigMap name. Shorten the stack, the environment, or the effect.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return name;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The project's receipt namespace: `k8s.receipts.namespace`, else `default`.
|
|
145
|
+
* The serializer reads it off `SerializeContext.config` and the store reads it
|
|
146
|
+
* off the project config, so both derive one namespace from one setting. */
|
|
147
|
+
export function receiptNamespaceFrom(config: Record<string, unknown> | undefined): string {
|
|
148
|
+
const k8s = config?.k8s as { receipts?: { namespace?: unknown } } | undefined;
|
|
149
|
+
const declared = k8s?.receipts?.namespace;
|
|
150
|
+
return typeof declared === "string" && declared.length > 0 ? declared : RECEIPT_DEFAULT_NAMESPACE;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Name and namespace of one effect's receipt ConfigMap. */
|
|
154
|
+
export interface ReceiptConfigMapRef {
|
|
155
|
+
name: string;
|
|
156
|
+
namespace: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The full address of one effect's receipt ConfigMap. */
|
|
160
|
+
export function receiptConfigMapRef(
|
|
161
|
+
stack: string,
|
|
162
|
+
env: string,
|
|
163
|
+
effect: string,
|
|
164
|
+
namespace: string = RECEIPT_DEFAULT_NAMESPACE,
|
|
165
|
+
): ReceiptConfigMapRef {
|
|
166
|
+
return { name: receiptConfigMapName(stack, env, effect), namespace };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** True when a live object's labels mark it a chant effect receipt. Any
|
|
170
|
+
* non-empty value counts: the label's presence is the claim, and a sweep must
|
|
171
|
+
* err on the side of keeping. */
|
|
172
|
+
export function isEffectReceiptObject(labels: Record<string, unknown> | undefined): boolean {
|
|
173
|
+
const value = labels?.[RECEIPT_LABEL_KEY];
|
|
174
|
+
return typeof value === "string" && value.length > 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** One rendered receipt row, as the comment block carries it. */
|
|
178
|
+
export interface RenderedReceiptRow {
|
|
179
|
+
kind: typeof RECEIPT_CONFIGMAP_REF.kind;
|
|
180
|
+
namespace: string;
|
|
181
|
+
name: string;
|
|
182
|
+
data: Record<string, string>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Render the receipt block the serializer appends to the manifest stream.
|
|
187
|
+
* Deterministic: one line, entity names sorted.
|
|
188
|
+
*/
|
|
189
|
+
export function renderReceiptComment(rows: Record<string, RenderedReceiptRow>): string {
|
|
190
|
+
const sorted: Record<string, RenderedReceiptRow> = {};
|
|
191
|
+
for (const name of Object.keys(rows).sort()) sorted[name] = rows[name];
|
|
192
|
+
return `${EFFECT_RECEIPTS_COMMENT_MARKER}${JSON.stringify(sorted)}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read the receipt rows back out of a build output. Returns an empty record
|
|
197
|
+
* for an output that carries no block, which is every project that declares no
|
|
198
|
+
* receipt. Never throws: an unparseable block is no block, and the observation
|
|
199
|
+
* leg reports the receipts it could not address as absent rather than
|
|
200
|
+
* inventing one.
|
|
201
|
+
*/
|
|
202
|
+
export function parseReceiptComment(buildOutput: string): Record<string, RenderedReceiptRow> {
|
|
203
|
+
const start = buildOutput.lastIndexOf(EFFECT_RECEIPTS_COMMENT_MARKER);
|
|
204
|
+
if (start < 0) return {};
|
|
205
|
+
const end = buildOutput.indexOf("\n", start);
|
|
206
|
+
const line = buildOutput.slice(start + EFFECT_RECEIPTS_COMMENT_MARKER.length, end < 0 ? undefined : end);
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(line) as unknown;
|
|
209
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
210
|
+
return parsed as Record<string, RenderedReceiptRow>;
|
|
211
|
+
} catch {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A k8s-materialized effect receipt: core's declaration shape (so
|
|
218
|
+
* `isEffectReceipt`, `effect(...)`, lint, and the plan engine all recognize it
|
|
219
|
+
* through the marker), under the k8s lexicon and the real resource kind.
|
|
220
|
+
*/
|
|
221
|
+
export interface K8sEffectReceiptDeclaration extends Declarable {
|
|
222
|
+
readonly [EFFECT_RECEIPT_MARKER]: true;
|
|
223
|
+
readonly lexicon: "k8s";
|
|
224
|
+
readonly entityType: typeof K8S_EFFECT_RECEIPT_ENTITY_TYPE;
|
|
225
|
+
/** The receipt's own name (the export-level identity of the witness). */
|
|
226
|
+
readonly name: string;
|
|
227
|
+
/** The effect this receipt witnesses, the name's final segment. */
|
|
228
|
+
readonly effect: string;
|
|
229
|
+
/** How the receipt is compared: mere presence, or a digest of the inputs. */
|
|
230
|
+
readonly flavor: EffectReceiptFlavor;
|
|
231
|
+
/** The effect's inputs as recorded at synthesis (references as placeholders). */
|
|
232
|
+
readonly inputs: Readonly<Record<string, unknown>>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Declare a k8s-materialized effect receipt. Same signature and semantics as
|
|
237
|
+
* core's `EffectReceipt` (#1831), whose factory validates and freezes the
|
|
238
|
+
* options, but the declaration lands in the k8s partition, so the k8s
|
|
239
|
+
* serializer renders the ConfigMap row and the receipt store
|
|
240
|
+
* (./receipt-store.ts) is its writer. Pass the returned const straight to the
|
|
241
|
+
* `effect(...)` op step.
|
|
242
|
+
*/
|
|
243
|
+
export function EffectReceipt(name: string, options: EffectReceiptOptions): K8sEffectReceiptDeclaration {
|
|
244
|
+
// Fail at declaration, not at serialize: the effect is the name's final
|
|
245
|
+
// segment, and a name that cannot become a segment has no receipt address.
|
|
246
|
+
if (typeof options?.effect === "string" && options.effect.length > 0) {
|
|
247
|
+
checkSegment("effect", options.effect);
|
|
248
|
+
}
|
|
249
|
+
const core = CoreEffectReceipt(name, options);
|
|
250
|
+
const decl: K8sEffectReceiptDeclaration = {
|
|
251
|
+
[DECLARABLE_MARKER]: true,
|
|
252
|
+
[EFFECT_RECEIPT_MARKER]: true,
|
|
253
|
+
lexicon: "k8s",
|
|
254
|
+
entityType: K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
255
|
+
name: core.name,
|
|
256
|
+
effect: core.effect,
|
|
257
|
+
flavor: core.flavor,
|
|
258
|
+
// The same frozen structure the core factory built: intrinsic inputs stay
|
|
259
|
+
// live so discovery can stamp logical names onto attr-refs.
|
|
260
|
+
inputs: core.inputs,
|
|
261
|
+
};
|
|
262
|
+
// Declared fields immutable, object extensible for discovery's own
|
|
263
|
+
// symbol-keyed metadata, the same lock the core factory applies.
|
|
264
|
+
for (const key of Object.keys(decl)) {
|
|
265
|
+
Object.defineProperty(decl, key, { writable: false, configurable: false });
|
|
266
|
+
}
|
|
267
|
+
return decl;
|
|
268
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,28 @@ export { DEFAULT_LABELS_MARKER, DEFAULT_ANNOTATIONS_MARKER } from "./default-lab
|
|
|
27
27
|
// Variables / label constants
|
|
28
28
|
export { K8sLabels, K8sAnnotations } from "./variables";
|
|
29
29
|
|
|
30
|
+
// Effect receipts (#2074, epic #1703): the k8s materialization row, meaning the
|
|
31
|
+
// factory a project declares (core's shape under the k8s lexicon, carrying
|
|
32
|
+
// the marker) and the name/label constants. The ConfigMap-backed store itself
|
|
33
|
+
// lives at the `/receipt-store` subpath, off the build path (#1074).
|
|
34
|
+
export {
|
|
35
|
+
EffectReceipt,
|
|
36
|
+
receiptConfigMapName,
|
|
37
|
+
receiptConfigMapRef,
|
|
38
|
+
receiptNamespaceFrom,
|
|
39
|
+
isEffectReceiptObject,
|
|
40
|
+
K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
41
|
+
EFFECT_RECEIPTS_COMMENT_MARKER,
|
|
42
|
+
RECEIPT_NAME_PREFIX,
|
|
43
|
+
RECEIPT_DATA_KEY,
|
|
44
|
+
RECEIPT_LABEL_KEY,
|
|
45
|
+
RECEIPT_DEFAULT_NAMESPACE,
|
|
46
|
+
RECEIPT_UNRESOLVED_VALUE_NOTE,
|
|
47
|
+
parseReceiptComment,
|
|
48
|
+
renderReceiptComment,
|
|
49
|
+
} from "./effect-receipt-row";
|
|
50
|
+
export type { K8sEffectReceiptDeclaration, ReceiptConfigMapRef, RenderedReceiptRow } from "./effect-receipt-row";
|
|
51
|
+
|
|
30
52
|
// Generated-once secret marker (#1830) — constants only; the store adapter
|
|
31
53
|
// itself lives at the `/secret-store` subpath, off the build path (#1074).
|
|
32
54
|
export { GENERATED_ONCE_LABEL_KEY, GENERATED_ONCE_LABEL_VALUE, isGeneratedOnce } from "./secret-labels";
|
|
@@ -45,7 +45,7 @@ export const k8sAuditCatalog: Record<string, RuleMeta> = {
|
|
|
45
45
|
WK8502: auditRule("WK8502", "merge-worthy", "guidance", "Custom resource spec field has the wrong type or enum value", "Match the CRD schema: use the declared scalar type and one of the enum values.", { category: "correctness" }),
|
|
46
46
|
WK8503: auditRule("WK8503", "merge-worthy", "guidance", "Workload consumes a Secret nothing in the output produces", "Produce the Secret in the same build (Secret, ExternalSecret, InfisicalSecret, cert-manager Certificate), or declare its provenance with declareSecret() if it exists out of band.", { category: "correctness" }),
|
|
47
47
|
WK8504: auditRule("WK8504", "merge-worthy", "guidance", "committed-encrypted secret declaration does not resolve", "Point `file` at the committed ciphertext, make its metadata.name match the declaration, and re-run `sops -e -i` so every data/stringData value is ENC[...].", { authority: [K8S_SECRETS] }),
|
|
48
|
-
WK8505: auditRule("WK8505", "report-only", "guidance", "committed-encrypted secret with no Flux decryption wiring", "Add decryption: \"sops\" to the FluxAppFor reconciling the path that carries the ciphertext sidecar, or ignore if that Kustomization is declared in a different build.", { category: "correctness" }),
|
|
48
|
+
WK8505: auditRule("WK8505", "report-only", "guidance", "committed-encrypted secret with no Flux decryption wiring", "Add decryption: \"sops\" to the FluxAppFor reconciling the path that carries the ciphertext sidecar, or ignore if that Kustomization is declared in a different build. This check sees one build root at a time, so it goes silent once the secret and the Kustomization live in different build roots (chant #1939).", { category: "correctness" }),
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|