@google-cloud/cloud-run-mcp 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +24 -0
  2. package/SECURITY.md +7 -0
  3. package/cloudbuild.yaml +37 -0
  4. package/example-sources-to-deploy/java/pom.xml +41 -0
  5. package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
  6. package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
  7. package/example-sources-to-deploy/nodejs/index.js +15 -0
  8. package/example-sources-to-deploy/nodejs/package.json +12 -0
  9. package/example-sources-to-deploy/python/pip-project/main.py +11 -0
  10. package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
  11. package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
  12. package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
  13. package/lib/clients.js +104 -3
  14. package/lib/cloud-api/build.js +192 -28
  15. package/lib/cloud-api/run.js +62 -0
  16. package/lib/cloud-api/secrets.js +196 -0
  17. package/lib/cloud-api/storage.js +164 -16
  18. package/lib/deployment/compose.js +517 -0
  19. package/lib/deployment/constants.js +8 -0
  20. package/lib/deployment/deployer.js +283 -28
  21. package/lib/deployment/helpers.js +58 -1
  22. package/lib/deployment/universal-maker.js +21 -148
  23. package/lib/util/archive.js +3 -3
  24. package/lib/util/artifacts.js +189 -0
  25. package/lib/util/helpers.js +109 -0
  26. package/mcp-server.js +4 -0
  27. package/package.json +6 -3
  28. package/skills/cloud-run/SKILL.md +109 -0
  29. package/test/local/archive.test.js +88 -0
  30. package/test/local/clients.test.js +224 -2
  31. package/test/local/cloud-api/build.test.js +330 -25
  32. package/test/local/cloud-api/run.test.js +129 -0
  33. package/test/local/cloud-api/secrets.test.js +291 -0
  34. package/test/local/cloud-api/storage.test.js +405 -0
  35. package/test/local/compose.test.js +618 -0
  36. package/test/local/deploy.test.js +408 -0
  37. package/test/local/deployment-helpers.test.js +100 -1
  38. package/test/local/test-utils.js +2 -18
  39. package/test/local/tools.test.js +43 -1
  40. package/test/local/universal-maker.test.js +23 -114
  41. package/test/local/util.test.js +361 -0
  42. package/test/need-gcp/deployer.test.js +1 -1
  43. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  44. package/tools/register-tools.js +19 -3
  45. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  46. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  47. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -32,6 +32,7 @@ describe('triggerCloudBuild', () => {
32
32
  let servicePathMock;
33
33
  let locationPathMock;
34
34
  let submitBuildMock;
35
+ let createBuildMock;
35
36
  let getBuildMock;
36
37
  let getEntriesMock;
37
38
  let context;
@@ -56,6 +57,18 @@ describe('triggerCloudBuild', () => {
56
57
  getEntriesMock = mock.fn(() =>
57
58
  Promise.resolve([[{ data: 'log line 1' }, { data: 'log line 2' }]])
58
59
  );
60
+ createBuildMock = mock.fn(() =>
61
+ Promise.resolve([
62
+ {
63
+ name: `projects/mock-project/locations/mock-location/operations/mock-op-id`,
64
+ metadata: {
65
+ build: {
66
+ id: mockBuildId,
67
+ },
68
+ },
69
+ },
70
+ ])
71
+ );
59
72
  setTimeoutMock = mock.fn((cb) => cb());
60
73
  mock.method(global, 'setTimeout', setTimeoutMock);
61
74
 
@@ -71,6 +84,7 @@ describe('triggerCloudBuild', () => {
71
84
  submitBuild: submitBuildMock,
72
85
  },
73
86
  cloudBuildClient: {
87
+ createBuild: createBuildMock,
74
88
  getBuild: getBuildMock,
75
89
  },
76
90
  loggingClient: {
@@ -90,15 +104,20 @@ describe('triggerCloudBuild', () => {
90
104
  },
91
105
  '../../../lib/util/helpers.js': {
92
106
  logAndProgress: logAndProgressMock,
107
+ sanitizeCloudRunServiceName: (name) =>
108
+ name
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9]+/g, '-')
111
+ .replace(/^-+|-+$/g, ''),
93
112
  },
94
113
  '../../../lib/cloud-api/run.js': {
95
114
  checkCloudRunServiceExists: checkServiceMock,
96
115
  },
97
116
  '../../../lib/clients.js': {
98
117
  getRunClient: () => Promise.resolve(context.runClient),
99
- getBuildsClient: () => Promise.resolve(context.buildsClient),
100
118
  getCloudBuildClient: () => Promise.resolve(context.cloudBuildClient),
101
119
  getLoggingClient: () => Promise.resolve(context.loggingClient),
120
+ getBuildsClient: () => Promise.resolve(context.buildsClient),
102
121
  },
103
122
  });
104
123
  }
@@ -113,15 +132,23 @@ describe('triggerCloudBuild', () => {
113
132
  'mock-repo',
114
133
  'gcr.io/mock-project/mock-image',
115
134
  true,
116
- () => {}
135
+ 'mock-token',
136
+ () => {},
137
+ undefined
117
138
  );
118
139
 
119
140
  assert.deepStrictEqual(result, mockSuccessResult);
120
141
  assert.strictEqual(checkServiceMock.mock.callCount(), 1);
121
- assert.strictEqual(submitBuildMock.mock.callCount(), 1);
142
+ assert.strictEqual(createBuildMock.mock.callCount(), 1);
122
143
  assert.strictEqual(getBuildMock.mock.callCount(), 1);
123
144
  assert.strictEqual(createServiceMock.mock.callCount(), 1); // 1 dry run
124
- assert.strictEqual(updateServiceMock.mock.callCount(), 0);
145
+
146
+ // Verify sanitized service name was used in dry run
147
+ const dryRunCall = createServiceMock.mock.calls[0].arguments[0];
148
+ assert.strictEqual(
149
+ dryRunCall.serviceId,
150
+ 'mock-image' // sanitized from gcr.io/.../mock-image
151
+ );
125
152
 
126
153
  const { calls: logCalls } = logAndProgressMock.mock;
127
154
  assert.match(logCalls[0].arguments[0], /Performing dry-run creation/);
@@ -143,11 +170,13 @@ describe('triggerCloudBuild', () => {
143
170
  'mock-repo',
144
171
  'gcr.io/mock-project/mock-image',
145
172
  true,
146
- () => {}
173
+ 'mock-token',
174
+ () => {},
175
+ undefined
147
176
  );
148
177
 
149
178
  assert.strictEqual(checkServiceMock.mock.callCount(), 1);
150
- assert.strictEqual(submitBuildMock.mock.callCount(), 1);
179
+ assert.strictEqual(createBuildMock.mock.callCount(), 1);
151
180
  assert.strictEqual(getBuildMock.mock.callCount(), 1);
152
181
  assert.strictEqual(createServiceMock.mock.callCount(), 0);
153
182
  assert.strictEqual(updateServiceMock.mock.callCount(), 1); // 1 dry run
@@ -165,7 +194,9 @@ describe('triggerCloudBuild', () => {
165
194
  'mock-repo',
166
195
  'gcr.io/mock-project/mock-image',
167
196
  false,
168
- () => {}
197
+ 'mock-token',
198
+ () => {},
199
+ undefined
169
200
  );
170
201
 
171
202
  assert.deepStrictEqual(result, mockSuccessResult);
@@ -173,9 +204,13 @@ describe('triggerCloudBuild', () => {
173
204
  const submitBuildRequest = submitBuildMock.mock.calls[0].arguments[0];
174
205
  assert.deepStrictEqual(submitBuildRequest.buildpackBuild, {});
175
206
  assert.strictEqual(submitBuildRequest.dockerBuild, undefined);
207
+ assert.strictEqual(
208
+ submitBuildRequest.imageUri,
209
+ 'gcr.io/mock-project/mock-image'
210
+ );
176
211
  });
177
212
 
178
- it('should use docker build when Dockerfile is present', async () => {
213
+ it('should use docker build when Dockerfile is present and enable BuildKit', async () => {
179
214
  const { triggerCloudBuild } = await getTriggerCloudBuild();
180
215
  await triggerCloudBuild(
181
216
  'mock-project',
@@ -185,13 +220,16 @@ describe('triggerCloudBuild', () => {
185
220
  'mock-repo',
186
221
  'gcr.io/mock-project/mock-image',
187
222
  true, // hasDockerfile = true
188
- () => {}
223
+ 'mock-token',
224
+ () => {},
225
+ undefined
189
226
  );
190
227
 
191
- assert.strictEqual(submitBuildMock.mock.callCount(), 1);
192
- const submitBuildRequest = submitBuildMock.mock.calls[0].arguments[0];
193
- assert.deepStrictEqual(submitBuildRequest.dockerBuild, {});
194
- assert.strictEqual(submitBuildRequest.buildpackBuild, undefined);
228
+ assert.strictEqual(createBuildMock.mock.callCount(), 1);
229
+ const createBuildRequest = createBuildMock.mock.calls[0].arguments[0];
230
+ const buildSteps = createBuildRequest.build.steps;
231
+ assert.strictEqual(buildSteps[0].name, 'gcr.io/cloud-builders/docker');
232
+ assert.deepStrictEqual(buildSteps[0].env, ['DOCKER_BUILDKIT=1']);
195
233
  });
196
234
 
197
235
  it('should poll for build status until completion', async () => {
@@ -215,7 +253,9 @@ describe('triggerCloudBuild', () => {
215
253
  'mock-repo',
216
254
  'gcr.io/mock-project/mock-image',
217
255
  true,
218
- () => {}
256
+ 'mock-token',
257
+ () => {},
258
+ undefined
219
259
  );
220
260
 
221
261
  assert.strictEqual(getBuildMock.mock.callCount(), 2);
@@ -241,7 +281,9 @@ describe('triggerCloudBuild', () => {
241
281
  'mock-repo',
242
282
  'gcr.io/mock-project/mock-image',
243
283
  true,
244
- () => {}
284
+ 'mock-token',
285
+ () => {},
286
+ undefined
245
287
  ),
246
288
  (err) => {
247
289
  assert.match(err.message, /Build mock-build-id failed/);
@@ -279,11 +321,13 @@ describe('triggerCloudBuild', () => {
279
321
  'mock-repo',
280
322
  'gcr.io/mock-project/mock-image',
281
323
  true,
282
- () => {}
324
+ 'mock-token',
325
+ () => {},
326
+ undefined
283
327
  ),
284
328
  /Dry-run deployment failed: Dry run fail/
285
329
  );
286
- assert.strictEqual(submitBuildMock.mock.callCount(), 0);
330
+ assert.strictEqual(createBuildMock.mock.callCount(), 0);
287
331
  });
288
332
 
289
333
  it('should throw if dry-run update fails', async () => {
@@ -303,11 +347,13 @@ describe('triggerCloudBuild', () => {
303
347
  'mock-repo',
304
348
  'gcr.io/mock-project/mock-image',
305
349
  true,
306
- () => {}
350
+ 'mock-token',
351
+ () => {},
352
+ undefined
307
353
  ),
308
354
  /Dry-run deployment failed: Dry run fail/
309
355
  );
310
- assert.strictEqual(submitBuildMock.mock.callCount(), 0);
356
+ assert.strictEqual(createBuildMock.mock.callCount(), 0);
311
357
  });
312
358
 
313
359
  it('should throw if checkCloudRunServiceExists fails with unexpected error', async () => {
@@ -325,13 +371,37 @@ describe('triggerCloudBuild', () => {
325
371
  'mock-repo',
326
372
  'gcr.io/mock-project/mock-image',
327
373
  true,
328
- () => {}
374
+ 'mock-token',
375
+ () => {},
376
+ undefined
329
377
  ),
330
378
  /Permission denied/
331
379
  );
332
380
  });
333
381
 
334
- it('should throw if submitBuild fails', async () => {
382
+ it('should throw if createBuild fails (hasDockerfile = true)', async () => {
383
+ createBuildMock = mock.fn(() => Promise.reject(new Error('Submit failed')));
384
+ context.cloudBuildClient.createBuild = createBuildMock;
385
+ const { triggerCloudBuild } = await getTriggerCloudBuild();
386
+ await assert.rejects(
387
+ () =>
388
+ triggerCloudBuild(
389
+ 'mock-project',
390
+ 'mock-location',
391
+ 'mock-bucket',
392
+ 'mock-blob',
393
+ 'mock-repo',
394
+ 'gcr.io/mock-project/mock-image',
395
+ true,
396
+ 'mock-token',
397
+ () => {},
398
+ undefined
399
+ ),
400
+ /Submit failed/
401
+ );
402
+ });
403
+
404
+ it('should throw if submitBuild fails (hasDockerfile = false)', async () => {
335
405
  submitBuildMock = mock.fn(() => Promise.reject(new Error('Submit failed')));
336
406
  context.buildsClient.submitBuild = submitBuildMock;
337
407
  const { triggerCloudBuild } = await getTriggerCloudBuild();
@@ -344,8 +414,10 @@ describe('triggerCloudBuild', () => {
344
414
  'mock-blob',
345
415
  'mock-repo',
346
416
  'gcr.io/mock-project/mock-image',
347
- true,
348
- () => {}
417
+ false,
418
+ 'mock-token',
419
+ () => {},
420
+ undefined
349
421
  ),
350
422
  /Submit failed/
351
423
  );
@@ -368,7 +440,9 @@ describe('triggerCloudBuild', () => {
368
440
  'mock-repo',
369
441
  'gcr.io/mock-project/mock-image',
370
442
  true,
371
- () => {}
443
+ 'mock-token',
444
+ () => {},
445
+ undefined
372
446
  ),
373
447
  (err) => {
374
448
  assert.match(err.message, /Build mock-build-id failed/);
@@ -401,7 +475,9 @@ describe('triggerCloudBuild', () => {
401
475
  'mock-repo',
402
476
  'gcr.io/mock-project/mock-image',
403
477
  true,
404
- () => {}
478
+ 'mock-token',
479
+ () => {},
480
+ undefined
405
481
  ),
406
482
  (err) => {
407
483
  assert.match(err.message, /Build mock-build-id failed/);
@@ -417,4 +493,233 @@ describe('triggerCloudBuild', () => {
417
493
  /Failed to fetch build logs snippet/
418
494
  );
419
495
  });
496
+
497
+ it('should pass ingress to dry run creation', async () => {
498
+ const { triggerCloudBuild } = await getTriggerCloudBuild();
499
+ await triggerCloudBuild(
500
+ 'mock-project',
501
+ 'mock-location',
502
+ 'mock-bucket',
503
+ 'mock-blob',
504
+ 'mock-repo',
505
+ 'gcr.io/mock-project/mock-image',
506
+ true,
507
+ 'mock-token',
508
+ () => {},
509
+ 'INGRESS_TRAFFIC_INTERNAL_ONLY'
510
+ );
511
+
512
+ assert.strictEqual(createServiceMock.mock.callCount(), 1);
513
+ const service = createServiceMock.mock.calls[0].arguments[0].service;
514
+ assert.strictEqual(service.ingress, 'INGRESS_TRAFFIC_INTERNAL_ONLY');
515
+ });
516
+ });
517
+
518
+ describe('composeBuild', () => {
519
+ let ensureARMock;
520
+ let ensureBucketMock;
521
+ let uploadToBucketMock;
522
+ let zipFilesMock;
523
+ let fsExistsSyncMock;
524
+ let logAndProgressMock;
525
+ let checkServiceMock;
526
+ let createBuildMock;
527
+ let getBuildMock;
528
+ let context;
529
+ const mockBuildId = 'mock-build-id';
530
+ const mockSuccessResult = {
531
+ results: {
532
+ images: [
533
+ {
534
+ name: 'us-central1-docker.pkg.dev/mock-project/mock-repo/default_web:latest',
535
+ },
536
+ ],
537
+ },
538
+ status: 'SUCCESS',
539
+ };
540
+
541
+ beforeEach(() => {
542
+ logAndProgressMock = mock.fn();
543
+ ensureARMock = mock.fn(() => Promise.resolve());
544
+ ensureBucketMock = mock.fn(() => Promise.resolve({}));
545
+ uploadToBucketMock = mock.fn(() => Promise.resolve());
546
+ zipFilesMock = mock.fn(() => Promise.resolve(Buffer.from('mock-zip')));
547
+ fsExistsSyncMock = mock.fn(() => true);
548
+ checkServiceMock = mock.fn(() => Promise.resolve(false));
549
+ createBuildMock = mock.fn(() =>
550
+ Promise.resolve([
551
+ {
552
+ metadata: { build: { id: mockBuildId } },
553
+ },
554
+ ])
555
+ );
556
+ getBuildMock = mock.fn(() => Promise.resolve([mockSuccessResult]));
557
+
558
+ context = {
559
+ runClient: {
560
+ createService: mock.fn(() => Promise.resolve()),
561
+ updateService: mock.fn(() => Promise.resolve()),
562
+ },
563
+ cloudBuildClient: {
564
+ createBuild: createBuildMock,
565
+ getBuild: getBuildMock,
566
+ },
567
+ buildsClient: {
568
+ submitBuild: mock.fn(() =>
569
+ Promise.resolve([
570
+ {
571
+ buildOperation: {
572
+ name: 'projects/p/locations/l/operations/bW9jay1pZA==',
573
+ },
574
+ },
575
+ ])
576
+ ),
577
+ },
578
+ };
579
+ });
580
+
581
+ async function getBuildModule() {
582
+ return await esmock('../../../lib/cloud-api/build.js', {
583
+ 'node:fs': {
584
+ default: {
585
+ existsSync: fsExistsSyncMock,
586
+ },
587
+ existsSync: fsExistsSyncMock,
588
+ },
589
+ 'node:path': {
590
+ default: {
591
+ join: (...args) => args.join('/'),
592
+ },
593
+ join: (...args) => args.join('/'),
594
+ },
595
+ '../../../lib/cloud-api/helpers.js': {
596
+ callWithRetry: (fn) => fn(),
597
+ },
598
+ '../../../lib/util/helpers.js': {
599
+ logAndProgress: logAndProgressMock,
600
+ calculateSourceFingerprint: () => Promise.resolve('mock-fingerprint'),
601
+ },
602
+ '../../../lib/cloud-api/run.js': {
603
+ checkCloudRunServiceExists: checkServiceMock,
604
+ },
605
+ '../../../lib/clients.js': {
606
+ getRunClient: () => Promise.resolve(context.runClient),
607
+ getCloudBuildClient: () => Promise.resolve(context.cloudBuildClient),
608
+ getLoggingClient: () => Promise.resolve({}),
609
+ getBuildsClient: () => Promise.resolve(context.buildsClient),
610
+ },
611
+ '../../../lib/cloud-api/registry.js': {
612
+ ensureArtifactRegistryRepoExists: ensureARMock,
613
+ },
614
+ '../../../lib/cloud-api/storage.js': {
615
+ ensureStorageBucketExists: ensureBucketMock,
616
+ uploadToStorageBucket: uploadToBucketMock,
617
+ },
618
+ '../../../lib/util/archive.js': {
619
+ zipFiles: zipFilesMock,
620
+ },
621
+ '../../../lib/deployment/constants.js': {
622
+ DEPLOYMENT_CONFIG: {
623
+ REPO_NAME: 'mock-repo',
624
+ IMAGE_TAG: 'latest',
625
+ },
626
+ },
627
+ });
628
+ }
629
+
630
+ it('should successfully build multiple services', async () => {
631
+ const resourcesConfig = {
632
+ source_builds: {
633
+ web: { context: 'web_dir' },
634
+ api: { context: 'api_dir' },
635
+ },
636
+ };
637
+ const { composeBuild } = await getBuildModule();
638
+
639
+ const result = await composeBuild(
640
+ resourcesConfig,
641
+ 'mock-token',
642
+ 'mock-project',
643
+ 'us-central1',
644
+ '/folder',
645
+ () => {}
646
+ );
647
+
648
+ assert.strictEqual(
649
+ result.source_builds.web.image_id,
650
+ 'us-central1-docker.pkg.dev/mock-project/mock-repo/default_web:latest'
651
+ );
652
+ assert.strictEqual(
653
+ result.source_builds.api.image_id,
654
+ 'us-central1-docker.pkg.dev/mock-project/mock-repo/default_web:latest'
655
+ );
656
+ assert.strictEqual(ensureARMock.mock.callCount(), 1);
657
+ assert.strictEqual(zipFilesMock.mock.callCount(), 2);
658
+ assert.strictEqual(ensureBucketMock.mock.callCount(), 1);
659
+ const ensureBucketCall = ensureBucketMock.mock.calls[0];
660
+ assert.strictEqual(ensureBucketCall.arguments[0], 'mock-project');
661
+ assert.strictEqual(
662
+ ensureBucketCall.arguments[1],
663
+ 'mock-project_cloudbuild'
664
+ );
665
+ assert.strictEqual(ensureBucketCall.arguments[2], 'us-central1');
666
+ assert.strictEqual(ensureBucketCall.arguments[3], 'mock-token');
667
+ assert.strictEqual(ensureBucketCall.arguments[4], undefined);
668
+ assert.strictEqual(ensureBucketCall.arguments[5], false);
669
+ assert.strictEqual(uploadToBucketMock.mock.callCount(), 2);
670
+
671
+ // Verify archive name pattern source/{epoch}.{fingerprint}.tgz
672
+ assert.match(
673
+ uploadToBucketMock.mock.calls[0].arguments[2],
674
+ /^source\/\d+\.mock-fingerprint\.tgz$/
675
+ );
676
+
677
+ assert.strictEqual(createBuildMock.mock.callCount(), 2);
678
+ });
679
+
680
+ it('should handle service with Dockerfile', async () => {
681
+ fsExistsSyncMock.mock.mockImplementation((p) => p.endsWith('Dockerfile'));
682
+ const resourcesConfig = {
683
+ source_builds: {
684
+ web: { context: 'web_dir' },
685
+ },
686
+ };
687
+ const { composeBuild } = await getBuildModule();
688
+
689
+ await composeBuild(
690
+ resourcesConfig,
691
+ 'mock-token',
692
+ 'mock-project',
693
+ 'us-central1',
694
+ '/folder',
695
+ () => {}
696
+ );
697
+
698
+ const createBuildArgs = createBuildMock.mock.calls[0].arguments[0];
699
+ assert.strictEqual(
700
+ createBuildArgs.build.steps[0].name,
701
+ 'gcr.io/cloud-builders/docker'
702
+ );
703
+ });
704
+
705
+ it('should handle service without Dockerfile (buildpacks)', async () => {
706
+ fsExistsSyncMock.mock.mockImplementation(() => false);
707
+ const resourcesConfig = {
708
+ source_builds: {
709
+ web: { context: 'web_dir' },
710
+ },
711
+ };
712
+ const { composeBuild } = await getBuildModule();
713
+
714
+ await composeBuild(
715
+ resourcesConfig,
716
+ 'mock-token',
717
+ 'mock-project',
718
+ 'us-central1',
719
+ '/folder',
720
+ () => {}
721
+ );
722
+
723
+ assert.strictEqual(context.buildsClient.submitBuild.mock.callCount(), 1);
724
+ });
420
725
  });
@@ -0,0 +1,129 @@
1
+ /*
2
+ Copyright 2025 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import assert from 'node:assert/strict';
18
+ import { describe, it, mock } from 'node:test';
19
+ import esmock from 'esmock';
20
+
21
+ describe('Cloud Run API Helpers', () => {
22
+ const projectId = 'test-project';
23
+ const location = 'europe-west1';
24
+ const serviceId = 'test-service';
25
+ const accessToken = 'test-token';
26
+ const resource = `projects/${projectId}/locations/${location}/services/${serviceId}`;
27
+
28
+ it('setServicePublicAccess should set IAM policy for allUsers', async () => {
29
+ const getIamPolicyMock = mock.fn(async () => [{ bindings: [] }]);
30
+ const setIamPolicyMock = mock.fn(async () => ({}));
31
+ const runClientMock = {
32
+ getIamPolicy: getIamPolicyMock,
33
+ setIamPolicy: setIamPolicyMock,
34
+ servicePath: mock.fn(() => resource),
35
+ };
36
+
37
+ const getRunClientMock = mock.fn(async () => runClientMock);
38
+ const logAndProgressMock = mock.fn();
39
+ const callWithRetryMock = mock.fn((fn) => fn());
40
+
41
+ const { setServicePublicAccess } = await esmock(
42
+ '../../../lib/cloud-api/run.js',
43
+ {
44
+ '../../../lib/clients.js': {
45
+ getRunClient: getRunClientMock,
46
+ },
47
+ '../../../lib/cloud-api/helpers.js': {
48
+ callWithRetry: callWithRetryMock,
49
+ },
50
+ '../../../lib/util/helpers.js': {
51
+ logAndProgress: logAndProgressMock,
52
+ },
53
+ }
54
+ );
55
+
56
+ await setServicePublicAccess(
57
+ projectId,
58
+ location,
59
+ serviceId,
60
+ accessToken,
61
+ logAndProgressMock
62
+ );
63
+
64
+ assert.equal(getRunClientMock.mock.callCount(), 1);
65
+ assert.deepEqual(getRunClientMock.mock.calls[0].arguments, [
66
+ projectId,
67
+ accessToken,
68
+ ]);
69
+
70
+ assert.equal(getIamPolicyMock.mock.callCount(), 1);
71
+ assert.equal(setIamPolicyMock.mock.callCount(), 1);
72
+ const setIamPolicyRequest = setIamPolicyMock.mock.calls[0].arguments[0];
73
+ assert.equal(setIamPolicyRequest.resource, resource);
74
+ assert.deepEqual(setIamPolicyRequest.policy.bindings[0], {
75
+ role: 'roles/run.invoker',
76
+ members: ['allUsers'],
77
+ });
78
+
79
+ assert.ok(
80
+ logAndProgressMock.mock.calls.some((call) =>
81
+ call.arguments[0].includes(
82
+ `Setting public access for service ${serviceId}`
83
+ )
84
+ )
85
+ );
86
+ });
87
+
88
+ it('setServicePublicAccess should throw if setIamPolicy fails', async () => {
89
+ const error = new Error('IAM Update Failed');
90
+ const getIamPolicyMock = mock.fn(async () => [{ bindings: [] }]);
91
+ const setIamPolicyMock = mock.fn(async () => {
92
+ throw error;
93
+ });
94
+ const runClientMock = {
95
+ getIamPolicy: getIamPolicyMock,
96
+ setIamPolicy: setIamPolicyMock,
97
+ servicePath: mock.fn(() => resource),
98
+ };
99
+
100
+ const getRunClientMock = mock.fn(async () => runClientMock);
101
+ const logAndProgressMock = mock.fn();
102
+
103
+ const { setServicePublicAccess } = await esmock(
104
+ '../../../lib/cloud-api/run.js',
105
+ {
106
+ '../../../lib/clients.js': {
107
+ getRunClient: getRunClientMock,
108
+ },
109
+ '../../../lib/cloud-api/helpers.js': {
110
+ callWithRetry: (fn) => fn(),
111
+ },
112
+ '../../../lib/util/helpers.js': {
113
+ logAndProgress: logAndProgressMock,
114
+ },
115
+ }
116
+ );
117
+
118
+ await assert.rejects(
119
+ setServicePublicAccess(
120
+ projectId,
121
+ location,
122
+ serviceId,
123
+ accessToken,
124
+ logAndProgressMock
125
+ ),
126
+ error
127
+ );
128
+ });
129
+ });