@intentius/chant-lexicon-gcp 0.0.22 → 0.0.24

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.
@@ -1,7 +1,13 @@
1
1
  /**
2
- * CloudRunService composite — RunService + optional IAMPolicyMember for public access.
2
+ * CloudRunServiceComposite composite — RunService + optional IAMPolicyMember for public access.
3
3
  */
4
4
 
5
+ import { Composite, mergeDefaults } from "@intentius/chant";
6
+ import {
7
+ CloudRunService as CloudRunServiceResource,
8
+ IAMPolicyMember,
9
+ } from "../generated";
10
+
5
11
  export interface CloudRunServiceProps {
6
12
  /** Service name. */
7
13
  name: string;
@@ -29,11 +35,11 @@ export interface CloudRunServiceProps {
29
35
  labels?: Record<string, string>;
30
36
  /** Namespace for all resources. */
31
37
  namespace?: string;
32
- }
33
-
34
- export interface CloudRunServiceResult {
35
- service: Record<string, unknown>;
36
- publicIam?: Record<string, unknown>;
38
+ /** Per-member defaults for customizing individual resources. */
39
+ defaults?: {
40
+ service?: Partial<ConstructorParameters<typeof CloudRunServiceResource>[0]>;
41
+ publicIam?: Partial<ConstructorParameters<typeof IAMPolicyMember>[0]>;
42
+ };
37
43
  }
38
44
 
39
45
  /**
@@ -50,7 +56,7 @@ export interface CloudRunServiceResult {
50
56
  * });
51
57
  * ```
52
58
  */
53
- export function CloudRunService(props: CloudRunServiceProps): CloudRunServiceResult {
59
+ export const CloudRunServiceComposite = Composite<CloudRunServiceProps>((props) => {
54
60
  const {
55
61
  name,
56
62
  image,
@@ -65,6 +71,7 @@ export function CloudRunService(props: CloudRunServiceProps): CloudRunServiceRes
65
71
  env,
66
72
  labels: extraLabels = {},
67
73
  namespace,
74
+ defaults: defs,
68
75
  } = props;
69
76
 
70
77
  const commonLabels: Record<string, string> = {
@@ -84,7 +91,7 @@ export function CloudRunService(props: CloudRunServiceProps): CloudRunServiceRes
84
91
  },
85
92
  ];
86
93
 
87
- const service: Record<string, unknown> = {
94
+ const service = new CloudRunServiceResource(mergeDefaults({
88
95
  metadata: {
89
96
  name,
90
97
  ...(namespace && { namespace }),
@@ -99,12 +106,12 @@ export function CloudRunService(props: CloudRunServiceProps): CloudRunServiceRes
99
106
  },
100
107
  containers,
101
108
  },
102
- };
109
+ } as Record<string, unknown>, defs?.service));
103
110
 
104
- const result: CloudRunServiceResult = { service };
111
+ const result: Record<string, any> = { service };
105
112
 
106
113
  if (publicAccess) {
107
- result.publicIam = {
114
+ result.publicIam = new IAMPolicyMember(mergeDefaults({
108
115
  metadata: {
109
116
  name: `${name}-public`,
110
117
  ...(namespace && { namespace }),
@@ -117,8 +124,8 @@ export function CloudRunService(props: CloudRunServiceProps): CloudRunServiceRes
117
124
  kind: "RunService",
118
125
  name,
119
126
  },
120
- };
127
+ } as Record<string, unknown>, defs?.publicIam));
121
128
  }
122
129
 
123
130
  return result;
124
- }
131
+ }, "CloudRunServiceComposite");
@@ -2,6 +2,9 @@
2
2
  * CloudSqlInstance composite — SQLInstance + SQLDatabase + SQLUser.
3
3
  */
4
4
 
