@google-cloud/cloud-run-mcp 1.9.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 (50) 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/gemini-extension.json +12 -0
  14. package/lib/clients.js +104 -3
  15. package/lib/cloud-api/build.js +192 -28
  16. package/lib/cloud-api/run.js +62 -0
  17. package/lib/cloud-api/secrets.js +196 -0
  18. package/lib/cloud-api/storage.js +164 -16
  19. package/lib/deployment/compose.js +517 -0
  20. package/lib/deployment/constants.js +11 -0
  21. package/lib/deployment/deployer.js +334 -30
  22. package/lib/deployment/helpers.js +99 -49
  23. package/lib/deployment/source-processor.js +0 -48
  24. package/lib/deployment/universal-maker.js +147 -0
  25. package/lib/util/archive.js +3 -3
  26. package/lib/util/artifacts.js +189 -0
  27. package/lib/util/helpers.js +109 -0
  28. package/mcp-server.js +4 -0
  29. package/package.json +6 -3
  30. package/skills/cloud-run/SKILL.md +109 -0
  31. package/test/local/archive.test.js +88 -0
  32. package/test/local/clients.test.js +224 -2
  33. package/test/local/cloud-api/build.test.js +330 -25
  34. package/test/local/cloud-api/run.test.js +129 -0
  35. package/test/local/cloud-api/secrets.test.js +291 -0
  36. package/test/local/cloud-api/storage.test.js +405 -0
  37. package/test/local/compose.test.js +618 -0
  38. package/test/local/deploy.test.js +408 -0
  39. package/test/local/deployment-helpers.test.js +126 -79
  40. package/test/local/source-processor.test.js +0 -165
  41. package/test/local/test-utils.js +2 -18
  42. package/test/local/tools.test.js +43 -1
  43. package/test/local/universal-maker.test.js +157 -0
  44. package/test/local/util.test.js +361 -0
  45. package/test/need-gcp/deployer.test.js +1 -1
  46. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  47. package/tools/register-tools.js +19 -3
  48. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  49. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  50. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -0,0 +1,618 @@
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 { test, describe, mock, beforeEach } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import esmock from 'esmock';
20
+ import path from 'path';
21
+
22
+ describe('Compose Deployment', () => {
23
+ const osMock = {
24
+ homedir: () => '/home/user',
25
+ };
26
+
27
+ const helpersMock = {
28
+ logAndProgress: mock.fn(),
29
+ };
30
+
31
+ const artifactsMock = {
32
+ ensureRepositoryDownloaded: mock.fn(),
33
+ };
34
+
35
+ const childProcessMock = {
36
+ execFile: mock.fn(),
37
+ };
38
+
39
+ test('runCompose successfully ensures download', async () => {
40
+ artifactsMock.ensureRepositoryDownloaded.mock.resetCalls();
41
+ artifactsMock.ensureRepositoryDownloaded.mock.mockImplementation(
42
+ () => '/home/user/.cloud-run-mcp/bin/run-compose'
43
+ );
44
+
45
+ const compose = await esmock('../../lib/deployment/compose.js', {
46
+ os: osMock,
47
+ '../../lib/util/helpers.js': helpersMock,
48
+ '../../lib/util/artifacts.js': artifactsMock,
49
+ });
50
+
51
+ try {
52
+ const result = await compose.runCompose('fake-token', mock.fn());
53
+
54
+ assert.strictEqual(result, '/home/user/.cloud-run-mcp/bin/run-compose');
55
+ assert.strictEqual(
56
+ artifactsMock.ensureRepositoryDownloaded.mock.callCount(),
57
+ 1
58
+ );
59
+
60
+ // Verify parameters passed to ensureRepositoryDownloaded
61
+ const call = artifactsMock.ensureRepositoryDownloaded.mock.calls[0];
62
+ assert.strictEqual(
63
+ call.arguments[0],
64
+ '/home/user/.cloud-run-mcp/bin/run-compose'
65
+ );
66
+ assert.strictEqual(call.arguments[1].project, 'serverless-runtimes-qa');
67
+ assert.strictEqual(call.arguments[1].location, 'us-central1');
68
+ assert.strictEqual(call.arguments[1].repository, 'run-compose');
69
+ assert.strictEqual(call.arguments[2], 'fake-token');
70
+ } finally {
71
+ // No need to restore env since we didn't use it
72
+ }
73
+ });
74
+
75
+ test('runCompose returns null if download fails', async () => {
76
+ artifactsMock.ensureRepositoryDownloaded.mock.resetCalls();
77
+ artifactsMock.ensureRepositoryDownloaded.mock.mockImplementation(
78
+ () => null
79
+ );
80
+
81
+ const compose = await esmock('../../lib/deployment/compose.js', {
82
+ os: osMock,
83
+ '../../lib/util/helpers.js': helpersMock,
84
+ '../../lib/util/artifacts.js': artifactsMock,
85
+ });
86
+
87
+ const result = await compose.runCompose('fake-token', mock.fn());
88
+
89
+ assert.strictEqual(result, null);
90
+ assert.strictEqual(
91
+ artifactsMock.ensureRepositoryDownloaded.mock.callCount(),
92
+ 1
93
+ );
94
+ });
95
+
96
+ test('runCompose returns null if platform not supported', async () => {
97
+ // Save original platform
98
+ const originalPlatform = process.platform;
99
+ Object.defineProperty(process, 'platform', {
100
+ value: 'win32',
101
+ configurable: true,
102
+ });
103
+
104
+ try {
105
+ const compose = await esmock('../../lib/deployment/compose.js', {
106
+ os: osMock,
107
+ '../../lib/util/helpers.js': helpersMock,
108
+ '../../lib/util/artifacts.js': artifactsMock,
109
+ });
110
+
111
+ const result = await compose.runCompose('fake-token', mock.fn());
112
+ assert.strictEqual(result, null);
113
+ } finally {
114
+ Object.defineProperty(process, 'platform', {
115
+ value: originalPlatform,
116
+ configurable: true,
117
+ });
118
+ }
119
+ });
120
+
121
+ test('resourceCompose returns stdout on success', async () => {
122
+ childProcessMock.execFile.mock.resetCalls();
123
+ childProcessMock.execFile.mock.mockImplementation(
124
+ (file, args, opts, cb) => {
125
+ if (typeof opts === 'function') {
126
+ cb = opts;
127
+ }
128
+ cb(null, { stdout: '{"resources": []}', stderr: '' });
129
+ }
130
+ );
131
+
132
+ const compose = await esmock('../../lib/deployment/compose.js', {
133
+ child_process: childProcessMock,
134
+ '../../lib/util/helpers.js': helpersMock,
135
+ });
136
+
137
+ const result = await compose.resourceCompose(
138
+ '/bin/run-compose',
139
+ '/path/to/compose.yaml',
140
+ 'us-central1',
141
+ mock.fn()
142
+ );
143
+
144
+ assert.strictEqual(result, '{"resources": []}');
145
+ assert.strictEqual(childProcessMock.execFile.mock.callCount(), 1);
146
+ const call = childProcessMock.execFile.mock.calls[0];
147
+ assert.strictEqual(call.arguments[0], '/bin/run-compose');
148
+ assert.deepEqual(call.arguments[1], [
149
+ 'resource',
150
+ '/path/to/compose.yaml',
151
+ '--region',
152
+ 'us-central1',
153
+ '--out',
154
+ '.',
155
+ ]);
156
+ assert.strictEqual(call.arguments[2].cwd, '/path/to');
157
+ });
158
+
159
+ test('resourceCompose logs warning on stderr but returns stdout', async () => {
160
+ childProcessMock.execFile.mock.resetCalls();
161
+ helpersMock.logAndProgress.mock.resetCalls();
162
+ childProcessMock.execFile.mock.mockImplementation(
163
+ (file, args, opts, cb) => {
164
+ if (typeof opts === 'function') {
165
+ cb = opts;
166
+ }
167
+ cb(null, { stdout: 'output', stderr: 'some warning' });
168
+ }
169
+ );
170
+
171
+ const compose = await esmock('../../lib/deployment/compose.js', {
172
+ child_process: childProcessMock,
173
+ '../../lib/util/helpers.js': helpersMock,
174
+ });
175
+
176
+ const result = await compose.resourceCompose(
177
+ '/bin/run-compose',
178
+ '/path/to/compose.yaml',
179
+ 'us-central1',
180
+ mock.fn()
181
+ );
182
+
183
+ assert.strictEqual(result, 'output');
184
+ // Verify logAndProgress was called with warn for stderr
185
+ const warnCall = helpersMock.logAndProgress.mock.calls.find(
186
+ (c) => c.arguments[2] === 'warn'
187
+ );
188
+ assert.ok(warnCall);
189
+ assert.ok(warnCall.arguments[0].includes('some warning'));
190
+ });
191
+
192
+ test('resourceCompose throws error if execFile fails', async () => {
193
+ childProcessMock.execFile.mock.resetCalls();
194
+ childProcessMock.execFile.mock.mockImplementation(
195
+ (file, args, opts, cb) => {
196
+ if (typeof opts === 'function') {
197
+ cb = opts;
198
+ }
199
+ cb(new Error('execFile failed'), { stdout: '', stderr: '' });
200
+ }
201
+ );
202
+
203
+ const compose = await esmock('../../lib/deployment/compose.js', {
204
+ child_process: childProcessMock,
205
+ '../../lib/util/helpers.js': helpersMock,
206
+ });
207
+
208
+ await assert.rejects(
209
+ compose.resourceCompose(
210
+ '/bin/run-compose',
211
+ '/path/to/compose.yaml',
212
+ 'us-central1',
213
+ mock.fn()
214
+ ),
215
+ {
216
+ message: /Failed to get resources for compose file: execFile failed/,
217
+ }
218
+ );
219
+ });
220
+
221
+ test('translateCompose returns stdout on success', async () => {
222
+ childProcessMock.execFile.mock.resetCalls();
223
+ childProcessMock.execFile.mock.mockImplementation(
224
+ (file, args, opts, cb) => {
225
+ if (typeof opts === 'function') {
226
+ cb = opts;
227
+ }
228
+ cb(null, { stdout: 'translated output', stderr: '' });
229
+ }
230
+ );
231
+
232
+ const compose = await esmock('../../lib/deployment/compose.js', {
233
+ child_process: childProcessMock,
234
+ '../../lib/util/helpers.js': helpersMock,
235
+ });
236
+
237
+ const result = await compose.translateCompose(
238
+ '/bin/run-compose',
239
+ '/path/to/compose.yaml',
240
+ 'us-central1',
241
+ '123456789',
242
+ mock.fn()
243
+ );
244
+
245
+ assert.strictEqual(result, 'translated output');
246
+ assert.strictEqual(childProcessMock.execFile.mock.callCount(), 1);
247
+ const call = childProcessMock.execFile.mock.calls[0];
248
+ assert.strictEqual(call.arguments[0], '/bin/run-compose');
249
+ assert.strictEqual(call.arguments[1][0], 'translate');
250
+ assert.strictEqual(call.arguments[1][1], '/path/to/compose.yaml');
251
+ assert.strictEqual(call.arguments[1][7], '.'); // Currently it's '.'
252
+ assert.strictEqual(call.arguments[2].cwd, '/path/to');
253
+ });
254
+
255
+ test('translateCompose handles resourcesConfig', async () => {
256
+ childProcessMock.execFile.mock.resetCalls();
257
+ childProcessMock.execFile.mock.mockImplementation(
258
+ (file, args, opts, cb) => {
259
+ if (typeof opts === 'function') {
260
+ cb = opts;
261
+ }
262
+ cb(null, { stdout: 'translated output with config', stderr: '' });
263
+ }
264
+ );
265
+
266
+ const compose = await esmock('../../lib/deployment/compose.js', {
267
+ child_process: childProcessMock,
268
+ '../../lib/util/helpers.js': helpersMock,
269
+ });
270
+
271
+ const resourcesConfig = { some: 'config' };
272
+ const result = await compose.translateCompose(
273
+ '/bin/run-compose',
274
+ '/path/to/compose.yaml',
275
+ 'us-central1',
276
+ '123456789',
277
+ mock.fn(),
278
+ resourcesConfig
279
+ );
280
+
281
+ assert.strictEqual(result, 'translated output with config');
282
+ const call = childProcessMock.execFile.mock.calls[0];
283
+ const args = call.arguments[1];
284
+ assert.ok(args.includes('--resources-config'));
285
+ assert.ok(args.includes(JSON.stringify(resourcesConfig)));
286
+ });
287
+
288
+ test('translateCompose throws error if execFile fails', async () => {
289
+ childProcessMock.execFile.mock.resetCalls();
290
+ childProcessMock.execFile.mock.mockImplementation(
291
+ (file, args, opts, cb) => {
292
+ if (typeof opts === 'function') {
293
+ cb = opts;
294
+ }
295
+ cb(new Error('translation failed'), { stdout: '', stderr: '' });
296
+ }
297
+ );
298
+
299
+ const compose = await esmock('../../lib/deployment/compose.js', {
300
+ child_process: childProcessMock,
301
+ '../../lib/util/helpers.js': helpersMock,
302
+ });
303
+
304
+ await assert.rejects(
305
+ compose.translateCompose(
306
+ '/bin/run-compose',
307
+ '/path/to/compose.yaml',
308
+ 'us-central1',
309
+ '123456789',
310
+ mock.fn()
311
+ ),
312
+ {
313
+ message: /Failed to translate compose file: translation failed/,
314
+ }
315
+ );
316
+ });
317
+
318
+ describe('composeVolumes', () => {
319
+ const getProjectNumberMock = mock.fn(async () => '987654321');
320
+ const ensureStorageBucketExistsMock = mock.fn(async () => ({
321
+ name: 'mock-bucket',
322
+ }));
323
+ const uploadToStorageBucketMock = mock.fn(async () => ({}));
324
+ const uploadDirectoryMock = mock.fn(async () => {});
325
+ const grantBucketAccessMock = mock.fn(async () => {});
326
+ const fsMock = {};
327
+ const fsPromisesMock = {
328
+ access: mock.fn(async () => {}),
329
+ stat: mock.fn(async () => ({ isDirectory: () => true })),
330
+ readFile: mock.fn(async () => Buffer.from('file-content')),
331
+ };
332
+
333
+ beforeEach(() => {
334
+ getProjectNumberMock.mock.resetCalls();
335
+ ensureStorageBucketExistsMock.mock.resetCalls();
336
+ uploadToStorageBucketMock.mock.resetCalls();
337
+ uploadDirectoryMock.mock.resetCalls();
338
+ grantBucketAccessMock.mock.resetCalls();
339
+ });
340
+
341
+ const setupCompose = async () => {
342
+ return await esmock('../../lib/deployment/compose.js', {
343
+ '../../lib/util/helpers.js': {
344
+ ...helpersMock,
345
+ getProjectNumber: getProjectNumberMock,
346
+ },
347
+ '../../lib/cloud-api/storage.js': {
348
+ ensureStorageBucketExists: ensureStorageBucketExistsMock,
349
+ uploadToStorageBucket: uploadToStorageBucketMock,
350
+ grantBucketAccess: grantBucketAccessMock,
351
+ },
352
+ '../../lib/deployment/helpers.js': {
353
+ uploadDirectory: uploadDirectoryMock,
354
+ },
355
+ fs: {
356
+ ...fsMock,
357
+ promises: fsPromisesMock,
358
+ default: {
359
+ ...fsMock,
360
+ promises: fsPromisesMock,
361
+ },
362
+ },
363
+ path: {
364
+ ...path,
365
+ resolve: (...args) => path.resolve(...args),
366
+ relative: (from, to) => path.relative(from, to),
367
+ basename: (p) => path.basename(p),
368
+ },
369
+ });
370
+ };
371
+
372
+ test('should return resourcesConfig unchanged if no volumes present', async () => {
373
+ const compose = await setupCompose();
374
+ const resourcesConfig = { project: 'test-project' };
375
+ const result = await compose.composeVolumes(
376
+ resourcesConfig,
377
+ 'token',
378
+ 'project-id',
379
+ 'us-central1',
380
+ '/f-path',
381
+ mock.fn()
382
+ );
383
+ assert.deepEqual(result, resourcesConfig);
384
+ });
385
+
386
+ test('should handle bind mounts (directories)', async () => {
387
+ const compose = await setupCompose();
388
+ const resourcesConfig = {
389
+ project: 'my-project',
390
+ volumes: {
391
+ bind_mount: {
392
+ web: [{ source: './data', target: '/app/data' }],
393
+ },
394
+ },
395
+ };
396
+
397
+ fsPromisesMock.access.mock.mockImplementation(async () => {});
398
+ fsPromisesMock.stat.mock.mockImplementation(async () => ({
399
+ isDirectory: () => true,
400
+ }));
401
+
402
+ const result = await compose.composeVolumes(
403
+ resourcesConfig,
404
+ 'token',
405
+ 'project-id',
406
+ 'us-central1',
407
+ '/app-dir',
408
+ mock.fn()
409
+ );
410
+
411
+ assert.ok(result.volumes.bucket_name);
412
+ assert.ok(
413
+ result.volumes.bind_mount.web[0].mount_source.includes(
414
+ 'bind_mounts/web/data'
415
+ )
416
+ );
417
+ assert.strictEqual(uploadDirectoryMock.mock.callCount(), 1);
418
+ // Called once in composeVolumes
419
+ assert.strictEqual(ensureStorageBucketExistsMock.mock.callCount(), 1);
420
+ const call = ensureStorageBucketExistsMock.mock.calls[0];
421
+ assert.deepStrictEqual(call.arguments[4], {
422
+ 'managed-by': 'runcompose',
423
+ 'run-compose-project': 'my-project',
424
+ });
425
+ assert.strictEqual(grantBucketAccessMock.mock.callCount(), 1);
426
+ // Called once in composeVolumes (reused in handleBindMounts)
427
+ assert.strictEqual(getProjectNumberMock.mock.callCount(), 1);
428
+ });
429
+
430
+ test('should handle long project names with hashing', async () => {
431
+ const compose = await setupCompose();
432
+ const longProjectName =
433
+ 'very-long-project-name-that-exceeds-the-maximum-limit-for-bucket-names';
434
+ const resourcesConfig = {
435
+ project: longProjectName,
436
+ volumes: {
437
+ bind_mount: {
438
+ web: [{ source: './data' }],
439
+ },
440
+ },
441
+ };
442
+
443
+ const result = await compose.composeVolumes(
444
+ resourcesConfig,
445
+ 'token',
446
+ 'project-id',
447
+ 'us-central1',
448
+ '/app-dir',
449
+ mock.fn()
450
+ );
451
+
452
+ const bucketName = result.volumes.bucket_name;
453
+ assert.ok(bucketName.length <= 63);
454
+ // Verify hashing (the first digits are project number, then hash)
455
+ assert.ok(bucketName.startsWith('987654321-'));
456
+ assert.ok(bucketName.endsWith('-us-central1-compose'));
457
+ });
458
+
459
+ test('should handle named volumes', async () => {
460
+ const compose = await setupCompose();
461
+ ensureStorageBucketExistsMock.mock.resetCalls();
462
+ const resourcesConfig = {
463
+ project: 'my-project',
464
+ volumes: {
465
+ named_volume: {
466
+ 'my-vol': { name: 'my-vol' },
467
+ },
468
+ },
469
+ };
470
+
471
+ await compose.composeVolumes(
472
+ resourcesConfig,
473
+ 'token',
474
+ 'project-id',
475
+ 'us-central1',
476
+ '/app-dir',
477
+ mock.fn()
478
+ );
479
+
480
+ // Should call ensureStorageBucketExists for the default bucketName
481
+ const calls = ensureStorageBucketExistsMock.mock.calls;
482
+ assert.strictEqual(calls.length, 1);
483
+ // Bucket name is constructed from project number, sanitized name, region, and suffix
484
+ const bucketName = calls[0].arguments[1];
485
+ assert.ok(
486
+ bucketName.startsWith('987654321-my-project-us-central1-compose')
487
+ );
488
+ assert.deepStrictEqual(calls[0].arguments[4], {
489
+ 'managed-by': 'runcompose',
490
+ 'run-compose-project': 'my-project',
491
+ });
492
+ });
493
+ });
494
+
495
+ describe('composeSecrets', () => {
496
+ const getProjectNumberMock = mock.fn(async () => '123456789');
497
+ const ensureApisEnabledMock = mock.fn(async () => {});
498
+ const getSecretMock = mock.fn();
499
+ const createSecretMock = mock.fn();
500
+ const addSecretAccessorBindingMock = mock.fn();
501
+ const addSecretVersionMock = mock.fn();
502
+ const fsPromisesMock = {
503
+ access: mock.fn(async () => {}),
504
+ readFile: mock.fn(async () => Buffer.from('secret-data')),
505
+ };
506
+
507
+ const setupCompose = async () => {
508
+ return await esmock('../../lib/deployment/compose.js', {
509
+ '../../lib/util/helpers.js': {
510
+ ...helpersMock,
511
+ getProjectNumber: getProjectNumberMock,
512
+ },
513
+ '../../lib/cloud-api/helpers.js': {
514
+ ensureApisEnabled: ensureApisEnabledMock,
515
+ },
516
+ '../../lib/cloud-api/secrets.js': {
517
+ getSecret: getSecretMock,
518
+ createSecret: createSecretMock,
519
+ addSecretVersion: addSecretVersionMock,
520
+ addSecretAccessorBinding: addSecretAccessorBindingMock,
521
+ },
522
+ fs: {
523
+ promises: fsPromisesMock,
524
+ default: {
525
+ promises: fsPromisesMock,
526
+ },
527
+ },
528
+ });
529
+ };
530
+
531
+ test('should return resourcesConfig unchanged if no secrets present', async () => {
532
+ const compose = await setupCompose();
533
+ const resourcesConfig = { project: 'test-project' };
534
+ const result = await compose.composeSecrets(
535
+ resourcesConfig,
536
+ 'token',
537
+ 'project-id',
538
+ '/f-path',
539
+ mock.fn()
540
+ );
541
+ assert.deepEqual(result, resourcesConfig);
542
+ });
543
+
544
+ test('should provision secrets and update resourcesConfig', async () => {
545
+ const compose = await setupCompose();
546
+ const resourcesConfig = {
547
+ project: 'my-project',
548
+ secrets: {
549
+ 'my-secret': {
550
+ name: 'target-secret-name',
551
+ file: './my_secret.txt',
552
+ mount: 'my-secret',
553
+ },
554
+ },
555
+ };
556
+
557
+ getSecretMock.mock.mockImplementation(async () => null); // Secret doesn't exist
558
+ createSecretMock.mock.mockImplementation(async () => ({
559
+ name: 'target-secret-name',
560
+ }));
561
+ addSecretVersionMock.mock.mockImplementation(async () => ({
562
+ name: 'projects/p/secrets/s/versions/1',
563
+ }));
564
+ addSecretAccessorBindingMock.mock.mockImplementation(async () => ({}));
565
+
566
+ const result = await compose.composeSecrets(
567
+ resourcesConfig,
568
+ 'token',
569
+ 'project-id',
570
+ '/app-dir',
571
+ mock.fn()
572
+ );
573
+
574
+ assert.strictEqual(
575
+ result.secrets['my-secret'].secret_version,
576
+ 'projects/p/secrets/s/versions/1'
577
+ );
578
+ assert.strictEqual(ensureApisEnabledMock.mock.callCount(), 1);
579
+ assert.strictEqual(createSecretMock.mock.callCount(), 1);
580
+ const call = createSecretMock.mock.calls[0];
581
+ assert.deepStrictEqual(call.arguments[3], {
582
+ 'managed-by': 'runcompose',
583
+ 'run-compose-project': 'my-project',
584
+ });
585
+ assert.strictEqual(addSecretVersionMock.mock.callCount(), 1);
586
+ assert.strictEqual(addSecretAccessorBindingMock.mock.callCount(), 1);
587
+ });
588
+
589
+ test('should skip provisioning if secret config is incomplete', async () => {
590
+ const compose = await setupCompose();
591
+ const resourcesConfig = {
592
+ project: 'my-project',
593
+ secrets: {
594
+ 'bad-secret': {
595
+ name: 'only-name',
596
+ // missing file and mount
597
+ },
598
+ },
599
+ };
600
+
601
+ ensureApisEnabledMock.mock.resetCalls();
602
+
603
+ const result = await compose.composeSecrets(
604
+ resourcesConfig,
605
+ 'token',
606
+ 'project-id',
607
+ '/app-dir',
608
+ mock.fn()
609
+ );
610
+
611
+ assert.strictEqual(
612
+ result.secrets['bad-secret'].secret_version,
613
+ undefined
614
+ );
615
+ assert.strictEqual(ensureApisEnabledMock.mock.callCount(), 1);
616
+ });
617
+ });
618
+ });