@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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The k8s `ReceiptStore` (#2074, epic #1703): core's injectable receipt seam
|
|
3
|
+
* (#1834, `@intentius/chant/op/receipt-store`) implemented over a core
|
|
4
|
+
* `ConfigMap`, at the address ./effect-receipt-row.ts derives from the
|
|
5
|
+
* ownership marker fields, plus the plan-side live read of the same rows.
|
|
6
|
+
*
|
|
7
|
+
* The transport is the lexicon's own typed client (./api/connect.ts), so a
|
|
8
|
+
* receipt read and write take the same environment-to-cluster binding
|
|
9
|
+
* (#1100/#1155), the same credential policy and the same field manager as
|
|
10
|
+
* every other k8s mutation. No `kubectl` binary is involved.
|
|
11
|
+
*
|
|
12
|
+
* Write discipline (epic decision 3): `write` exists for the `effect()` step
|
|
13
|
+
* alone, because the step's read-compare-run-write is the only path that reaches it,
|
|
14
|
+
* on success, last. It is a server-side apply as `chant:<stack>`, stamping the
|
|
15
|
+
* ownership marker labels and {@link RECEIPT_LABEL_KEY}, so a later write
|
|
16
|
+
* updates the value in place and the owned-only prune retains rather than
|
|
17
|
+
* deletes it (./op/activities/kubectl.ts).
|
|
18
|
+
*
|
|
19
|
+
* Identity: the ConfigMap name needs `<stack>` and `<env>`, which the activity
|
|
20
|
+
* args deliberately do not carry (the `EffectReceiptRef` is
|
|
21
|
+
* identity-of-the-effect, not identity-of-the-deployment). The store resolves
|
|
22
|
+
* them once, lazily, at first use: an explicit option, else `CHANT_ENV` (what
|
|
23
|
+
* `chant run --env` sets) and the project's `ownership` block, the same fields
|
|
24
|
+
* that stamp markers (epic decision 4). Nothing resolving is an error, never a
|
|
25
|
+
* guessed segment.
|
|
26
|
+
*
|
|
27
|
+
* Not exported from the package entry point. Like ./secret-store.ts and
|
|
28
|
+
* ./teardown.ts, this module names the API client, which must stay off the
|
|
29
|
+
* build path (chant #1074, examples/k8s-client-boundary.test.ts). Consumers
|
|
30
|
+
* reach it by subpath: `@intentius/chant-lexicon-k8s/receipt-store`.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { loadChantConfigUpward, resolveOwnershipStack } from "@intentius/chant/config";
|
|
34
|
+
import {
|
|
35
|
+
LABEL_OWNERSHIP_KEYS,
|
|
36
|
+
ownershipEntries,
|
|
37
|
+
classifyOwnership,
|
|
38
|
+
readOwnership,
|
|
39
|
+
} from "@intentius/chant/ownership";
|
|
40
|
+
import type { EffectReceiptRef, ReceiptStore } from "@intentius/chant/op/receipt-store";
|
|
41
|
+
import type { ResourceMetadata, UnobservedEntity } from "@intentius/chant/lexicon";
|
|
42
|
+
import type { DeepResourceObservation } from "@intentius/chant/deep-observation";
|
|
43
|
+
import type { K8sClient, K8sObject } from "@intentius/chant-k8s-client";
|
|
44
|
+
import { defaultK8sConnector, type K8sConnector } from "./api/connect";
|
|
45
|
+
import { classifyApiFailure } from "./api/classify";
|
|
46
|
+
import {
|
|
47
|
+
K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
48
|
+
RECEIPT_CONFIGMAP_REF,
|
|
49
|
+
RECEIPT_DATA_KEY,
|
|
50
|
+
RECEIPT_LABEL_KEY,
|
|
51
|
+
parseReceiptComment,
|
|
52
|
+
receiptConfigMapRef,
|
|
53
|
+
receiptNamespaceFrom,
|
|
54
|
+
type RenderedReceiptRow,
|
|
55
|
+
} from "./effect-receipt-row";
|
|
56
|
+
|
|
57
|
+
/** Options for {@link k8sReceiptStore}. All optional: the default store reads
|
|
58
|
+
* its identity and its namespace from the project, and its cluster from the
|
|
59
|
+
* environment binding. */
|
|
60
|
+
export interface K8sReceiptStoreOptions {
|
|
61
|
+
/** The name's `<stack>` segment. Omitted, the project's `ownership.stack`
|
|
62
|
+
* (chant.config.ts, found upward from `cwd`) answers. */
|
|
63
|
+
stack?: string;
|
|
64
|
+
/** The name's `<env>` segment, explicit by decision 4. Omitted, `CHANT_ENV`
|
|
65
|
+
* (set by `chant run --env`) answers, then a literal `ownership.env`. */
|
|
66
|
+
environment?: string;
|
|
67
|
+
/** Namespace the receipts live in. Omitted, `k8s.receipts.namespace`
|
|
68
|
+
* answers, then `default`. */
|
|
69
|
+
namespace?: string;
|
|
70
|
+
/** Where to look for chant.config.ts. Defaults to the working directory. */
|
|
71
|
+
cwd?: string;
|
|
72
|
+
/** Explicit kubectl context, for a caller that already resolved one. */
|
|
73
|
+
context?: string;
|
|
74
|
+
/** The connector to build a client with. Test seam. */
|
|
75
|
+
connect?: K8sConnector;
|
|
76
|
+
/** Environment record the identity fallback reads. Defaults to `process.env`. */
|
|
77
|
+
env?: Record<string, string | undefined>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The resolved name identity. */
|
|
81
|
+
interface ReceiptIdentity {
|
|
82
|
+
stack: string;
|
|
83
|
+
env: string;
|
|
84
|
+
namespace: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function resolveIdentity(options: K8sReceiptStoreOptions): Promise<ReceiptIdentity> {
|
|
88
|
+
const processEnv = options.env ?? process.env;
|
|
89
|
+
let stack = options.stack;
|
|
90
|
+
let env = options.environment ?? processEnv.CHANT_ENV;
|
|
91
|
+
let namespace = options.namespace;
|
|
92
|
+
if (!stack || !env || !namespace) {
|
|
93
|
+
let config;
|
|
94
|
+
try {
|
|
95
|
+
config = (await loadChantConfigUpward(options.cwd ?? process.cwd())).config;
|
|
96
|
+
} catch {
|
|
97
|
+
config = undefined;
|
|
98
|
+
}
|
|
99
|
+
if (config) {
|
|
100
|
+
stack = stack ?? resolveOwnershipStack(config);
|
|
101
|
+
// Only a literal env can answer here: a `{ param }` reference resolves
|
|
102
|
+
// per build, and an op run has no build parameters, and `--env` does.
|
|
103
|
+
const configEnv = config.ownership?.env;
|
|
104
|
+
env = env ?? (typeof configEnv === "string" ? configEnv : undefined);
|
|
105
|
+
namespace = namespace ?? receiptNamespaceFrom(config as unknown as Record<string, unknown>);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (!stack) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"k8s receipt store: no stack identity. The receipt ConfigMap is named " +
|
|
111
|
+
"chant-receipt.<stack>.<env>.<effect>, derived from the same ownership fields that stamp " +
|
|
112
|
+
"markers (chant #1703, decision 4). Set ownership: { stack } in chant.config.ts.",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (!env) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"k8s receipt store: no environment resolved. The receipt name's <env> segment is explicit " +
|
|
118
|
+
"(chant #1703, decision 4). Run with --env <name>, set CHANT_ENV, or set a literal " +
|
|
119
|
+
"ownership.env in chant.config.ts.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return { stack, env, namespace: namespace ?? receiptNamespaceFrom(undefined) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The stored expectation on a live receipt ConfigMap, or undefined when the
|
|
126
|
+
* object holds none. */
|
|
127
|
+
export function receiptValueOf(object: K8sObject | undefined): string | undefined {
|
|
128
|
+
const data = (object as { data?: Record<string, unknown> } | undefined)?.data;
|
|
129
|
+
const value = data?.[RECEIPT_DATA_KEY];
|
|
130
|
+
return typeof value === "string" ? value : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The `ReceiptStore` over ConfigMaps. Bind it once in the op activities barrel
|
|
135
|
+
* as `receiptActivities(k8sReceiptStore())`, and the registry resolves
|
|
136
|
+
* `receiptRead`/`receiptWrite`/`receiptStaleness` by name, exactly like
|
|
137
|
+
* `ensureSecret` (#1830). Identity and cluster resolve lazily at first use, so
|
|
138
|
+
* module load never reads the project or connects to anything.
|
|
139
|
+
*/
|
|
140
|
+
export function k8sReceiptStore(options: K8sReceiptStoreOptions = {}): ReceiptStore {
|
|
141
|
+
let identity: Promise<ReceiptIdentity> | undefined;
|
|
142
|
+
const identityOf = () => (identity ??= resolveIdentity(options));
|
|
143
|
+
|
|
144
|
+
let pending: Promise<K8sClient> | undefined;
|
|
145
|
+
const clientOf = (): Promise<K8sClient> =>
|
|
146
|
+
(pending ??= (options.connect ?? defaultK8sConnector)({
|
|
147
|
+
...(options.environment !== undefined ? { environment: options.environment } : {}),
|
|
148
|
+
...(options.context !== undefined ? { context: options.context } : {}),
|
|
149
|
+
...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
|
|
150
|
+
}).then((connected) => connected.client));
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
async read(receipt: EffectReceiptRef): Promise<string | undefined> {
|
|
154
|
+
const { stack, env, namespace } = await identityOf();
|
|
155
|
+
const ref = receiptConfigMapRef(stack, env, receipt.effect, namespace);
|
|
156
|
+
const client = await clientOf();
|
|
157
|
+
const live = await client.readIfPresent({ ...RECEIPT_CONFIGMAP_REF, ...ref });
|
|
158
|
+
return receiptValueOf(live);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async write(receipt: EffectReceiptRef, expectation: string): Promise<void> {
|
|
162
|
+
const { stack, env, namespace } = await identityOf();
|
|
163
|
+
const ref = receiptConfigMapRef(stack, env, receipt.effect, namespace);
|
|
164
|
+
const client = await clientOf();
|
|
165
|
+
const configMap: K8sObject = {
|
|
166
|
+
...RECEIPT_CONFIGMAP_REF,
|
|
167
|
+
metadata: {
|
|
168
|
+
...ref,
|
|
169
|
+
labels: {
|
|
170
|
+
...ownershipEntries(LABEL_OWNERSHIP_KEYS, { stack, env }),
|
|
171
|
+
[RECEIPT_LABEL_KEY]: receipt.effect,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
data: { [RECEIPT_DATA_KEY]: expectation },
|
|
175
|
+
} as K8sObject;
|
|
176
|
+
await client.apply(configMap, { fieldManager: `chant:${stack}` });
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** What the observation leg learned about the declared receipt rows. */
|
|
182
|
+
export interface ReceiptRowObservation {
|
|
183
|
+
resources: Record<string, ResourceMetadata>;
|
|
184
|
+
unobserved: Record<string, UnobservedEntity>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The receipt rows a build output carries, keyed by entity name. Only the
|
|
188
|
+
* entities this observation was asked about. */
|
|
189
|
+
export function receiptRowsFor(
|
|
190
|
+
entityNames: readonly string[],
|
|
191
|
+
buildOutput: string | undefined,
|
|
192
|
+
): Map<string, RenderedReceiptRow> {
|
|
193
|
+
const rows = parseReceiptComment(buildOutput ?? "");
|
|
194
|
+
const wanted = new Set(entityNames);
|
|
195
|
+
const out = new Map<string, RenderedReceiptRow>();
|
|
196
|
+
for (const [name, row] of Object.entries(rows)) {
|
|
197
|
+
if (!wanted.has(name)) continue;
|
|
198
|
+
if (typeof row?.name !== "string" || typeof row?.namespace !== "string") continue;
|
|
199
|
+
out.set(name, row);
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The plan-side live read of the receipt rows (#2074's observation leg).
|
|
206
|
+
*
|
|
207
|
+
* A receipt is not a document the applier ever wrote (#1832), and it carries
|
|
208
|
+
* no `props` on the declared side, so the generic declared-entity sweep in
|
|
209
|
+
* ./describe-resources.ts has neither a `metadata.name` to query by nor an
|
|
210
|
+
* honest verdict to give. The serializer rendered each receipt's derived
|
|
211
|
+
* ConfigMap address into the build output's receipt comment, so this leg reads
|
|
212
|
+
* the addresses back from there, one derivation, decision 4, and asks the
|
|
213
|
+
* cluster for each.
|
|
214
|
+
*
|
|
215
|
+
* Present maps the stored value onto `attributes.value` (core's
|
|
216
|
+
* `RECEIPT_VALUE_ATTRIBUTE`); a genuine 404 is a real absence and stays one; a
|
|
217
|
+
* failed read is an `unobserved` hole, never a wrong answer: a receipt nobody
|
|
218
|
+
* could read must not arrive downstream as "the effect never ran".
|
|
219
|
+
*/
|
|
220
|
+
export async function observeReceiptRows(
|
|
221
|
+
client: K8sClient,
|
|
222
|
+
rows: ReadonlyMap<string, RenderedReceiptRow>,
|
|
223
|
+
): Promise<ReceiptRowObservation> {
|
|
224
|
+
const out: ReceiptRowObservation = { resources: {}, unobserved: {} };
|
|
225
|
+
await client.concurrently([...rows], async ([entityName, row]) => {
|
|
226
|
+
try {
|
|
227
|
+
const live = await client.read({
|
|
228
|
+
...RECEIPT_CONFIGMAP_REF,
|
|
229
|
+
name: row.name,
|
|
230
|
+
namespace: row.namespace,
|
|
231
|
+
});
|
|
232
|
+
out.resources[entityName] = {
|
|
233
|
+
type: K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
234
|
+
physicalId: live.metadata?.uid,
|
|
235
|
+
// Live outside anything the applier wrote, by design. The same word
|
|
236
|
+
// the aws row's observation uses for a receipt parameter (#1835).
|
|
237
|
+
status: "EXTERNAL",
|
|
238
|
+
ownership: classifyOwnership(live.metadata?.labels, LABEL_OWNERSHIP_KEYS),
|
|
239
|
+
marker: readOwnership(live.metadata?.labels, LABEL_OWNERSHIP_KEYS),
|
|
240
|
+
attributes: {
|
|
241
|
+
namespace: row.namespace,
|
|
242
|
+
// Core's RECEIPT_VALUE_ATTRIBUTE, which is what `readReceiptValue` reads.
|
|
243
|
+
value: receiptValueOf(live) ?? "",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const outcome = classifyApiFailure(err);
|
|
248
|
+
if (outcome.kind === "unobserved") {
|
|
249
|
+
out.unobserved[entityName] = {
|
|
250
|
+
type: K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
251
|
+
reason: outcome.reason,
|
|
252
|
+
detail: `reading receipt ConfigMap ${row.namespace}/${row.name}: ${outcome.detail}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// `absent` records nothing: in neither map is how the contract spells
|
|
256
|
+
// "asked, and it is not there", which is what the plan reads as "the
|
|
257
|
+
// effect has not fired for these inputs".
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The deep read's answer for the receipt rows.
|
|
265
|
+
*
|
|
266
|
+
* A receipt is read back here for the same reason the thin path reads it: a
|
|
267
|
+
* declared entity nobody looked at is a hole, and a hole in the deep read is
|
|
268
|
+
* noise on every `lifecycle diff --live --deep` a project with receipts runs.
|
|
269
|
+
* What it deliberately contributes is an EMPTY property tree: the declaration
|
|
270
|
+
* has no `props`, so every live path would land outside the claimed-field set
|
|
271
|
+
* (`@intentius/chant/claimed-fields`) and be reported unclaimed, and the
|
|
272
|
+
* receipt's stored value is not drift on any reading, because a stale receipt is an
|
|
273
|
+
* `effect` row from `planReceipts` (#1832), never an update. Presence and the
|
|
274
|
+
* uid are the whole of what the deep read has to say about a receipt.
|
|
275
|
+
*/
|
|
276
|
+
export async function observeReceiptRowsDeep(
|
|
277
|
+
client: K8sClient,
|
|
278
|
+
rows: ReadonlyMap<string, RenderedReceiptRow>,
|
|
279
|
+
): Promise<{ resources: Record<string, DeepResourceObservation>; unobserved: Record<string, UnobservedEntity> }> {
|
|
280
|
+
const thin = await observeReceiptRows(client, rows);
|
|
281
|
+
const resources: Record<string, DeepResourceObservation> = {};
|
|
282
|
+
for (const [name, meta] of Object.entries(thin.resources)) {
|
|
283
|
+
resources[name] = {
|
|
284
|
+
type: K8S_EFFECT_RECEIPT_ENTITY_TYPE,
|
|
285
|
+
...(meta.physicalId ? { physicalId: meta.physicalId } : {}),
|
|
286
|
+
properties: {},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { resources, unobserved: thin.unobserved };
|
|
290
|
+
}
|
package/src/serializer.ts
CHANGED
|
@@ -10,6 +10,21 @@ import type { Declarable } from "@intentius/chant/declarable";
|
|
|
10
10
|
import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
|
|
11
11
|
import type { Serializer, SerializerResult, SerializeContext } from "@intentius/chant/serializer";
|
|
12
12
|
import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
13
|
+
import {
|
|
14
|
+
isEffectReceipt,
|
|
15
|
+
receiptExpectation,
|
|
16
|
+
referenceInputPaths,
|
|
17
|
+
type EffectReceiptDeclaration,
|
|
18
|
+
} from "@intentius/chant/effect-receipt";
|
|
19
|
+
import {
|
|
20
|
+
RECEIPT_CONFIGMAP_REF,
|
|
21
|
+
RECEIPT_DATA_KEY,
|
|
22
|
+
RECEIPT_UNRESOLVED_VALUE_NOTE,
|
|
23
|
+
receiptConfigMapRef,
|
|
24
|
+
receiptNamespaceFrom,
|
|
25
|
+
renderReceiptComment,
|
|
26
|
+
type RenderedReceiptRow,
|
|
27
|
+
} from "./effect-receipt-row";
|
|
13
28
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
14
29
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
15
30
|
import { emitYAML } from "@intentius/chant/yaml";
|
|
@@ -180,6 +195,65 @@ function resolveK8sAttr(entity: Declarable | undefined, logicalName: string, att
|
|
|
180
195
|
);
|
|
181
196
|
}
|
|
182
197
|
|
|
198
|
+
/** The rendered expectation: the synthesis-time value when the receipt is
|
|
199
|
+
* fully static, the placeholder note when reference inputs remain (#1703
|
|
200
|
+
* decision 5, since synthesis resolves nothing). */
|
|
201
|
+
function receiptRowValue(receipt: EffectReceiptDeclaration): string {
|
|
202
|
+
if (receipt.flavor === "hash" && referenceInputPaths(receipt).length > 0) {
|
|
203
|
+
return RECEIPT_UNRESOLVED_VALUE_NOTE;
|
|
204
|
+
}
|
|
205
|
+
return receiptExpectation(receipt);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render the effect receipts (#2074) the build withheld from the apply-bound
|
|
210
|
+
* entity set (`SerializeContext.receipts`, #1832) as ConfigMap rows: named
|
|
211
|
+
* `chant-receipt.<stack>.<env>.<effect>` from the ownership marker fields
|
|
212
|
+
* (epic decision 4), in the project's receipt namespace, with the expectation
|
|
213
|
+
* under `data.expectation`.
|
|
214
|
+
*
|
|
215
|
+
* Visibility only. The rows ride a YAML comment at the end of the manifest
|
|
216
|
+
* stream (./effect-receipt-row.ts) rather than a document, because a document
|
|
217
|
+
* is what an applier applies and the `effect()` step is a receipt's sole
|
|
218
|
+
* writer (#1832, epic #1703 decision 3).
|
|
219
|
+
*
|
|
220
|
+
* The env segment is explicit: a receipt with no resolved `ownership.env` is
|
|
221
|
+
* an error, never a guessed segment.
|
|
222
|
+
*/
|
|
223
|
+
function renderReceiptRows(
|
|
224
|
+
receipts: ReadonlyMap<string, Declarable>,
|
|
225
|
+
ownership: { stack: string; env?: string } | undefined,
|
|
226
|
+
namespace: string,
|
|
227
|
+
): Record<string, RenderedReceiptRow> {
|
|
228
|
+
const rows: Record<string, RenderedReceiptRow> = {};
|
|
229
|
+
const names = [...receipts.keys()].join(", ");
|
|
230
|
+
if (!ownership?.stack) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`k8s receipts (${names}): no ownership marker resolved. The receipt ConfigMap is named ` +
|
|
233
|
+
`chant-receipt.<stack>.<env>.<effect>, derived from the same ownership fields that ` +
|
|
234
|
+
`stamp markers (chant #1703, decision 4). Set ownership: { stack } in chant.config.ts.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (!ownership.env) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`k8s receipts (${names}): ownership resolved no env. The receipt name's <env> segment is ` +
|
|
240
|
+
`explicit (chant #1703, decision 4). Set ownership: { env } in chant.config.ts, or ` +
|
|
241
|
+
`build with an env-valued parameter that resolves it.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
for (const [name, entity] of receipts) {
|
|
245
|
+
if (!isEffectReceipt(entity)) continue;
|
|
246
|
+
const ref = receiptConfigMapRef(ownership.stack, ownership.env, entity.effect, namespace);
|
|
247
|
+
rows[name] = {
|
|
248
|
+
kind: RECEIPT_CONFIGMAP_REF.kind,
|
|
249
|
+
namespace: ref.namespace,
|
|
250
|
+
name: ref.name,
|
|
251
|
+
data: { [RECEIPT_DATA_KEY]: receiptRowValue(entity) },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return rows;
|
|
255
|
+
}
|
|
256
|
+
|
|
183
257
|
/**
|
|
184
258
|
* K8s visitor for the generic serializer walker.
|
|
185
259
|
*/
|
|
@@ -416,7 +490,24 @@ export const k8sSerializer: Serializer = {
|
|
|
416
490
|
}
|
|
417
491
|
}
|
|
418
492
|
|
|
419
|
-
|
|
493
|
+
let primary = [...namespaceDocs, ...otherDocs].join("\n---\n");
|
|
494
|
+
|
|
495
|
+
// Effect receipt rows (#2074): visibility only, deliberately NOT a
|
|
496
|
+
// document: appliers apply documents, and the `effect()` step is a
|
|
497
|
+
// receipt's sole writer (#1832, epic #1703 decision 3). The comment rides
|
|
498
|
+
// the same build output the observation leg is handed, which is how it
|
|
499
|
+
// learns each receipt's ConfigMap address.
|
|
500
|
+
if (context?.receipts && context.receipts.size > 0) {
|
|
501
|
+
const rows = renderReceiptRows(
|
|
502
|
+
context.receipts,
|
|
503
|
+
context.ownership,
|
|
504
|
+
receiptNamespaceFrom(context.config),
|
|
505
|
+
);
|
|
506
|
+
if (Object.keys(rows).length > 0) {
|
|
507
|
+
primary = primary.length > 0 ? `${primary}\n${renderReceiptComment(rows)}\n` : `${renderReceiptComment(rows)}\n`;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
420
511
|
// A bare string when there is nothing extra to write, so the common case
|
|
421
512
|
// stays byte-identical to what every existing consumer already reads.
|
|
422
513
|
if (Object.keys(files).length === 0 && warnings.length === 0) return primary;
|