@google-cloud/cloud-run-mcp 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +24 -0
  2. package/SECURITY.md +7 -0
  3. package/cloudbuild.yaml +37 -0
  4. package/example-sources-to-deploy/java/pom.xml +41 -0
  5. package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
  6. package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
  7. package/example-sources-to-deploy/nodejs/index.js +15 -0
  8. package/example-sources-to-deploy/nodejs/package.json +12 -0
  9. package/example-sources-to-deploy/python/pip-project/main.py +11 -0
  10. package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
  11. package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
  12. package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
  13. package/lib/clients.js +104 -3
  14. package/lib/cloud-api/build.js +192 -28
  15. package/lib/cloud-api/run.js +62 -0
  16. package/lib/cloud-api/secrets.js +196 -0
  17. package/lib/cloud-api/storage.js +164 -16
  18. package/lib/deployment/compose.js +517 -0
  19. package/lib/deployment/constants.js +8 -0
  20. package/lib/deployment/deployer.js +283 -28
  21. package/lib/deployment/helpers.js +58 -1
  22. package/lib/deployment/universal-maker.js +21 -148
  23. package/lib/util/archive.js +3 -3
  24. package/lib/util/artifacts.js +189 -0
  25. package/lib/util/helpers.js +109 -0
  26. package/mcp-server.js +4 -0
  27. package/package.json +6 -3
  28. package/skills/cloud-run/SKILL.md +109 -0
  29. package/test/local/archive.test.js +88 -0
  30. package/test/local/clients.test.js +224 -2
  31. package/test/local/cloud-api/build.test.js +330 -25
  32. package/test/local/cloud-api/run.test.js +129 -0
  33. package/test/local/cloud-api/secrets.test.js +291 -0
  34. package/test/local/cloud-api/storage.test.js +405 -0
  35. package/test/local/compose.test.js +618 -0
  36. package/test/local/deploy.test.js +408 -0
  37. package/test/local/deployment-helpers.test.js +100 -1
  38. package/test/local/test-utils.js +2 -18
  39. package/test/local/tools.test.js +43 -1
  40. package/test/local/universal-maker.test.js +23 -114
  41. package/test/local/util.test.js +361 -0
  42. package/test/need-gcp/deployer.test.js +1 -1
  43. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  44. package/tools/register-tools.js +19 -3
  45. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  46. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  47. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: cloud-run
