@intentius/chant-lexicon-gcp 0.124.33 → 0.124.35
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 -3
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/api/read-client.js +36 -15
- package/dist/composites/cloud-run-service.d.ts +2 -0
- package/dist/composites/cloud-run-service.d.ts.map +1 -1
- package/dist/composites/cloud-run-service.js +4 -1
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-observe.js +125 -1
- package/dist/describe-resources.d.ts +4 -3
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +8 -7
- package/dist/health.js +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/gcp-apply.d.ts +62 -18
- package/dist/op/activities/gcp-apply.d.ts.map +1 -1
- package/dist/op/activities/gcp-apply.js +118 -18
- package/dist/op/activities/gcp-compute.d.ts +56 -0
- package/dist/op/activities/gcp-compute.d.ts.map +1 -0
- package/dist/op/activities/gcp-compute.js +313 -0
- package/dist/op/activities/gcp-iam.d.ts +82 -0
- package/dist/op/activities/gcp-iam.d.ts.map +1 -0
- package/dist/op/activities/gcp-iam.js +215 -0
- package/dist/op/activities/index.d.ts +1 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/index.js +1 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +36 -1
- package/package.json +2 -2
- package/src/api/read-client.ts +35 -16
- package/src/composites/cloud-run-service.ts +6 -0
- package/src/composites/composites.test.ts +7 -0
- package/src/deep-observe.test.ts +16 -1
- package/src/deep-observe.ts +125 -1
- package/src/describe-resources.test.ts +1 -1
- package/src/describe-resources.ts +8 -6
- package/src/health.ts +1 -1
- package/src/op/activities/gcp-apply.test.ts +53 -0
- package/src/op/activities/gcp-apply.ts +157 -24
- package/src/op/activities/gcp-compute.test.ts +212 -0
- package/src/op/activities/gcp-compute.ts +319 -0
- package/src/op/activities/gcp-iam.test.ts +152 -0
- package/src/op/activities/gcp-iam.ts +284 -0
- package/src/op/activities/index.ts +1 -0
- package/src/project-resolution.test.ts +41 -0
- package/src/serializer-pseudo.test.ts +38 -0
- package/src/serializer.ts +43 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IAMPolicyMember (#4233): one member in one role on one resource's IAM policy.
|
|
3
|
+
*
|
|
4
|
+
* GCP has no REST resource for a single binding member. The member lives inside
|
|
5
|
+
* the target's policy, read with `getIamPolicy` and written whole with
|
|
6
|
+
* `setIamPolicy`. So this mapper does not fit the GET-then-create plan the other
|
|
7
|
+
* kinds use: apply reads the policy, adds the member to the role's binding when
|
|
8
|
+
* it is missing and writes the policy back with its `etag`; delete removes it
|
|
9
|
+
* the same way. Other members and bindings are kept as read (a merge, never an
|
|
10
|
+
* overwrite), and a write that loses an `etag` race is retried.
|
|
11
|
+
*
|
|
12
|
+
* Targets are the kinds `resourceRef` can name that have an IAM policy chant can
|
|
13
|
+
* reach: Project, RunService, IAMServiceAccount, StorageBucket, PubSubTopic,
|
|
14
|
+
* PubSubSubscription and SecretManagerSecret. Any other target is reported as
|
|
15
|
+
* not attempted, the way a kind with no mapper is.
|
|
16
|
+
*/
|
|
17
|
+
import type { GcpResource, ResourceMapper } from "./gcp-apply.js";
|
|
18
|
+
/** The CNRM IAMPolicyMember spec chant reads. */
|
|
19
|
+
export interface IamPolicyMemberSpec {
|
|
20
|
+
member?: string;
|
|
21
|
+
role?: string;
|
|
22
|
+
resourceRef?: {
|
|
23
|
+
kind?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
external?: string;
|
|
26
|
+
apiVersion?: string;
|
|
27
|
+
};
|
|
28
|
+
condition?: {
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
expression?: string;
|
|
32
|
+
};
|
|
33
|
+
location?: string;
|
|
34
|
+
}
|
|
35
|
+
interface Binding {
|
|
36
|
+
role: string;
|
|
37
|
+
members?: string[];
|
|
38
|
+
condition?: {
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
expression?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
interface Policy {
|
|
45
|
+
version?: number;
|
|
46
|
+
etag?: string;
|
|
47
|
+
bindings?: Binding[];
|
|
48
|
+
}
|
|
49
|
+
/** Where a target's policy is read and written. */
|
|
50
|
+
export interface IamTarget {
|
|
51
|
+
/** Real-GCP host for the target's service. */
|
|
52
|
+
host: string;
|
|
53
|
+
/** The target's resource path, used in messages and in the read body. */
|
|
54
|
+
path: string;
|
|
55
|
+
get: (base: string) => {
|
|
56
|
+
method: "GET" | "POST";
|
|
57
|
+
url: string;
|
|
58
|
+
body?: unknown;
|
|
59
|
+
};
|
|
60
|
+
set: (base: string, policy: Policy) => {
|
|
61
|
+
method: "POST" | "PUT";
|
|
62
|
+
url: string;
|
|
63
|
+
body: unknown;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** The default host this mapper reports; a target's own host replaces it when there is no endpoint override. */
|
|
67
|
+
export declare const IAM_DEFAULT_HOST = "https://iam.googleapis.com";
|
|
68
|
+
/**
|
|
69
|
+
* Resolve `resourceRef` to the target's policy endpoints, or undefined for a
|
|
70
|
+
* target chant cannot reach. `manifest` is the rest of the applied manifest, so
|
|
71
|
+
* a RunService named by `name` takes its region from its own spec. Pure.
|
|
72
|
+
*/
|
|
73
|
+
export declare function iamTarget(spec: IamPolicyMemberSpec, project: string, manifest?: readonly GcpResource[], env?: NodeJS.ProcessEnv): IamTarget | undefined;
|
|
74
|
+
/** True when the policy grants the spec's member in its role (and condition). Pure. */
|
|
75
|
+
export declare function policyHasMember(policy: Policy, spec: IamPolicyMemberSpec): boolean;
|
|
76
|
+
/** The policy with the spec's member added to its binding, every other binding kept. Pure. */
|
|
77
|
+
export declare function withMember(policy: Policy, spec: IamPolicyMemberSpec): Policy;
|
|
78
|
+
/** The policy with the spec's member removed; a binding left with no members is dropped. Pure. */
|
|
79
|
+
export declare function withoutMember(policy: Policy, spec: IamPolicyMemberSpec): Policy;
|
|
80
|
+
export declare const iamPolicyMemberMapper: ResourceMapper;
|
|
81
|
+
export {};
|
|
82
|
+
//# sourceMappingURL=gcp-iam.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gcp-iam.d.ts","sourceRoot":"","sources":["../../../src/op/activities/gcp-iam.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAW,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE,iDAAiD;AACjD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvF,SAAS,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,OAAO;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3E;AAED,UAAU,MAAM;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,mDAAmD;AACnD,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/E,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK;QAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;CAC/F;AAED,gHAAgH;AAChH,eAAO,MAAM,gBAAgB,+BAA+B,CAAC;AAqB7D;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,SAAS,WAAW,EAAO,EACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,SAAS,GAAG,SAAS,CAgDvB;AAYD,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAElF;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAY5E;AAED,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAS/E;AAkED,eAAO,MAAM,qBAAqB,EAAE,cA8CnC,CAAC"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/** The default host this mapper reports; a target's own host replaces it when there is no endpoint override. */
|
|
2
|
+
export const IAM_DEFAULT_HOST = "https://iam.googleapis.com";
|
|
3
|
+
const enc = encodeURIComponent;
|
|
4
|
+
function region(env = process.env) {
|
|
5
|
+
return env.GCP_REGION || "us-central1";
|
|
6
|
+
}
|
|
7
|
+
/** A `:getIamPolicy` / `:setIamPolicy` pair on a REST resource path (the Google API convention). */
|
|
8
|
+
function verbTarget(host, version, path, getMethod) {
|
|
9
|
+
return {
|
|
10
|
+
host,
|
|
11
|
+
path,
|
|
12
|
+
get: (base) => getMethod === "GET"
|
|
13
|
+
? { method: "GET", url: `${base}/${version}/${path}:getIamPolicy?options.requestedPolicyVersion=3` }
|
|
14
|
+
: { method: "POST", url: `${base}/${version}/${path}:getIamPolicy`, body: { options: { requestedPolicyVersion: 3 } } },
|
|
15
|
+
set: (base, policy) => ({ method: "POST", url: `${base}/${version}/${path}:setIamPolicy`, body: { policy } }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve `resourceRef` to the target's policy endpoints, or undefined for a
|
|
20
|
+
* target chant cannot reach. `manifest` is the rest of the applied manifest, so
|
|
21
|
+
* a RunService named by `name` takes its region from its own spec. Pure.
|
|
22
|
+
*/
|
|
23
|
+
export function iamTarget(spec, project, manifest = [], env = process.env) {
|
|
24
|
+
const ref = spec.resourceRef ?? {};
|
|
25
|
+
const external = typeof ref.external === "string" && ref.external !== "" ? ref.external : undefined;
|
|
26
|
+
const name = typeof ref.name === "string" && ref.name !== "" ? ref.name : undefined;
|
|
27
|
+
switch (ref.kind) {
|
|
28
|
+
case "Project": {
|
|
29
|
+
const id = external ? external.replace(/^projects\//, "") : (name ?? project);
|
|
30
|
+
return verbTarget("https://cloudresourcemanager.googleapis.com", "v1", `projects/${enc(id)}`, "POST");
|
|
31
|
+
}
|
|
32
|
+
case "RunService": {
|
|
33
|
+
if (external)
|
|
34
|
+
return verbTarget("https://run.googleapis.com", "v2", external, "GET");
|
|
35
|
+
if (!name)
|
|
36
|
+
return undefined;
|
|
37
|
+
const declared = manifest.find((r) => r.kind === "RunService" && r.metadata?.name === name);
|
|
38
|
+
const location = declared?.spec?.location;
|
|
39
|
+
const loc = typeof location === "string" ? location : (spec.location ?? region(env));
|
|
40
|
+
return verbTarget("https://run.googleapis.com", "v2", `projects/${enc(project)}/locations/${enc(loc)}/services/${enc(name)}`, "GET");
|
|
41
|
+
}
|
|
42
|
+
case "IAMServiceAccount": {
|
|
43
|
+
if (external?.startsWith("projects/"))
|
|
44
|
+
return verbTarget(IAM_DEFAULT_HOST, "v1", external, "POST");
|
|
45
|
+
const email = external ?? (name ? `${name}@${project}.iam.gserviceaccount.com` : undefined);
|
|
46
|
+
if (!email)
|
|
47
|
+
return undefined;
|
|
48
|
+
return verbTarget(IAM_DEFAULT_HOST, "v1", `projects/${enc(project)}/serviceAccounts/${email}`, "POST");
|
|
49
|
+
}
|
|
50
|
+
case "PubSubTopic":
|
|
51
|
+
case "PubSubSubscription": {
|
|
52
|
+
const collection = ref.kind === "PubSubTopic" ? "topics" : "subscriptions";
|
|
53
|
+
const path = external ?? (name ? `projects/${enc(project)}/${collection}/${enc(name)}` : undefined);
|
|
54
|
+
return path ? verbTarget("https://pubsub.googleapis.com", "v1", path, "GET") : undefined;
|
|
55
|
+
}
|
|
56
|
+
case "SecretManagerSecret": {
|
|
57
|
+
const path = external ?? (name ? `projects/${enc(project)}/secrets/${enc(name)}` : undefined);
|
|
58
|
+
return path ? verbTarget("https://secretmanager.googleapis.com", "v1", path, "GET") : undefined;
|
|
59
|
+
}
|
|
60
|
+
case "StorageBucket": {
|
|
61
|
+
const bucket = external ? external.replace(/^(gs:\/\/|projects\/_\/buckets\/)/, "") : name;
|
|
62
|
+
if (!bucket)
|
|
63
|
+
return undefined;
|
|
64
|
+
// GCS has its own IAM shape: GET and PUT on `b/<bucket>/iam`, with the policy as the body.
|
|
65
|
+
const url = (base) => `${base}/storage/v1/b/${enc(bucket)}/iam`;
|
|
66
|
+
return {
|
|
67
|
+
host: "https://storage.googleapis.com",
|
|
68
|
+
path: `b/${bucket}`,
|
|
69
|
+
get: (base) => ({ method: "GET", url: `${url(base)}?optionsRequestedPolicyVersion=3` }),
|
|
70
|
+
set: (base, policy) => ({ method: "PUT", url: url(base), body: policy }),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function sameCondition(a, b) {
|
|
78
|
+
if (!a && !b)
|
|
79
|
+
return true;
|
|
80
|
+
if (!a || !b)
|
|
81
|
+
return false;
|
|
82
|
+
return (a.expression ?? "") === (b.expression ?? "") && (a.title ?? "") === (b.title ?? "");
|
|
83
|
+
}
|
|
84
|
+
function findBinding(policy, spec) {
|
|
85
|
+
return policy.bindings?.find((b) => b.role === spec.role && sameCondition(b.condition, spec.condition));
|
|
86
|
+
}
|
|
87
|
+
/** True when the policy grants the spec's member in its role (and condition). Pure. */
|
|
88
|
+
export function policyHasMember(policy, spec) {
|
|
89
|
+
return findBinding(policy, spec)?.members?.includes(spec.member ?? "") ?? false;
|
|
90
|
+
}
|
|
91
|
+
/** The policy with the spec's member added to its binding, every other binding kept. Pure. */
|
|
92
|
+
export function withMember(policy, spec) {
|
|
93
|
+
const member = spec.member;
|
|
94
|
+
const bindings = (policy.bindings ?? []).map((b) => ({ ...b, members: [...(b.members ?? [])] }));
|
|
95
|
+
const existing = bindings.find((b) => b.role === spec.role && sameCondition(b.condition, spec.condition));
|
|
96
|
+
if (existing) {
|
|
97
|
+
if (!existing.members.includes(member))
|
|
98
|
+
existing.members.push(member);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
bindings.push({ role: spec.role, members: [member], ...(spec.condition ? { condition: spec.condition } : {}) });
|
|
102
|
+
}
|
|
103
|
+
// A conditional binding needs policy version 3; otherwise keep what was read.
|
|
104
|
+
const version = spec.condition || bindings.some((b) => b.condition) ? 3 : policy.version;
|
|
105
|
+
return { ...policy, bindings, ...(version !== undefined ? { version } : {}) };
|
|
106
|
+
}
|
|
107
|
+
/** The policy with the spec's member removed; a binding left with no members is dropped. Pure. */
|
|
108
|
+
export function withoutMember(policy, spec) {
|
|
109
|
+
const bindings = (policy.bindings ?? [])
|
|
110
|
+
.map((b) => b.role === spec.role && sameCondition(b.condition, spec.condition)
|
|
111
|
+
? { ...b, members: (b.members ?? []).filter((m) => m !== spec.member) }
|
|
112
|
+
: b)
|
|
113
|
+
.filter((b) => (b.members ?? []).length > 0);
|
|
114
|
+
return { ...policy, bindings };
|
|
115
|
+
}
|
|
116
|
+
function specOf(resource) {
|
|
117
|
+
const spec = (resource.spec ?? {});
|
|
118
|
+
const name = resource.metadata?.name ?? "?";
|
|
119
|
+
if (!spec.member)
|
|
120
|
+
throw new Error(`IAMPolicyMember ${name} has no spec.member`);
|
|
121
|
+
if (!spec.role)
|
|
122
|
+
throw new Error(`IAMPolicyMember ${name} has no spec.role`);
|
|
123
|
+
return spec;
|
|
124
|
+
}
|
|
125
|
+
function targetOf(resource, ctx, env) {
|
|
126
|
+
const target = iamTarget(specOf(resource), ctx.project, ctx.manifest, env);
|
|
127
|
+
if (!target) {
|
|
128
|
+
const ref = resource.spec?.resourceRef;
|
|
129
|
+
throw new Error(`IAMPolicyMember ${resource.metadata?.name ?? "?"}: no IAM policy endpoint for resourceRef kind ${ref?.kind ?? "(none)"}`);
|
|
130
|
+
}
|
|
131
|
+
// No endpoint override: each target's own service host. An override (floci-gcp) serves every service.
|
|
132
|
+
const base = ctx.base === IAM_DEFAULT_HOST ? target.host : ctx.base;
|
|
133
|
+
return { target, base };
|
|
134
|
+
}
|
|
135
|
+
async function readPolicy(target, base, http, signal) {
|
|
136
|
+
const req = target.get(base);
|
|
137
|
+
const res = await http(req.method, req.url, req.body, signal);
|
|
138
|
+
if (res.status >= 300)
|
|
139
|
+
return res;
|
|
140
|
+
let policy;
|
|
141
|
+
try {
|
|
142
|
+
policy = JSON.parse(res.text);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
policy = {};
|
|
146
|
+
}
|
|
147
|
+
return { ...res, policy };
|
|
148
|
+
}
|
|
149
|
+
/** How many times a write that loses an `etag` race (409, or 412 on GCS) is retried. */
|
|
150
|
+
const ETAG_ATTEMPTS = 3;
|
|
151
|
+
async function writeWith(resource, ctx, http, signal, change) {
|
|
152
|
+
const spec = specOf(resource);
|
|
153
|
+
const { target, base } = targetOf(resource, ctx);
|
|
154
|
+
const name = resource.metadata?.name ?? "?";
|
|
155
|
+
for (let attempt = 1;; attempt++) {
|
|
156
|
+
const read = await readPolicy(target, base, http, signal);
|
|
157
|
+
if (!read.policy)
|
|
158
|
+
throw new Error(`IAMPolicyMember ${name}: reading the policy of ${target.path} failed (${read.status}): ${read.text}`);
|
|
159
|
+
const next = change(read.policy, spec);
|
|
160
|
+
if (!next)
|
|
161
|
+
return false;
|
|
162
|
+
const req = target.set(base, next);
|
|
163
|
+
const res = await http(req.method, req.url, req.body, signal);
|
|
164
|
+
if (res.status < 300)
|
|
165
|
+
return true;
|
|
166
|
+
if ((res.status === 409 || res.status === 412) && attempt < ETAG_ATTEMPTS)
|
|
167
|
+
continue;
|
|
168
|
+
throw new Error(`IAMPolicyMember ${name}: writing the policy of ${target.path} failed (${res.status}): ${res.text}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export const iamPolicyMemberMapper = {
|
|
172
|
+
kind: "IAMPolicyMember",
|
|
173
|
+
defaultHost: IAM_DEFAULT_HOST,
|
|
174
|
+
accepts(resource, ctx) {
|
|
175
|
+
return iamTarget((resource.spec ?? {}), ctx.project, ctx.manifest) !== undefined;
|
|
176
|
+
},
|
|
177
|
+
plan(resource, ctx) {
|
|
178
|
+
// Only the read is a single URL here; apply and delete go through `custom`.
|
|
179
|
+
const { target, base } = targetOf(resource, ctx);
|
|
180
|
+
const get = target.get(base);
|
|
181
|
+
return { getUrl: get.url, create: { method: "POST", url: get.url, body: get.body ?? {} } };
|
|
182
|
+
},
|
|
183
|
+
custom: {
|
|
184
|
+
async apply(resource, ctx, http, signal) {
|
|
185
|
+
const created = await writeWith(resource, ctx, http, signal, (policy, spec) => policyHasMember(policy, spec) ? undefined : withMember(policy, spec));
|
|
186
|
+
return { created, updated: false };
|
|
187
|
+
},
|
|
188
|
+
async delete(resource, ctx, http, signal) {
|
|
189
|
+
return writeWith(resource, ctx, http, signal, (policy, spec) => policyHasMember(policy, spec) ? withoutMember(policy, spec) : undefined);
|
|
190
|
+
},
|
|
191
|
+
async read(resource, ctx, http, signal) {
|
|
192
|
+
const spec = specOf(resource);
|
|
193
|
+
const { target, base } = targetOf(resource, ctx);
|
|
194
|
+
const read = await readPolicy(target, base, http, signal);
|
|
195
|
+
if (!read.policy)
|
|
196
|
+
return read;
|
|
197
|
+
// The binding member is the resource: absent from the policy reads as absent.
|
|
198
|
+
if (!policyHasMember(read.policy, spec))
|
|
199
|
+
return { status: 404, text: `${spec.member} has no ${spec.role} on ${target.path}` };
|
|
200
|
+
const binding = findBinding(read.policy, spec);
|
|
201
|
+
return {
|
|
202
|
+
status: 200,
|
|
203
|
+
// `resourceRef` is the declared one: the policy read is that target's, so the binding is on it.
|
|
204
|
+
text: JSON.stringify({
|
|
205
|
+
member: spec.member,
|
|
206
|
+
role: spec.role,
|
|
207
|
+
resource: target.path,
|
|
208
|
+
...(spec.resourceRef ? { resourceRef: spec.resourceRef } : {}),
|
|
209
|
+
...(binding?.condition ? { condition: binding.condition } : {}),
|
|
210
|
+
...(read.policy.etag ? { etag: read.policy.etag } : {}),
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`gcpApply`), which maps CNRM resources to GCP REST calls since GCP has no
|
|
5
5
|
* native deployment service to shell out to.
|
|
6
6
|
*/
|
|
7
|
-
export { gcpApply, gcpDelete, applyResource, deleteResource, waitForOperation, longRunningOperation, bucketInsertBody, pubSubTopicBody, pubSubSubscriptionBody, cloudRunServiceBody, secretBody, serviceAccountBody, storageBucketMapper, pubSubTopicMapper, pubSubSubscriptionMapper, cloudRunServiceMapper, secretManagerSecretMapper, gcpServiceAccountMapper, MAPPERS, referencedNames, orderByReferences, pruneOrphans, toApplyResult, type GcpNotAttempted, type GcpNotPrunable, chantOwnershipLabels, isChantOwned, resolveGcpEndpoint, resolveGcpProject, parseManifest, } from "./gcp-apply.js";
|
|
7
|
+
export { gcpApply, gcpDelete, applyResource, deleteResource, waitForOperation, longRunningOperation, bucketInsertBody, pubSubTopicBody, pubSubSubscriptionBody, cloudRunServiceBody, secretBody, serviceAccountBody, storageBucketMapper, pubSubTopicMapper, pubSubSubscriptionMapper, cloudRunServiceMapper, secretManagerSecretMapper, gcpServiceAccountMapper, MAPPERS, referencedNames, orderByReferences, pruneOrphans, toApplyResult, type GcpNotAttempted, type GcpNotPrunable, chantOwnershipLabels, isChantOwned, resolveGcpEndpoint, resolveGcpProject, projectRefProject, parseManifest, } from "./gcp-apply.js";
|
|
8
8
|
export type { GcpApplyArgs, GcpResource, CnrmStorageBucket, BucketInsertBody, PubSubTopicBody, ResourceMapper, OperationSpec, ListSpec, ApplyPlan, GcpHttp, } from "./gcp-apply.js";
|
|
9
9
|
export { flociGcpUp, flociGcpDown, flociGcpRunCommand, flociGcpRmCommand, flociGcpExistsCommand, flociGcpHealthUrl, flociGcpEndpoint, } from "./floci-gcp.js";
|
|
10
10
|
export type { FlociGcpUpArgs, FlociGcpDownArgs } from "./floci-gcp.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,OAAO,EACP,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,oBAAoB,EACpB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACb,QAAQ,EACR,SAAS,EACT,OAAO,GACR,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,mBAAmB,EACnB,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACxB,qBAAqB,EACrB,yBAAyB,EACzB,uBAAuB,EACvB,OAAO,EACP,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,oBAAoB,EACpB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,YAAY,EACZ,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,aAAa,EACb,QAAQ,EACR,SAAS,EACT,OAAO,GACR,MAAM,aAAa,CAAC;AAIrB,OAAO,EACL,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (`gcpApply`), which maps CNRM resources to GCP REST calls since GCP has no
|
|
5
5
|
* native deployment service to shell out to.
|
|
6
6
|
*/
|
|
7
|
-
export { gcpApply, gcpDelete, applyResource, deleteResource, waitForOperation, longRunningOperation, bucketInsertBody, pubSubTopicBody, pubSubSubscriptionBody, cloudRunServiceBody, secretBody, serviceAccountBody, storageBucketMapper, pubSubTopicMapper, pubSubSubscriptionMapper, cloudRunServiceMapper, secretManagerSecretMapper, gcpServiceAccountMapper, MAPPERS, referencedNames, orderByReferences, pruneOrphans, toApplyResult, chantOwnershipLabels, isChantOwned, resolveGcpEndpoint, resolveGcpProject, parseManifest, } from "./gcp-apply.js";
|
|
7
|
+
export { gcpApply, gcpDelete, applyResource, deleteResource, waitForOperation, longRunningOperation, bucketInsertBody, pubSubTopicBody, pubSubSubscriptionBody, cloudRunServiceBody, secretBody, serviceAccountBody, storageBucketMapper, pubSubTopicMapper, pubSubSubscriptionMapper, cloudRunServiceMapper, secretManagerSecretMapper, gcpServiceAccountMapper, MAPPERS, referencedNames, orderByReferences, pruneOrphans, toApplyResult, chantOwnershipLabels, isChantOwned, resolveGcpEndpoint, resolveGcpProject, projectRefProject, parseManifest, } from "./gcp-apply.js";
|
|
8
8
|
// floci-gcp (GCP emulator) lifecycle — the typed twin of aws's flociUp/Down, so
|
|
9
9
|
// the trio's GCP op boots/tears down the emulator as a modeled step, not a shell.
|
|
10
10
|
export { flociGcpUp, flociGcpDown, flociGcpRunCommand, flociGcpRmCommand, flociGcpExistsCommand, flociGcpHealthUrl, flociGcpEndpoint, } from "./floci-gcp.js";
|
package/dist/serializer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AAiClG;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW1F;AAsED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AAiClG;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW1F;AAsED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAiG3B,CAAC"}
|
package/dist/serializer.js
CHANGED
|
@@ -131,6 +131,7 @@ export const gcpSerializer = {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
const documents = [];
|
|
134
|
+
const unresolved = [];
|
|
134
135
|
for (const [name, entity] of entities) {
|
|
135
136
|
if (isPropertyDeclarable(entity))
|
|
136
137
|
continue;
|
|
@@ -140,7 +141,7 @@ export const gcpSerializer = {
|
|
|
140
141
|
const gvk = resolveGVK(entityType);
|
|
141
142
|
if (!gvk)
|
|
142
143
|
continue;
|
|
143
|
-
const props = toYAMLValue(isResourceDeclarable(entity) ? entity.props : undefined, entityNames);
|
|
144
|
+
const props = resolvePseudoParameters(toYAMLValue(isResourceDeclarable(entity) ? entity.props : undefined, entityNames), name, unresolved);
|
|
144
145
|
if (!props)
|
|
145
146
|
continue;
|
|
146
147
|
// Build the Config Connector manifest structure
|
|
@@ -181,6 +182,8 @@ export const gcpSerializer = {
|
|
|
181
182
|
const yamlDoc = emitK8sManifest(manifest);
|
|
182
183
|
documents.push(yamlDoc);
|
|
183
184
|
}
|
|
185
|
+
if (unresolved.length > 0)
|
|
186
|
+
throw new Error(unresolvedMessage(unresolved));
|
|
184
187
|
const primary = documents.join("\n---\n");
|
|
185
188
|
const warnings = unwritableOutputWarnings(outputs);
|
|
186
189
|
return warnings.length > 0 ? { primary, warnings } : primary;
|
|
@@ -196,6 +199,38 @@ const PSEUDO_ENV_MAP = {
|
|
|
196
199
|
"GCP::Region": { envVar: "GCP_REGION", fallback: "us-central1" },
|
|
197
200
|
"GCP::Zone": { envVar: "GCP_ZONE", fallback: "us-central1-a" },
|
|
198
201
|
};
|
|
202
|
+
/**
|
|
203
|
+
* Replace each GCP pseudo-parameter (`{ Ref: "GCP::Region" }`) in a
|
|
204
|
+
* serialized resource with its environment variable's value (#4200).
|
|
205
|
+
* Config Connector has no parameters, so a `Ref` left in the YAML reaches
|
|
206
|
+
* the API as an object. A variable that isn't set is recorded, and the
|
|
207
|
+
* build refuses with every one named.
|
|
208
|
+
*/
|
|
209
|
+
function resolvePseudoParameters(value, entity, unresolved) {
|
|
210
|
+
if (Array.isArray(value))
|
|
211
|
+
return value.map((v) => resolvePseudoParameters(v, entity, unresolved));
|
|
212
|
+
if (value === null || typeof value !== "object")
|
|
213
|
+
return value;
|
|
214
|
+
const obj = value;
|
|
215
|
+
const keys = Object.keys(obj);
|
|
216
|
+
if (keys.length === 1 && keys[0] === "Ref" && typeof obj.Ref === "string" && obj.Ref in PSEUDO_ENV_MAP) {
|
|
217
|
+
const { envVar } = PSEUDO_ENV_MAP[obj.Ref];
|
|
218
|
+
const resolved = process.env[envVar];
|
|
219
|
+
if (resolved)
|
|
220
|
+
return resolved;
|
|
221
|
+
unresolved.push({ entity, ref: obj.Ref, envVar });
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const [k, v] of Object.entries(obj))
|
|
226
|
+
out[k] = resolvePseudoParameters(v, entity, unresolved);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
function unresolvedMessage(unresolved) {
|
|
230
|
+
const vars = [...new Set(unresolved.map((u) => u.envVar))];
|
|
231
|
+
const lines = unresolved.map((u) => ` ${u.entity}: ${u.ref} needs ${u.envVar}`);
|
|
232
|
+
return `gcp: ${vars.join(", ")} ${vars.length === 1 ? "is" : "are"} not set, and Config Connector YAML can't carry a pseudo-parameter. Set ${vars.length === 1 ? "it" : "them"} at build, or pass the value explicitly:\n${lines.join("\n")}`;
|
|
233
|
+
}
|
|
199
234
|
/**
|
|
200
235
|
* Resolve an annotation value to a plain string.
|
|
201
236
|
* PseudoParameter intrinsics are resolved from environment variables;
|
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.35",
|
|
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.35"
|
|
80
80
|
}
|
|
81
81
|
}
|
package/src/api/read-client.ts
CHANGED
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Coverage is exactly the applier's dispatch table (`MAPPERS`) — StorageBucket,
|
|
24
24
|
* PubSubTopic, PubSubSubscription, SecretManagerSecret, IAMServiceAccount,
|
|
25
|
-
* RunService
|
|
25
|
+
* RunService, IAMPolicyMember and the Cloud Run load balancer kinds
|
|
26
|
+
* (ComputeAddress, ComputeRegionNetworkEndpointGroup, ComputeBackendService,
|
|
27
|
+
* ComputeURLMap, ComputeTargetHTTPProxy, ComputeForwardingRule; #4233). An
|
|
28
|
+
* IAMPolicyMember is read through its target's policy and is present when the
|
|
29
|
+
* policy grants its member the role. The kubectl path could fetch any CNRM kind the cluster knew
|
|
26
30
|
* about, so this is narrower on paper. It is not narrower in practice for
|
|
27
31
|
* anything chant can act on: a kind with no mapper cannot be applied either, so
|
|
28
32
|
* observing it produced a live tree nothing could ever reconcile against.
|
|
@@ -44,11 +48,14 @@ export interface GcpReadClientOptions {
|
|
|
44
48
|
http?: GcpReadHttp;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. */
|
|
48
|
-
export type GcpReadHttp = (method: string, url: string) => Promise<{ status: number; text: string }>;
|
|
51
|
+
/** Injectable HTTP client — mirrors the applier's `GcpHttp`. A body is sent only for a POST read (`:getIamPolicy`). */
|
|
52
|
+
export type GcpReadHttp = (method: string, url: string, body?: unknown) => Promise<{ status: number; text: string }>;
|
|
49
53
|
|
|
50
|
-
const defaultHttp: GcpReadHttp = async (method, url) => {
|
|
51
|
-
const res = await fetch(url, {
|
|
54
|
+
const defaultHttp: GcpReadHttp = async (method, url, body) => {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
...(body === undefined ? {} : { headers: { "content-type": "application/json" }, body: JSON.stringify(body) }),
|
|
58
|
+
});
|
|
52
59
|
return { status: res.status, text: await res.text() };
|
|
53
60
|
};
|
|
54
61
|
|
|
@@ -100,21 +107,33 @@ export async function getResource(
|
|
|
100
107
|
spec: (declared?.spec as Record<string, unknown> | undefined) ?? declared ?? {},
|
|
101
108
|
};
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} catch (err) {
|
|
107
|
-
// A mapper that cannot plan from the declared spec cannot produce a URL, so
|
|
108
|
-
// there is nothing to read — a hole, never an absence.
|
|
109
|
-
throw new GcpReadError(`could not build a read URL for ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
const ctx = { base, project: client.project };
|
|
111
|
+
if (mapper.accepts && !mapper.accepts(resource, ctx)) {
|
|
112
|
+
throw new GcpReadError(`no REST read for ${kind}/${name}: its target has no endpoint chant can read`);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
const http = client.http ?? defaultHttp;
|
|
113
116
|
let res: { status: number; text: string };
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
if (mapper.custom) {
|
|
118
|
+
try {
|
|
119
|
+
res = await mapper.custom.read(resource, ctx, (method, url, body) => http(method, url, body));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new GcpReadError(`could not read ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
let getUrl: string;
|
|
125
|
+
try {
|
|
126
|
+
getUrl = mapper.plan(resource, ctx).getUrl;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// A mapper that cannot plan from the declared spec cannot produce a URL, so
|
|
129
|
+
// there is nothing to read — a hole, never an absence.
|
|
130
|
+
throw new GcpReadError(`could not build a read URL for ${kind}/${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
res = await http("GET", getUrl);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
throw new GcpReadError(err instanceof Error ? err.message : String(err));
|
|
136
|
+
}
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
if (res.status === 404) throw new GcpReadError(`${kind}/${name} not found`, 404);
|
|
@@ -34,6 +34,8 @@ export interface CloudRunServiceProps {
|
|
|
34
34
|
publicAccess?: boolean;
|
|
35
35
|
/** Environment variables. */
|
|
36
36
|
env?: Array<{ name: string; value: string }>;
|
|
37
|
+
/** Service account the service runs as: an email, or the account id of an IAMServiceAccount in the project. */
|
|
38
|
+
serviceAccount?: string;
|
|
37
39
|
/** Additional labels. */
|
|
38
40
|
labels?: Record<string, string>;
|
|
39
41
|
/** Namespace for all resources. */
|
|
@@ -75,6 +77,7 @@ export const CloudRunServiceComposite = Composite((props: CloudRunServiceProps)
|
|
|
75
77
|
maxInstanceCount = 100,
|
|
76
78
|
publicAccess = false,
|
|
77
79
|
env,
|
|
80
|
+
serviceAccount,
|
|
78
81
|
labels: extraLabels = {},
|
|
79
82
|
namespace,
|
|
80
83
|
defaults: defs,
|
|
@@ -112,6 +115,9 @@ export const CloudRunServiceComposite = Composite((props: CloudRunServiceProps)
|
|
|
112
115
|
maxInstanceCount,
|
|
113
116
|
},
|
|
114
117
|
containers,
|
|
118
|
+
...(serviceAccount && {
|
|
119
|
+
serviceAccountRef: serviceAccount.includes("@") ? { external: serviceAccount } : { name: serviceAccount },
|
|
120
|
+
}),
|
|
115
121
|
},
|
|
116
122
|
}, defs?.service));
|
|
117
123
|
|
|
@@ -83,6 +83,13 @@ describe("CloudRunServiceComposite", () => {
|
|
|
83
83
|
expect(result.service).toBeDefined();
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
+
test("serviceAccount sets serviceAccountRef: an account id by name, an email as external (#4240)", () => {
|
|
87
|
+
const byName = CloudRunServiceComposite({ name: "api", image: "i", serviceAccount: "run-sa" });
|
|
88
|
+
expect((p(byName.service).template as Record<string, unknown>).serviceAccountRef).toEqual({ name: "run-sa" });
|
|
89
|
+
const byEmail = CloudRunServiceComposite({ name: "api", image: "i", serviceAccount: "x@o.iam.gserviceaccount.com" });
|
|
90
|
+
expect((p(byEmail.service).template as Record<string, unknown>).serviceAccountRef).toEqual({ external: "x@o.iam.gserviceaccount.com" });
|
|
91
|
+
});
|
|
92
|
+
|
|
86
93
|
test("sets projectRef and location, which the RunService CRD requires", () => {
|
|
87
94
|
const result = CloudRunServiceComposite({
|
|
88
95
|
name: "api",
|
package/src/deep-observe.test.ts
CHANGED
|
@@ -124,6 +124,21 @@ describe("toCnrmTree — each REST shape in the declared vocabulary (#1210)", ()
|
|
|
124
124
|
expect(tree).toEqual({ metadata: { name: "t" }, messageRetentionDuration: "86400s" });
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
test("run service: the v2 serviceAccount email maps back to the declared serviceAccountRef (#4240)", () => {
|
|
128
|
+
const same = toCnrmTree(
|
|
129
|
+
"RunService",
|
|
130
|
+
{ name: "projects/my-project/locations/us-central1/services/web", template: { serviceAccount: "run-sa@my-project.iam.gserviceaccount.com" } },
|
|
131
|
+
ctx,
|
|
132
|
+
) as { template: Record<string, unknown> };
|
|
133
|
+
expect(same.template).toEqual({ serviceAccountRef: { name: "run-sa" } });
|
|
134
|
+
const other = toCnrmTree(
|
|
135
|
+
"RunService",
|
|
136
|
+
{ name: "projects/my-project/locations/us-central1/services/web", template: { serviceAccount: "x@other.iam.gserviceaccount.com" } },
|
|
137
|
+
ctx,
|
|
138
|
+
) as { template: Record<string, unknown> };
|
|
139
|
+
expect(other.template).toEqual({ serviceAccountRef: { external: "x@other.iam.gserviceaccount.com" } });
|
|
140
|
+
});
|
|
141
|
+
|
|
127
142
|
test("bucket: GCS's iamConfiguration and lifecycle map onto the CNRM field names", () => {
|
|
128
143
|
const tree = toCnrmTree(
|
|
129
144
|
"StorageBucket",
|
|
@@ -235,7 +250,7 @@ describe("observeResourcesDeepGcp — over REST (#1209)", () => {
|
|
|
235
250
|
});
|
|
236
251
|
|
|
237
252
|
test("a kind the applier cannot write is unsupported-kind, and is never queried", async () => {
|
|
238
|
-
const out = await observe(entities([["e", "GCP::Compute::
|
|
253
|
+
const out = await observe(entities([["e", "GCP::Compute::Network", { metadata: { name: "n" } }]]));
|
|
239
254
|
expect(out.unobserved?.e?.reason).toBe("unsupported-kind");
|
|
240
255
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
241
256
|
});
|