@google-cloud/cloud-run-mcp 1.10.0 → 1.12.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/.github/workflows/npm-publish.yml +10 -6
- package/README.md +26 -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/lib/clients.js +142 -12
- 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 +8 -0
- package/lib/deployment/deployer.js +283 -28
- package/lib/deployment/helpers.js +58 -1
- package/lib/deployment/universal-maker.js +21 -148
- package/lib/middleware/oauth.js +26 -15
- 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 +83 -20
- package/package.json +7 -4
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +307 -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 +100 -1
- package/test/local/mcp-server-streamable-http.test.js +132 -12
- package/test/local/middleware/oauth.test.js +106 -5
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +23 -114
- package/test/local/util.test.js +361 -0
- package/test/need-gcp/deployer.test.js +1 -1
- package/test/need-gcp/test-helpers.js +5 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +98 -78
- /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
|
@@ -0,0 +1,361 @@
|
|
|
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 } from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import esmock from 'esmock';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
|
|
23
|
+
describe('Artifact Utilities', () => {
|
|
24
|
+
const fsMock = {
|
|
25
|
+
existsSync: mock.fn(),
|
|
26
|
+
mkdirSync: mock.fn(),
|
|
27
|
+
chmodSync: mock.fn(),
|
|
28
|
+
readFileSync: mock.fn(),
|
|
29
|
+
unlinkSync: mock.fn(),
|
|
30
|
+
createWriteStream: mock.fn(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const cryptoMock = {
|
|
34
|
+
createHash: mock.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const helpersMock = {
|
|
38
|
+
logAndProgress: mock.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const clientsMock = {
|
|
42
|
+
getArtifactRegistryClient: mock.fn(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const artifactParams = {
|
|
46
|
+
project: 'test-project',
|
|
47
|
+
location: 'test-location',
|
|
48
|
+
repository: 'test-repo',
|
|
49
|
+
artifactPath: 'test-path:1.0:bin',
|
|
50
|
+
displayName: 'Test Binary',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sha256 =
|
|
54
|
+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
|
55
|
+
|
|
56
|
+
test('isBinaryUpToDate returns true if hashes match', async () => {
|
|
57
|
+
fsMock.existsSync.mock.mockImplementation(() => true);
|
|
58
|
+
fsMock.readFileSync.mock.mockImplementation(() => 'local-content');
|
|
59
|
+
cryptoMock.createHash.mock.mockImplementation(() => ({
|
|
60
|
+
update: mock.fn(() => ({
|
|
61
|
+
digest: mock.fn(() => sha256),
|
|
62
|
+
})),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const artifactRegistryClientMock = {
|
|
66
|
+
filePath: mock.fn(() => 'resource-name'),
|
|
67
|
+
getFile: mock.fn(() => [
|
|
68
|
+
{
|
|
69
|
+
hashes: [
|
|
70
|
+
{
|
|
71
|
+
type: 'SHA256',
|
|
72
|
+
value: Buffer.from(sha256, 'hex').toString('base64'),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
]),
|
|
77
|
+
};
|
|
78
|
+
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
79
|
+
() => artifactRegistryClientMock
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { isBinaryUpToDate } = await esmock('../../lib/util/artifacts.js', {
|
|
83
|
+
fs: fsMock,
|
|
84
|
+
crypto: cryptoMock,
|
|
85
|
+
'../../lib/util/helpers.js': helpersMock,
|
|
86
|
+
'../../lib/clients.js': clientsMock,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await isBinaryUpToDate(
|
|
90
|
+
'/bin/path',
|
|
91
|
+
artifactParams,
|
|
92
|
+
'token',
|
|
93
|
+
mock.fn()
|
|
94
|
+
);
|
|
95
|
+
assert.strictEqual(result, true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('isBinaryUpToDate returns false and unlinks if hashes do not match', async () => {
|
|
99
|
+
fsMock.existsSync.mock.mockImplementation(() => true);
|
|
100
|
+
fsMock.readFileSync.mock.mockImplementation(() => 'local-content');
|
|
101
|
+
fsMock.unlinkSync.mock.resetCalls();
|
|
102
|
+
|
|
103
|
+
cryptoMock.createHash.mock.mockImplementation(() => ({
|
|
104
|
+
update: mock.fn(() => ({
|
|
105
|
+
digest: mock.fn(() => 'different-hash'),
|
|
106
|
+
})),
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const artifactRegistryClientMock = {
|
|
110
|
+
filePath: mock.fn(() => 'resource-name'),
|
|
111
|
+
getFile: mock.fn(() => [
|
|
112
|
+
{
|
|
113
|
+
hashes: [
|
|
114
|
+
{
|
|
115
|
+
type: 'SHA256',
|
|
116
|
+
value: Buffer.from(sha256, 'hex').toString('base64'),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
]),
|
|
121
|
+
};
|
|
122
|
+
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
123
|
+
() => artifactRegistryClientMock
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const { isBinaryUpToDate } = await esmock('../../lib/util/artifacts.js', {
|
|
127
|
+
fs: fsMock,
|
|
128
|
+
crypto: cryptoMock,
|
|
129
|
+
'../../lib/util/helpers.js': helpersMock,
|
|
130
|
+
'../../lib/clients.js': clientsMock,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await isBinaryUpToDate(
|
|
134
|
+
'/bin/path',
|
|
135
|
+
artifactParams,
|
|
136
|
+
'token',
|
|
137
|
+
mock.fn()
|
|
138
|
+
);
|
|
139
|
+
assert.strictEqual(result, false);
|
|
140
|
+
assert.strictEqual(fsMock.unlinkSync.mock.callCount(), 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('ensureRepositoryDownloaded downloads binary if it does not exist', async () => {
|
|
144
|
+
fsMock.existsSync.mock.mockImplementation((p) => {
|
|
145
|
+
if (p === '/bin/path') return false;
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
fsMock.createWriteStream.mock.resetCalls();
|
|
149
|
+
|
|
150
|
+
const artifactRegistryClientMock = {
|
|
151
|
+
filePath: mock.fn(() => 'resource-name'),
|
|
152
|
+
getFile: mock.fn(() => [{ hashes: [] }]),
|
|
153
|
+
auth: {
|
|
154
|
+
request: mock.fn(() => ({
|
|
155
|
+
data: {
|
|
156
|
+
pipe: (dest) => {
|
|
157
|
+
setTimeout(() => dest.emit('finish'), 10);
|
|
158
|
+
return dest;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
})),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
165
|
+
() => artifactRegistryClientMock
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const writeStreamMock = {
|
|
169
|
+
on: mock.fn(function (event, cb) {
|
|
170
|
+
if (event === 'finish') this.finishCb = cb;
|
|
171
|
+
return this;
|
|
172
|
+
}),
|
|
173
|
+
emit: mock.fn(function (event) {
|
|
174
|
+
if (event === 'finish' && this.finishCb) this.finishCb();
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
fsMock.createWriteStream.mock.mockImplementation(() => writeStreamMock);
|
|
178
|
+
|
|
179
|
+
const { ensureRepositoryDownloaded } = await esmock(
|
|
180
|
+
'../../lib/util/artifacts.js',
|
|
181
|
+
{
|
|
182
|
+
fs: fsMock,
|
|
183
|
+
crypto: cryptoMock,
|
|
184
|
+
'../../lib/util/helpers.js': helpersMock,
|
|
185
|
+
'../../lib/clients.js': clientsMock,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const result = await ensureRepositoryDownloaded(
|
|
190
|
+
'/bin/path',
|
|
191
|
+
artifactParams,
|
|
192
|
+
'token',
|
|
193
|
+
mock.fn()
|
|
194
|
+
);
|
|
195
|
+
assert.strictEqual(result, '/bin/path');
|
|
196
|
+
assert.strictEqual(fsMock.createWriteStream.mock.callCount(), 1);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('Utility Helpers', () => {
|
|
201
|
+
test('extractAccessToken correctly extracts token from Bearer header', async () => {
|
|
202
|
+
const { extractAccessToken } = await import('../../lib/util/helpers.js');
|
|
203
|
+
|
|
204
|
+
assert.equal(extractAccessToken('Bearer my-token'), 'my-token');
|
|
205
|
+
assert.equal(extractAccessToken(''), undefined);
|
|
206
|
+
assert.equal(extractAccessToken(null), undefined);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('getProjectNumber', () => {
|
|
210
|
+
test('extracts project number from name in projects/NNNN format', async () => {
|
|
211
|
+
const getProjectMock = mock.fn(async () => [
|
|
212
|
+
{ name: 'projects/123456789' },
|
|
213
|
+
]);
|
|
214
|
+
const getProjectsClientMock = mock.fn(async () => ({
|
|
215
|
+
getProject: getProjectMock,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
const { getProjectNumber } = await esmock('../../lib/util/helpers.js', {
|
|
219
|
+
'../../lib/clients.js': {
|
|
220
|
+
getProjectsClient: getProjectsClientMock,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const projectId = 'test-project';
|
|
225
|
+
const accessToken = 'test-token';
|
|
226
|
+
const result = await getProjectNumber(projectId, accessToken);
|
|
227
|
+
|
|
228
|
+
assert.equal(result, '123456789');
|
|
229
|
+
assert.equal(getProjectsClientMock.mock.callCount(), 1);
|
|
230
|
+
assert.equal(
|
|
231
|
+
getProjectMock.mock.calls[0].arguments[0].name,
|
|
232
|
+
'projects/test-project'
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('falls back to projectNumber property if name structure is unexpected', async () => {
|
|
237
|
+
const getProjectMock = mock.fn(async () => [
|
|
238
|
+
{ name: 'unexpected', projectNumber: '987654321' },
|
|
239
|
+
]);
|
|
240
|
+
const getProjectsClientMock = mock.fn(async () => ({
|
|
241
|
+
getProject: getProjectMock,
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const { getProjectNumber } = await esmock('../../lib/util/helpers.js', {
|
|
245
|
+
'../../lib/clients.js': {
|
|
246
|
+
getProjectsClient: getProjectsClientMock,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const result = await getProjectNumber('test-project', 'test-token');
|
|
251
|
+
|
|
252
|
+
assert.equal(result, '987654321');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('calculateSourceFingerprint', () => {
|
|
257
|
+
test('calculates SHA256 of directory content deterministically', async () => {
|
|
258
|
+
const fsMock = {
|
|
259
|
+
promises: {
|
|
260
|
+
readdir: mock.fn(async (dir) => {
|
|
261
|
+
if (dir === '/src') {
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
name: 'b.txt',
|
|
265
|
+
isFile: () => true,
|
|
266
|
+
isDirectory: () => false,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'a.txt',
|
|
270
|
+
isFile: () => true,
|
|
271
|
+
isDirectory: () => false,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'subdir',
|
|
275
|
+
isFile: () => false,
|
|
276
|
+
isDirectory: () => true,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
if (dir === '/src/subdir') {
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
name: 'c.txt',
|
|
284
|
+
isFile: () => true,
|
|
285
|
+
isDirectory: () => false,
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
return [];
|
|
290
|
+
}),
|
|
291
|
+
readFile: mock.fn(
|
|
292
|
+
async (file) => `content of ${path.basename(file)}`
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const { calculateSourceFingerprint } = await esmock(
|
|
298
|
+
'../../lib/util/helpers.js',
|
|
299
|
+
{
|
|
300
|
+
'node:fs': { promises: fsMock.promises },
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const result = await calculateSourceFingerprint('/src');
|
|
305
|
+
assert.ok(result);
|
|
306
|
+
assert.equal(typeof result, 'string');
|
|
307
|
+
assert.equal(result.length, 64); // SHA256 hex length
|
|
308
|
+
|
|
309
|
+
// Verify readdir calls (recursive)
|
|
310
|
+
assert.equal(fsMock.promises.readdir.mock.callCount(), 2);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('sanitizeCloudRunServiceName', () => {
|
|
315
|
+
test('sanitizes various inputs correctly', async () => {
|
|
316
|
+
const { sanitizeCloudRunServiceName } =
|
|
317
|
+
await import('../../lib/util/helpers.js');
|
|
318
|
+
|
|
319
|
+
assert.equal(
|
|
320
|
+
sanitizeCloudRunServiceName('My-Service_Name'),
|
|
321
|
+
'my-service-name'
|
|
322
|
+
);
|
|
323
|
+
assert.equal(sanitizeCloudRunServiceName('123service'), 's-123service');
|
|
324
|
+
assert.equal(sanitizeCloudRunServiceName('-start-end-'), 'start-end');
|
|
325
|
+
assert.equal(
|
|
326
|
+
sanitizeCloudRunServiceName('a'.repeat(100)),
|
|
327
|
+
'a'.repeat(49)
|
|
328
|
+
);
|
|
329
|
+
assert.equal(
|
|
330
|
+
sanitizeCloudRunServiceName('Special!@#$%Chars'),
|
|
331
|
+
'special-chars'
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('sanitizeLabelValue', () => {
|
|
337
|
+
test('sanitizes various inputs correctly', async () => {
|
|
338
|
+
const { sanitizeLabelValue } = await import('../../lib/util/helpers.js');
|
|
339
|
+
|
|
340
|
+
assert.equal(sanitizeLabelValue(''), '');
|
|
341
|
+
assert.equal(sanitizeLabelValue(null), '');
|
|
342
|
+
assert.equal(sanitizeLabelValue(undefined), '');
|
|
343
|
+
assert.equal(sanitizeLabelValue('valid-label'), 'valid-label');
|
|
344
|
+
assert.equal(sanitizeLabelValue('Upper-Case'), 'upper-case');
|
|
345
|
+
assert.equal(sanitizeLabelValue('invalid@char'), 'invalid-char');
|
|
346
|
+
assert.equal(
|
|
347
|
+
sanitizeLabelValue('1starts-with-number'),
|
|
348
|
+
'1starts-with-number'
|
|
349
|
+
);
|
|
350
|
+
assert.equal(
|
|
351
|
+
sanitizeLabelValue('-starts-with-dash'),
|
|
352
|
+
'-starts-with-dash'
|
|
353
|
+
);
|
|
354
|
+
assert.equal(sanitizeLabelValue('a'.repeat(70)), 'a'.repeat(63));
|
|
355
|
+
assert.equal(sanitizeLabelValue(12345), '12345');
|
|
356
|
+
assert.equal(sanitizeLabelValue(0), '0');
|
|
357
|
+
assert.equal(sanitizeLabelValue(true), 'true');
|
|
358
|
+
assert.equal(sanitizeLabelValue(false), 'false');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -51,7 +51,7 @@ describe('Cloud Run Deployments', () => {
|
|
|
51
51
|
const config = {
|
|
52
52
|
serviceName: 'hello-from-image',
|
|
53
53
|
region: 'europe-west1',
|
|
54
|
-
files: ['example-sources-to-deploy/main.go'],
|
|
54
|
+
files: ['example-sources-to-deploy/golang/main.go'],
|
|
55
55
|
};
|
|
56
56
|
await assert.rejects(deploy(config), {
|
|
57
57
|
message: 'Error: projectId is required in the configuration object.',
|
|
@@ -163,7 +163,11 @@ export async function setSourceDeployProjectPermissions(projectId) {
|
|
|
163
163
|
const context = {
|
|
164
164
|
serviceUsageClient: serviceUsageClient,
|
|
165
165
|
};
|
|
166
|
-
await ensureApisEnabled(projectId, [
|
|
166
|
+
await ensureApisEnabled(projectId, [
|
|
167
|
+
'run.googleapis.com',
|
|
168
|
+
'cloudbuild.googleapis.com',
|
|
169
|
+
'compute.googleapis.com',
|
|
170
|
+
]);
|
|
167
171
|
console.log('Adding editor role to Compute SA...');
|
|
168
172
|
const projectNumber = await getProjectNumber(projectId);
|
|
169
173
|
const member = `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
@@ -26,6 +26,27 @@ import {
|
|
|
26
26
|
setupProject,
|
|
27
27
|
} from '../test-helpers.js';
|
|
28
28
|
|
|
29
|
+
const GCP_REGION = 'asia-southeast1';
|
|
30
|
+
const RUN_INGRESS_POLICY = process.env.RUN_INGRESS_POLICY || undefined;
|
|
31
|
+
const ZIP_DEPLOY_SUCCESS_MESSAGE =
|
|
32
|
+
'Deployment completed successfully with zip deploy';
|
|
33
|
+
const SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE =
|
|
34
|
+
'Deployment completed successfully with source build deploy';
|
|
35
|
+
|
|
36
|
+
async function assertDeploymentSuccess(config, expectedMessage) {
|
|
37
|
+
let successMessage = '';
|
|
38
|
+
config.progressCallback = (p) => {
|
|
39
|
+
if (p.data === expectedMessage) {
|
|
40
|
+
successMessage = p.data;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
if (RUN_INGRESS_POLICY) {
|
|
44
|
+
config.ingress = RUN_INGRESS_POLICY;
|
|
45
|
+
}
|
|
46
|
+
await deploy(config);
|
|
47
|
+
assert.strictEqual(successMessage, expectedMessage);
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
describe('Deployment workflows', () => {
|
|
30
51
|
let projectId;
|
|
31
52
|
|
|
@@ -33,6 +54,8 @@ describe('Deployment workflows', () => {
|
|
|
33
54
|
try {
|
|
34
55
|
projectId = await setupProject();
|
|
35
56
|
await setSourceDeployProjectPermissions(projectId);
|
|
57
|
+
console.log('Waiting 2 minutes for IAM propagation...');
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 120000));
|
|
36
59
|
} catch (err) {
|
|
37
60
|
console.error('Error during project creation and setup:', err);
|
|
38
61
|
throw err;
|
|
@@ -43,9 +66,12 @@ describe('Deployment workflows', () => {
|
|
|
43
66
|
const configImageDeploy = {
|
|
44
67
|
projectId: projectId,
|
|
45
68
|
serviceName: 'hello-scenario',
|
|
46
|
-
region:
|
|
69
|
+
region: GCP_REGION,
|
|
47
70
|
imageUrl: 'gcr.io/cloudrun/hello',
|
|
48
71
|
};
|
|
72
|
+
if (RUN_INGRESS_POLICY) {
|
|
73
|
+
configImageDeploy.ingress = RUN_INGRESS_POLICY;
|
|
74
|
+
}
|
|
49
75
|
await deployImage(configImageDeploy);
|
|
50
76
|
|
|
51
77
|
console.log('Scenario-1: Deployment completed.');
|
|
@@ -55,7 +81,7 @@ describe('Deployment workflows', () => {
|
|
|
55
81
|
const configFailingBuild = {
|
|
56
82
|
projectId: projectId,
|
|
57
83
|
serviceName: 'example-failing-app',
|
|
58
|
-
region:
|
|
84
|
+
region: GCP_REGION,
|
|
59
85
|
files: [
|
|
60
86
|
{
|
|
61
87
|
filename: 'main.txt',
|
|
@@ -64,6 +90,9 @@ describe('Deployment workflows', () => {
|
|
|
64
90
|
},
|
|
65
91
|
],
|
|
66
92
|
};
|
|
93
|
+
if (RUN_INGRESS_POLICY) {
|
|
94
|
+
configFailingBuild.ingress = RUN_INGRESS_POLICY;
|
|
95
|
+
}
|
|
67
96
|
await assert.rejects(
|
|
68
97
|
deploy(configFailingBuild),
|
|
69
98
|
{ message: /ERROR: failed to detect: no buildpacks participating/ },
|
|
@@ -73,26 +102,163 @@ describe('Deployment workflows', () => {
|
|
|
73
102
|
|
|
74
103
|
test('Scenario-3: Starting deployment of Go app with file content...', async () => {
|
|
75
104
|
const mainGoContent = await fs.readFile(
|
|
76
|
-
path.resolve('example-sources-to-deploy/main.go'),
|
|
105
|
+
path.resolve('example-sources-to-deploy/golang/main.go'),
|
|
77
106
|
'utf-8'
|
|
78
107
|
);
|
|
79
108
|
const goModContent = await fs.readFile(
|
|
80
|
-
path.resolve('example-sources-to-deploy/go.mod'),
|
|
109
|
+
path.resolve('example-sources-to-deploy/golang/go.mod'),
|
|
81
110
|
'utf-8'
|
|
82
111
|
);
|
|
83
112
|
const configGoWithContent = {
|
|
84
113
|
projectId: projectId,
|
|
85
114
|
serviceName: 'example-go-app-content',
|
|
86
|
-
region:
|
|
115
|
+
region: GCP_REGION,
|
|
87
116
|
files: [
|
|
88
117
|
{ filename: 'main.go', content: mainGoContent },
|
|
89
118
|
{ filename: 'go.mod', content: goModContent },
|
|
90
119
|
],
|
|
91
120
|
};
|
|
92
|
-
await
|
|
121
|
+
await assertDeploymentSuccess(
|
|
122
|
+
configGoWithContent,
|
|
123
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
124
|
+
);
|
|
93
125
|
console.log('Scenario-3: Deployment completed.');
|
|
94
126
|
});
|
|
95
127
|
|
|
128
|
+
test('Scenario-4: Starting deployment of pip-based Python app with folder path... uses zip deploy', async () => {
|
|
129
|
+
const configPipProject = {
|
|
130
|
+
projectId: projectId,
|
|
131
|
+
serviceName: 'example-pip-project-folder-path',
|
|
132
|
+
region: GCP_REGION,
|
|
133
|
+
files: ['example-sources-to-deploy/python/pip-project'],
|
|
134
|
+
};
|
|
135
|
+
await assertDeploymentSuccess(configPipProject, ZIP_DEPLOY_SUCCESS_MESSAGE);
|
|
136
|
+
console.log('Scenario-4: Deployment completed.');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('Scenario-5: Starting deployment of pip-based Python app with file-based content... uses source build deploy', async () => {
|
|
140
|
+
const mainPyContent = await fs.readFile(
|
|
141
|
+
path.resolve('example-sources-to-deploy/python/pip-project/main.py'),
|
|
142
|
+
'utf-8'
|
|
143
|
+
);
|
|
144
|
+
const requirementsTxtContent = await fs.readFile(
|
|
145
|
+
path.resolve(
|
|
146
|
+
'example-sources-to-deploy/python/pip-project/requirements.txt'
|
|
147
|
+
),
|
|
148
|
+
'utf-8'
|
|
149
|
+
);
|
|
150
|
+
const configPipProject = {
|
|
151
|
+
projectId: projectId,
|
|
152
|
+
serviceName: 'example-pip-project-file-content',
|
|
153
|
+
region: GCP_REGION,
|
|
154
|
+
files: [
|
|
155
|
+
{ filename: 'main.py', content: mainPyContent },
|
|
156
|
+
{ filename: 'requirements.txt', content: requirementsTxtContent },
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
await assertDeploymentSuccess(
|
|
160
|
+
configPipProject,
|
|
161
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
162
|
+
);
|
|
163
|
+
console.log('Scenario-5: Deployment completed.');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('Scenario-6: Starting deployment of pyproject-based Python app with folder path... uses zip deploy', async () => {
|
|
167
|
+
const configPyprojectProject = {
|
|
168
|
+
projectId: projectId,
|
|
169
|
+
serviceName: 'example-pyproject-project-folder-path',
|
|
170
|
+
region: GCP_REGION,
|
|
171
|
+
files: ['example-sources-to-deploy/python/pyproject-project'],
|
|
172
|
+
};
|
|
173
|
+
await assertDeploymentSuccess(
|
|
174
|
+
configPyprojectProject,
|
|
175
|
+
ZIP_DEPLOY_SUCCESS_MESSAGE
|
|
176
|
+
);
|
|
177
|
+
console.log('Scenario-6: Deployment completed.');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('Scenario-7: Starting deployment of pyproject-based Python app with file-based content... uses source build deploy', async () => {
|
|
181
|
+
const mainPyContent = await fs.readFile(
|
|
182
|
+
path.resolve(
|
|
183
|
+
'example-sources-to-deploy/python/pyproject-project/main.py'
|
|
184
|
+
),
|
|
185
|
+
'utf-8'
|
|
186
|
+
);
|
|
187
|
+
const pyprojectContent = await fs.readFile(
|
|
188
|
+
path.resolve(
|
|
189
|
+
'example-sources-to-deploy/python/pyproject-project/pyproject.toml'
|
|
190
|
+
),
|
|
191
|
+
'utf-8'
|
|
192
|
+
);
|
|
193
|
+
const configPyprojectProject = {
|
|
194
|
+
projectId: projectId,
|
|
195
|
+
serviceName: 'example-pyproject-project-file-content',
|
|
196
|
+
region: GCP_REGION,
|
|
197
|
+
files: [
|
|
198
|
+
{ filename: 'main.py', content: mainPyContent },
|
|
199
|
+
{ filename: 'pyproject.toml', content: pyprojectContent },
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
await assertDeploymentSuccess(
|
|
203
|
+
configPyprojectProject,
|
|
204
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
205
|
+
);
|
|
206
|
+
console.log('Scenario-7: Deployment completed.');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('Scenario-8: Starting deployment of Node.js app with folder path... uses zip deploy', async () => {
|
|
210
|
+
const configNodeProject = {
|
|
211
|
+
projectId: projectId,
|
|
212
|
+
serviceName: 'example-node-project-folder-path',
|
|
213
|
+
region: GCP_REGION,
|
|
214
|
+
files: ['example-sources-to-deploy/nodejs'],
|
|
215
|
+
};
|
|
216
|
+
await assertDeploymentSuccess(
|
|
217
|
+
configNodeProject,
|
|
218
|
+
ZIP_DEPLOY_SUCCESS_MESSAGE
|
|
219
|
+
);
|
|
220
|
+
console.log('Scenario-8: Deployment completed.');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Scenario-9: Starting deployment of Node.js app with file content... uses build deploy', async () => {
|
|
224
|
+
const packageJsonContent = await fs.readFile(
|
|
225
|
+
path.resolve('example-sources-to-deploy/nodejs/package.json'),
|
|
226
|
+
'utf-8'
|
|
227
|
+
);
|
|
228
|
+
const indexJsContent = await fs.readFile(
|
|
229
|
+
path.resolve('example-sources-to-deploy/nodejs/index.js'),
|
|
230
|
+
'utf-8'
|
|
231
|
+
);
|
|
232
|
+
const configNodeWithContent = {
|
|
233
|
+
projectId: projectId,
|
|
234
|
+
serviceName: 'example-node-project-file-content',
|
|
235
|
+
region: GCP_REGION,
|
|
236
|
+
files: [
|
|
237
|
+
{ filename: 'package.json', content: packageJsonContent },
|
|
238
|
+
{ filename: 'index.js', content: indexJsContent },
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
await assertDeploymentSuccess(
|
|
242
|
+
configNodeWithContent,
|
|
243
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
244
|
+
);
|
|
245
|
+
console.log('Scenario-9: Deployment completed.');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('Scenario-10: Starting deployment of Java app with folder path... uses source build deploy', async () => {
|
|
249
|
+
const configJavaProject = {
|
|
250
|
+
projectId: projectId,
|
|
251
|
+
serviceName: 'example-java-project-folder-path',
|
|
252
|
+
region: GCP_REGION,
|
|
253
|
+
files: ['example-sources-to-deploy/java'],
|
|
254
|
+
};
|
|
255
|
+
await assertDeploymentSuccess(
|
|
256
|
+
configJavaProject,
|
|
257
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
258
|
+
);
|
|
259
|
+
console.log('Scenario-10: Deployment completed.');
|
|
260
|
+
});
|
|
261
|
+
|
|
96
262
|
after(async () => {
|
|
97
263
|
// Clean up: delete the project created for tests
|
|
98
264
|
cleanupProject(projectId);
|