3
+ description: 'Manage Cloud Run services and jobs.'
4
+ metadata:
5
+ version: 0.1.0
6
+ openclaw:
7
+ category: 'compute'
8
+ requires:
9
+ bins:
10
+ - gcloud
11
+ cliHelp: 'gcloud run --help'
12
+ ---
13
+
14
+ # run (v1)
15
+
16
+ > **PREREQUISITE:** Ensure you have the `gcloud` CLI installed and authenticated with `gcloud auth login`. Set your project with `gcloud config set project [PROJECT_ID]`.
17
+
18
+ ```bash
19
+ gcloud run <resource> <method> [flags]
20
+ ```
21
+
22
+ ## Helper Commands
23
+
24
+ | Command | Description |
25
+ | -------- | -------------------------------------------- |
26
+ | `deploy` | Create or update a Cloud Run service or job. |
27
+
28
+ ## API Resources
29
+
30
+ ### services
31
+
32
+ - `list` — List available services in the specified region.
33
+ - `describe` — Obtain details about a given service, such as its URL and configuration.
34
+ - `update` — Update Cloud Run environment variables, concurrency settings, and other configuration.
35
+ - `delete` — Delete a service and its associated revisions.
36
+ - `update-traffic` — Adjust the traffic assignments for a Cloud Run service.
37
+ - `proxy` — Proxy a service to localhost authenticating as the active account.
38
+ - `logs read` — Read logs for a Cloud Run service.
39
+ - `replace` - Create or replace a service from a YAML service specification.
40
+ - `add-iam-policy-binding` — Add IAM policy binding to a Cloud Run service (e.g., to make it public).
41
+ - `remove-iam-policy-binding` - Remove IAM policy binding of a Cloud Run service.
42
+ - `get-iam-policy` - Get the IAM policy for a Cloud Run service.
43
+ - `set-iam-policy` - Set the IAM policy for a Cloud Run service.
44
+
45
+ ### jobs
46
+
47
+ - `create` — Create a Cloud Run job.
48
+ - `execute` — Start an execution of a Cloud Run job.
49
+ - `list` — List available jobs.
50
+ - `describe` — Obtain details about a given job.
51
+ - `deploy` - Create or update a Cloud Run job
52
+ - `update` — Update a Cloud Run job configuration.
53
+ - `delete` — Delete a Cloud Run job.
54
+ - `replace` - Create or replace a Cloud Run job from a YAML job specification.
55
+ - `executions list` — List executions of a Cloud Run job.
56
+ - `logs read` — Read logs for a Cloud Run job.
57
+ - `add-iam-policy-binding` — Add IAM policy binding to a Cloud Run job.
58
+ - `remove-iam-policy-binding` - Remove IAM policy binding of a Cloud Run job.
59
+ - `get-iam-policy` - Get the IAM policy for a Cloud Run job.
60
+ - `set-iam-policy` - Set the IAM policy for a Cloud Run job.
61
+
62
+ ### domain-mappings
63
+
64
+ - `list` — List domain mappings.
65
+ - `create` — Create a new domain mapping.
66
+ - `describe` — Obtain details about a domain mapping.
67
+ - `delete` — Delete a domain mapping.
68
+
69
+ ### multi-region-services
70
+
71
+ - `list` — List multi-region services.
72
+ - `describe` — Obtain details about a multi-region service.
73
+ - `update` — Update settings for multi-region services.
74
+ - `delete` — Delete a multi-region service.
75
+ - `replace` - Create or Update multi-region service from YAML specification.
76
+
77
+ ### revisions
78
+
79
+ - `list` — List available revisions for a service.
80
+ - `describe` — Obtain details about a specific revision.
81
+ - `delete` — Delete a specific revision.
82
+
83
+ ### regions
84
+
85
+ - `list` — View available Cloud Run (fully managed) regions.
86
+
87
+ ### compose
88
+
89
+ - `up` — Deploy to Cloud Run from a compose specification.
90
+
91
+ ## Discovering Commands
92
+
93
+ Before calling any command, inspect it for help:
94
+
95
+ ```bash
96
+ # Browse resources and methods
97
+ gcloud run --help
98
+
99
+ # Inspect a specific resource methods
100
+ gcloud run services --help
101
+
102
+ # Inspect a specific resource's sub-group methods
103
+ gcloud run jobs executions --help
104
+
105
+ # Inspect a method's specific flags and arguments
106
+ gcloud run deploy --help
107
+ ```
108
+
109
+ Use the output of `--help` to discover available flags like `--image`, `--env-vars`, `--memory`, etc.
@@ -0,0 +1,88 @@
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, beforeEach, afterEach } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import os from 'os';
22
+ import { zipFiles } from '../../lib/util/archive.js';
23
+
24
+ describe('Archive Utility (zipFiles)', () => {
25
+ let tempDir;
26
+
27
+ beforeEach(() => {
28
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'archive-test-'));
29
+ });
30
+
31
+ afterEach(() => {
32
+ if (tempDir && fs.existsSync(tempDir)) {
33
+ fs.rmSync(tempDir, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ test('creates a valid zip archive in memory from buffer objects', async () => {
38
+ const files = [
39
+ { filename: 'file1.txt', content: 'Hello World' },
40
+ { filename: 'file2.json', content: JSON.stringify({ key: 'value' }) },
41
+ ];
42
+
43
+ const zipBuffer = await zipFiles(files, false);
44
+ assert.ok(Buffer.isBuffer(zipBuffer));
45
+ assert.ok(zipBuffer.length > 0);
46
+ });
47
+
48
+ test('creates a valid tar.gz archive in memory from buffer objects', async () => {
49
+ const files = [{ filename: 'file1.txt', content: 'Hello World Tar' }];
50
+
51
+ const tarBuffer = await zipFiles(files, true);
52
+ assert.ok(Buffer.isBuffer(tarBuffer));
53
+ assert.ok(tarBuffer.length > 0);
54
+ });
55
+
56
+ test('creates archive from actual file paths on disk', async () => {
57
+ const testFilePath = path.join(tempDir, 'sample.txt');
58
+ fs.writeFileSync(testFilePath, 'Sample file content');
59
+
60
+ const zipBuffer = await zipFiles([testFilePath], false);
61
+ assert.ok(Buffer.isBuffer(zipBuffer));
62
+ assert.ok(zipBuffer.length > 0);
63
+ });
64
+
65
+ test('creates archive from directory path on disk', async () => {
66
+ const subDir = path.join(tempDir, 'subdir');
67
+ fs.mkdirSync(subDir);
68
+ fs.writeFileSync(path.join(subDir, 'nested.txt'), 'Nested content');
69
+
70
+ const zipBuffer = await zipFiles([subDir], false);
71
+ assert.ok(Buffer.isBuffer(zipBuffer));
72
+ assert.ok(zipBuffer.length > 0);
73
+ });
74
+
75
+ test('throws error if file path does not exist', async () => {
76
+ const nonExistentPath = path.join(tempDir, 'does-not-exist.txt');
77
+ await assert.rejects(
78
+ async () => {
79
+ await zipFiles([nonExistentPath], false);
80
+ },
81
+ {
82
+ message: new RegExp(
83
+ `File or directory not found: ${nonExistentPath.replace(/\\/g, '\\\\')}`
84
+ ),
85
+ }
86
+ );
87
+ });
88
+ });
@@ -1,6 +1,14 @@
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 { getClient } from '../../lib/clients.js';
3
+ import { google } from 'googleapis';
4
+ import {
5
+ clients,
6
+ getClient,
7
+ getRunV1Client,
8
+ getCloudRunRegions,
9
+ resetCachedRegions,
10
+ } from '../../lib/clients.js';
11
+ import { GCLOUD_AUTH } from '../../constants.js';
4
12
 
5
13
  describe('getClient Helper', () => {
6
14
  class MockClient {
@@ -174,3 +182,217 @@ describe('getClient Helper', () => {
174
182
  assert.strictEqual(client.options.extraOpt, extraOpt);
175
183
  });
176
184
  });
185
+
186
+ describe('getRunV1Client', () => {
187
+ test('creates new client instance with Application Default Credentials for GCLOUD_AUTH', async () => {
188
+ // Mock the GoogleAuth class to avoid looking up ADC
189
+ const originalGoogleAuth = google.auth.GoogleAuth;
190
+ const authInstances = [];
191
+ google.auth.GoogleAuth = class MockGoogleAuth {
192
+ constructor(options) {
193
+ this.options = options;
194
+ authInstances.push(this);
195
+ }
196
+ };
197
+
198
+ const runMock = mock.method(google, 'run', (options) => {
199
+ return { options };
200
+ });
201
+
202
+ try {
203
+ const projectId = 'test-project';
204
+ const client = await getRunV1Client(projectId, GCLOUD_AUTH);
205
+
206
+ assert.ok(client);
207
+ assert.strictEqual(authInstances.length, 1);
208
+ assert.deepEqual(authInstances[0].options.scopes, [
209
+ 'https://www.googleapis.com/auth/cloud-platform',
210
+ ]);
211
+ assert.strictEqual(runMock.mock.calls.length, 1);
212
+ assert.strictEqual(
213
+ runMock.mock.calls[0].arguments[0].auth,
214
+ authInstances[0]
215
+ );
216
+ } finally {
217
+ google.auth.GoogleAuth = originalGoogleAuth;
218
+ runMock.mock.restore();
219
+ }
220
+ });
221
+
222
+ test('creates new client instance with OAuth client when access token provided', async () => {
223
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
224
+ try {
225
+ const projectId = 'test-project-oauth';
226
+ const accessToken = 'fake-access-token';
227
+ const client = await getRunV1Client(projectId, accessToken);
228
+
229
+ assert.ok(client);
230
+ assert.strictEqual(runMock.mock.calls.length, 1);
231
+ const lastCall = runMock.mock.calls[0];
232
+ assert.ok(lastCall.arguments[0].auth);
233
+ assert.strictEqual(
234
+ lastCall.arguments[0].auth.credentials.access_token,
235
+ accessToken
236
+ );
237
+ } finally {
238
+ runMock.mock.restore();
239
+ }
240
+ });
241
+
242
+ test('sets rootUrl if region is provided', async () => {
243
+ const originalGoogleAuth = google.auth.GoogleAuth;
244
+ google.auth.GoogleAuth = class MockGoogleAuth {
245
+ constructor(options) {
246
+ this.options = options;
247
+ }
248
+ };
249
+
250
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
251
+
252
+ const projectId = 'test-project-region';
253
+ const region = 'us-central1';
254
+ const key = projectId; // If GCLOUD_AUTH, key is just projectId
255
+
256
+ // Mock getCloudRunRegions call by injecting into clients.run
257
+ clients.run.set(key, {
258
+ async *listLocationsAsync() {
259
+ yield { locationId: 'us-central1' };
260
+ },
261
+ });
262
+
263
+ try {
264
+ await getRunV1Client(projectId, GCLOUD_AUTH, region);
265
+
266
+ const lastCall = runMock.mock.calls[0];
267
+ assert.strictEqual(
268
+ lastCall.arguments[0].rootUrl,
269
+ 'https://us-central1-run.googleapis.com/'
270
+ );
271
+ } finally {
272
+ google.auth.GoogleAuth = originalGoogleAuth;
273
+ runMock.mock.restore();
274
+ clients.run.delete(key);
275
+ }
276
+ });
277
+
278
+ test('caches client instances by key', async () => {
279
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
280
+ try {
281
+ const projectId = 'test-project-cache';
282
+ const client1 = await getRunV1Client(projectId, GCLOUD_AUTH);
283
+ const client2 = await getRunV1Client(projectId, GCLOUD_AUTH);
284
+
285
+ assert.strictEqual(client1, client2);
286
+ assert.strictEqual(runMock.mock.calls.length, 1);
287
+ } finally {
288
+ runMock.mock.restore();
289
+ }
290
+ });
291
+
292
+ test('caches client instances separately for different regions', async () => {
293
+ resetCachedRegions();
294
+ const runMock = mock.method(google, 'run', (options) => ({ options }));
295
+ try {
296
+ const projectId = 'test-project-region-cache';
297
+ const region1 = 'us-central1';
298
+ const region2 = 'europe-west1';
299
+
300
+ // Mock getCloudRunRegions for both regions
301
+ const key = projectId; // GCLOUD_AUTH key for getRunClient/getCloudRunRegions
302
+ clients.run.set(key, {
303
+ async *listLocationsAsync() {
304
+ yield { locationId: region1 };
305
+ yield { locationId: region2 };
306
+ },
307
+ });
308
+
309
+ const client1 = await getRunV1Client(projectId, GCLOUD_AUTH, region1);
310
+ const client2 = await getRunV1Client(projectId, GCLOUD_AUTH, region2);
311
+
312
+ assert.notStrictEqual(client1, client2);
313
+ assert.strictEqual(runMock.mock.calls.length, 2);
314
+ assert.strictEqual(
315
+ runMock.mock.calls[0].arguments[0].rootUrl,
316
+ `https://${region1}-run.googleapis.com/`
317
+ );
318
+ assert.strictEqual(
319
+ runMock.mock.calls[1].arguments[0].rootUrl,
320
+ `https://${region2}-run.googleapis.com/`
321
+ );
322
+ } finally {
323
+ runMock.mock.restore();
324
+ clients.run.clear();
325
+ }
326
+ });
327
+ });
328
+
329
+ describe('getCloudRunRegions', () => {
330
+ beforeEach(() => {
331
+ resetCachedRegions();
332
+ // Clear the run map to ensure isolation between tests
333
+ clients.run.clear();
334
+ });
335
+
336
+ test('returns list of regions using a mock injected into the clients cache', async () => {
337
+ const projectId = 'test-project-1';
338
+ const accessToken = 'token-1';
339
+ // Match the key generation logic
340
+ const key = projectId + accessToken;
341
+
342
+ const mockRunClient = {
343
+ async *listLocationsAsync() {
344
+ yield { locationId: 'us-central1' };
345
+ yield { locationId: 'europe-west1' };
346
+ },
347
+ };
348
+
349
+ clients.run.set(key, mockRunClient);
350
+
351
+ const regions = await getCloudRunRegions(projectId, accessToken);
352
+ assert.deepStrictEqual(regions, ['us-central1', 'europe-west1']);
353
+ });
354
+
355
+ test('caches the regions after first call', async () => {
356
+ const projectId = 'test-project-2';
357
+ const accessToken = 'token-2';
358
+ const key = projectId + accessToken;
359
+
360
+ let callCount = 0;
361
+ const mockRunClient = {
362
+ async *listLocationsAsync() {
363
+ callCount++;
364
+ yield { locationId: 'us-central1' };
365
+ },
366
+ };
367
+
368
+ clients.run.set(key, mockRunClient);
369
+
370
+ await getCloudRunRegions(projectId, accessToken);
371
+ const regions = await getCloudRunRegions(projectId, accessToken);
372
+
373
+ assert.strictEqual(callCount, 1);
374
+ assert.deepStrictEqual(regions, ['us-central1']);
375
+ });
376
+
377
+ test('using different accessToken allows isolated client mocks', async () => {
378
+ const projectId = 'test-project-3';
379
+
380
+ clients.run.set(projectId + 'token-a', {
381
+ async *listLocationsAsync() {
382
+ yield { locationId: 'region-a' };
383
+ },
384
+ });
385
+ clients.run.set(projectId + 'token-b', {
386
+ async *listLocationsAsync() {
387
+ yield { locationId: 'region-b' };
388
+ },
389
+ });
390
+
391
+ const resA = await getCloudRunRegions(projectId, 'token-a');
392
+ resetCachedRegions();
393
+ const resB = await getCloudRunRegions(projectId, 'token-b');
394
+
395
+ assert.deepStrictEqual(resA, ['region-a']);
396
+ assert.deepStrictEqual(resB, ['region-b']);
397
+ });
398
+ });