5
+ import { Composite, mergeDefaults } from "@intentius/chant";
6
+ import { SQLInstance, SQLDatabase, SQLUser } from "../generated";
7
+
5
8
  export interface CloudSqlInstanceProps {
6
9
  /** Instance name. */
7
10
  name: string;
@@ -27,12 +30,12 @@ export interface CloudSqlInstanceProps {
27
30
  labels?: Record<string, string>;
28
31
  /** Namespace for all resources. */
29
32
  namespace?: string;
30
- }
31
-
32
- export interface CloudSqlInstanceResult {
33
- instance: Record<string, unknown>;
34
- database: Record<string, unknown>;
35
- user: Record<string, unknown>;
33
+ /** Per-member defaults for customizing individual resources. */
34
+ defaults?: {
35
+ instance?: Partial<ConstructorParameters<typeof SQLInstance>[0]>;
36
+ database?: Partial<ConstructorParameters<typeof SQLDatabase>[0]>;
37
+ user?: Partial<ConstructorParameters<typeof SQLUser>[0]>;
38
+ };
36
39
  }
37
40
 
38
41
  /**
@@ -49,7 +52,7 @@ export interface CloudSqlInstanceResult {
49
52
  * });
50
53
  * ```
51
54
  */
52
- export function CloudSqlInstance(props: CloudSqlInstanceProps): CloudSqlInstanceResult {
55
+ export const CloudSqlInstance = Composite<CloudSqlInstanceProps>((props) => {
53
56
  const {
54
57
  name,
55
58
  databaseVersion = "POSTGRES_15",
@@ -63,6 +66,7 @@ export function CloudSqlInstance(props: CloudSqlInstanceProps): CloudSqlInstance
63
66
  highAvailability = false,
64
67
  labels: extraLabels = {},
65
68
  namespace,
69
+ defaults: defs,
66
70
  } = props;
67
71
 
68
72
  const commonLabels: Record<string, string> = {
@@ -85,7 +89,7 @@ export function CloudSqlInstance(props: CloudSqlInstanceProps): CloudSqlInstance
85
89
  };
86
90
  }
87
91
 
88
- const instance: Record<string, unknown> = {
92
+ const instance = new SQLInstance(mergeDefaults({
89
93
  metadata: {
90
94
  name,
91
95
  ...(namespace && { namespace }),
@@ -94,18 +98,18 @@ export function CloudSqlInstance(props: CloudSqlInstanceProps): CloudSqlInstance
94
98
  databaseVersion,
95
99
  ...(region && { region }),
96
100
  settings,
97
- };
101
+ } as Record<string, unknown>, defs?.instance));
98
102
 
99
- const database: Record<string, unknown> = {
103
+ const database = new SQLDatabase(mergeDefaults({
100
104
  metadata: {
101
105
  name: databaseName,
102
106
  ...(namespace && { namespace }),
103
107
  labels: { ...commonLabels, "app.kubernetes.io/component": "database" },
104
108
  },
105
109
  instanceRef: { name },
106
- };
110
+ } as Record<string, unknown>, defs?.database));
107
111
 
108
- const user: Record<string, unknown> = {
112
+ const user = new SQLUser(mergeDefaults({
109
113
  metadata: {
110
114
  name: `${name}-${userName}`,
111
115
  ...(namespace && { namespace }),
@@ -120,7 +124,7 @@ export function CloudSqlInstance(props: CloudSqlInstanceProps): CloudSqlInstance
120
124
  },
121
125
  },
122
126
  },
123
- };
127
+ } as Record<string, unknown>, defs?.user));
124
128
 
125
129
  return { instance, database, user };
126
- }
130
+ }, "CloudSqlInstance");
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect } from "bun:test";
2
2
  import { GkeCluster } from "./gke-cluster";
3
- import { CloudRunService } from "./cloud-run-service";
3
+ import { CloudRunServiceComposite } from "./cloud-run-service";
4
4
  import { CloudSqlInstance } from "./cloud-sql-instance";
5
5
  import { GcsBucket } from "./gcs-bucket";
6
6
  import { VpcNetwork } from "./vpc-network";
@@ -10,6 +10,11 @@ import { PrivateService } from "./private-service";
10
10
  import { ManagedCertificate } from "./managed-certificate";
11
11
  import { SecureProject } from "./secure-project";
12
12
 
