@xplane/core 0.15.2 → 1.0.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/index.d.mts +504 -328
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1103 -655
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/index.mjs
CHANGED
|
@@ -1,540 +1,547 @@
|
|
|
1
1
|
import { Construct, Construct as Construct$1 } from "constructs";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
//#region src/core/context.ts
|
|
4
|
+
/**
|
|
5
|
+
* AsyncLocalStorage instance that carries the CompositionContext.
|
|
6
|
+
* The handler sets it before constructing the user's Composition,
|
|
7
|
+
* and the Composition constructor reads from it.
|
|
8
|
+
*/
|
|
9
|
+
const compositionStorage = new AsyncLocalStorage();
|
|
10
|
+
/**
|
|
11
|
+
* Get the current composition context from AsyncLocalStorage.
|
|
12
|
+
* Throws if called outside of a composition construction scope.
|
|
13
|
+
*/
|
|
14
|
+
function getCompositionContext() {
|
|
15
|
+
const ctx = compositionStorage.getStore();
|
|
16
|
+
if (ctx) return ctx;
|
|
17
|
+
throw new Error("No composition context found. Ensure the Composition is constructed within compositionStorage.run().");
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/core/composition.ts
|
|
21
|
+
/**
|
|
22
|
+
* Base class for user-authored Crossplane compositions.
|
|
23
|
+
*
|
|
24
|
+
* Users extend this class and define resources in the constructor:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* class MyComposition extends Composition<MySpec, MyStatus> {
|
|
28
|
+
* constructor() {
|
|
29
|
+
* super();
|
|
30
|
+
* const vpc = new Vpc(this, 'vpc', { spec: { ... } });
|
|
31
|
+
* this.xr.status.vpcId = vpc.status.atProvider.vpcId;
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* `this.xr` is a "desired-first, fallback-to-observed" proxy over the XR.
|
|
37
|
+
* `this.pipelineContext` provides typed read-only access to Crossplane function context.
|
|
38
|
+
*/
|
|
39
|
+
var Composition = class extends Construct$1 {
|
|
40
|
+
/**
|
|
41
|
+
* The XR proxy — reads from desired first (status writes), falls through to observed.
|
|
42
|
+
* Writing to `this.xr.status.*` sets the composite status output.
|
|
43
|
+
*/
|
|
44
|
+
xr;
|
|
45
|
+
/** The dependency graph tracking resource relationships. */
|
|
46
|
+
graph;
|
|
47
|
+
/** The edge collector accumulating dependency edges. */
|
|
48
|
+
collector;
|
|
49
|
+
constructor() {
|
|
50
|
+
super(void 0, "Composition");
|
|
51
|
+
const ctx = getCompositionContext();
|
|
52
|
+
this.graph = ctx.graph;
|
|
53
|
+
this.collector = ctx.collector;
|
|
54
|
+
this.node.setContext("xplane:graph", ctx.graph);
|
|
55
|
+
this.node.setContext("xplane:collector", ctx.collector);
|
|
56
|
+
const xrMeta = ctx.xr.metadata;
|
|
57
|
+
if (xrMeta) this.node.setContext("xplane:xr-meta", xrMeta);
|
|
58
|
+
this.xr = createXrProxy(ctx);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read-only accessor for Crossplane function pipeline context.
|
|
62
|
+
* Keys are the context keys set by Crossplane or prior functions in the pipeline.
|
|
63
|
+
*/
|
|
64
|
+
get pipelineContext() {
|
|
65
|
+
const ctx = getCompositionContext();
|
|
66
|
+
return {
|
|
67
|
+
get(key) {
|
|
68
|
+
return ctx.pipelineContext.get(key);
|
|
69
|
+
},
|
|
70
|
+
has(key) {
|
|
71
|
+
return ctx.pipelineContext.has(key);
|
|
72
|
+
},
|
|
73
|
+
keys() {
|
|
74
|
+
return ctx.pipelineContext.keys();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Creates the "desired-first, fallback-to-observed" proxy for the XR.
|
|
81
|
+
*
|
|
82
|
+
* - Reading `xr.spec.*` reads from observed XR spec (creates ReadProxy for tracking)
|
|
83
|
+
* - Writing `xr.status.*` writes to a desired-status store (emitted as composite status)
|
|
84
|
+
* - Other reads fall through to observed
|
|
85
|
+
*/
|
|
86
|
+
function createXrProxy(ctx) {
|
|
87
|
+
const xrObserved = ctx.xr;
|
|
88
|
+
const xrDesiredStatus = {};
|
|
89
|
+
ctx.graph.addResource({ id: "__xr__" });
|
|
90
|
+
const statusProxy = new Proxy(xrDesiredStatus, {
|
|
91
|
+
get(_target, prop) {
|
|
92
|
+
if (typeof prop === "symbol") return void 0;
|
|
93
|
+
const key = String(prop);
|
|
94
|
+
if (key in xrDesiredStatus) return xrDesiredStatus[key];
|
|
95
|
+
const observedStatus = xrObserved.status;
|
|
96
|
+
if (observedStatus && key in observedStatus) return observedStatus[key];
|
|
97
|
+
},
|
|
98
|
+
set(_target, prop, value) {
|
|
99
|
+
if (typeof prop === "symbol") return false;
|
|
100
|
+
xrDesiredStatus[String(prop)] = value;
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
has(_target, prop) {
|
|
104
|
+
if (typeof prop === "symbol") return false;
|
|
105
|
+
const key = String(prop);
|
|
106
|
+
if (key in xrDesiredStatus) return true;
|
|
107
|
+
const observedStatus = xrObserved.status;
|
|
108
|
+
return observedStatus ? key in observedStatus : false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return new Proxy({}, {
|
|
112
|
+
get(_target, prop) {
|
|
113
|
+
if (typeof prop === "symbol") return void 0;
|
|
114
|
+
const key = String(prop);
|
|
115
|
+
if (key === "status") return statusProxy;
|
|
116
|
+
if (key in xrObserved) return xrObserved[key];
|
|
117
|
+
},
|
|
118
|
+
set(_target, prop, value) {
|
|
119
|
+
if (typeof prop === "symbol") return false;
|
|
120
|
+
const key = String(prop);
|
|
121
|
+
if (key === "status") {
|
|
122
|
+
Object.assign(xrDesiredStatus, value);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
xrObserved[key] = value;
|
|
126
|
+
return true;
|
|
127
|
+
},
|
|
128
|
+
has(_target, prop) {
|
|
129
|
+
if (typeof prop === "symbol") return false;
|
|
130
|
+
return String(prop) in xrObserved || String(prop) === "status";
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Extract the desired XR status from a Composition instance.
|
|
136
|
+
* Used by the emit pipeline phase to produce composite status output.
|
|
137
|
+
*/
|
|
138
|
+
function getXrDesiredStatus(composition) {
|
|
139
|
+
const statusProxy = composition.xr.status;
|
|
140
|
+
const result = {};
|
|
141
|
+
if (statusProxy && typeof statusProxy === "object") for (const key of Object.keys(statusProxy)) {
|
|
142
|
+
const value = statusProxy[key];
|
|
143
|
+
if (value != null) result[key] = value;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
2
148
|
//#region src/tracking/dependency-graph.ts
|
|
3
149
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and cycle detection to surface configuration errors.
|
|
150
|
+
* Tracks dependency relationships between resources as a directed acyclic graph.
|
|
151
|
+
* Edges are added as resources reference each other's observed state.
|
|
7
152
|
*/
|
|
8
153
|
var DependencyGraph = class {
|
|
9
|
-
|
|
10
|
-
_deps = /* @__PURE__ */ new Map();
|
|
11
|
-
/** all registered resource refs by id */
|
|
154
|
+
_adjacency = /* @__PURE__ */ new Map();
|
|
12
155
|
_resources = /* @__PURE__ */ new Map();
|
|
13
|
-
/** raw edges for introspection */
|
|
14
156
|
_edges = [];
|
|
15
|
-
/** Register a resource node in the graph. */
|
|
16
157
|
addResource(ref) {
|
|
17
158
|
this._resources.set(ref.id, ref);
|
|
18
|
-
if (!this.
|
|
159
|
+
if (!this._adjacency.has(ref.id)) this._adjacency.set(ref.id, /* @__PURE__ */ new Set());
|
|
160
|
+
}
|
|
161
|
+
addEdge(edge) {
|
|
162
|
+
this.addResource(edge.from);
|
|
163
|
+
this.addResource(edge.to);
|
|
164
|
+
this._adjacency.get(edge.to.id).add(edge.from.id);
|
|
165
|
+
this._edges.push(edge);
|
|
19
166
|
}
|
|
20
|
-
/** Add dependency edges from the collector. */
|
|
21
167
|
addEdges(edges) {
|
|
22
|
-
for (const edge of edges)
|
|
23
|
-
this.addResource(edge.from);
|
|
24
|
-
this.addResource(edge.to);
|
|
25
|
-
const deps = this._deps.get(edge.to.id);
|
|
26
|
-
if (deps) deps.add(edge.from.id);
|
|
27
|
-
this._edges.push(edge);
|
|
28
|
-
}
|
|
168
|
+
for (const edge of edges) this.addEdge(edge);
|
|
29
169
|
}
|
|
30
|
-
/** Add an explicit dependency: `dependent` depends on `dependency`. */
|
|
31
170
|
addExplicitDependency(dependent, dependency) {
|
|
32
171
|
this.addResource(dependent);
|
|
33
172
|
this.addResource(dependency);
|
|
34
|
-
|
|
35
|
-
if (deps) deps.add(dependency.id);
|
|
173
|
+
this._adjacency.get(dependent.id).add(dependency.id);
|
|
36
174
|
}
|
|
37
|
-
/** Get
|
|
175
|
+
/** Get the set of resource IDs that `resourceId` depends on. */
|
|
38
176
|
getDependencies(resourceId) {
|
|
39
|
-
return this.
|
|
177
|
+
return this._adjacency.get(resourceId) ?? /* @__PURE__ */ new Set();
|
|
40
178
|
}
|
|
41
|
-
/** Get all registered resource IDs. */
|
|
42
179
|
get resourceIds() {
|
|
43
180
|
return [...this._resources.keys()];
|
|
44
181
|
}
|
|
45
|
-
/** Get all raw edges. */
|
|
46
182
|
get edges() {
|
|
47
183
|
return this._edges;
|
|
48
184
|
}
|
|
49
185
|
/**
|
|
50
|
-
* Returns
|
|
51
|
-
*
|
|
186
|
+
* Returns a topological ordering of resources.
|
|
187
|
+
* If a cycle is detected, returns { order: null, cycle: string[] }.
|
|
52
188
|
*/
|
|
53
189
|
topologicalSort() {
|
|
54
190
|
const visited = /* @__PURE__ */ new Set();
|
|
55
191
|
const visiting = /* @__PURE__ */ new Set();
|
|
56
192
|
const sorted = [];
|
|
57
193
|
const visit = (id) => {
|
|
58
|
-
if (visited.has(id)) return;
|
|
59
|
-
if (visiting.has(id))
|
|
60
|
-
const cycle = [...visiting, id].join(" → ");
|
|
61
|
-
throw new Error(`Dependency cycle detected: ${cycle}`);
|
|
62
|
-
}
|
|
194
|
+
if (visited.has(id)) return null;
|
|
195
|
+
if (visiting.has(id)) return [...visiting, id];
|
|
63
196
|
visiting.add(id);
|
|
64
|
-
const deps = this.
|
|
65
|
-
if (deps) for (const depId of deps)
|
|
197
|
+
const deps = this._adjacency.get(id);
|
|
198
|
+
if (deps) for (const depId of deps) {
|
|
199
|
+
const cycle = visit(depId);
|
|
200
|
+
if (cycle) return cycle;
|
|
201
|
+
}
|
|
66
202
|
visiting.delete(id);
|
|
67
203
|
visited.add(id);
|
|
68
204
|
sorted.push(id);
|
|
205
|
+
return null;
|
|
69
206
|
};
|
|
70
|
-
for (const id of this._resources.keys())
|
|
71
|
-
|
|
207
|
+
for (const id of this._resources.keys()) {
|
|
208
|
+
const cycle = visit(id);
|
|
209
|
+
if (cycle) {
|
|
210
|
+
const start = cycle[cycle.length - 1];
|
|
211
|
+
const startIdx = cycle.indexOf(start);
|
|
212
|
+
return {
|
|
213
|
+
order: null,
|
|
214
|
+
cycle: cycle.slice(startIdx)
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { order: sorted };
|
|
72
219
|
}
|
|
73
220
|
};
|
|
74
221
|
//#endregion
|
|
222
|
+
//#region src/tracking/read-proxy.ts
|
|
223
|
+
/**
|
|
224
|
+
* WeakMap storing metadata for ReadProxy instances.
|
|
225
|
+
* This avoids polluting proxy objects with symbols.
|
|
226
|
+
*/
|
|
227
|
+
const proxyMeta = /* @__PURE__ */ new WeakMap();
|
|
228
|
+
/** Sentinel symbol to identify ReadProxy instances. */
|
|
229
|
+
const READ_PROXY_TAG = Symbol.for("xplane.readProxy");
|
|
230
|
+
/**
|
|
231
|
+
* Check if a value is a ReadProxy.
|
|
232
|
+
*/
|
|
233
|
+
function isReadProxy(value) {
|
|
234
|
+
return typeof value === "object" && value !== null && value[READ_PROXY_TAG] === true;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get the metadata for a ReadProxy value.
|
|
238
|
+
*/
|
|
239
|
+
function getReadProxyMeta(value) {
|
|
240
|
+
if (!isReadProxy(value)) return void 0;
|
|
241
|
+
return proxyMeta.get(value);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Creates a ReadProxy that wraps observed data.
|
|
245
|
+
*
|
|
246
|
+
* - Property access navigates into the data, building up the path.
|
|
247
|
+
* - Missing paths return `undefined` (no placeholder proxies).
|
|
248
|
+
* - The proxy carries owner + path metadata so that when it's assigned
|
|
249
|
+
* to a WriteProxy, the dependency edge can be recorded.
|
|
250
|
+
*/
|
|
251
|
+
function createReadProxy(target, owner, basePath) {
|
|
252
|
+
const proxy = new Proxy(target, {
|
|
253
|
+
get(obj, prop, receiver) {
|
|
254
|
+
if (prop === READ_PROXY_TAG) return true;
|
|
255
|
+
if (typeof prop === "symbol") return Reflect.get(obj, prop, receiver);
|
|
256
|
+
if (prop === "toJSON") return () => obj;
|
|
257
|
+
const childPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
258
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
259
|
+
if (value === void 0 || value === null) return createLeafReadProxy(owner, childPath);
|
|
260
|
+
if (typeof value === "object") return createReadProxy(value, owner, childPath);
|
|
261
|
+
return createPrimitiveReadProxy(value, owner, childPath);
|
|
262
|
+
},
|
|
263
|
+
has(obj, prop) {
|
|
264
|
+
if (prop === READ_PROXY_TAG) return true;
|
|
265
|
+
return Reflect.has(obj, prop);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
proxyMeta.set(proxy, {
|
|
269
|
+
owner,
|
|
270
|
+
path: basePath
|
|
271
|
+
});
|
|
272
|
+
return proxy;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* A "leaf" ReadProxy for paths that don't exist in observed data yet.
|
|
276
|
+
* Carries metadata for edge creation. Resolves to `undefined` when
|
|
277
|
+
* coerced to a primitive.
|
|
278
|
+
*/
|
|
279
|
+
function createLeafReadProxy(owner, path) {
|
|
280
|
+
const proxy = new Proxy(Object.create(null), {
|
|
281
|
+
get(_obj, prop) {
|
|
282
|
+
if (prop === READ_PROXY_TAG) return true;
|
|
283
|
+
if (prop === Symbol.toPrimitive) return () => `__pending__${owner.id}__${path}`;
|
|
284
|
+
if (typeof prop === "symbol") return void 0;
|
|
285
|
+
if (prop === "toJSON") return () => void 0;
|
|
286
|
+
if (prop === "toString") return () => `__pending__${owner.id}__${path}`;
|
|
287
|
+
if (prop === "valueOf") return () => proxy;
|
|
288
|
+
return createLeafReadProxy(owner, `${path}.${String(prop)}`);
|
|
289
|
+
},
|
|
290
|
+
has(_obj, prop) {
|
|
291
|
+
if (prop === READ_PROXY_TAG) return true;
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
proxyMeta.set(proxy, {
|
|
296
|
+
owner,
|
|
297
|
+
path
|
|
298
|
+
});
|
|
299
|
+
return proxy;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Wraps a concrete primitive value so it carries ReadProxy metadata.
|
|
303
|
+
* This allows the WriteProxy to detect it during assignment and
|
|
304
|
+
* record the dependency edge, while the value itself resolves correctly.
|
|
305
|
+
*/
|
|
306
|
+
function createPrimitiveReadProxy(value, owner, path) {
|
|
307
|
+
const proxy = new Proxy(Object.create(null), {
|
|
308
|
+
get(_obj, prop) {
|
|
309
|
+
if (prop === READ_PROXY_TAG) return true;
|
|
310
|
+
if (prop === Symbol.toPrimitive) return () => value;
|
|
311
|
+
if (prop === "valueOf") return () => value;
|
|
312
|
+
if (prop === "toString") return () => String(value);
|
|
313
|
+
if (prop === "toJSON") return () => value;
|
|
314
|
+
if (typeof prop === "symbol") return void 0;
|
|
315
|
+
return createLeafReadProxy(owner, `${path}.${String(prop)}`);
|
|
316
|
+
},
|
|
317
|
+
has(_obj, prop) {
|
|
318
|
+
if (prop === READ_PROXY_TAG) return true;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
proxyMeta.set(proxy, {
|
|
323
|
+
owner,
|
|
324
|
+
path
|
|
325
|
+
});
|
|
326
|
+
return proxy;
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
75
329
|
//#region src/tracking/types.ts
|
|
76
330
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
331
|
+
* A Pending marker stored in a desired document when a ReadProxy value
|
|
332
|
+
* is assigned. Carries full source info so the resolve phase knows
|
|
333
|
+
* where to look for the concrete value.
|
|
79
334
|
*/
|
|
80
|
-
const
|
|
81
|
-
|
|
335
|
+
const PENDING_TAG = Symbol.for("xplane.pending");
|
|
336
|
+
var Pending = class {
|
|
337
|
+
source;
|
|
338
|
+
path;
|
|
339
|
+
static TAG = PENDING_TAG;
|
|
340
|
+
[PENDING_TAG] = true;
|
|
341
|
+
constructor(source, path) {
|
|
342
|
+
this.source = source;
|
|
343
|
+
this.path = path;
|
|
344
|
+
}
|
|
345
|
+
static is(value) {
|
|
346
|
+
return typeof value === "object" && value !== null && value[PENDING_TAG] === true;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
82
349
|
//#endregion
|
|
83
|
-
//#region src/tracking/proxy.ts
|
|
350
|
+
//#region src/tracking/write-proxy.ts
|
|
84
351
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
352
|
+
* Collector that accumulates dependency edges discovered during
|
|
353
|
+
* WriteProxy assignments.
|
|
87
354
|
*/
|
|
88
|
-
var
|
|
355
|
+
var EdgeCollector = class {
|
|
89
356
|
_edges = [];
|
|
90
|
-
|
|
357
|
+
add(edge) {
|
|
91
358
|
if (!this._edges.some((e) => e.from.id === edge.from.id && e.fromPath === edge.fromPath && e.to.id === edge.to.id && e.toPath === edge.toPath)) this._edges.push(edge);
|
|
92
359
|
}
|
|
93
360
|
get edges() {
|
|
94
361
|
return this._edges;
|
|
95
362
|
}
|
|
96
|
-
clear() {
|
|
97
|
-
this._edges.length = 0;
|
|
98
|
-
}
|
|
99
363
|
};
|
|
100
364
|
/**
|
|
101
|
-
*
|
|
102
|
-
*/
|
|
103
|
-
function isTracked(value) {
|
|
104
|
-
return typeof value === "object" && value !== null && value[IS_TRACKED] === true;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Retrieves the tracking metadata from a tracked proxy.
|
|
108
|
-
* Returns undefined if the value is not tracked.
|
|
109
|
-
*/
|
|
110
|
-
function getTrackingMeta(value) {
|
|
111
|
-
if (isTracked(value)) return value[TRACKING_META];
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Creates a proxy around `target` that:
|
|
115
|
-
* 1. Returns nested proxies for property access (building up dot-paths).
|
|
116
|
-
* 2. On set: if the assigned value is itself a tracked proxy from a *different*
|
|
117
|
-
* resource, records a DependencyEdge in the collector.
|
|
118
|
-
* 3. Stores concrete values normally so the underlying object is populated.
|
|
365
|
+
* Creates a WriteProxy that wraps a desired document.
|
|
119
366
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
path: opts.path,
|
|
128
|
-
observed: opts.observed
|
|
129
|
-
};
|
|
367
|
+
* - Writes store values in the target.
|
|
368
|
+
* - When a ReadProxy value is assigned, it records a dependency edge
|
|
369
|
+
* and stores a Pending marker if the value is not yet concrete.
|
|
370
|
+
* - Reads return the stored value (desired-first).
|
|
371
|
+
*/
|
|
372
|
+
function createWriteProxy(target, opts) {
|
|
373
|
+
const { owner, collector, basePath = "" } = opts;
|
|
130
374
|
return new Proxy(target, {
|
|
131
375
|
get(obj, prop, receiver) {
|
|
132
|
-
if (prop === TRACKING_META) return meta;
|
|
133
|
-
if (prop === IS_TRACKED) return true;
|
|
134
|
-
if (prop === Symbol.toPrimitive) {
|
|
135
|
-
if (opts.observed && Object.keys(obj).length === 0) return () => {
|
|
136
|
-
throw new Error(`Cannot coerce XR path '${opts.path}' to a primitive — the field does not exist in the composite resource`);
|
|
137
|
-
};
|
|
138
|
-
return Reflect.get(obj, prop, receiver);
|
|
139
|
-
}
|
|
140
376
|
if (typeof prop === "symbol") return Reflect.get(obj, prop, receiver);
|
|
141
377
|
if (prop === "toJSON") return () => obj;
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
path: opts.path ? `${opts.path}.${prop}` : String(prop),
|
|
148
|
-
observed: opts.observed,
|
|
149
|
-
collector: opts.collector,
|
|
150
|
-
strict: opts.strict
|
|
151
|
-
});
|
|
152
|
-
Reflect.set(obj, prop, wrapped);
|
|
153
|
-
return wrapped;
|
|
154
|
-
}
|
|
155
|
-
if (existing !== void 0 || prop in obj) return existing;
|
|
156
|
-
if (opts.observed) {
|
|
157
|
-
if (opts.strict) return;
|
|
158
|
-
return createTrackedProxy({}, {
|
|
159
|
-
owner: opts.owner,
|
|
160
|
-
path: opts.path ? `${opts.path}.${prop}` : String(prop),
|
|
161
|
-
observed: true,
|
|
162
|
-
collector: opts.collector
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
const wrapped = createTrackedProxy({}, {
|
|
166
|
-
owner: opts.owner,
|
|
167
|
-
path: opts.path ? `${opts.path}.${prop}` : String(prop),
|
|
168
|
-
observed: false,
|
|
169
|
-
collector: opts.collector
|
|
378
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
379
|
+
if (typeof value === "object" && value !== null && !Pending.is(value)) return createWriteProxy(value, {
|
|
380
|
+
owner,
|
|
381
|
+
collector,
|
|
382
|
+
basePath: basePath ? `${basePath}.${String(prop)}` : String(prop)
|
|
170
383
|
});
|
|
171
|
-
|
|
172
|
-
return wrapped;
|
|
384
|
+
return value;
|
|
173
385
|
},
|
|
174
386
|
set(obj, prop, value) {
|
|
175
387
|
if (typeof prop === "symbol") return Reflect.set(obj, prop, value);
|
|
176
|
-
const targetPath =
|
|
177
|
-
if (
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
388
|
+
const targetPath = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
389
|
+
if (isReadProxy(value)) {
|
|
390
|
+
const meta = getReadProxyMeta(value);
|
|
391
|
+
if (meta && meta.owner.id !== owner.id) {
|
|
392
|
+
collector.add({
|
|
393
|
+
from: meta.owner,
|
|
394
|
+
fromPath: meta.path,
|
|
395
|
+
to: owner,
|
|
396
|
+
toPath: targetPath
|
|
397
|
+
});
|
|
398
|
+
const primitive = tryExtractPrimitive$2(value);
|
|
399
|
+
if (primitive !== void 0) return Reflect.set(obj, prop, primitive);
|
|
400
|
+
return Reflect.set(obj, prop, new Pending(meta.owner, meta.path));
|
|
401
|
+
}
|
|
402
|
+
const primitive = tryExtractPrimitive$2(value);
|
|
403
|
+
if (primitive !== void 0) return Reflect.set(obj, prop, primitive);
|
|
404
|
+
if (meta) {
|
|
405
|
+
collector.add({
|
|
406
|
+
from: meta.owner,
|
|
407
|
+
fromPath: meta.path,
|
|
408
|
+
to: owner,
|
|
409
|
+
toPath: targetPath
|
|
410
|
+
});
|
|
411
|
+
return Reflect.set(obj, prop, new Pending(meta.owner, meta.path));
|
|
182
412
|
}
|
|
183
|
-
if (sourceMeta && sourceMeta.owner.id !== opts.owner.id) opts.collector.addEdge({
|
|
184
|
-
from: sourceMeta.owner,
|
|
185
|
-
fromPath: sourceMeta.path,
|
|
186
|
-
to: opts.owner,
|
|
187
|
-
toPath: targetPath
|
|
188
|
-
});
|
|
189
|
-
const concrete = resolveTrackedValue(value);
|
|
190
|
-
return Reflect.set(obj, prop, concrete);
|
|
191
413
|
}
|
|
192
|
-
if (typeof value === "object" && value !== null && !
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
path: targetPath,
|
|
196
|
-
observed: opts.observed,
|
|
197
|
-
collector: opts.collector
|
|
198
|
-
});
|
|
199
|
-
return Reflect.set(obj, prop, wrapped);
|
|
414
|
+
if (typeof value === "object" && value !== null && !Pending.is(value)) {
|
|
415
|
+
const processed = deepProcessValue(value, owner, targetPath, collector);
|
|
416
|
+
return Reflect.set(obj, prop, processed);
|
|
200
417
|
}
|
|
201
418
|
return Reflect.set(obj, prop, value);
|
|
202
419
|
},
|
|
203
|
-
|
|
204
|
-
return Reflect.
|
|
205
|
-
},
|
|
206
|
-
getOwnPropertyDescriptor(obj, prop) {
|
|
207
|
-
const desc = Reflect.getOwnPropertyDescriptor(obj, prop);
|
|
208
|
-
if (desc) return {
|
|
209
|
-
...desc,
|
|
210
|
-
configurable: true,
|
|
211
|
-
enumerable: true
|
|
212
|
-
};
|
|
213
|
-
if (opts.observed && typeof prop === "string") return {
|
|
214
|
-
configurable: true,
|
|
215
|
-
enumerable: true,
|
|
216
|
-
writable: true,
|
|
217
|
-
value: void 0
|
|
218
|
-
};
|
|
219
|
-
},
|
|
220
|
-
has(obj, prop) {
|
|
221
|
-
if (prop === IS_TRACKED || prop === TRACKING_META) return true;
|
|
222
|
-
return Reflect.has(obj, prop);
|
|
420
|
+
deleteProperty(obj, prop) {
|
|
421
|
+
return Reflect.deleteProperty(obj, prop);
|
|
223
422
|
}
|
|
224
423
|
});
|
|
225
424
|
}
|
|
226
425
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
426
|
+
* Try to extract a primitive value from a ReadProxy via Symbol.toPrimitive.
|
|
427
|
+
* Returns `undefined` if the proxy represents a non-existent (leaf) value.
|
|
229
428
|
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const meta = getTrackingMeta(tracked);
|
|
240
|
-
if (keys.length === 0 && meta?.observed) return UNRESOLVED;
|
|
241
|
-
return obj;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Strips the proxy layer and returns the raw underlying object.
|
|
245
|
-
*/
|
|
246
|
-
function unwrapProxy(tracked) {
|
|
247
|
-
const result = {};
|
|
248
|
-
for (const key of Object.keys(tracked)) result[key] = tracked[key];
|
|
249
|
-
return result;
|
|
429
|
+
function tryExtractPrimitive$2(proxy) {
|
|
430
|
+
const toPrim = proxy[Symbol.toPrimitive];
|
|
431
|
+
if (typeof toPrim === "function") {
|
|
432
|
+
const result = toPrim();
|
|
433
|
+
if (result !== void 0 && result !== null && typeof result !== "object") {
|
|
434
|
+
if (typeof result === "string" && result.startsWith("__pending__")) return void 0;
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
250
438
|
}
|
|
251
|
-
//#endregion
|
|
252
|
-
//#region src/core/construct.ts
|
|
253
|
-
/**
|
|
254
|
-
* Context keys used to propagate tracking infrastructure through the construct tree.
|
|
255
|
-
* Set by Composition (root), read by Resource and other constructs.
|
|
256
|
-
* @internal
|
|
257
|
-
*/
|
|
258
|
-
const CONTEXT_COLLECTOR = "xplane:collector";
|
|
259
|
-
const CONTEXT_GRAPH = "xplane:graph";
|
|
260
|
-
/** Raw XR name and namespace stored at composition root for use by uniqueName. */
|
|
261
|
-
const CONTEXT_XR_META = "xplane:xr-meta";
|
|
262
|
-
/** Registry of existing resource references on the composition root. */
|
|
263
|
-
const CONTEXT_EXISTING = "xplane:existing";
|
|
264
|
-
//#endregion
|
|
265
|
-
//#region src/core/composition.ts
|
|
266
439
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
* Resources and constructs are created in the constructor.
|
|
270
|
-
*
|
|
271
|
-
* Usage:
|
|
272
|
-
* ```ts
|
|
273
|
-
* class MyComposition extends Composition {
|
|
274
|
-
* constructor() {
|
|
275
|
-
* super();
|
|
276
|
-
* const vpc = new aws.ec2.VPC(this, 'vpc', { ... });
|
|
277
|
-
* const subnet = new aws.ec2.Subnet(this, 'subnet', {
|
|
278
|
-
* spec: { forProvider: { vpcId: vpc.status.atProvider.vpcId } }
|
|
279
|
-
* });
|
|
280
|
-
* }
|
|
281
|
-
* }
|
|
282
|
-
* ```
|
|
440
|
+
* Deep-process a plain object/array being assigned to a WriteProxy.
|
|
441
|
+
* Replaces any nested ReadProxy values with Pending markers or concrete values.
|
|
283
442
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
*/
|
|
295
|
-
static _pendingEnvironment;
|
|
296
|
-
/** The composite resource (XR) — proxy-wrapped for tracking. */
|
|
297
|
-
xr;
|
|
298
|
-
/** Environment data from function-environment-configs or other pipeline steps. */
|
|
299
|
-
environment;
|
|
300
|
-
/** Raw name from the XR metadata (not proxy-tracked). */
|
|
301
|
-
xrName;
|
|
302
|
-
/** Raw namespace from the XR metadata (not proxy-tracked). */
|
|
303
|
-
xrNamespace;
|
|
304
|
-
/** Dependency collector shared across all resources. */
|
|
305
|
-
collector;
|
|
306
|
-
/** Dependency graph built during compose(). */
|
|
307
|
-
graph;
|
|
308
|
-
/** Registry of existing (read-only) resource references keyed by refKey. */
|
|
309
|
-
_existingResources = /* @__PURE__ */ new Map();
|
|
310
|
-
/** Registered status output function. @internal */
|
|
311
|
-
_statusFn;
|
|
312
|
-
constructor() {
|
|
313
|
-
super(void 0, "");
|
|
314
|
-
this.collector = new DependencyCollector();
|
|
315
|
-
this.graph = new DependencyGraph();
|
|
316
|
-
this.node.setContext(CONTEXT_COLLECTOR, this.collector);
|
|
317
|
-
this.node.setContext(CONTEXT_GRAPH, this.graph);
|
|
318
|
-
this.node.setContext(CONTEXT_EXISTING, this._existingResources);
|
|
319
|
-
const xrData = Composition._pendingXR ?? {};
|
|
320
|
-
Composition._pendingXR = void 0;
|
|
321
|
-
const envData = Composition._pendingEnvironment ?? {};
|
|
322
|
-
Composition._pendingEnvironment = void 0;
|
|
323
|
-
const xrMeta = xrData.metadata ?? {};
|
|
324
|
-
this.xrName = typeof xrMeta.name === "string" ? xrMeta.name : void 0;
|
|
325
|
-
this.xrNamespace = typeof xrMeta.namespace === "string" ? xrMeta.namespace : void 0;
|
|
326
|
-
this.node.setContext(CONTEXT_XR_META, {
|
|
327
|
-
name: this.xrName,
|
|
328
|
-
namespace: this.xrNamespace
|
|
443
|
+
function deepProcessValue(value, owner, basePath, collector) {
|
|
444
|
+
if (value === null || value === void 0) return value;
|
|
445
|
+
if (typeof value !== "object") return value;
|
|
446
|
+
if (isReadProxy(value)) {
|
|
447
|
+
const meta = getReadProxyMeta(value);
|
|
448
|
+
collector.add({
|
|
449
|
+
from: meta.owner,
|
|
450
|
+
fromPath: meta.path,
|
|
451
|
+
to: owner,
|
|
452
|
+
toPath: basePath
|
|
329
453
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
observed: true,
|
|
334
|
-
collector: this.collector,
|
|
335
|
-
strict: true
|
|
336
|
-
});
|
|
337
|
-
this.environment = envData;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* Register a function that computes the desired XR status output.
|
|
341
|
-
*
|
|
342
|
-
* The function is called by the framework **after** observed state has been
|
|
343
|
-
* fed into all resources, so `resource.observed` contains real data.
|
|
344
|
-
*
|
|
345
|
-
* @example
|
|
346
|
-
* ```ts
|
|
347
|
-
* this.setStatusOutput(() => ({
|
|
348
|
-
* config: {
|
|
349
|
-
* projectHostedZoneId: hostedZone.observed?.status?.atProvider?.id,
|
|
350
|
-
* },
|
|
351
|
-
* }));
|
|
352
|
-
* ```
|
|
353
|
-
*/
|
|
354
|
-
setStatusOutput(fn) {
|
|
355
|
-
this._statusFn = fn;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Compute and return the desired status output.
|
|
359
|
-
* Returns an empty object if no status function was registered.
|
|
360
|
-
* @internal
|
|
361
|
-
*/
|
|
362
|
-
computeStatusOutput() {
|
|
363
|
-
return this._statusFn?.() ?? {};
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Walk up the construct tree and return the root Composition.
|
|
367
|
-
* Throws if the scope is not within a Composition.
|
|
368
|
-
*/
|
|
369
|
-
static of(scope) {
|
|
370
|
-
let current = scope;
|
|
371
|
-
while (current !== void 0) {
|
|
372
|
-
if (current instanceof Composition) return current;
|
|
373
|
-
current = current.node.scope;
|
|
374
|
-
}
|
|
375
|
-
throw new Error("No Composition found in the scope chain. Ensure constructs are created within a Composition.");
|
|
454
|
+
const primitive = tryExtractPrimitive$2(value);
|
|
455
|
+
if (primitive !== void 0) return primitive;
|
|
456
|
+
return new Pending(meta.owner, meta.path);
|
|
376
457
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
return map;
|
|
382
|
-
}
|
|
383
|
-
/** Get all existing (read-only) resource references keyed by refKey. */
|
|
384
|
-
get existingResources() {
|
|
385
|
-
return this._existingResources;
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
/**
|
|
389
|
-
* Type guard for Resource — avoids circular import by checking for
|
|
390
|
-
* characteristic properties rather than instanceof.
|
|
391
|
-
*/
|
|
392
|
-
function isResource(construct) {
|
|
393
|
-
return construct !== null && typeof construct === "object" && "apiVersion" in construct && "kind" in construct && "resourceRef" in construct && "isExisting" in construct;
|
|
458
|
+
if (Array.isArray(value)) return value.map((item, i) => deepProcessValue(item, owner, `${basePath}[${i}]`, collector));
|
|
459
|
+
const result = {};
|
|
460
|
+
for (const [key, val] of Object.entries(value)) result[key] = deepProcessValue(val, owner, `${basePath}.${key}`, collector);
|
|
461
|
+
return result;
|
|
394
462
|
}
|
|
395
463
|
//#endregion
|
|
396
464
|
//#region src/core/resource.ts
|
|
465
|
+
const internals = /* @__PURE__ */ new WeakMap();
|
|
397
466
|
/**
|
|
398
|
-
* A
|
|
467
|
+
* A Kubernetes resource within a Composition.
|
|
468
|
+
*
|
|
469
|
+
* The Resource instance acts as a "desired-first, fallback-to-observed" proxy:
|
|
470
|
+
* - Reading a path that exists in the desired document returns the desired value.
|
|
471
|
+
* - Reading a path that does NOT exist in desired falls through to a tracked
|
|
472
|
+
* ReadProxy over observed state (creates dependency edges).
|
|
473
|
+
* - Writing always goes to the desired document.
|
|
399
474
|
*
|
|
400
|
-
* The
|
|
401
|
-
*
|
|
402
|
-
* to this resource's spec automatically records a dependency edge.
|
|
475
|
+
* The only reserved properties are `node` (from Construct) and `resource`
|
|
476
|
+
* (framework config namespace).
|
|
403
477
|
*/
|
|
404
478
|
var Resource = class Resource extends Construct$1 {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
resourceRef;
|
|
408
|
-
/** Proxy-wrapped desired spec — writes are tracked. */
|
|
409
|
-
spec;
|
|
410
|
-
/** Proxy-wrapped observed status — reads create dependency tracking. */
|
|
411
|
-
status;
|
|
412
|
-
/** Proxy-wrapped desired metadata. */
|
|
413
|
-
metadata;
|
|
414
|
-
/**
|
|
415
|
-
* Observed-mode tracked proxy over the entire resource document.
|
|
416
|
-
* Use this to access arbitrary top-level fields on existing resources
|
|
417
|
-
* (e.g., `secret.root.data.myKey` for a Secret, `configMap.root.data.key` for a ConfigMap).
|
|
418
|
-
*/
|
|
419
|
-
root;
|
|
420
|
-
/** Whether auto-ready is enabled for this resource. */
|
|
421
|
-
autoReady;
|
|
422
|
-
/** Whether this is a read-only reference to an existing cluster resource. */
|
|
423
|
-
isExisting;
|
|
424
|
-
/** If this is an existing resource, holds the reference metadata for the handler. */
|
|
425
|
-
existingRef;
|
|
426
|
-
/** Extra top-level fields (e.g. data/stringData for Secret). Not proxy-tracked. */
|
|
427
|
-
_extra;
|
|
428
|
-
/** Observed state populated by the bridge before construction. */
|
|
429
|
-
_observed;
|
|
430
|
-
/** Backing object for the status proxy — populated by setObserved(). */
|
|
431
|
-
_statusTarget;
|
|
432
|
-
/** Backing object for the spec proxy (existing resources only) — populated by setObservedFull(). */
|
|
433
|
-
_specTarget;
|
|
434
|
-
/** Backing object for the root proxy — populated by setObservedFull(). */
|
|
435
|
-
_rootTarget;
|
|
436
|
-
/** Backing object for the metadata proxy — populated by setObserved(). */
|
|
437
|
-
_metaTarget;
|
|
438
|
-
/** Keys the user explicitly declared in constructor metadata props. */
|
|
439
|
-
_desiredMetaKeys;
|
|
440
|
-
/** Explicit dependency refs. */
|
|
441
|
-
_explicitDeps = [];
|
|
442
|
-
/** @internal */
|
|
443
|
-
_graph;
|
|
444
|
-
constructor(scope, id, props, options) {
|
|
479
|
+
resource;
|
|
480
|
+
constructor(scope, id, props) {
|
|
445
481
|
super(scope, id);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this.
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
collector
|
|
471
|
-
});
|
|
472
|
-
this._metaTarget = props.metadata ?? {};
|
|
473
|
-
this._desiredMetaKeys = new Set(Object.keys(this._metaTarget));
|
|
474
|
-
this.metadata = createTrackedProxy(this._metaTarget, {
|
|
475
|
-
owner: this.resourceRef,
|
|
476
|
-
path: "metadata",
|
|
477
|
-
observed: true,
|
|
478
|
-
collector
|
|
479
|
-
});
|
|
480
|
-
this._statusTarget = {};
|
|
481
|
-
this.status = createTrackedProxy(this._statusTarget, {
|
|
482
|
-
owner: this.resourceRef,
|
|
483
|
-
path: "status",
|
|
484
|
-
observed: true,
|
|
485
|
-
collector
|
|
486
|
-
});
|
|
487
|
-
this._rootTarget = {};
|
|
488
|
-
this.root = createTrackedProxy(this._rootTarget, {
|
|
489
|
-
owner: this.resourceRef,
|
|
490
|
-
path: "",
|
|
491
|
-
observed: true,
|
|
482
|
+
const graph = scope.node.tryGetContext("xplane:graph");
|
|
483
|
+
const collector = scope.node.tryGetContext("xplane:collector");
|
|
484
|
+
if (!graph || !collector) throw new Error("Resource must be created within a Composition tree.");
|
|
485
|
+
const ref = { id: this.node.path };
|
|
486
|
+
graph.addResource(ref);
|
|
487
|
+
const readyChecks = [];
|
|
488
|
+
const config = {
|
|
489
|
+
autoReady: true,
|
|
490
|
+
addReadyCheck(fn, priority = 50) {
|
|
491
|
+
readyChecks.push({
|
|
492
|
+
fn,
|
|
493
|
+
priority
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
this.resource = config;
|
|
498
|
+
const internal = {
|
|
499
|
+
ref,
|
|
500
|
+
desired: processDesiredProps(props, ref, collector),
|
|
501
|
+
observed: {},
|
|
502
|
+
external: false,
|
|
503
|
+
config,
|
|
504
|
+
readyChecks,
|
|
505
|
+
graph,
|
|
492
506
|
collector
|
|
493
|
-
}
|
|
494
|
-
this
|
|
495
|
-
this
|
|
496
|
-
}
|
|
497
|
-
/** Fully qualified path in the construct tree. */
|
|
498
|
-
get path() {
|
|
499
|
-
return this.node.path;
|
|
500
|
-
}
|
|
501
|
-
/** Add an explicit dependency on another resource. */
|
|
502
|
-
addDependency(other) {
|
|
503
|
-
this._explicitDeps.push(other.resourceRef);
|
|
504
|
-
this._graph.addExplicitDependency(this.resourceRef, other.resourceRef);
|
|
505
|
-
}
|
|
506
|
-
/** Get explicit dependency refs. */
|
|
507
|
-
get explicitDependencies() {
|
|
508
|
-
return this._explicitDeps;
|
|
509
|
-
}
|
|
510
|
-
/** Set observed state (called by the bridge before compose). */
|
|
511
|
-
setObserved(observed) {
|
|
512
|
-
this._observed = observed;
|
|
513
|
-
for (const key of Object.keys(this._metaTarget)) this._desiredMetaKeys.add(key);
|
|
514
|
-
if (observed.metadata && typeof observed.metadata === "object") Object.assign(this._metaTarget, observed.metadata);
|
|
515
|
-
if (observed.status && typeof observed.status === "object") Object.assign(this._statusTarget, observed.status);
|
|
516
|
-
}
|
|
517
|
-
/** Get observed state. */
|
|
518
|
-
get observed() {
|
|
519
|
-
return this._observed;
|
|
507
|
+
};
|
|
508
|
+
internals.set(this, internal);
|
|
509
|
+
return createResourceProxy(this, internal);
|
|
520
510
|
}
|
|
521
511
|
/**
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* The name is structured as:
|
|
526
|
-
* `[namespace-]claimName-PathSegments[-extra]-hash8`
|
|
512
|
+
* Look up an existing cluster resource by name.
|
|
513
|
+
* Returns a Resource that only has observed state (no desired output).
|
|
527
514
|
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
515
|
+
* The `name` parameter accepts either a plain string or a PrimitiveReadProxy
|
|
516
|
+
* (returned when reading a tracked property like `ns.metadata.labels['x']`).
|
|
517
|
+
* Proxies are coerced to their underlying string via `Symbol.toPrimitive`.
|
|
518
|
+
*/
|
|
519
|
+
static fromExistingByName(scope, apiVersion, kind, name, namespace) {
|
|
520
|
+
const resolvedName = coerceToString(name);
|
|
521
|
+
const refKey = computeRefKey(apiVersion, kind, resolvedName, namespace);
|
|
522
|
+
const instance = new Resource(scope, `__existing__${refKey.replace(/\//g, "_")}`, {
|
|
523
|
+
apiVersion,
|
|
524
|
+
kind
|
|
525
|
+
});
|
|
526
|
+
const internal = internals.get(instance);
|
|
527
|
+
internal.external = true;
|
|
528
|
+
internal.externalRef = {
|
|
529
|
+
apiVersion,
|
|
530
|
+
kind,
|
|
531
|
+
name: resolvedName ?? name,
|
|
532
|
+
namespace,
|
|
533
|
+
refKey
|
|
534
|
+
};
|
|
535
|
+
const ctx = compositionStorage.getStore();
|
|
536
|
+
if (ctx) {
|
|
537
|
+
const observed = ctx.requiredResources.get(refKey);
|
|
538
|
+
if (observed) Object.assign(internal.observed, observed);
|
|
539
|
+
}
|
|
540
|
+
return instance;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Generate a deterministic unique name based on the XR identity and construct path.
|
|
544
|
+
* Useful for resource fields that need unique names (e.g., AWS resource names).
|
|
538
545
|
*/
|
|
539
546
|
static uniqueName(scope, options = {}) {
|
|
540
547
|
const maxLength = options.maxLength ?? 63;
|
|
@@ -543,12 +550,10 @@ var Resource = class Resource extends Construct$1 {
|
|
|
543
550
|
const escapedSep = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
544
551
|
const collapseRe = new RegExp(`${escapedSep}+`, "g");
|
|
545
552
|
const clean = (s) => s.replace(/\s+/g, "").replace(allowedPattern, separator).replace(collapseRe, separator).replace(new RegExp(`^${escapedSep}|${escapedSep}$`, "g"), "");
|
|
546
|
-
const xrMeta = scope.node.tryGetContext(
|
|
547
|
-
const xrName = xrMeta?.name;
|
|
548
|
-
const xrNamespace = xrMeta?.namespace;
|
|
553
|
+
const xrMeta = scope.node.tryGetContext("xplane:xr-meta");
|
|
549
554
|
const parts = [];
|
|
550
|
-
if (
|
|
551
|
-
if (
|
|
555
|
+
if (xrMeta?.namespace) parts.push(clean(xrMeta.namespace));
|
|
556
|
+
if (xrMeta?.name) parts.push(clean(xrMeta.name));
|
|
552
557
|
for (const s of scope.node.scopes.slice(1)) {
|
|
553
558
|
const c = clean(s.node.id);
|
|
554
559
|
if (c) parts.push(c);
|
|
@@ -563,91 +568,48 @@ var Resource = class Resource extends Construct$1 {
|
|
|
563
568
|
if (withHash.length <= maxLength) return withHash;
|
|
564
569
|
return `${full.slice(0, maxLength - hash.length - separator.length)}${separator}${hash}`;
|
|
565
570
|
}
|
|
566
|
-
/**
|
|
567
|
-
* Serialize to a plain Kubernetes resource object for the desired state.
|
|
568
|
-
* Strips proxy wrappers, UNRESOLVED sentinels, and server-managed metadata
|
|
569
|
-
* fields (uid, resourceVersion, etc.) that must not appear in desired state.
|
|
570
|
-
*/
|
|
571
|
-
toDesired() {
|
|
572
|
-
const fullMeta = JSON.parse(JSON.stringify(this.metadata));
|
|
573
|
-
const desiredMeta = {};
|
|
574
|
-
for (const key of this._desiredMetaKeys) if (key in fullMeta) desiredMeta[key] = fullMeta[key];
|
|
575
|
-
const cleanMeta = stripUnresolved(desiredMeta);
|
|
576
|
-
const desired = {
|
|
577
|
-
...this._extra,
|
|
578
|
-
apiVersion: this.apiVersion,
|
|
579
|
-
kind: this.kind,
|
|
580
|
-
metadata: cleanMeta,
|
|
581
|
-
spec: stripUnresolved(JSON.parse(JSON.stringify(this.spec)))
|
|
582
|
-
};
|
|
583
|
-
if (desired.spec && typeof desired.spec === "object" && Object.keys(desired.spec).length === 0) delete desired.spec;
|
|
584
|
-
return desired;
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Populate the full observed state for an existing resource.
|
|
588
|
-
* Feeds data into `root`, `status`, `spec`, and `metadata` backing targets
|
|
589
|
-
* so that proxy reads resolve to real values.
|
|
590
|
-
*/
|
|
591
|
-
setObservedFull(resource) {
|
|
592
|
-
this._observed = resource;
|
|
593
|
-
for (const [key, value] of Object.entries(resource)) if (value !== null && value !== void 0) this._rootTarget[key] = value;
|
|
594
|
-
if (resource.status && typeof resource.status === "object") Object.assign(this._statusTarget, resource.status);
|
|
595
|
-
if (this._specTarget && resource.spec && typeof resource.spec === "object") Object.assign(this._specTarget, resource.spec);
|
|
596
|
-
if (resource.metadata && typeof resource.metadata === "object") Object.assign(this._metaTarget, resource.metadata);
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Create a read-only reference to an existing cluster resource.
|
|
600
|
-
* The resource will be fetched by Crossplane via the Required Resources mechanism.
|
|
601
|
-
* Its `status`, `spec`, and `root` proxies can be read to create dependency edges.
|
|
602
|
-
*
|
|
603
|
-
* @param scope - Parent construct (typically `this` inside a Composition constructor)
|
|
604
|
-
* @param apiVersion - API version of the resource (e.g. "example.io/v1")
|
|
605
|
-
* @param kind - Kind of the resource (e.g. "Project")
|
|
606
|
-
* @param name - Name of the resource (can be a literal string or a tracked proxy value)
|
|
607
|
-
* @param namespace - Optional namespace of the resource
|
|
608
|
-
*/
|
|
609
|
-
static fromExistingByName(scope, apiVersion, kind, name, namespace) {
|
|
610
|
-
const refKey = computeRefKey(apiVersion, kind, typeof name === "string" ? name : void 0, namespace);
|
|
611
|
-
const resource = new Resource(scope, `__existing__${refKey.replace(/\//g, "_")}`, {
|
|
612
|
-
apiVersion,
|
|
613
|
-
kind,
|
|
614
|
-
spec: {}
|
|
615
|
-
});
|
|
616
|
-
resource.isExisting = true;
|
|
617
|
-
resource.existingRef = {
|
|
618
|
-
apiVersion,
|
|
619
|
-
kind,
|
|
620
|
-
name,
|
|
621
|
-
namespace,
|
|
622
|
-
refKey
|
|
623
|
-
};
|
|
624
|
-
const collector = resource.node.tryGetContext(CONTEXT_COLLECTOR);
|
|
625
|
-
const specTarget = {};
|
|
626
|
-
resource._specTarget = specTarget;
|
|
627
|
-
resource.spec = createTrackedProxy(specTarget, {
|
|
628
|
-
owner: resource.resourceRef,
|
|
629
|
-
path: "spec",
|
|
630
|
-
observed: true,
|
|
631
|
-
collector
|
|
632
|
-
});
|
|
633
|
-
const existingMap = resource.node.tryGetContext(CONTEXT_EXISTING);
|
|
634
|
-
if (existingMap) existingMap.set(refKey, resource);
|
|
635
|
-
return resource;
|
|
636
|
-
}
|
|
637
571
|
};
|
|
572
|
+
function getResourceInternals(resource) {
|
|
573
|
+
const internal = internals.get(resource);
|
|
574
|
+
if (!internal) throw new Error("Resource internals not found");
|
|
575
|
+
return internal;
|
|
576
|
+
}
|
|
577
|
+
function getResourceRef(resource) {
|
|
578
|
+
return getResourceInternals(resource).ref;
|
|
579
|
+
}
|
|
580
|
+
function getDesiredDocument(resource) {
|
|
581
|
+
return getResourceInternals(resource).desired;
|
|
582
|
+
}
|
|
583
|
+
function getObservedDocument(resource) {
|
|
584
|
+
return getResourceInternals(resource).observed;
|
|
585
|
+
}
|
|
586
|
+
function hydrateObserved(resource, data) {
|
|
587
|
+
const internal = getResourceInternals(resource);
|
|
588
|
+
Object.assign(internal.observed, data);
|
|
589
|
+
}
|
|
590
|
+
function isExternal(resource) {
|
|
591
|
+
return getResourceInternals(resource).external;
|
|
592
|
+
}
|
|
593
|
+
function getExternalRef(resource) {
|
|
594
|
+
return getResourceInternals(resource).externalRef;
|
|
595
|
+
}
|
|
596
|
+
function getReadyChecks(resource) {
|
|
597
|
+
return getResourceInternals(resource).readyChecks;
|
|
598
|
+
}
|
|
638
599
|
/**
|
|
639
|
-
*
|
|
640
|
-
*
|
|
600
|
+
* Coerce a value to a string, handling PrimitiveReadProxy objects that wrap
|
|
601
|
+
* primitive values behind a `Symbol.toPrimitive` method.
|
|
602
|
+
* Returns `undefined` if the value cannot be resolved to a string.
|
|
641
603
|
*/
|
|
604
|
+
function coerceToString(value) {
|
|
605
|
+
if (typeof value === "string") return value;
|
|
606
|
+
if (value != null && typeof value[Symbol.toPrimitive] === "function") return String(value);
|
|
607
|
+
}
|
|
642
608
|
function computeRefKey(apiVersion, kind, name, namespace) {
|
|
643
609
|
const namePart = name ?? "__unresolved__";
|
|
644
610
|
if (namespace) return `${apiVersion}/${kind}/${namespace}/${namePart}`;
|
|
645
611
|
return `${apiVersion}/${kind}/${namePart}`;
|
|
646
612
|
}
|
|
647
|
-
/**
|
|
648
|
-
* Produce an 8-character hex hash of a string using a simple djb2-style
|
|
649
|
-
* algorithm — no crypto dependency required.
|
|
650
|
-
*/
|
|
651
613
|
function shortHash(s) {
|
|
652
614
|
let h = 5381;
|
|
653
615
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -656,165 +618,651 @@ function shortHash(s) {
|
|
|
656
618
|
}
|
|
657
619
|
return h.toString(16).padStart(8, "0");
|
|
658
620
|
}
|
|
659
|
-
/** Recursively remove UNRESOLVED sentinel values from an object. */
|
|
660
|
-
function stripUnresolved(obj) {
|
|
661
|
-
if (obj === null || obj === void 0) return obj;
|
|
662
|
-
if (typeof obj === "symbol" && obj === UNRESOLVED) return void 0;
|
|
663
|
-
if (Array.isArray(obj)) return obj.map(stripUnresolved);
|
|
664
|
-
if (typeof obj === "object") {
|
|
665
|
-
const result = {};
|
|
666
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
667
|
-
const cleaned = stripUnresolved(value);
|
|
668
|
-
if (cleaned !== void 0) result[key] = cleaned;
|
|
669
|
-
}
|
|
670
|
-
return result;
|
|
671
|
-
}
|
|
672
|
-
return obj;
|
|
673
|
-
}
|
|
674
621
|
/**
|
|
675
|
-
*
|
|
676
|
-
*
|
|
622
|
+
* Process the desired props — deep-scan for ReadProxy values and replace them
|
|
623
|
+
* with Pending markers (recording edges in the collector).
|
|
677
624
|
*/
|
|
678
|
-
function
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
return
|
|
625
|
+
function processDesiredProps(props, owner, collector) {
|
|
626
|
+
const result = {};
|
|
627
|
+
for (const [key, value] of Object.entries(props)) result[key] = processValue(value, owner, key, collector);
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
function processValue(value, owner, path, collector) {
|
|
631
|
+
if (value === null || value === void 0) return value;
|
|
632
|
+
if (typeof value !== "object") return value;
|
|
633
|
+
if (isReadProxy(value)) {
|
|
634
|
+
const meta = getReadProxyMeta(value);
|
|
635
|
+
collector.add({
|
|
636
|
+
from: meta.owner,
|
|
637
|
+
fromPath: meta.path,
|
|
638
|
+
to: owner,
|
|
639
|
+
toPath: path
|
|
640
|
+
});
|
|
641
|
+
const prim = tryExtractPrimitive$1(value);
|
|
642
|
+
if (prim !== void 0) return prim;
|
|
643
|
+
return new Pending(meta.owner, meta.path);
|
|
644
|
+
}
|
|
645
|
+
if (Array.isArray(value)) return value.map((item, i) => processValue(item, owner, `${path}[${i}]`, collector));
|
|
646
|
+
const result = {};
|
|
647
|
+
for (const [key, val] of Object.entries(value)) result[key] = processValue(val, owner, `${path}.${key}`, collector);
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
function tryExtractPrimitive$1(proxy) {
|
|
651
|
+
const toPrim = proxy[Symbol.toPrimitive];
|
|
652
|
+
if (typeof toPrim === "function") {
|
|
653
|
+
const result = toPrim();
|
|
654
|
+
if (result !== void 0 && result !== null && typeof result !== "object") {
|
|
655
|
+
if (typeof result === "string" && result.startsWith("__pending__")) return void 0;
|
|
656
|
+
return result;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
686
659
|
}
|
|
687
660
|
/**
|
|
688
|
-
*
|
|
689
|
-
* For each one found, record a dependency edge and replace the value with
|
|
690
|
-
* the UNRESOLVED sentinel. This handles values passed via object literals
|
|
691
|
-
* in constructor props, which bypass the proxy's set trap.
|
|
661
|
+
* Creates the "desired-first, fallback-to-observed" proxy around a Resource.
|
|
692
662
|
*/
|
|
693
|
-
function
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
663
|
+
function createResourceProxy(resource, internal) {
|
|
664
|
+
const { ref, desired, collector } = internal;
|
|
665
|
+
const proxy = new Proxy(resource, {
|
|
666
|
+
get(target, prop, receiver) {
|
|
667
|
+
if (prop === "node") return Reflect.get(target, prop, receiver);
|
|
668
|
+
if (prop === "resource") return Reflect.get(target, prop, receiver);
|
|
669
|
+
if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
|
|
670
|
+
if (prop === "constructor") return Reflect.get(target, prop, receiver);
|
|
671
|
+
if (prop in desired) {
|
|
672
|
+
const value = desired[String(prop)];
|
|
673
|
+
if (typeof value === "object" && value !== null && !Pending.is(value)) return createWriteProxy(value, {
|
|
674
|
+
owner: ref,
|
|
675
|
+
collector,
|
|
676
|
+
basePath: String(prop)
|
|
704
677
|
});
|
|
705
|
-
|
|
678
|
+
return value;
|
|
706
679
|
}
|
|
707
|
-
|
|
680
|
+
const observed = internal.observed;
|
|
681
|
+
if (String(prop) in observed) {
|
|
682
|
+
const value = observed[String(prop)];
|
|
683
|
+
if (typeof value === "object" && value !== null) return createReadProxy(value, ref, String(prop));
|
|
684
|
+
return createPrimitiveReadProxyFromResource(value, ref, String(prop));
|
|
685
|
+
}
|
|
686
|
+
return createReadProxy({}, ref, String(prop));
|
|
687
|
+
},
|
|
688
|
+
set(target, prop, value) {
|
|
689
|
+
if (typeof prop === "symbol") return Reflect.set(target, prop, value);
|
|
690
|
+
if (prop === "resource") return Reflect.set(target, prop, value);
|
|
691
|
+
const path = String(prop);
|
|
692
|
+
desired[path] = processValue(value, ref, path, collector);
|
|
693
|
+
return true;
|
|
694
|
+
},
|
|
695
|
+
has(target, prop) {
|
|
696
|
+
if (typeof prop === "symbol") return Reflect.has(target, prop);
|
|
697
|
+
if (prop === "node" || prop === "resource") return true;
|
|
698
|
+
return prop in desired || prop in internal.observed;
|
|
708
699
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
700
|
+
});
|
|
701
|
+
internals.set(proxy, internal);
|
|
702
|
+
return proxy;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Wrap a primitive observed value so it carries ReadProxy metadata
|
|
706
|
+
* for dependency tracking when assigned elsewhere.
|
|
707
|
+
*/
|
|
708
|
+
function createPrimitiveReadProxyFromResource(value, owner, path) {
|
|
709
|
+
if (value === null || value === void 0) return value;
|
|
710
|
+
return createPrimitiveReadProxy(value, owner, path);
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/logging/context.ts
|
|
714
|
+
const noopLogger = {
|
|
715
|
+
debug() {},
|
|
716
|
+
info() {},
|
|
717
|
+
warn() {}
|
|
718
|
+
};
|
|
719
|
+
const loggerStorage = new AsyncLocalStorage();
|
|
720
|
+
/**
|
|
721
|
+
* Get the current logger from async context.
|
|
722
|
+
* Returns a no-op logger if none has been set (silent by default).
|
|
723
|
+
*/
|
|
724
|
+
function getLogger() {
|
|
725
|
+
return loggerStorage.getStore() ?? noopLogger;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Run a function with a logger available in async context.
|
|
729
|
+
* All code within `fn` (and any nested async calls) can access
|
|
730
|
+
* the logger via `getLogger()`.
|
|
731
|
+
*/
|
|
732
|
+
function withLogger(logger, fn) {
|
|
733
|
+
return loggerStorage.run(logger, fn);
|
|
734
|
+
}
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/pipeline/diagnose.ts
|
|
737
|
+
/**
|
|
738
|
+
* DIAGNOSE phase: produce structured diagnostics for blocked resources.
|
|
739
|
+
*
|
|
740
|
+
* For each resource classified as 'blocked':
|
|
741
|
+
* - Find all Pending markers in the desired document
|
|
742
|
+
* - Report what they're waiting on (source resource + path)
|
|
743
|
+
*
|
|
744
|
+
* For circular dependencies (detected in sequence phase):
|
|
745
|
+
* - Produce a 'cycle' diagnostic with the full cycle path
|
|
746
|
+
*/
|
|
747
|
+
function diagnose(state) {
|
|
748
|
+
const diagnostics = [];
|
|
749
|
+
const sortResult = state.graph.topologicalSort();
|
|
750
|
+
if (sortResult.order === null && sortResult.cycle) {
|
|
751
|
+
const firstCycleMember = sortResult.cycle[0];
|
|
752
|
+
if (firstCycleMember) diagnostics.push({
|
|
753
|
+
resource: firstCycleMember,
|
|
754
|
+
reason: "cycle",
|
|
755
|
+
cycle: sortResult.cycle
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
for (const resource of state.resources) {
|
|
759
|
+
if (!isExternal(resource)) continue;
|
|
760
|
+
const ref = getExternalRef(resource);
|
|
761
|
+
if (!ref) continue;
|
|
762
|
+
const observed = getObservedDocument(resource);
|
|
763
|
+
if (Object.keys(observed).length > 0) continue;
|
|
764
|
+
const nameDisplay = typeof ref.name === "string" ? ref.name : ref.name == null ? "(unresolved)" : "(unresolved)";
|
|
765
|
+
const nsDisplay = ref.namespace ? ` in namespace '${ref.namespace}'` : "";
|
|
766
|
+
diagnostics.push({
|
|
767
|
+
resource: getResourceRef(resource).id,
|
|
768
|
+
reason: "not-found",
|
|
769
|
+
detail: `External resource ${ref.apiVersion}/${ref.kind} '${nameDisplay}'${nsDisplay} was required but not found by Crossplane`
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const pendingDiagnostics = [];
|
|
773
|
+
for (const resource of state.resources) {
|
|
774
|
+
if (isExternal(resource)) continue;
|
|
775
|
+
const ref = getResourceRef(resource);
|
|
776
|
+
if (state.classification.get(ref.id) !== "blocked") continue;
|
|
777
|
+
if (sortResult.order === null && sortResult.cycle?.includes(ref.id)) continue;
|
|
778
|
+
const pendingPaths = findPendingPaths(getDesiredDocument(resource), "");
|
|
779
|
+
if (pendingPaths.length > 0) pendingDiagnostics.push({
|
|
780
|
+
resource: ref.id,
|
|
781
|
+
reason: "pending",
|
|
782
|
+
pendingPaths
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const notFoundIds = new Set(diagnostics.filter((d) => d.reason === "not-found").map((d) => d.resource));
|
|
786
|
+
const blockedIds = new Set(pendingDiagnostics.map((d) => d.resource));
|
|
787
|
+
for (const diag of pendingDiagnostics) if (diag.pendingPaths?.some((p) => {
|
|
788
|
+
const dep = p.waitingOn.resource;
|
|
789
|
+
return !blockedIds.has(dep) && !notFoundIds.has(dep);
|
|
790
|
+
})) diagnostics.push(diag);
|
|
791
|
+
return {
|
|
792
|
+
...state,
|
|
793
|
+
diagnostics
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Recursively find all Pending markers in a document and report their source.
|
|
798
|
+
*/
|
|
799
|
+
function findPendingPaths(obj, basePath) {
|
|
800
|
+
const results = [];
|
|
801
|
+
if (obj === null || obj === void 0) return results;
|
|
802
|
+
if (Pending.is(obj)) {
|
|
803
|
+
results.push({
|
|
804
|
+
path: basePath,
|
|
805
|
+
waitingOn: {
|
|
806
|
+
resource: obj.source.id,
|
|
807
|
+
path: obj.path
|
|
724
808
|
}
|
|
725
|
-
|
|
809
|
+
});
|
|
810
|
+
return results;
|
|
811
|
+
}
|
|
812
|
+
if (typeof obj !== "object") return results;
|
|
813
|
+
if (Array.isArray(obj)) {
|
|
814
|
+
for (let i = 0; i < obj.length; i++) {
|
|
815
|
+
const childPath = basePath ? `${basePath}[${i}]` : `[${i}]`;
|
|
816
|
+
results.push(...findPendingPaths(obj[i], childPath));
|
|
726
817
|
}
|
|
727
|
-
|
|
818
|
+
return results;
|
|
819
|
+
}
|
|
820
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
821
|
+
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
822
|
+
results.push(...findPendingPaths(value, childPath));
|
|
728
823
|
}
|
|
824
|
+
return results;
|
|
729
825
|
}
|
|
730
826
|
//#endregion
|
|
731
|
-
//#region src/
|
|
827
|
+
//#region src/pipeline/emit.ts
|
|
732
828
|
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
829
|
+
* EMIT phase: serialize each resource classified as 'emit' into a plain
|
|
830
|
+
* Kubernetes resource document ready for Crossplane.
|
|
735
831
|
*
|
|
736
|
-
*
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
832
|
+
* Also extracts the XR desired status from this.xr.status assignments.
|
|
833
|
+
*/
|
|
834
|
+
function emit(state) {
|
|
835
|
+
const emitted = [];
|
|
836
|
+
for (const resource of state.resources) {
|
|
837
|
+
if (isExternal(resource)) continue;
|
|
838
|
+
const ref = getResourceRef(resource);
|
|
839
|
+
if (state.classification.get(ref.id) !== "emit") continue;
|
|
840
|
+
const internal = getResourceInternals(resource);
|
|
841
|
+
const desired = getDesiredDocument(resource);
|
|
842
|
+
const name = ref.id.startsWith("Composition/") ? ref.id.slice(12) : ref.id;
|
|
843
|
+
emitted.push({
|
|
844
|
+
name,
|
|
845
|
+
document: deepClean(desired),
|
|
846
|
+
autoReady: internal.config.autoReady,
|
|
847
|
+
readyChecks: getReadyChecks(resource)
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
const resourceById = new Map(state.resources.map((r) => [getResourceRef(r).id, r]));
|
|
851
|
+
const xrStatusPatches = resolveXrStatus(getXrDesiredStatus(state.composition), resourceById);
|
|
852
|
+
return {
|
|
853
|
+
...state,
|
|
854
|
+
emitted,
|
|
855
|
+
xrStatusPatches
|
|
856
|
+
};
|
|
749
857
|
}
|
|
750
858
|
/**
|
|
751
|
-
*
|
|
859
|
+
* Deep-clone an object, stripping any remaining framework internals.
|
|
860
|
+
* This produces a clean JSON-serializable Kubernetes document.
|
|
752
861
|
*/
|
|
753
|
-
function
|
|
754
|
-
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
862
|
+
function deepClean(obj) {
|
|
863
|
+
const result = {};
|
|
864
|
+
for (const [key, value] of Object.entries(obj)) result[key] = cleanValue(value);
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
function cleanValue(value) {
|
|
868
|
+
if (value === null || value === void 0) return value;
|
|
869
|
+
if (typeof value !== "object") return value;
|
|
870
|
+
if (Array.isArray(value)) return value.map(cleanValue);
|
|
871
|
+
const result = {};
|
|
872
|
+
for (const [key, val] of Object.entries(value)) result[key] = cleanValue(val);
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Resolve ReadProxy values in XR status using observed resource data.
|
|
877
|
+
* This is needed because XR status is written at construction time (before hydration),
|
|
878
|
+
* so read proxy references need to be resolved post-hydration.
|
|
879
|
+
*/
|
|
880
|
+
function resolveXrStatus(status, resourceById) {
|
|
881
|
+
const result = {};
|
|
882
|
+
for (const [key, value] of Object.entries(status)) {
|
|
883
|
+
const resolved = resolveStatusValue(value, resourceById);
|
|
884
|
+
if (resolved !== void 0) result[key] = resolved;
|
|
885
|
+
}
|
|
886
|
+
return result;
|
|
887
|
+
}
|
|
888
|
+
function resolveStatusValue(value, resourceById) {
|
|
889
|
+
if (value === null || value === void 0) return void 0;
|
|
890
|
+
if (isReadProxy(value)) {
|
|
891
|
+
const meta = getReadProxyMeta(value);
|
|
892
|
+
if (!meta) return void 0;
|
|
893
|
+
const prim = tryExtractPrimitive(value);
|
|
894
|
+
if (prim !== void 0) return prim;
|
|
895
|
+
const resource = resourceById.get(meta.owner.id);
|
|
896
|
+
if (!resource) return void 0;
|
|
897
|
+
return getNestedValue$1(getObservedDocument(resource), meta.path);
|
|
898
|
+
}
|
|
899
|
+
if (Array.isArray(value)) return value.map((v) => resolveStatusValue(v, resourceById)).filter((v) => v != null);
|
|
900
|
+
if (typeof value === "object") {
|
|
901
|
+
const out = {};
|
|
902
|
+
for (const [k, v] of Object.entries(value)) {
|
|
903
|
+
const resolved = resolveStatusValue(v, resourceById);
|
|
904
|
+
if (resolved !== void 0) out[k] = resolved;
|
|
905
|
+
}
|
|
906
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
907
|
+
}
|
|
908
|
+
return value;
|
|
909
|
+
}
|
|
910
|
+
function tryExtractPrimitive(proxy) {
|
|
911
|
+
const toPrim = proxy[Symbol.toPrimitive];
|
|
912
|
+
if (typeof toPrim === "function") {
|
|
913
|
+
const result = toPrim();
|
|
914
|
+
if (result !== void 0 && result !== null && typeof result !== "object") {
|
|
915
|
+
if (typeof result === "string" && result.startsWith("__pending__")) return void 0;
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function getNestedValue$1(obj, path) {
|
|
921
|
+
const parts = path.split(".");
|
|
922
|
+
let current = obj;
|
|
923
|
+
for (const part of parts) {
|
|
924
|
+
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
925
|
+
current = current[part];
|
|
926
|
+
}
|
|
927
|
+
return current;
|
|
928
|
+
}
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/pipeline/hydrate.ts
|
|
931
|
+
/**
|
|
932
|
+
* HYDRATE phase: feed observed state from Crossplane into each resource.
|
|
933
|
+
*
|
|
934
|
+
* - Composed resources are matched by their construct path (resource name).
|
|
935
|
+
* - External resources are matched by their refKey.
|
|
936
|
+
*/
|
|
937
|
+
function hydrate(state) {
|
|
938
|
+
for (const resource of state.resources) if (isExternal(resource)) {
|
|
939
|
+
const ref = getExternalRef(resource);
|
|
940
|
+
if (ref) {
|
|
941
|
+
const observed = state.observedRequired.get(ref.refKey);
|
|
942
|
+
if (observed) hydrateObserved(resource, observed);
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
const name = resource.node.path;
|
|
946
|
+
const observed = state.observedComposed.get(name);
|
|
947
|
+
if (observed) hydrateObserved(resource, observed);
|
|
948
|
+
}
|
|
949
|
+
return state;
|
|
758
950
|
}
|
|
759
951
|
//#endregion
|
|
760
|
-
//#region src/
|
|
952
|
+
//#region src/pipeline/resolve.ts
|
|
761
953
|
/**
|
|
762
|
-
*
|
|
763
|
-
*
|
|
954
|
+
* RESOLVE phase: walk dependency edges and replace Pending markers
|
|
955
|
+
* with concrete values from observed state where available.
|
|
764
956
|
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const
|
|
782
|
-
if (
|
|
783
|
-
|
|
784
|
-
|
|
957
|
+
* For each resource's desired document, recursively find Pending values.
|
|
958
|
+
* Look up the source resource's observed state at the source path.
|
|
959
|
+
* If concrete → replace. If not available → leave Pending in place.
|
|
960
|
+
*/
|
|
961
|
+
function resolve(state) {
|
|
962
|
+
const resourceById = new Map(state.resources.map((r) => [getResourceRef(r).id, r]));
|
|
963
|
+
for (const resource of state.resources) resolvePending(getDesiredDocument(resource), resourceById);
|
|
964
|
+
return state;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Recursively walk an object and resolve any Pending markers.
|
|
968
|
+
*/
|
|
969
|
+
function resolvePending(obj, resourceById) {
|
|
970
|
+
for (const [key, value] of Object.entries(obj)) if (Pending.is(value)) {
|
|
971
|
+
const sourceResource = resourceById.get(value.source.id);
|
|
972
|
+
if (sourceResource) {
|
|
973
|
+
const resolved = getNestedValue(getObservedDocument(sourceResource), value.path);
|
|
974
|
+
if (resolved !== void 0) obj[key] = resolved;
|
|
975
|
+
}
|
|
976
|
+
} else if (Array.isArray(value)) resolveArray(value, resourceById);
|
|
977
|
+
else if (value !== null && typeof value === "object") resolvePending(value, resourceById);
|
|
978
|
+
}
|
|
979
|
+
function resolveArray(arr, resourceById) {
|
|
980
|
+
for (let i = 0; i < arr.length; i++) {
|
|
981
|
+
const value = arr[i];
|
|
982
|
+
if (Pending.is(value)) {
|
|
983
|
+
const sourceResource = resourceById.get(value.source.id);
|
|
984
|
+
if (sourceResource) {
|
|
985
|
+
const resolved = getNestedValue(getObservedDocument(sourceResource), value.path);
|
|
986
|
+
if (resolved !== void 0) arr[i] = resolved;
|
|
785
987
|
}
|
|
786
|
-
|
|
988
|
+
} else if (Array.isArray(value)) resolveArray(value, resourceById);
|
|
989
|
+
else if (value !== null && typeof value === "object") resolvePending(value, resourceById);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Get a nested value from an object by dot-separated path.
|
|
994
|
+
*/
|
|
995
|
+
function getNestedValue(obj, path) {
|
|
996
|
+
const segments = path.split(".");
|
|
997
|
+
let current = obj;
|
|
998
|
+
for (const segment of segments) {
|
|
999
|
+
if (current === null || current === void 0) return void 0;
|
|
1000
|
+
if (typeof current !== "object") return void 0;
|
|
1001
|
+
current = current[segment];
|
|
1002
|
+
}
|
|
1003
|
+
return current;
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/pipeline/sequence.ts
|
|
1007
|
+
/**
|
|
1008
|
+
* SEQUENCE phase: topological sort and classification.
|
|
1009
|
+
*
|
|
1010
|
+
* - Runs topological sort on the dependency graph.
|
|
1011
|
+
* - Classifies each resource as 'emit', 'blocked', or 'external'.
|
|
1012
|
+
* - A resource is 'blocked' if it still has Pending markers in its desired document.
|
|
1013
|
+
* - External resources are classified as 'external' (never emitted).
|
|
1014
|
+
* - Circular dependencies are detected via the graph's topological sort.
|
|
1015
|
+
*/
|
|
1016
|
+
function sequence(state) {
|
|
1017
|
+
const classification = /* @__PURE__ */ new Map();
|
|
1018
|
+
const sortResult = state.graph.topologicalSort();
|
|
1019
|
+
for (const resource of state.resources) {
|
|
1020
|
+
const ref = getResourceRef(resource);
|
|
1021
|
+
if (isExternal(resource)) {
|
|
1022
|
+
classification.set(ref.id, "external");
|
|
1023
|
+
continue;
|
|
787
1024
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
1025
|
+
if (containsPending(getDesiredDocument(resource))) classification.set(ref.id, "blocked");
|
|
1026
|
+
else classification.set(ref.id, "emit");
|
|
1027
|
+
}
|
|
1028
|
+
if (sortResult.order === null && sortResult.cycle) {
|
|
1029
|
+
for (const id of sortResult.cycle) if (classification.get(id) !== "external") classification.set(id, "blocked");
|
|
791
1030
|
}
|
|
792
1031
|
return {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
order
|
|
1032
|
+
...state,
|
|
1033
|
+
classification
|
|
796
1034
|
};
|
|
797
1035
|
}
|
|
798
1036
|
/**
|
|
799
|
-
*
|
|
800
|
-
* Uses the raw spec/metadata before stripping, so UNRESOLVED symbols are visible.
|
|
1037
|
+
* Recursively check if an object contains any Pending markers.
|
|
801
1038
|
*/
|
|
802
|
-
function
|
|
803
|
-
return containsUnresolved(resource.spec) || containsUnresolved(resource.metadata);
|
|
804
|
-
}
|
|
805
|
-
/** Recursively check if an object contains UNRESOLVED sentinels. */
|
|
806
|
-
function containsUnresolved(obj) {
|
|
807
|
-
if (obj === UNRESOLVED) return true;
|
|
1039
|
+
function containsPending(obj) {
|
|
808
1040
|
if (obj === null || obj === void 0) return false;
|
|
809
|
-
if (
|
|
810
|
-
if (typeof obj
|
|
1041
|
+
if (Pending.is(obj)) return true;
|
|
1042
|
+
if (typeof obj !== "object") return false;
|
|
1043
|
+
if (Array.isArray(obj)) return obj.some(containsPending);
|
|
1044
|
+
for (const value of Object.values(obj)) if (containsPending(value)) return true;
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region src/pipeline/index.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* Run the full rendering pipeline.
|
|
1051
|
+
*
|
|
1052
|
+
* Phases: hydrate → resolve → sequence → diagnose → emit
|
|
1053
|
+
*/
|
|
1054
|
+
function runPipeline(input) {
|
|
1055
|
+
const resources = collectResources(input.composition);
|
|
1056
|
+
let state = {
|
|
1057
|
+
composition: input.composition,
|
|
1058
|
+
resources,
|
|
1059
|
+
graph: input.composition.graph,
|
|
1060
|
+
observedComposed: input.observedComposed,
|
|
1061
|
+
observedRequired: input.observedRequired,
|
|
1062
|
+
classification: /* @__PURE__ */ new Map(),
|
|
1063
|
+
diagnostics: [],
|
|
1064
|
+
emitted: [],
|
|
1065
|
+
xrStatusPatches: {}
|
|
1066
|
+
};
|
|
1067
|
+
state = hydrate(state);
|
|
1068
|
+
state = resolve(state);
|
|
1069
|
+
state = sequence(state);
|
|
1070
|
+
state = diagnose(state);
|
|
1071
|
+
state = emit(state);
|
|
1072
|
+
return state;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Collect all Resource instances from the composition's construct tree.
|
|
1076
|
+
*/
|
|
1077
|
+
function collectResources(composition) {
|
|
1078
|
+
const resources = [];
|
|
1079
|
+
collectFromNode(composition, resources);
|
|
1080
|
+
return resources;
|
|
1081
|
+
}
|
|
1082
|
+
function collectFromNode(construct, resources) {
|
|
1083
|
+
for (const child of construct.node.children) {
|
|
1084
|
+
if (isResourceInstance(child)) resources.push(child);
|
|
1085
|
+
collectFromNode(child, resources);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function isResourceInstance(obj) {
|
|
1089
|
+
if (obj === null || typeof obj !== "object") return false;
|
|
1090
|
+
const r = obj;
|
|
1091
|
+
if (!("resource" in r) || r.resource === null || typeof r.resource !== "object") return false;
|
|
1092
|
+
return "autoReady" in r.resource;
|
|
1093
|
+
}
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/readiness/defaults.ts
|
|
1096
|
+
/**
|
|
1097
|
+
* Checks if the resource has a `Ready` condition with status `True`.
|
|
1098
|
+
*/
|
|
1099
|
+
function conditionReady(observed) {
|
|
1100
|
+
const conditions = observed.status?.conditions;
|
|
1101
|
+
if (!conditions || !Array.isArray(conditions)) return void 0;
|
|
1102
|
+
const ready = conditions.find((c) => c.type === "Ready");
|
|
1103
|
+
if (!ready) return void 0;
|
|
1104
|
+
return ready.status === "True";
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Checks if the resource has `status.ready === true`.
|
|
1108
|
+
*/
|
|
1109
|
+
function statusReady(observed) {
|
|
1110
|
+
const status = observed.status;
|
|
1111
|
+
if (status === void 0) return void 0;
|
|
1112
|
+
if (!("ready" in status)) return void 0;
|
|
1113
|
+
return status.ready === true;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Fallback: resource exists in observed state → ready.
|
|
1117
|
+
* Always returns `true` (only called when observed is defined).
|
|
1118
|
+
*/
|
|
1119
|
+
function exists(_observed) {
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Built-in default readiness checks, appended at low priority.
|
|
1124
|
+
*/
|
|
1125
|
+
const DEFAULT_CHECKS = [
|
|
1126
|
+
{
|
|
1127
|
+
fn: conditionReady,
|
|
1128
|
+
priority: 100,
|
|
1129
|
+
name: "conditionReady"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
fn: statusReady,
|
|
1133
|
+
priority: 200,
|
|
1134
|
+
name: "statusReady"
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
fn: exists,
|
|
1138
|
+
priority: 1e3,
|
|
1139
|
+
name: "exists"
|
|
1140
|
+
}
|
|
1141
|
+
];
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/readiness/evaluate.ts
|
|
1144
|
+
/**
|
|
1145
|
+
* Evaluate readiness for a resource by running checks grouped by priority.
|
|
1146
|
+
*
|
|
1147
|
+
* - If `observed` is undefined, the resource doesn't exist yet → not ready.
|
|
1148
|
+
* - Checks are grouped by priority (ascending). Within a group, all checks
|
|
1149
|
+
* are AND-ed: if any returns `false`, the resource is not ready. If at least
|
|
1150
|
+
* one returns `true` and none return `false`, the resource is ready.
|
|
1151
|
+
* If all return `undefined`, cascade to the next priority group.
|
|
1152
|
+
* - Final fallback (no group had a definitive answer): not ready.
|
|
1153
|
+
*/
|
|
1154
|
+
function evaluateReadiness(checks, observed) {
|
|
1155
|
+
const log = getLogger();
|
|
1156
|
+
if (!observed) {
|
|
1157
|
+
log.debug("readiness: resource not observed, not ready");
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1161
|
+
for (const check of checks) {
|
|
1162
|
+
const group = groups.get(check.priority);
|
|
1163
|
+
if (group) group.push(check);
|
|
1164
|
+
else groups.set(check.priority, [check]);
|
|
1165
|
+
}
|
|
1166
|
+
const priorities = [...groups.keys()].sort((a, b) => a - b);
|
|
1167
|
+
for (const priority of priorities) {
|
|
1168
|
+
const group = groups.get(priority);
|
|
1169
|
+
let hasTrue = false;
|
|
1170
|
+
let hasFalse = false;
|
|
1171
|
+
const results = [];
|
|
1172
|
+
for (const check of group) {
|
|
1173
|
+
const result = check.fn(observed);
|
|
1174
|
+
results.push({
|
|
1175
|
+
name: check.name ?? "anonymous",
|
|
1176
|
+
result
|
|
1177
|
+
});
|
|
1178
|
+
if (result === false) {
|
|
1179
|
+
hasFalse = true;
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
if (result === true) hasTrue = true;
|
|
1183
|
+
}
|
|
1184
|
+
log.debug("readiness: evaluated group", {
|
|
1185
|
+
priority,
|
|
1186
|
+
results
|
|
1187
|
+
});
|
|
1188
|
+
if (hasFalse) {
|
|
1189
|
+
log.debug("readiness: group returned false, not ready", { priority });
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
if (hasTrue) {
|
|
1193
|
+
log.debug("readiness: group returned true, ready", { priority });
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
log.debug("readiness: no group had definitive answer, not ready");
|
|
811
1198
|
return false;
|
|
812
1199
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region src/run.ts
|
|
1202
|
+
/**
|
|
1203
|
+
* Run a composition class with the given input and return a plain-data result.
|
|
1204
|
+
*
|
|
1205
|
+
* This is the single entry point that bridges the composition author's class
|
|
1206
|
+
* with the runtime. It handles:
|
|
1207
|
+
* 1. Setting up internal context (DependencyGraph, EdgeCollector, ALS)
|
|
1208
|
+
* 2. Instantiating the Composition class
|
|
1209
|
+
* 3. Running the full pipeline (hydrate → resolve → sequence → diagnose → emit)
|
|
1210
|
+
* 4. Evaluating readiness per resource
|
|
1211
|
+
* 5. Returning a fully serializable CompositionResult
|
|
1212
|
+
*
|
|
1213
|
+
* The runtime never needs to access framework internals — everything it needs
|
|
1214
|
+
* is in the returned plain-data structure.
|
|
1215
|
+
*/
|
|
1216
|
+
function runComposition(CompositionClass, input) {
|
|
1217
|
+
const observedComposed = new Map(Object.entries(input.observedComposed));
|
|
1218
|
+
const observedRequired = new Map(Object.entries(input.observedRequired));
|
|
1219
|
+
const graph = new DependencyGraph();
|
|
1220
|
+
const collector = new EdgeCollector();
|
|
1221
|
+
const pipelineContext = new Map(Object.entries(input.pipelineContext));
|
|
1222
|
+
const ctx = {
|
|
1223
|
+
xr: input.xr,
|
|
1224
|
+
pipelineContext,
|
|
1225
|
+
requiredResources: observedRequired,
|
|
1226
|
+
graph,
|
|
1227
|
+
collector
|
|
1228
|
+
};
|
|
1229
|
+
const state = runPipeline({
|
|
1230
|
+
composition: compositionStorage.run(ctx, () => new CompositionClass()),
|
|
1231
|
+
observedComposed,
|
|
1232
|
+
observedRequired
|
|
1233
|
+
});
|
|
1234
|
+
const resources = state.emitted.map((emitted) => {
|
|
1235
|
+
const allChecks = [...emitted.readyChecks, ...DEFAULT_CHECKS];
|
|
1236
|
+
const observed = observedComposed.get(`Composition/${emitted.name}`);
|
|
1237
|
+
const ready = emitted.autoReady ? evaluateReadiness(allChecks, observed) : true;
|
|
1238
|
+
return {
|
|
1239
|
+
name: emitted.name,
|
|
1240
|
+
document: emitted.document,
|
|
1241
|
+
ready
|
|
1242
|
+
};
|
|
1243
|
+
});
|
|
1244
|
+
const externalResources = [];
|
|
1245
|
+
for (const resource of state.resources) {
|
|
1246
|
+
if (!isExternal(resource)) continue;
|
|
1247
|
+
const ref = getExternalRef(resource);
|
|
1248
|
+
if (!ref || typeof ref.name !== "string") continue;
|
|
1249
|
+
if (ref.name.startsWith("__pending__")) continue;
|
|
1250
|
+
externalResources.push({
|
|
1251
|
+
refKey: ref.refKey,
|
|
1252
|
+
apiVersion: ref.apiVersion,
|
|
1253
|
+
kind: ref.kind,
|
|
1254
|
+
name: ref.name,
|
|
1255
|
+
...ref.namespace ? { namespace: ref.namespace } : {}
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
resources,
|
|
1260
|
+
externalResources,
|
|
1261
|
+
xrStatus: state.xrStatusPatches,
|
|
1262
|
+
diagnostics: state.diagnostics
|
|
1263
|
+
};
|
|
816
1264
|
}
|
|
817
1265
|
//#endregion
|
|
818
|
-
export { Composition, Construct,
|
|
1266
|
+
export { Composition, Construct, DEFAULT_CHECKS, DependencyGraph, EdgeCollector, Pending, Resource, compositionStorage, createPrimitiveReadProxy, createReadProxy, createWriteProxy, diagnose, emit, evaluateReadiness, getCompositionContext, getDesiredDocument, getExternalRef, getLogger, getObservedDocument, getReadProxyMeta, getReadyChecks, getResourceInternals, getResourceRef, getXrDesiredStatus, hydrate, hydrateObserved, isExternal, isReadProxy, resolve, runComposition, runPipeline, sequence, withLogger };
|
|
819
1267
|
|
|
820
1268
|
//# sourceMappingURL=index.mjs.map
|