@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
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
1
|
+
import { test, describe, mock, beforeEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import {
|
|
3
|
+
import { google } from 'googleapis';
|
|
4
|
+
import {
|
|
5
|
+
clients,
|
|
6
|
+
getClient,
|
|
7
|
+
getRunClient,
|
|
8
|
+
getRunV1Client,
|
|
9
|
+
getCloudRunRegions,
|
|
10
|
+
resetCachedRegions,
|
|
11
|
+
} from '../../lib/clients.js';
|
|
12
|
+
import { GCLOUD_AUTH } from '../../constants.js';
|
|
4
13
|
|
|
5
14
|
describe('getClient Helper', () => {
|
|
6
15
|
class MockClient {
|
|
@@ -173,4 +182,300 @@ describe('getClient Helper', () => {
|
|
|
173
182
|
|
|
174
183
|
assert.strictEqual(client.options.extraOpt, extraOpt);
|
|
175
184
|
});
|
|
185
|
+
|
|
186
|
+
test('throws error when OAUTH_ENABLED=true and accessToken is missing or GCLOUD_AUTH', async () => {
|
|
187
|
+
const originalOAuth = process.env.OAUTH_ENABLED;
|
|
188
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
189
|
+
try {
|
|
190
|
+
const projectId = 'test-project-oauth-err';
|
|
191
|
+
const service = 'run';
|
|
192
|
+
|
|
193
|
+
await assert.rejects(
|
|
194
|
+
async () =>
|
|
195
|
+
getClient(
|
|
196
|
+
service,
|
|
197
|
+
projectId,
|
|
198
|
+
async () => MockClient,
|
|
199
|
+
{ projectId },
|
|
200
|
+
null
|
|
201
|
+
),
|
|
202
|
+
/Access token is required when OAuth is enabled/
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
await assert.rejects(
|
|
206
|
+
async () =>
|
|
207
|
+
getClient(
|
|
208
|
+
service,
|
|
209
|
+
projectId,
|
|
210
|
+
async () => MockClient,
|
|
211
|
+
{ projectId },
|
|
212
|
+
undefined
|
|
213
|
+
),
|
|
214
|
+
/Access token is required when OAuth is enabled/
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await assert.rejects(
|
|
218
|
+
async () =>
|
|
219
|
+
getClient(
|
|
220
|
+
service,
|
|
221
|
+
projectId,
|
|
222
|
+
async () => MockClient,
|
|
223
|
+
{ projectId },
|
|
224
|
+
GCLOUD_AUTH
|
|
225
|
+
),
|
|
226
|
+
/Access token is required when OAuth is enabled/
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await assert.rejects(
|
|
230
|
+
async () => getRunClient(projectId),
|
|
231
|
+
/Access token is required when OAuth is enabled/
|
|
232
|
+
);
|
|
233
|
+
} finally {
|
|
234
|
+
if (originalOAuth === undefined) {
|
|
235
|
+
delete process.env.OAUTH_ENABLED;
|
|
236
|
+
} else {
|
|
237
|
+
process.env.OAUTH_ENABLED = originalOAuth;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('getRunV1Client', () => {
|
|
244
|
+
test('creates new client instance with Application Default Credentials for GCLOUD_AUTH', async () => {
|
|
245
|
+
// Mock the GoogleAuth class to avoid looking up ADC
|
|
246
|
+
const originalGoogleAuth = google.auth.GoogleAuth;
|
|
247
|
+
const authInstances = [];
|
|
248
|
+
google.auth.GoogleAuth = class MockGoogleAuth {
|
|
249
|
+
constructor(options) {
|
|
250
|
+
this.options = options;
|
|
251
|
+
authInstances.push(this);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const runMock = mock.method(google, 'run', (options) => {
|
|
256
|
+
return { options };
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const projectId = 'test-project';
|
|
261
|
+
const client = await getRunV1Client(projectId, GCLOUD_AUTH);
|
|
262
|
+
|
|
263
|
+
assert.ok(client);
|
|
264
|
+
assert.strictEqual(authInstances.length, 1);
|
|
265
|
+
assert.deepEqual(authInstances[0].options.scopes, [
|
|
266
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
267
|
+
]);
|
|
268
|
+
assert.strictEqual(runMock.mock.calls.length, 1);
|
|
269
|
+
assert.strictEqual(
|
|
270
|
+
runMock.mock.calls[0].arguments[0].auth,
|
|
271
|
+
authInstances[0]
|
|
272
|
+
);
|
|
273
|
+
} finally {
|
|
274
|
+
google.auth.GoogleAuth = originalGoogleAuth;
|
|
275
|
+
runMock.mock.restore();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('creates new client instance with OAuth client when access token provided', async () => {
|
|
280
|
+
const runMock = mock.method(google, 'run', (options) => ({ options }));
|
|
281
|
+
try {
|
|
282
|
+
const projectId = 'test-project-oauth';
|
|
283
|
+
const accessToken = 'fake-access-token';
|
|
284
|
+
const client = await getRunV1Client(projectId, accessToken);
|
|
285
|
+
|
|
286
|
+
assert.ok(client);
|
|
287
|
+
assert.strictEqual(runMock.mock.calls.length, 1);
|
|
288
|
+
const lastCall = runMock.mock.calls[0];
|
|
289
|
+
assert.ok(lastCall.arguments[0].auth);
|
|
290
|
+
assert.strictEqual(
|
|
291
|
+
lastCall.arguments[0].auth.credentials.access_token,
|
|
292
|
+
accessToken
|
|
293
|
+
);
|
|
294
|
+
} finally {
|
|
295
|
+
runMock.mock.restore();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('sets rootUrl if region is provided', async () => {
|
|
300
|
+
const originalGoogleAuth = google.auth.GoogleAuth;
|
|
301
|
+
google.auth.GoogleAuth = class MockGoogleAuth {
|
|
302
|
+
constructor(options) {
|
|
303
|
+
this.options = options;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const runMock = mock.method(google, 'run', (options) => ({ options }));
|
|
308
|
+
|
|
309
|
+
const projectId = 'test-project-region';
|
|
310
|
+
const region = 'us-central1';
|
|
311
|
+
const key = projectId; // If GCLOUD_AUTH, key is just projectId
|
|
312
|
+
|
|
313
|
+
// Mock getCloudRunRegions call by injecting into clients.run
|
|
314
|
+
clients.run.set(key, {
|
|
315
|
+
async *listLocationsAsync() {
|
|
316
|
+
yield { locationId: 'us-central1' };
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await getRunV1Client(projectId, GCLOUD_AUTH, region);
|
|
322
|
+
|
|
323
|
+
const lastCall = runMock.mock.calls[0];
|
|
324
|
+
assert.strictEqual(
|
|
325
|
+
lastCall.arguments[0].rootUrl,
|
|
326
|
+
'https://us-central1-run.googleapis.com/'
|
|
327
|
+
);
|
|
328
|
+
} finally {
|
|
329
|
+
google.auth.GoogleAuth = originalGoogleAuth;
|
|
330
|
+
runMock.mock.restore();
|
|
331
|
+
clients.run.delete(key);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('caches client instances by key', async () => {
|
|
336
|
+
const runMock = mock.method(google, 'run', (options) => ({ options }));
|
|
337
|
+
try {
|
|
338
|
+
const projectId = 'test-project-cache';
|
|
339
|
+
const client1 = await getRunV1Client(projectId, GCLOUD_AUTH);
|
|
340
|
+
const client2 = await getRunV1Client(projectId, GCLOUD_AUTH);
|
|
341
|
+
|
|
342
|
+
assert.strictEqual(client1, client2);
|
|
343
|
+
assert.strictEqual(runMock.mock.calls.length, 1);
|
|
344
|
+
} finally {
|
|
345
|
+
runMock.mock.restore();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('caches client instances separately for different regions', async () => {
|
|
350
|
+
resetCachedRegions();
|
|
351
|
+
const runMock = mock.method(google, 'run', (options) => ({ options }));
|
|
352
|
+
try {
|
|
353
|
+
const projectId = 'test-project-region-cache';
|
|
354
|
+
const region1 = 'us-central1';
|
|
355
|
+
const region2 = 'europe-west1';
|
|
356
|
+
|
|
357
|
+
// Mock getCloudRunRegions for both regions
|
|
358
|
+
const key = projectId; // GCLOUD_AUTH key for getRunClient/getCloudRunRegions
|
|
359
|
+
clients.run.set(key, {
|
|
360
|
+
async *listLocationsAsync() {
|
|
361
|
+
yield { locationId: region1 };
|
|
362
|
+
yield { locationId: region2 };
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const client1 = await getRunV1Client(projectId, GCLOUD_AUTH, region1);
|
|
367
|
+
const client2 = await getRunV1Client(projectId, GCLOUD_AUTH, region2);
|
|
368
|
+
|
|
369
|
+
assert.notStrictEqual(client1, client2);
|
|
370
|
+
assert.strictEqual(runMock.mock.calls.length, 2);
|
|
371
|
+
assert.strictEqual(
|
|
372
|
+
runMock.mock.calls[0].arguments[0].rootUrl,
|
|
373
|
+
`https://${region1}-run.googleapis.com/`
|
|
374
|
+
);
|
|
375
|
+
assert.strictEqual(
|
|
376
|
+
runMock.mock.calls[1].arguments[0].rootUrl,
|
|
377
|
+
`https://${region2}-run.googleapis.com/`
|
|
378
|
+
);
|
|
379
|
+
} finally {
|
|
380
|
+
runMock.mock.restore();
|
|
381
|
+
clients.run.clear();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('throws error when OAUTH_ENABLED=true and accessToken is missing or GCLOUD_AUTH', async () => {
|
|
386
|
+
const originalOAuth = process.env.OAUTH_ENABLED;
|
|
387
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
388
|
+
try {
|
|
389
|
+
const projectId = 'test-project-v1-oauth-err';
|
|
390
|
+
await assert.rejects(
|
|
391
|
+
async () => getRunV1Client(projectId),
|
|
392
|
+
/Access token is required when OAuth is enabled/
|
|
393
|
+
);
|
|
394
|
+
await assert.rejects(
|
|
395
|
+
async () => getRunV1Client(projectId, undefined),
|
|
396
|
+
/Access token is required when OAuth is enabled/
|
|
397
|
+
);
|
|
398
|
+
await assert.rejects(
|
|
399
|
+
async () => getRunV1Client(projectId, GCLOUD_AUTH),
|
|
400
|
+
/Access token is required when OAuth is enabled/
|
|
401
|
+
);
|
|
402
|
+
} finally {
|
|
403
|
+
if (originalOAuth === undefined) {
|
|
404
|
+
delete process.env.OAUTH_ENABLED;
|
|
405
|
+
} else {
|
|
406
|
+
process.env.OAUTH_ENABLED = originalOAuth;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('getCloudRunRegions', () => {
|
|
413
|
+
beforeEach(() => {
|
|
414
|
+
resetCachedRegions();
|
|
415
|
+
// Clear the run map to ensure isolation between tests
|
|
416
|
+
clients.run.clear();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('returns list of regions using a mock injected into the clients cache', async () => {
|
|
420
|
+
const projectId = 'test-project-1';
|
|
421
|
+
const accessToken = 'token-1';
|
|
422
|
+
// Match the key generation logic
|
|
423
|
+
const key = projectId + accessToken;
|
|
424
|
+
|
|
425
|
+
const mockRunClient = {
|
|
426
|
+
async *listLocationsAsync() {
|
|
427
|
+
yield { locationId: 'us-central1' };
|
|
428
|
+
yield { locationId: 'europe-west1' };
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
clients.run.set(key, mockRunClient);
|
|
433
|
+
|
|
434
|
+
const regions = await getCloudRunRegions(projectId, accessToken);
|
|
435
|
+
assert.deepStrictEqual(regions, ['us-central1', 'europe-west1']);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('caches the regions after first call', async () => {
|
|
439
|
+
const projectId = 'test-project-2';
|
|
440
|
+
const accessToken = 'token-2';
|
|
441
|
+
const key = projectId + accessToken;
|
|
442
|
+
|
|
443
|
+
let callCount = 0;
|
|
444
|
+
const mockRunClient = {
|
|
445
|
+
async *listLocationsAsync() {
|
|
446
|
+
callCount++;
|
|
447
|
+
yield { locationId: 'us-central1' };
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
clients.run.set(key, mockRunClient);
|
|
452
|
+
|
|
453
|
+
await getCloudRunRegions(projectId, accessToken);
|
|
454
|
+
const regions = await getCloudRunRegions(projectId, accessToken);
|
|
455
|
+
|
|
456
|
+
assert.strictEqual(callCount, 1);
|
|
457
|
+
assert.deepStrictEqual(regions, ['us-central1']);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('using different accessToken allows isolated client mocks', async () => {
|
|
461
|
+
const projectId = 'test-project-3';
|
|
462
|
+
|
|
463
|
+
clients.run.set(projectId + 'token-a', {
|
|
464
|
+
async *listLocationsAsync() {
|
|
465
|
+
yield { locationId: 'region-a' };
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
clients.run.set(projectId + 'token-b', {
|
|
469
|
+
async *listLocationsAsync() {
|
|
470
|
+
yield { locationId: 'region-b' };
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const resA = await getCloudRunRegions(projectId, 'token-a');
|
|
475
|
+
resetCachedRegions();
|
|
476
|
+
const resB = await getCloudRunRegions(projectId, 'token-b');
|
|
477
|
+
|
|
478
|
+
assert.deepStrictEqual(resA, ['region-a']);
|
|
479
|
+
assert.deepStrictEqual(resB, ['region-b']);
|
|
480
|
+
});
|
|
176
481
|
});
|