@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
|
@@ -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.',
|
|
@@ -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);
|
package/tools/register-tools.js
CHANGED
|
@@ -241,11 +241,21 @@ function registerGetServiceTool(server, options) {
|
|
|
241
241
|
options.accessToken
|
|
242
242
|
);
|
|
243
243
|
if (serviceDetails) {
|
|
244
|
+
// Format last deployment date, handling both Protobuf Timestamp (with
|
|
245
|
+
// seconds as string or number) and standard Date/ISO strings.
|
|
246
|
+
const lastDeployedDate = serviceDetails.updateTime
|
|
247
|
+
? new Date(
|
|
248
|
+
serviceDetails.updateTime.seconds
|
|
249
|
+
? Number(serviceDetails.updateTime.seconds) * 1000
|
|
250
|
+
: serviceDetails.updateTime
|
|
251
|
+
).toUTCString()
|
|
252
|
+
: 'N/A';
|
|
253
|
+
const consoleUrl = `https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}`;
|
|
244
254
|
return {
|
|
245
255
|
content: [
|
|
246
256
|
{
|
|
247
257
|
type: 'text',
|
|
248
|
-
text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
|
|
258
|
+
text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nConsole URL: ${consoleUrl}\nLast deployed by: ${serviceDetails.lastModifier}\nLast deployed date: ${lastDeployedDate}`,
|
|
249
259
|
},
|
|
250
260
|
],
|
|
251
261
|
};
|
|
@@ -366,7 +376,9 @@ function registerDeployLocalFolderTool(server, options) {
|
|
|
366
376
|
.string()
|
|
367
377
|
.optional()
|
|
368
378
|
.default(options.defaultServiceName)
|
|
369
|
-
.describe(
|
|
379
|
+
.describe(
|
|
380
|
+
'Name of the Cloud Run service to deploy to. If not provided, it will be inferred.'
|
|
381
|
+
),
|
|
370
382
|
folderPath: z
|
|
371
383
|
.string()
|
|
372
384
|
.describe(
|
|
@@ -405,13 +417,15 @@ function registerDeployLocalFolderTool(server, options) {
|
|
|
405
417
|
files: [folderPath],
|
|
406
418
|
skipIamCheck: options.skipIamCheck, // Pass the new flag
|
|
407
419
|
accessToken: options.accessToken,
|
|
420
|
+
ingress: options.ingress,
|
|
408
421
|
progressCallback,
|
|
409
422
|
});
|
|
423
|
+
const serviceName = service || response.service;
|
|
410
424
|
return {
|
|
411
425
|
content: [
|
|
412
426
|
{
|
|
413
427
|
type: 'text',
|
|
414
|
-
text: `Cloud Run service ${
|
|
428
|
+
text: `Cloud Run service ${serviceName} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${serviceName}?project=${project}\nService URL: ${response.uri}`,
|
|
415
429
|
},
|
|
416
430
|
],
|
|
417
431
|
};
|
|
@@ -507,6 +521,7 @@ function registerDeployFileContentsTool(server, options) {
|
|
|
507
521
|
files: files,
|
|
508
522
|
skipIamCheck: options.skipIamCheck, // Pass the new flag
|
|
509
523
|
accessToken: options.accessToken,
|
|
524
|
+
ingress: options.ingress,
|
|
510
525
|
progressCallback,
|
|
511
526
|
});
|
|
512
527
|
return {
|
|
@@ -588,6 +603,7 @@ function registerDeployContainerImageTool(server, options) {
|
|
|
588
603
|
imageUrl: imageUrl,
|
|
589
604
|
skipIamCheck: options.skipIamCheck,
|
|
590
605
|
accessToken: options.accessToken,
|
|
606
|
+
ingress: options.ingress,
|
|
591
607
|
progressCallback,
|
|
592
608
|
});
|
|
593
609
|
return {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|