13
+ /** Helper to extract props from a Declarable member. */
14
+ function p(member: unknown): Record<string, any> {
15
+ return (member as any).props;
16
+ }
17
+
13
18
  // ── GkeCluster ──────────────────────────────────────────────────────
14
19
 
15
20
  describe("GkeCluster", () => {
@@ -21,60 +26,81 @@ describe("GkeCluster", () => {
21
26
 
22
27
  test("includes common labels", () => {
23
28
  const result = GkeCluster({ name: "my-cluster" });
24
- const labels = result.cluster.metadata as any;
25
- expect(labels.labels["app.kubernetes.io/managed-by"]).toBe("chant");
29
+ const meta = p(result.cluster).metadata;
30
+ expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
26
31
  });
27
32
 
28
33
  test("node pool references cluster", () => {
29
34
  const result = GkeCluster({ name: "my-cluster" });
30
- expect((result.nodePool as any).clusterRef.name).toBe("my-cluster");
35
+ expect(p(result.nodePool).clusterRef.name).toBe("my-cluster");
31
36
  });
32
37
 
33
38
  test("respects maxNodeCount", () => {
34
39
  const result = GkeCluster({ name: "c", maxNodeCount: 20 });
35
- expect((result.nodePool as any).autoscaling.maxNodeCount).toBe(20);
40
+ expect(p(result.nodePool).autoscaling.maxNodeCount).toBe(20);
36
41
  });
37
42
 
38
43
  test("sets namespace when provided", () => {
39
44
  const result = GkeCluster({ name: "c", namespace: "infra" });
40
- expect((result.cluster.metadata as any).namespace).toBe("infra");
41
- expect((result.nodePool.metadata as any).namespace).toBe("infra");
45
+ expect(p(result.cluster).metadata.namespace).toBe("infra");
46
+ expect(p(result.nodePool).metadata.namespace).toBe("infra");
42
47
  });
43
48
 
44
49
  test("enables workload identity by default", () => {
45
50
  const result = GkeCluster({ name: "c" });
46
- expect((result.cluster as any).workloadIdentityConfig).toBeDefined();
47
- expect((result.nodePool as any).nodeConfig.workloadMetadataConfig).toBeDefined();
51
+ expect(p(result.cluster).workloadIdentityConfig).toBeDefined();
52
+ expect(p(result.nodePool).nodeConfig.workloadMetadataConfig).toBeDefined();
53
+ });
54
+
55
+ test("workloadPool uses explicit projectId", () => {
56
+ const result = GkeCluster({ name: "c", projectId: "my-project-123" });
57
+ expect(p(result.cluster).workloadIdentityConfig.workloadPool).toBe(
58
+ "my-project-123.svc.id.goog",
59
+ );
60
+ });
61
+
62
+ test("workloadPool falls back to GCP_PROJECT_ID env var", () => {
63
+ const prev = process.env.GCP_PROJECT_ID;
64
+ process.env.GCP_PROJECT_ID = "env-project-456";
65
+ try {
66
+ const result = GkeCluster({ name: "c" });
67
+ expect(p(result.cluster).workloadIdentityConfig.workloadPool).toBe(
68
+ "env-project-456.svc.id.goog",
69
+ );
70
+ } finally {
71
+ if (prev === undefined) delete process.env.GCP_PROJECT_ID;
72
+ else process.env.GCP_PROJECT_ID = prev;
73
+ }
48
74
  });
49
75
  });
50
76
 
51
77
  // ── CloudRunService ─────────────────────────────────────────────────
52
78
 
53
- describe("CloudRunService", () => {
79
+ describe("CloudRunServiceComposite", () => {
54
80
  test("returns service", () => {
55
- const result = CloudRunService({ name: "api", image: "gcr.io/p/api:1" });
81
+ const result = CloudRunServiceComposite({ name: "api", image: "gcr.io/p/api:1" });
56
82
  expect(result.service).toBeDefined();
57
83
  });
58
84
 
59
85
  test("no public IAM by default", () => {
60
- const result = CloudRunService({ name: "api", image: "gcr.io/p/api:1" });
86
+ const result = CloudRunServiceComposite({ name: "api", image: "gcr.io/p/api:1" });
61
87
  expect(result.publicIam).toBeUndefined();
62
88
  });
63
89
 
64
90
  test("creates public IAM when requested", () => {
65
- const result = CloudRunService({
91
+ const result = CloudRunServiceComposite({
66
92
  name: "api",
67
93
  image: "gcr.io/p/api:1",
68
94
  publicAccess: true,
69
95
  });
70
96
  expect(result.publicIam).toBeDefined();
71
- expect((result.publicIam as any).member).toBe("allUsers");
72
- expect((result.publicIam as any).role).toBe("roles/run.invoker");
97
+ expect(p(result.publicIam).member).toBe("allUsers");
98
+ expect(p(result.publicIam).role).toBe("roles/run.invoker");
73
99
  });
74
100
 
75
101
  test("sets custom port", () => {
76
- const result = CloudRunService({ name: "api", image: "img", port: 3000 });
77
- const containers = (result.service as any).template.containers;
102
+ const result = CloudRunServiceComposite({ name: "api", image: "img", port: 3000 });
103
+ const containers = p(result.service).template.containers;
78
104
  expect(containers[0].ports[0].containerPort).toBe(3000);
79
105
  });
80
106
  });
@@ -91,22 +117,22 @@ describe("CloudSqlInstance", () => {
91
117
 
92
118
  test("database references instance", () => {
93
119
  const result = CloudSqlInstance({ name: "db" });
94
- expect((result.database as any).instanceRef.name).toBe("db");
120
+ expect(p(result.database).instanceRef.name).toBe("db");
95
121
  });
96
122
 
97
123
  test("default database version is POSTGRES_15", () => {
98
124
  const result = CloudSqlInstance({ name: "db" });
99
- expect((result.instance as any).databaseVersion).toBe("POSTGRES_15");
125
+ expect(p(result.instance).databaseVersion).toBe("POSTGRES_15");
100
126
  });
101
127
 
102
128
  test("enables backups by default", () => {
103
129
  const result = CloudSqlInstance({ name: "db" });
104
- expect((result.instance as any).settings.backupConfiguration.enabled).toBe(true);
130
+ expect(p(result.instance).settings.backupConfiguration.enabled).toBe(true);
105
131
  });
106
132
 
107
133
  test("high availability when requested", () => {
108
134
  const result = CloudSqlInstance({ name: "db", highAvailability: true });
109
- expect((result.instance as any).settings.availabilityType).toBe("REGIONAL");
135
+ expect(p(result.instance).settings.availabilityType).toBe("REGIONAL");
110
136
  });
111
137
  });
112
138
 
@@ -120,7 +146,7 @@ describe("GcsBucket", () => {
120
146
 
121
147
  test("uniform access enabled by default", () => {
122
148
  const result = GcsBucket({ name: "my-bucket" });
123
- expect((result.bucket as any).uniformBucketLevelAccess).toBe(true);
149
+ expect(p(result.bucket).uniformBucketLevelAccess).toBe(true);
124
150
  });
125
151
 
126
152
  test("adds lifecycle rules", () => {
@@ -128,8 +154,8 @@ describe("GcsBucket", () => {
128
154
  name: "my-bucket",
129
155
  lifecycleDeleteAfterDays: 30,
130
156
  });
131
- expect((result.bucket as any).lifecycleRule).toHaveLength(1);
132
- expect((result.bucket as any).lifecycleRule[0].condition.age).toBe(30);
157
+ expect(p(result.bucket).lifecycleRule).toHaveLength(1);
158
+ expect(p(result.bucket).lifecycleRule[0].condition.age).toBe(30);
133
159
  });
134
160
 
135
161
  test("adds encryption when kmsKeyName provided", () => {
@@ -137,17 +163,17 @@ describe("GcsBucket", () => {
137
163
  name: "my-bucket",
138
164
  kmsKeyName: "projects/p/locations/l/keyRings/kr/cryptoKeys/k",
139
165
  });
140
- expect((result.bucket as any).encryption).toBeDefined();
166
+ expect(p(result.bucket).encryption).toBeDefined();
141
167
  });
142
168
 
143
169
  test("versioning disabled by default", () => {
144
170
  const result = GcsBucket({ name: "my-bucket" });
145
- expect((result.bucket as any).versioning).toBeUndefined();
171
+ expect(p(result.bucket).versioning).toBeUndefined();
146
172
  });
147
173
 
148
174
  test("versioning enabled when requested", () => {
149
175
  const result = GcsBucket({ name: "my-bucket", versioning: true });
150
- expect((result.bucket as any).versioning.enabled).toBe(true);
176
+ expect(p(result.bucket).versioning.enabled).toBe(true);
151
177
  });
152
178
  });
153
179
 
@@ -166,8 +192,8 @@ describe("VpcNetwork", () => {
166
192
  { name: "app", ipCidrRange: "10.0.0.0/24", region: "us-central1" },
167
193
  ],
168
194
  });
169
- expect(result.subnets).toHaveLength(1);
170
- expect((result.subnets[0] as any).ipCidrRange).toBe("10.0.0.0/24");
195
+ expect(result.subnet_app).toBeDefined();
196
+ expect(p(result.subnet_app).ipCidrRange).toBe("10.0.0.0/24");
171
197
  });
172
198
 
173
199
  test("subnet references network", () => {
@@ -175,7 +201,7 @@ describe("VpcNetwork", () => {
175
201
  name: "my-vpc",
176
202
  subnets: [{ name: "app", ipCidrRange: "10.0.0.0/24", region: "us-central1" }],
177
203
  });
178
- expect((result.subnets[0] as any).networkRef.name).toBe("my-vpc");
204
+ expect(p(result.subnet_app).networkRef.name).toBe("my-vpc");
179
205
  });
180
206
 
181
207
  test("creates internal firewall by default", () => {
@@ -183,8 +209,8 @@ describe("VpcNetwork", () => {
183
209
  name: "my-vpc",
184
210
  subnets: [{ name: "app", ipCidrRange: "10.0.0.0/24", region: "us-central1" }],
185
211
  });
186
- expect(result.firewalls.length).toBeGreaterThanOrEqual(1);
187
- expect((result.firewalls[0] as any).metadata.name).toBe("my-vpc-allow-internal");
212
+ expect(result.firewallAllowInternal).toBeDefined();
213
+ expect(p(result.firewallAllowInternal).metadata.name).toBe("my-vpc-allow-internal");
188
214
  });
189
215
 
190
216
  test("creates NAT when enabled", () => {
@@ -195,7 +221,7 @@ describe("VpcNetwork", () => {
195
221
  });
196
222
  expect(result.router).toBeDefined();
197
223
  expect(result.routerNat).toBeDefined();
198
- expect((result.routerNat as any).routerRef.name).toBe("my-vpc-router");
224
+ expect(p(result.routerNat).routerRef.name).toBe("my-vpc-router");
199
225
  });
200
226
 
201
227
  test("no NAT by default", () => {
@@ -206,9 +232,8 @@ describe("VpcNetwork", () => {
206
232
 
207
233
  test("IAP SSH firewall when requested", () => {
208
234
  const result = VpcNetwork({ name: "my-vpc", allowIapSsh: true });
209
- const iapFw = result.firewalls.find((f: any) => (f.metadata as any).name.includes("iap-ssh"));
210
- expect(iapFw).toBeDefined();
211
- expect((iapFw as any).sourceRanges).toContain("35.235.240.0/20");
235
+ expect(result.firewallAllowIapSsh).toBeDefined();
236
+ expect(p(result.firewallAllowIapSsh).sourceRanges).toContain("35.235.240.0/20");
212
237
  });
213
238
  });
214
239
 
@@ -223,7 +248,7 @@ describe("PubSubPipeline", () => {
223
248
 
224
249
  test("subscription references topic", () => {
225
250
  const result = PubSubPipeline({ name: "events" });
226
- expect((result.subscription as any).topicRef.name).toBe("events-topic");
251
+ expect(p(result.subscription).topicRef.name).toBe("events-topic");
227
252
  });
228
253
 
229
254
  test("no DLQ by default", () => {
@@ -234,8 +259,8 @@ describe("PubSubPipeline", () => {
234
259
  test("creates DLQ when enabled", () => {
235
260
  const result = PubSubPipeline({ name: "events", enableDeadLetterQueue: true });
236
261
  expect(result.deadLetterTopic).toBeDefined();
237
- expect((result.deadLetterTopic as any).metadata.name).toBe("events-dlq");
238
- expect((result.subscription as any).deadLetterPolicy.deadLetterTopicRef.name).toBe("events-dlq");
262
+ expect(p(result.deadLetterTopic).metadata.name).toBe("events-dlq");
263
+ expect(p(result.subscription).deadLetterPolicy.deadLetterTopicRef.name).toBe("events-dlq");
239
264
  });
240
265
 
241
266
  test("creates subscriber IAM when service account provided", () => {
@@ -244,18 +269,18 @@ describe("PubSubPipeline", () => {
244
269
  subscriberServiceAccount: "worker@project.iam.gserviceaccount.com",
245
270
  });
246
271
  expect(result.subscriberIam).toBeDefined();
247
- expect((result.subscriberIam as any).role).toBe("roles/pubsub.subscriber");
272
+ expect(p(result.subscriberIam).role).toBe("roles/pubsub.subscriber");
248
273
  });
249
274
 
250
275
  test("sets namespace when provided", () => {
251
276
  const result = PubSubPipeline({ name: "events", namespace: "infra" });
252
- expect((result.topic as any).metadata.namespace).toBe("infra");
253
- expect((result.subscription as any).metadata.namespace).toBe("infra");
277
+ expect(p(result.topic).metadata.namespace).toBe("infra");
278
+ expect(p(result.subscription).metadata.namespace).toBe("infra");
254
279
  });
255
280
 
256
281
  test("includes managed-by label", () => {
257
282
  const result = PubSubPipeline({ name: "events" });
258
- expect((result.topic as any).metadata.labels["app.kubernetes.io/managed-by"]).toBe("chant");
283
+ expect(p(result.topic).metadata.labels["app.kubernetes.io/managed-by"]).toBe("chant");
259
284
  });
260
285
  });
261
286
 
@@ -289,8 +314,8 @@ describe("CloudFunctionWithTrigger", () => {
289
314
  publicAccess: true,
290
315
  });
291
316
  expect(result.invokerIam).toBeDefined();
292
- expect((result.invokerIam as any).member).toBe("allUsers");
293
- expect((result.invokerIam as any).role).toBe("roles/cloudfunctions.invoker");
317
+ expect(p(result.invokerIam).member).toBe("allUsers");
318
+ expect(p(result.invokerIam).role).toBe("roles/cloudfunctions.invoker");
294
319
  });
295
320
 
296
321
  test("sets runtime and entry point", () => {
@@ -299,8 +324,8 @@ describe("CloudFunctionWithTrigger", () => {
299
324
  runtime: "python312",
300
325
  entryPoint: "main",
301
326
  });
302
- expect((result.function as any).runtime).toBe("python312");
303
- expect((result.function as any).entryPoint).toBe("main");
327
+ expect(p(result.function).runtime).toBe("python312");
328
+ expect(p(result.function).entryPoint).toBe("main");
304
329
  });
305
330
 
306
331
  test("configures pubsub trigger", () => {
@@ -311,8 +336,8 @@ describe("CloudFunctionWithTrigger", () => {
311
336
  triggerType: "pubsub",
312
337
  triggerTopic: "my-topic",
313
338
  });
314
- expect((result.function as any).eventTrigger).toBeDefined();
315
- expect((result.function as any).eventTrigger.pubsubTopic).toBe("my-topic");
339
+ expect(p(result.function).eventTrigger).toBeDefined();
340
+ expect(p(result.function).eventTrigger.pubsubTopic).toBe("my-topic");
316
341
  });
317
342
  });
318
343
 
@@ -327,12 +352,12 @@ describe("PrivateService", () => {
327
352
 
328
353
  test("address references network", () => {
329
354
  const result = PrivateService({ name: "db", networkName: "my-vpc" });
330
- expect((result.globalAddress as any).networkRef.name).toBe("my-vpc");
355
+ expect(p(result.globalAddress).networkRef.name).toBe("my-vpc");
331
356
  });
332
357
 
333
358
  test("connection references network", () => {
334
359
  const result = PrivateService({ name: "db", networkName: "my-vpc" });
335
- expect((result.serviceConnection as any).networkRef.name).toBe("my-vpc");
360
+ expect(p(result.serviceConnection).networkRef.name).toBe("my-vpc");
336
361
  });
337
362
 
338
363
  test("no DNS by default", () => {
@@ -343,7 +368,7 @@ describe("PrivateService", () => {
343
368
  test("creates DNS zone when enabled", () => {
344
369
  const result = PrivateService({ name: "db", networkName: "my-vpc", enableDns: true });
345
370
  expect(result.dnsZone).toBeDefined();
346
- expect((result.dnsZone as any).visibility).toBe("private");
371
+ expect(p(result.dnsZone).visibility).toBe("private");
347
372
  });
348
373
  });
349
374
 
@@ -357,7 +382,7 @@ describe("ManagedCertificate", () => {
357
382
 
358
383
  test("certificate includes domains", () => {
359
384
  const result = ManagedCertificate({ name: "my-cert", domains: ["example.com", "www.example.com"] });
360
- expect((result.certificate as any).managed.domains).toEqual(["example.com", "www.example.com"]);
385
+ expect(p(result.certificate).managed.domains).toEqual(["example.com", "www.example.com"]);
361
386
  });
362
387
 
363
388
  test("no proxy by default", () => {
@@ -375,7 +400,7 @@ describe("ManagedCertificate", () => {
375
400
  });
376
401
  expect(result.targetHttpsProxy).toBeDefined();
377
402
  expect(result.urlMap).toBeDefined();
378
- expect((result.urlMap as any).defaultService.backendServiceRef.name).toBe("my-backend");
403
+ expect(p(result.urlMap).defaultService.backendServiceRef.name).toBe("my-backend");
379
404
  });
380
405
  });
381
406
 
@@ -386,19 +411,26 @@ describe("SecureProject", () => {
386
411
  const result = SecureProject({ name: "my-project" });
387
412
  expect(result.project).toBeDefined();
388
413
  expect(result.auditConfig).toBeDefined();
389
- expect(result.services.length).toBeGreaterThan(0);
414
+ // Default 5 APIs become service_compute, service_container, etc.
415
+ expect(result.service_compute).toBeDefined();
416
+ expect(result.service_container).toBeDefined();
417
+ expect(result.service_iam).toBeDefined();
418
+ expect(result.service_logging).toBeDefined();
419
+ expect(result.service_monitoring).toBeDefined();
390
420
  });
391
421
 
392
422
  test("audit config covers all services", () => {
393
423
  const result = SecureProject({ name: "my-project" });
394
- expect((result.auditConfig as any).service).toBe("allServices");
395
- expect((result.auditConfig as any).auditLogConfigs.length).toBe(3);
424
+ expect(p(result.auditConfig).service).toBe("allServices");
425
+ expect(p(result.auditConfig).auditLogConfigs.length).toBe(3);
396
426
  });
397
427
 
398
428
  test("enables default APIs", () => {
399
429
  const result = SecureProject({ name: "my-project" });
400
- expect(result.services.length).toBe(5);
401
- expect(result.services.some((s: any) => s.resourceID === "compute.googleapis.com")).toBe(true);
430
+ // 5 services as individual members
431
+ const serviceKeys = Object.keys(result.members).filter((k) => k.startsWith("service_"));
432
+ expect(serviceKeys.length).toBe(5);
433
+ expect(p(result.service_compute).resourceID).toBe("compute.googleapis.com");
402
434
  });
403
435
 
404
436
  test("no owner IAM by default", () => {
@@ -412,8 +444,8 @@ describe("SecureProject", () => {
412
444
  owner: "user:admin@example.com",
413
445
  });
414
446
  expect(result.ownerIam).toBeDefined();
415
- expect((result.ownerIam as any).member).toBe("user:admin@example.com");
416
- expect((result.ownerIam as any).role).toBe("roles/owner");
447
+ expect(p(result.ownerIam).member).toBe("user:admin@example.com");
448
+ expect(p(result.ownerIam).role).toBe("roles/owner");
417
449
  });
418
450
 
419
451
  test("creates logging sink when destination provided", () => {
@@ -422,7 +454,7 @@ describe("SecureProject", () => {
422
454
  loggingSinkDestination: "bigquery.googleapis.com/projects/my-project/datasets/audit_logs",
423
455
  });
424
456
  expect(result.loggingSink).toBeDefined();
425
- expect((result.loggingSink as any).destination).toContain("bigquery");
457
+ expect(p(result.loggingSink).destination).toContain("bigquery");
426
458
  });
427
459
 
428
460
  test("no logging sink by default", () => {
@@ -2,6 +2,9 @@
2
2
  * GcsBucket composite — StorageBucket with encryption, uniform access, and lifecycle.
3
3
  */
4
4
 
5
+ import { Composite, mergeDefaults } from "@intentius/chant";
6
+ import { StorageBucket } from "../generated";
7
+
5
8
  export interface GcsBucketProps {
6
9
  /** Bucket name. */
7
10
  name: string;
@@ -23,10 +26,10 @@ export interface GcsBucketProps {
23
26
  labels?: Record<string, string>;
24
27
  /** Namespace for all resources. */
25
28
  namespace?: string;
26
- }
27
-
28
- export interface GcsBucketResult {
29
- bucket: Record<string, unknown>;
29
+ /** Per-member defaults for customizing individual resources. */
30
+ defaults?: {
31
+ bucket?: Partial<ConstructorParameters<typeof StorageBucket>[0]>;
32
+ };
30
33
  }
31
34
 
32
35
  /**
@@ -44,7 +47,7 @@ export interface GcsBucketResult {
44
47
  * });
45
48
  * ```
46
49
  */
47
- export function GcsBucket(props: GcsBucketProps): GcsBucketResult {
50
+ export const GcsBucket = Composite<GcsBucketProps>((props) => {
48
51
  const {
49
52
  name,
50
53
  location = "US",
@@ -56,6 +59,7 @@ export function GcsBucket(props: GcsBucketProps): GcsBucketResult {
56
59
  lifecycleNearlineAfterDays,
57
60
  labels: extraLabels = {},
58
61
  namespace,
62
+ defaults: defs,
59
63
  } = props;
60
64
 
61
65
  const commonLabels: Record<string, string> = {
@@ -75,7 +79,7 @@ export function GcsBucket(props: GcsBucketProps): GcsBucketResult {
75
79
  }
76
80
 
77
81
  if (kmsKeyName) {
78
- spec.encryption = { defaultKmsKeyName: kmsKeyName };
82
+ spec.encryption = { kmsKeyRef: { external: kmsKeyName } };
79
83
  }
80
84
 
81
85
  const lifecycleRules: Array<Record<string, unknown>> = [];
@@ -98,14 +102,14 @@ export function GcsBucket(props: GcsBucketProps): GcsBucketResult {
98
102
  spec.lifecycleRule = lifecycleRules;
99
103
  }
100
104
 
101
- const bucket: Record<string, unknown> = {
105
+ const bucket = new StorageBucket(mergeDefaults({
102
106
  metadata: {
103
107
  name,
104
108
  ...(namespace && { namespace }),
105
109
  labels: { ...commonLabels, "app.kubernetes.io/component": "storage" },
106
110
  },
107
111
  ...spec,
108
- };
112
+ } as Record<string, unknown>, defs?.bucket));
109
113
 
110
114
  return { bucket };
111
- }
115
+ }, "GcsBucket");