@intentius/chant-lexicon-gcp 0.124.37 → 0.124.38
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/read-client.d.ts +7 -1
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/api/read-client.js +1 -1
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +3 -1
- package/dist/describe-resources.d.ts +13 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +19 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/api/read-client.ts +7 -1
- package/src/deep-observe.ts +3 -1
- package/src/describe-resources.ts +23 -1
- package/src/project-config.test.ts +50 -3
- package/src/read-refs.test.ts +60 -0
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* quietly implying it is not there. Widening the table is #1209's stated
|
|
38
38
|
* non-goal and belongs with the applier.
|
|
39
39
|
*/
|
|
40
|
-
import { type ResourceMapper } from "../op/activities/gcp-apply.js";
|
|
40
|
+
import { type GcpResource, type ResourceMapper } from "../op/activities/gcp-apply.js";
|
|
41
41
|
/** Where to read, and as whom. */
|
|
42
42
|
export interface GcpReadClientOptions {
|
|
43
43
|
/** Endpoint override for every kind (floci-gcp `http://localhost:4588`). Omit for real GCP. */
|
|
@@ -46,6 +46,12 @@ export interface GcpReadClientOptions {
|
|
|
46
46
|
project: string;
|
|
47
47
|
/** Injectable HTTP, so tests never touch the network. */
|
|
48
48
|
http?: GcpReadHttp;
|
|
49
|
+
/**
|
|
50
|
+
* The declared resources, refs resolved, as apply passes them (#4251): an
|
|
51
|
+
* IAMPolicyMember on a RunService named by `name` takes the service's region
|
|
52
|
+
* from its declaration, which a read otherwise can't know.
|
|
53
|
+
*/
|
|
54
|
+
manifest?: readonly GcpResource[];
|
|
49
55
|
}
|
|
50
56
|
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. A body is sent only for a POST read (`:getIamPolicy`). */
|
|
51
57
|
export type GcpReadHttp = (method: string, url: string, body?: unknown) => Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"read-client.d.ts","sourceRoot":"","sources":["../../src/api/read-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAW,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5F,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,+FAA+F;IAC/F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACnC;AAED,uHAAuH;AACvH,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAUrH;;;;GAIG;AACH,qBAAa,YAAa,SAAQ,KAAK;IAGnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAF1B,YACE,OAAO,EAAE,MAAM,EACN,MAAM,CAAC,EAAE,MAAM,YAAA,EAIzB;CACF;AAED,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEhD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEtE;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAiDlC"}
|
package/dist/api/read-client.js
CHANGED
|
@@ -82,7 +82,7 @@ export async function getResource(client, kind, name, declared) {
|
|
|
82
82
|
metadata: { name },
|
|
83
83
|
spec: declared?.spec ?? declared ?? {},
|
|
84
84
|
};
|
|
85
|
-
const ctx = { base, project: client.project };
|
|
85
|
+
const ctx = { base, project: client.project, ...(client.manifest ? { manifest: client.manifest } : {}) };
|
|
86
86
|
if (mapper.accepts && !mapper.accepts(resource, ctx)) {
|
|
87
87
|
throw new GcpReadError(`no REST read for ${kind}/${name}: its target has no endpoint chant can read`);
|
|
88
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAajE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWtF;AAUD;6EAC6E;AAC7E,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA0OD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASnH;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,
|
|
1
|
+
{"version":3,"file":"deep-observe.d.ts","sourceRoot":"","sources":["../src/deep-observe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAGtB,MAAM,0BAA0B,CAAC;AAKlC,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAajE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWtF;AAUD;6EAC6E;AAC7E,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA0OD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASnH;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyF5G"}
|
package/dist/deep-observe.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
*/
|
|
56
56
|
import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
57
57
|
import { hasOwnershipMarker } from "@intentius/chant/ownership";
|
|
58
|
-
import { deriveGVK, manifestProjectAnnotations, readTargets, resolveReadProject } from "./describe-resources.js";
|
|
58
|
+
import { deriveGVK, manifestProjectAnnotations, readManifest, readTargets, resolveReadProject } from "./describe-resources.js";
|
|
59
59
|
import { getResource, mapperForKind, GcpReadError, isNotFound } from "./api/read-client.js";
|
|
60
60
|
import { gcpDeepNormalizationHooks } from "./deep-observe-hooks.js";
|
|
61
61
|
/** The labels the applier stamps — see describe-resources.ts. */
|
|
@@ -365,6 +365,7 @@ export async function observeResourcesDeepGcp(options) {
|
|
|
365
365
|
const endpoint = process.env.GCP_ENDPOINT_URL;
|
|
366
366
|
const manifestProjects = manifestProjectAnnotations(options.buildOutput);
|
|
367
367
|
const targets = readTargets(options.entities);
|
|
368
|
+
const manifest = readManifest(options.entities, targets);
|
|
368
369
|
const reads = [...options.entities].map(async ([entityName, { entityType }]) => {
|
|
369
370
|
const gvk = deriveGVK(entityType);
|
|
370
371
|
if (!gvk || !mapperForKind(gvk.kind)) {
|
|
@@ -384,6 +385,7 @@ export async function observeResourcesDeepGcp(options) {
|
|
|
384
385
|
client = {
|
|
385
386
|
project: resolveReadProject(gvk.kind, name, metadata, manifestProjects, props),
|
|
386
387
|
...(endpoint ? { endpoint } : {}),
|
|
388
|
+
manifest,
|
|
387
389
|
};
|
|
388
390
|
}
|
|
389
391
|
catch (err) {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* dropped.
|
|
37
37
|
*/
|
|
38
38
|
import type { ObservationResult } from "@intentius/chant/lexicon";
|
|
39
|
+
import { type GcpResource } from "./op/activities/gcp-apply.js";
|
|
39
40
|
/** The parts of a GCP REST payload this reader looks at. Every field is
|
|
40
41
|
* optional because they vary by kind — a bucket has `id` and `labels`, a
|
|
41
42
|
* Pub/Sub topic has neither. */
|
|
@@ -105,6 +106,18 @@ export declare function readTargets(entities: Map<string, {
|
|
|
105
106
|
name: string;
|
|
106
107
|
props: Record<string, unknown>;
|
|
107
108
|
}>;
|
|
109
|
+
/**
|
|
110
|
+
* The declared resources as the applier's manifest holds them (kind, CNRM
|
|
111
|
+
* name, spec), for a read that needs another declared resource: an
|
|
112
|
+
* IAMPolicyMember on a RunService takes the service's region from it (#4251).
|
|
113
|
+
*/
|
|
114
|
+
export declare function readManifest(entities: Map<string, {
|
|
115
|
+
entityType: string;
|
|
116
|
+
props: Record<string, unknown>;
|
|
117
|
+
}>, targets: Map<string, {
|
|
118
|
+
name: string;
|
|
119
|
+
props: Record<string, unknown>;
|
|
120
|
+
}>): GcpResource[];
|
|
108
121
|
/**
|
|
109
122
|
* Status from a REST payload.
|
|
110
123
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAsC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"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"}
|
|
@@ -137,6 +137,23 @@ export function readTargets(entities) {
|
|
|
137
137
|
}
|
|
138
138
|
return out;
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The declared resources as the applier's manifest holds them (kind, CNRM
|
|
142
|
+
* name, spec), for a read that needs another declared resource: an
|
|
143
|
+
* IAMPolicyMember on a RunService takes the service's region from it (#4251).
|
|
144
|
+
*/
|
|
145
|
+
export function readManifest(entities, targets) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const [logical, { entityType }] of entities) {
|
|
148
|
+
const gvk = deriveGVK(entityType);
|
|
149
|
+
const target = targets.get(logical);
|
|
150
|
+
if (!gvk || !target)
|
|
151
|
+
continue;
|
|
152
|
+
const spec = target.props.spec ?? target.props;
|
|
153
|
+
out.push({ kind: gvk.kind, metadata: { name: target.name }, spec });
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
140
157
|
function pruneUndefined(obj) {
|
|
141
158
|
const out = {};
|
|
142
159
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -185,6 +202,7 @@ export async function describeResources(options) {
|
|
|
185
202
|
let warnedOwnership = false;
|
|
186
203
|
const manifestProjects = manifestProjectAnnotations(options.buildOutput);
|
|
187
204
|
const targets = readTargets(options.entities);
|
|
205
|
+
const manifest = readManifest(options.entities, targets);
|
|
188
206
|
const reads = [...options.entities].map(async ([entityName, { entityType }]) => {
|
|
189
207
|
const gvk = deriveGVK(entityType);
|
|
190
208
|
if (!gvk) {
|
|
@@ -212,6 +230,7 @@ export async function describeResources(options) {
|
|
|
212
230
|
client = {
|
|
213
231
|
project: resolveReadProject(gvk.kind, name, metadata, manifestProjects, props),
|
|
214
232
|
...(endpoint ? { endpoint } : {}),
|
|
233
|
+
manifest,
|
|
215
234
|
};
|
|
216
235
|
}
|
|
217
236
|
catch (err) {
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "abad2dc852fe087b6167993403f8fe827512b85c33a02d66b35dab64b4b0b5f3",
|
|
5
5
|
"meta.json": "75643660010c044c6f465bef2688a864ab30cc96fd9b68111969749748ac64ca",
|
|
6
6
|
"types/index.d.ts": "da1ebc6bcf83e1a018a773b59714194aeeb74ae1d5c1252b737b7e730ca98bd5",
|
|
7
7
|
"rules/hardcoded-project.ts": "6d5ae9efa74d73800eebe1a0a0d43c619ee0fdebe3347195df93fcbe63b34f0d",
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"skills/chant-gcp-patterns.md": "4c6809fc7c37fa1879379ad4c0e2d114dec9497bc6eb9d1bca4e79fddea4f509",
|
|
41
41
|
"skills/chant-gcp-gke.md": "be277019da9a722c851e47cd2dfb9c9536668948c3535fb20db7697e934c4e2b"
|
|
42
42
|
},
|
|
43
|
-
"composite": "
|
|
43
|
+
"composite": "2417c2778b2c78b3a629c866e8b25766423b79094722bdec3ef566b47a151c31"
|
|
44
44
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-gcp",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.38",
|
|
4
4
|
"description": "Google Cloud lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -76,6 +76,6 @@
|
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
},
|
|
78
78
|
"peerDependencies": {
|
|
79
|
-
"@intentius/chant": "^0.124.
|
|
79
|
+
"@intentius/chant": "^0.124.38"
|
|
80
80
|
}
|
|
81
81
|
}
|
package/src/api/read-client.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface GcpReadClientOptions {
|
|
|
47
47
|
project: string;
|
|
48
48
|
/** Injectable HTTP, so tests never touch the network. */
|
|
49
49
|
http?: GcpReadHttp;
|
|
50
|
+
/**
|
|
51
|
+
* The declared resources, refs resolved, as apply passes them (#4251): an
|
|
52
|
+
* IAMPolicyMember on a RunService named by `name` takes the service's region
|
|
53
|
+
* from its declaration, which a read otherwise can't know.
|
|
54
|
+
*/
|
|
55
|
+
manifest?: readonly GcpResource[];
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. A body is sent only for a POST read (`:getIamPolicy`). */
|
|
@@ -108,7 +114,7 @@ export async function getResource(
|
|
|
108
114
|
spec: (declared?.spec as Record<string, unknown> | undefined) ?? declared ?? {},
|
|
109
115
|
};
|
|
110
116
|
|
|
111
|
-
const ctx = { base, project: client.project };
|
|
117
|
+
const ctx = { base, project: client.project, ...(client.manifest ? { manifest: client.manifest } : {}) };
|
|
112
118
|
if (mapper.accepts && !mapper.accepts(resource, ctx)) {
|
|
113
119
|
throw new GcpReadError(`no REST read for ${kind}/${name}: its target has no endpoint chant can read`);
|
|
114
120
|
}
|
package/src/deep-observe.ts
CHANGED
|
@@ -61,7 +61,7 @@ import type {
|
|
|
61
61
|
} from "@intentius/chant/lexicon";
|
|
62
62
|
import { deepObservation, normalizeDeepProperties } from "@intentius/chant/deep-observation";
|
|
63
63
|
import { hasOwnershipMarker, type ChannelKeys } from "@intentius/chant/ownership";
|
|
64
|
-
import { deriveGVK, manifestProjectAnnotations, readTargets, resolveReadProject } from "./describe-resources";
|
|
64
|
+
import { deriveGVK, manifestProjectAnnotations, readManifest, readTargets, resolveReadProject } from "./describe-resources";
|
|
65
65
|
import { getResource, mapperForKind, GcpReadError, isNotFound, type GcpReadClientOptions } from "./api/read-client";
|
|
66
66
|
import { gcpDeepNormalizationHooks } from "./deep-observe-hooks";
|
|
67
67
|
|
|
@@ -388,6 +388,7 @@ export async function observeResourcesDeepGcp(options: GcpDeepObserveOptions): P
|
|
|
388
388
|
const endpoint = process.env.GCP_ENDPOINT_URL;
|
|
389
389
|
const manifestProjects = manifestProjectAnnotations(options.buildOutput);
|
|
390
390
|
const targets = readTargets(options.entities);
|
|
391
|
+
const manifest = readManifest(options.entities, targets);
|
|
391
392
|
|
|
392
393
|
const reads = [...options.entities].map(async ([entityName, { entityType }]) => {
|
|
393
394
|
const gvk = deriveGVK(entityType);
|
|
@@ -410,6 +411,7 @@ export async function observeResourcesDeepGcp(options: GcpDeepObserveOptions): P
|
|
|
410
411
|
client = {
|
|
411
412
|
project: resolveReadProject(gvk.kind, name, metadata, manifestProjects, props),
|
|
412
413
|
...(endpoint ? { endpoint } : {}),
|
|
414
|
+
manifest,
|
|
413
415
|
};
|
|
414
416
|
} catch (err) {
|
|
415
417
|
unobserved[entityName] = {
|
|
@@ -40,7 +40,7 @@ import type { ObservationResult, ResourceMetadata, UnobservedEntity } from "@int
|
|
|
40
40
|
import { observation } from "@intentius/chant/observation";
|
|
41
41
|
import { hasOwnershipMarker, classifyOwnership, readOwnership, type ChannelKeys } from "@intentius/chant/ownership";
|
|
42
42
|
import { getResource, mapperForKind, GcpReadError, isNotFound, type GcpReadClientOptions } from "./api/read-client";
|
|
43
|
-
import { NO_PROJECT, parseManifest, projectRefProject } from "./op/activities/gcp-apply";
|
|
43
|
+
import { NO_PROJECT, parseManifest, projectRefProject, type GcpResource } from "./op/activities/gcp-apply";
|
|
44
44
|
import { cnrmNameOf, resolveCnrmRefs, resolveGVK } from "./serializer";
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -167,6 +167,26 @@ export function readTargets(
|
|
|
167
167
|
return out;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* The declared resources as the applier's manifest holds them (kind, CNRM
|
|
172
|
+
* name, spec), for a read that needs another declared resource: an
|
|
173
|
+
* IAMPolicyMember on a RunService takes the service's region from it (#4251).
|
|
174
|
+
*/
|
|
175
|
+
export function readManifest(
|
|
176
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>,
|
|
177
|
+
targets: Map<string, { name: string; props: Record<string, unknown> }>,
|
|
178
|
+
): GcpResource[] {
|
|
179
|
+
const out: GcpResource[] = [];
|
|
180
|
+
for (const [logical, { entityType }] of entities) {
|
|
181
|
+
const gvk = deriveGVK(entityType);
|
|
182
|
+
const target = targets.get(logical);
|
|
183
|
+
if (!gvk || !target) continue;
|
|
184
|
+
const spec = (target.props.spec as Record<string, unknown> | undefined) ?? target.props;
|
|
185
|
+
out.push({ kind: gvk.kind, metadata: { name: target.name }, spec });
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
170
190
|
function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
|
|
171
191
|
const out: Record<string, unknown> = {};
|
|
172
192
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -223,6 +243,7 @@ export async function describeResources(options: {
|
|
|
223
243
|
|
|
224
244
|
const manifestProjects = manifestProjectAnnotations(options.buildOutput);
|
|
225
245
|
const targets = readTargets(options.entities);
|
|
246
|
+
const manifest = readManifest(options.entities, targets);
|
|
226
247
|
|
|
227
248
|
const reads = [...options.entities].map(async ([entityName, { entityType }]) => {
|
|
228
249
|
const gvk = deriveGVK(entityType);
|
|
@@ -253,6 +274,7 @@ export async function describeResources(options: {
|
|
|
253
274
|
client = {
|
|
254
275
|
project: resolveReadProject(gvk.kind, name, metadata, manifestProjects, props),
|
|
255
276
|
...(endpoint ? { endpoint } : {}),
|
|
277
|
+
manifest,
|
|
256
278
|
};
|
|
257
279
|
} catch (err) {
|
|
258
280
|
// No project resolvable — nothing was queried, so nothing is known.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { mkdir, writeFile, rm, realpath } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -6,9 +6,9 @@ import type { Declarable } from "@intentius/chant/declarable";
|
|
|
6
6
|
import { loadChantConfig } from "@intentius/chant/config";
|
|
7
7
|
import { armSandboxConfigEvaluation, resetSandboxConfigEvaluationForTests } from "@intentius/chant/config-sandbox";
|
|
8
8
|
import { gcpSerializer } from "./serializer";
|
|
9
|
-
import { manifestProjectAnnotations, resolveReadProject } from "./describe-resources";
|
|
9
|
+
import { describeResources, manifestProjectAnnotations, resolveReadProject } from "./describe-resources";
|
|
10
10
|
import { parseManifest, resolveGcpProject } from "./op/activities/gcp-apply";
|
|
11
|
-
import { CloudRunService, StorageBucket } from "./generated";
|
|
11
|
+
import { CloudRunService, PubSubTopic, StorageBucket } from "./generated";
|
|
12
12
|
import { defaultAnnotations } from "./default-labels";
|
|
13
13
|
import { GCP } from "./pseudo";
|
|
14
14
|
|
|
@@ -117,3 +117,50 @@ describe("gcp.project under --sandbox (#4252)", () => {
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
describe("the live read takes gcp.project with no project env var (#4252)", () => {
|
|
122
|
+
let saved: NodeJS.ProcessEnv;
|
|
123
|
+
const fetchMock = vi.fn();
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
saved = { ...process.env };
|
|
126
|
+
delete process.env.GCP_PROJECT_ID;
|
|
127
|
+
delete process.env.GOOGLE_CLOUD_PROJECT;
|
|
128
|
+
delete process.env.GCP_ENDPOINT_URL;
|
|
129
|
+
fetchMock.mockReset();
|
|
130
|
+
fetchMock.mockImplementation(async (url: string) => new Response(JSON.stringify({ name: "projects/p/topics/assets" }), { status: 200 }));
|
|
131
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
132
|
+
});
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
process.env = saved;
|
|
135
|
+
vi.unstubAllGlobals();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
async function read(config: Record<string, unknown> | undefined, props: Record<string, unknown>) {
|
|
139
|
+
const yaml = build({ assets: new PubSubTopic(props as never) }, config);
|
|
140
|
+
return describeResources({
|
|
141
|
+
environment: "local",
|
|
142
|
+
buildOutput: yaml,
|
|
143
|
+
entityNames: ["assets"],
|
|
144
|
+
entities: new Map([["assets", { entityType: "GCP::Pubsub::Topic", props }]]),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
test("only gcp.project in config: the read targets that project", async () => {
|
|
149
|
+
const result = await read({ gcp: { project: "three-cloud" } }, { metadata: { name: "assets" } });
|
|
150
|
+
expect(result.unobserved?.assets).toBeUndefined();
|
|
151
|
+
expect(result.resources.assets).toBeDefined();
|
|
152
|
+
expect(String(fetchMock.mock.calls[0]?.[0])).toContain("three-cloud");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("only a project-id annotation on the resource: the read targets that project", async () => {
|
|
156
|
+
const result = await read(undefined, { metadata: { name: "assets", annotations: { [ANNOTATION]: "annotated" } } });
|
|
157
|
+
expect(result.unobserved?.assets).toBeUndefined();
|
|
158
|
+
expect(String(fetchMock.mock.calls[0]?.[0])).toContain("annotated");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("neither: no-binding, naming every place a project can come from", async () => {
|
|
162
|
+
const result = await read(undefined, { metadata: { name: "assets" } });
|
|
163
|
+
expect(result.unobserved?.assets?.reason).toBe("no-binding");
|
|
164
|
+
expect(result.unobserved?.assets?.detail).toMatch(/gcp\.project in chant\.config/);
|
|
165
|
+
});
|
|
166
|
+
});
|
package/src/read-refs.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { describeResources } from "./describe-resources";
|
|
3
|
+
import { CloudRunServiceComposite } from "./composites/cloud-run-service";
|
|
3
4
|
import {
|
|
4
5
|
BackendService,
|
|
5
6
|
CloudRunService,
|
|
@@ -133,3 +134,62 @@ describe("gcp live read resolves .name references as the build does (#4251)", ()
|
|
|
133
134
|
expect(out.unobserved?.publicIam).toBeUndefined();
|
|
134
135
|
});
|
|
135
136
|
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* #4251, the IAM binding behold saw as pending: the public IAMPolicyMember of a
|
|
140
|
+
* CloudRunServiceComposite whose service runs outside us-central1. The binding
|
|
141
|
+
* names its service by `name`, so the read has to take the service's region from
|
|
142
|
+
* the declared service, as apply does, or it asks a region where the service
|
|
143
|
+
* isn't, gets a 404 and reports the binding absent.
|
|
144
|
+
*/
|
|
145
|
+
describe("the public binding of a service outside us-central1 reads as observed (#4251)", () => {
|
|
146
|
+
const policyUrl = "/v2/projects/three-cloud/locations/asia-northeast1/services/three-cloud-site:getIamPolicy";
|
|
147
|
+
|
|
148
|
+
function composite() {
|
|
149
|
+
const { service, publicIam } = CloudRunServiceComposite({
|
|
150
|
+
name: "three-cloud-site",
|
|
151
|
+
image: "gcr.io/p/site:1",
|
|
152
|
+
projectId: "three-cloud",
|
|
153
|
+
location: "asia-northeast1",
|
|
154
|
+
publicAccess: true,
|
|
155
|
+
}) as unknown as Record<string, { entityType: string; props: Record<string, unknown> }>;
|
|
156
|
+
return [
|
|
157
|
+
["webService", { entityType: service.entityType, props: service.props }],
|
|
158
|
+
["webPublicIam", { entityType: publicIam.entityType, props: publicIam.props }],
|
|
159
|
+
] as Array<[string, { entityType: string; props: Record<string, unknown> }]>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
fetchMock.mockReset();
|
|
164
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
165
|
+
process.env.GCP_ENDPOINT_URL = "http://localhost:4588";
|
|
166
|
+
process.env.GCP_PROJECT_ID = "three-cloud";
|
|
167
|
+
delete process.env.GCP_REGION;
|
|
168
|
+
fetchMock.mockImplementation(async (url: string) => {
|
|
169
|
+
if (url.includes(":getIamPolicy")) {
|
|
170
|
+
return url.includes(policyUrl)
|
|
171
|
+
? reply(200, { bindings: [{ role: "roles/run.invoker", members: ["allUsers"] }] })
|
|
172
|
+
: reply(404, { error: { code: 404 } });
|
|
173
|
+
}
|
|
174
|
+
return reply(200, { name: url.split("?")[0].split("/").slice(-6).join("/") });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
vi.unstubAllGlobals();
|
|
180
|
+
delete process.env.GCP_ENDPOINT_URL;
|
|
181
|
+
delete process.env.GCP_PROJECT_ID;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
for (const order of ["service first", "binding first"]) {
|
|
185
|
+
test(`webPublicIam is observed with a physical id, ${order}`, async () => {
|
|
186
|
+
const pairs = composite();
|
|
187
|
+
const entities = new Map(order === "service first" ? pairs : [...pairs].reverse());
|
|
188
|
+
const out = await describeResources({ environment: "local", buildOutput: "", entityNames: [...entities.keys()], entities });
|
|
189
|
+
expect(out.unobserved?.webPublicIam).toBeUndefined();
|
|
190
|
+
expect(out.resources.webPublicIam?.physicalId).toBe(
|
|
191
|
+
"projects/three-cloud/locations/asia-northeast1/services/three-cloud-site#roles/run.invoker#allUsers",
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|