@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.
- package/README.md +24 -0
- package/SECURITY.md +7 -0
- package/cloudbuild.yaml +37 -0
- package/example-sources-to-deploy/java/pom.xml +41 -0
- package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
- package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
- package/example-sources-to-deploy/nodejs/index.js +15 -0
- package/example-sources-to-deploy/nodejs/package.json +12 -0
- package/example-sources-to-deploy/python/pip-project/main.py +11 -0
- package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
- package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
- package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
- package/gemini-extension.json +12 -0
- package/lib/clients.js +104 -3
- package/lib/cloud-api/build.js +192 -28
- package/lib/cloud-api/run.js +62 -0
- package/lib/cloud-api/secrets.js +196 -0
- package/lib/cloud-api/storage.js +164 -16
- package/lib/deployment/compose.js +517 -0
- package/lib/deployment/constants.js +11 -0
- package/lib/deployment/deployer.js +334 -30
- package/lib/deployment/helpers.js +99 -49
- package/lib/deployment/source-processor.js +0 -48
- package/lib/deployment/universal-maker.js +147 -0
- package/lib/util/archive.js +3 -3
- package/lib/util/artifacts.js +189 -0
- package/lib/util/helpers.js +109 -0
- package/mcp-server.js +4 -0
- package/package.json +6 -3
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +224 -2
- package/test/local/cloud-api/build.test.js +330 -25
- package/test/local/cloud-api/run.test.js +129 -0
- package/test/local/cloud-api/secrets.test.js +291 -0
- package/test/local/cloud-api/storage.test.js +405 -0
- package/test/local/compose.test.js +618 -0
- package/test/local/deploy.test.js +408 -0
- package/test/local/deployment-helpers.test.js +126 -79
- package/test/local/source-processor.test.js +0 -165
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +157 -0
- package/test/local/util.test.js +361 -0
- package/test/need-gcp/deployer.test.js +1 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +19 -3
- /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
- /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
- /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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
assert.strictEqual(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|