@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.
Files changed (50) 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/gemini-extension.json +12 -0
  14. package/lib/clients.js +104 -3
  15. package/lib/cloud-api/build.js +192 -28
  16. package/lib/cloud-api/run.js +62 -0
  17. package/lib/cloud-api/secrets.js +196 -0
  18. package/lib/cloud-api/storage.js +164 -16
  19. package/lib/deployment/compose.js +517 -0
  20. package/lib/deployment/constants.js +11 -0
  21. package/lib/deployment/deployer.js +334 -30
  22. package/lib/deployment/helpers.js +99 -49
  23. package/lib/deployment/source-processor.js +0 -48
  24. package/lib/deployment/universal-maker.js +147 -0
  25. package/lib/util/archive.js +3 -3
  26. package/lib/util/artifacts.js +189 -0
  27. package/lib/util/helpers.js +109 -0
  28. package/mcp-server.js +4 -0
  29. package/package.json +6 -3
  30. package/skills/cloud-run/SKILL.md +109 -0
  31. package/test/local/archive.test.js +88 -0
  32. package/test/local/clients.test.js +224 -2
  33. package/test/local/cloud-api/build.test.js +330 -25
  34. package/test/local/cloud-api/run.test.js +129 -0
  35. package/test/local/cloud-api/secrets.test.js +291 -0
  36. package/test/local/cloud-api/storage.test.js +405 -0
  37. package/test/local/compose.test.js +618 -0
  38. package/test/local/deploy.test.js +408 -0
  39. package/test/local/deployment-helpers.test.js +126 -79
  40. package/test/local/source-processor.test.js +0 -165
  41. package/test/local/test-utils.js +2 -18
  42. package/test/local/tools.test.js +43 -1
  43. package/test/local/universal-maker.test.js +157 -0
  44. package/test/local/util.test.js +361 -0
  45. package/test/need-gcp/deployer.test.js +1 -1
  46. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  47. package/tools/register-tools.js +19 -3
  48. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  49. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  50. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -23,6 +23,9 @@ import {
23
23
  import { callWithRetry, ensureApisEnabled } from './helpers.js';
24
24
  import { logAndProgress } from '../util/helpers.js';
25
25
 
26
+ const INVOKER_ROLE = 'roles/run.invoker';
27
+ const PUBLIC_MEMBER = 'allUsers';
28
+
26
29
  async function listCloudRunLocations(projectId, accessToken) {
27
30
  const listLocationsRequest = {
28
31
  name: `projects/${projectId}`,
@@ -255,3 +258,62 @@ export async function checkCloudRunServiceExists(
255
258
  throw error;
256
259
  }
257
260
  }
261
+
262
+ /**
263
+ * Sets public access (allUsers) for a Cloud Run service by assigning the 'roles/run.invoker' role.
264
+ *
265
+ * @async
266
+ * @param {string} projectId - The Google Cloud project ID.
267
+ * @param {string} location - The Google Cloud region.
268
+ * @param {string} serviceId - The ID of the Cloud Run service.
269
+ * @param {string} accessToken - Access token for authentication.
270
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
271
+ * @returns {Promise<void>} A promise that resolves when the IAM policy is successfully set.
272
+ * @throws {Error} If there's an error setting the IAM policy.
273
+ */
274
+ export async function setServicePublicAccess(
275
+ projectId,
276
+ location,
277
+ serviceId,
278
+ accessToken,
279
+ progressCallback
280
+ ) {
281
+ const runClient = await getRunClient(projectId, accessToken);
282
+ const resource = runClient.servicePath(projectId, location, serviceId);
283
+
284
+ try {
285
+ await logAndProgress(
286
+ `Setting public access for service ${serviceId}...`,
287
+ progressCallback
288
+ );
289
+ // For Cloud Run v2, we should use getIamPolicy then merge or just set.
290
+ // Given this is usually for fresh deployments or simple public services,
291
+ // we set it directly.
292
+ await callWithRetry(async () => {
293
+ const [policy] = await runClient.getIamPolicy({ resource });
294
+
295
+ // Find existing invoker binding or create a new one
296
+ let invokerBinding = policy.bindings.find((b) => b.role === INVOKER_ROLE);
297
+ if (!invokerBinding) {
298
+ invokerBinding = { role: INVOKER_ROLE, members: [] };
299
+ policy.bindings.push(invokerBinding);
300
+ }
301
+
302
+ // Add public member if not present
303
+ if (!invokerBinding.members.includes(PUBLIC_MEMBER)) {
304
+ invokerBinding.members.push(PUBLIC_MEMBER);
305
+ }
306
+
307
+ // Set the updated policy
308
+ await runClient.setIamPolicy({ resource, policy });
309
+ }, 'run.setIamPolicy');
310
+
311
+ await logAndProgress(
312
+ `Public access set successfully for service ${serviceId}.`,
313
+ progressCallback
314
+ );
315
+ } catch (error) {
316
+ console.error(`Error setting public access for ${serviceId}:`, error);
317
+ throw error;
318
+ }
319
+ }
@@ -0,0 +1,196 @@
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 { getSecretManagerClient } from '../clients.js';
18
+ import { logAndProgress } from '../util/helpers.js';
19
+
20
+ const SECRET_ACCESSOR_ROLE = 'roles/secretmanager.secretAccessor';
21
+
22
+ /**
23
+ * Retrieves secret metadata.
24
+ * @param {string} projectId - GCP project ID.
25
+ * @param {string} secretName - Name of the secret.
26
+ * @param {string} accessToken - OAuth2 access token.
27
+ * @returns {Promise<Object|null>} Secret data or null if not found.
28
+ */
29
+ export async function getSecret(projectId, secretName, accessToken) {
30
+ const secretManager = await getSecretManagerClient(projectId, accessToken);
31
+ try {
32
+ const [res] = await secretManager.getSecret({
33
+ name: `projects/${projectId}/secrets/${secretName}`,
34
+ });
35
+ return res;
36
+ } catch (err) {
37
+ if (err.code === 5) {
38
+ // NOT_FOUND in gRPC
39
+ return null;
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Creates a new secret in Secret Manager.
47
+ * @param {string} projectId - GCP project ID.
48
+ * @param {string} secretName - Name of the secret to create.
49
+ * @param {string} accessToken - OAuth2 access token.
50
+ * @param {Object.<string, string>} [labels] - Optional labels to apply to the secret.
51
+ * @param {Function} progressCallback - Progress reporting callback.
52
+ * @returns {Promise<Object>} The created secret.
53
+ */
54
+ export async function createSecret(
55
+ projectId,
56
+ secretName,
57
+ accessToken,
58
+ labels,
59
+ progressCallback
60
+ ) {
61
+ const secretManager = await getSecretManagerClient(projectId, accessToken);
62
+ await logAndProgress(`Creating secret '${secretName}'...`, progressCallback);
63
+ const secret = {
64
+ replication: {
65
+ automatic: {},
66
+ },
67
+ };
68
+ if (labels) {
69
+ secret.labels = labels;
70
+ }
71
+ const [res] = await secretManager.createSecret({
72
+ parent: `projects/${projectId}`,
73
+ secretId: secretName,
74
+ secret: secret,
75
+ });
76
+ return res;
77
+ }
78
+
79
+ /**
80
+ * Adds a new version to an existing secret.
81
+ * @param {string} projectId - GCP project ID.
82
+ * @param {string} secretName - Name of the secret.
83
+ * @param {Buffer|string} content - Secret content.
84
+ * @param {string} accessToken - OAuth2 access token.
85
+ * @param {Function} progressCallback - Progress reporting callback.
86
+ * @returns {Promise<Object>} Information about the created version.
87
+ */
88
+ export async function addSecretVersion(
89
+ projectId,
90
+ secretName,
91
+ content,
92
+ accessToken,
93
+ progressCallback
94
+ ) {
95
+ const secretManager = await getSecretManagerClient(projectId, accessToken);
96
+ await logAndProgress(
97
+ `Adding new version to secret '${secretName}'...`,
98
+ progressCallback
99
+ );
100
+
101
+ const payload = typeof content === 'string' ? Buffer.from(content) : content;
102
+
103
+ const [res] = await secretManager.addSecretVersion({
104
+ parent: `projects/${projectId}/secrets/${secretName}`,
105
+ payload: {
106
+ data: payload,
107
+ },
108
+ });
109
+ return res;
110
+ }
111
+
112
+ /**
113
+ * Gets the IAM policy for a secret.
114
+ * @param {string} projectId - GCP project ID.
115
+ * @param {string} secretName - Name of the secret.
116
+ * @param {string} accessToken - OAuth2 access token.
117
+ * @returns {Promise<Object>} The IAM policy.
118
+ */
119
+ export async function getSecretIamPolicy(projectId, secretName, accessToken) {
120
+ const secretManager = await getSecretManagerClient(projectId, accessToken);
121
+ const [res] = await secretManager.getIamPolicy({
122
+ resource: `projects/${projectId}/secrets/${secretName}`,
123
+ });
124
+ return res;
125
+ }
126
+
127
+ /**
128
+ * Sets the IAM policy for a secret.
129
+ * @param {string} projectId - GCP project ID.
130
+ * @param {string} secretName - Name of the secret.
131
+ * @param {Object} policy - The IAM policy to set.
132
+ * @param {string} accessToken - OAuth2 access token.
133
+ * @param {Function} progressCallback - Progress reporting callback.
134
+ * @returns {Promise<Object>} Result of the policy update.
135
+ */
136
+ export async function setSecretIamPolicy(
137
+ projectId,
138
+ secretName,
139
+ policy,
140
+ accessToken,
141
+ progressCallback
142
+ ) {
143
+ const secretManager = await getSecretManagerClient(projectId, accessToken);
144
+ await logAndProgress(
145
+ `Updating IAM policy for secret '${secretName}'...`,
146
+ progressCallback
147
+ );
148
+ const [res] = await secretManager.setIamPolicy({
149
+ resource: `projects/${projectId}/secrets/${secretName}`,
150
+ policy: policy,
151
+ });
152
+ return res;
153
+ }
154
+
155
+ /**
156
+ * Adds a role binding to a secret's IAM policy.
157
+ * @param {string} projectId - GCP project ID.
158
+ * @param {string} secretName - Name of the secret.
159
+ * @param {string} member - Member to add (e.g. 'serviceAccount:...').
160
+ * @param {string} accessToken - OAuth2 access token.
161
+ * @param {Function} progressCallback - Progress reporting callback.
162
+ * @returns {Promise<Object>} Final IAM policy.
163
+ */
164
+ export async function addSecretAccessorBinding(
165
+ projectId,
166
+ secretName,
167
+ member,
168
+ accessToken,
169
+ progressCallback
170
+ ) {
171
+ const policy = await getSecretIamPolicy(projectId, secretName, accessToken);
172
+ const role = SECRET_ACCESSOR_ROLE;
173
+
174
+ policy.bindings = policy.bindings || [];
175
+ const binding = policy.bindings.find((b) => b.role === role);
176
+
177
+ if (binding) {
178
+ if (binding.members.includes(member)) {
179
+ return policy;
180
+ }
181
+ binding.members.push(member);
182
+ } else {
183
+ policy.bindings.push({
184
+ role: role,
185
+ members: [member],
186
+ });
187
+ }
188
+
189
+ return setSecretIamPolicy(
190
+ projectId,
191
+ secretName,
192
+ policy,
193
+ accessToken,
194
+ progressCallback
195
+ );
196
+ }
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import crypto from 'node:crypto';
17
18
  import { getStorageClient } from '../clients.js';
18
19
  import { callWithRetry } from './helpers.js';
19
- import { logAndProgress } from '../util/helpers.js';
20
+ import { logAndProgress, getProjectNumber } from '../util/helpers.js';
20
21
 
21
22
  /**
22
23
  * Ensures that a Google Cloud Storage bucket exists.
@@ -25,7 +26,10 @@ import { logAndProgress } from '../util/helpers.js';
25
26
  * @async
26
27
  * @param {string} projectId - The Google Cloud project ID.
27
28
  * @param {string} bucketName - The name of the storage bucket.
28
- * @param {string} [location='us'] - The location to create the bucket in if it doesn't exist. Defaults to 'us'.
29
+ * @param {string} [location] - The location to create the bucket in if it doesn't exist.
30
+ * @param {string} [accessToken] - Optional access token.
31
+ * @param {Object.<string, string>} [labels] - Optional labels to apply to the bucket.
32
+ * @param {boolean} [isFallback=false] - Whether this is a fallback attempt (avoids infinite recursion).
29
33
  * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
30
34
  * @returns {Promise<import('@google-cloud/storage').Bucket>} A promise that resolves with the GCS Bucket object.
31
35
  * @throws {Error} If there's an error checking or creating the bucket.
@@ -33,18 +37,73 @@ import { logAndProgress } from '../util/helpers.js';
33
37
  export async function ensureStorageBucketExists(
34
38
  projectId,
35
39
  bucketName,
36
- location = 'us',
40
+ location,
37
41
  accessToken,
42
+ labels,
43
+ isFallback = false,
38
44
  progressCallback
39
45
  ) {
40
46
  const storage = await getStorageClient(projectId, accessToken);
41
47
  const bucket = storage.bucket(bucketName);
42
48
  try {
43
- const [exists] = await callWithRetry(
44
- () => bucket.exists(),
45
- `storage.bucket.exists ${bucketName}`
46
- );
49
+ let exists = false;
50
+ try {
51
+ const [bucketExists] = await callWithRetry(
52
+ () => bucket.exists(),
53
+ `storage.bucket.exists ${bucketName}`
54
+ );
55
+ exists = bucketExists;
56
+ } catch (err) {
57
+ if (err.code === 403 || err.status === 403 || err.statusCode === 403) {
58
+ exists = true;
59
+ } else {
60
+ throw err;
61
+ }
62
+ }
63
+
47
64
  if (exists) {
65
+ const expectedProjectNumber = await getProjectNumber(
66
+ projectId,
67
+ accessToken
68
+ );
69
+ let metadata;
70
+ try {
71
+ [metadata] = await callWithRetry(
72
+ () => bucket.getMetadata(),
73
+ `storage.bucket.getMetadata ${bucketName}`
74
+ );
75
+ } catch (metaErr) {
76
+ metadata = {};
77
+ }
78
+
79
+ if (
80
+ !expectedProjectNumber ||
81
+ !metadata.projectNumber ||
82
+ String(metadata.projectNumber) !== String(expectedProjectNumber)
83
+ ) {
84
+ if (!isFallback) {
85
+ const randomSuffix = crypto.randomBytes(4).toString('hex');
86
+ const fallbackBucketName = `${bucketName}-${randomSuffix}`;
87
+ await logAndProgress(
88
+ `Bucket ${bucketName} belongs to project number ${metadata.projectNumber || 'unknown'}, not expected project number ${expectedProjectNumber}. Falling back to randomized bucket ${fallbackBucketName}...`,
89
+ progressCallback,
90
+ 'warn'
91
+ );
92
+ return await ensureStorageBucketExists(
93
+ projectId,
94
+ fallbackBucketName,
95
+ location,
96
+ accessToken,
97
+ labels,
98
+ true,
99
+ progressCallback
100
+ );
101
+ } else {
102
+ const errorMsg = `Security Error: Fallback bucket ${bucketName} belongs to project number ${metadata.projectNumber || 'unknown'}, not expected project number ${expectedProjectNumber}.`;
103
+ await logAndProgress(errorMsg, progressCallback, 'error');
104
+ throw new Error(errorMsg);
105
+ }
106
+ }
48
107
  await logAndProgress(
49
108
  `Bucket ${bucketName} already exists.`,
50
109
  progressCallback
@@ -56,8 +115,12 @@ export async function ensureStorageBucketExists(
56
115
  progressCallback
57
116
  );
58
117
  try {
118
+ const options = { location: location };
119
+ if (labels) {
120
+ options.metadata = { labels: labels };
121
+ }
59
122
  const [createdBucket] = await callWithRetry(
60
- () => storage.createBucket(bucketName, { location: location }),
123
+ () => storage.createBucket(bucketName, options),
61
124
  `storage.createBucket ${bucketName}`
62
125
  );
63
126
  await logAndProgress(
@@ -66,6 +129,30 @@ export async function ensureStorageBucketExists(
66
129
  );
67
130
  return createdBucket;
68
131
  } catch (createError) {
132
+ if (
133
+ !isFallback &&
134
+ (createError.code === 409 ||
135
+ createError.status === 409 ||
136
+ createError.statusCode === 409 ||
137
+ createError.message?.includes('already exists'))
138
+ ) {
139
+ const randomSuffix = crypto.randomBytes(4).toString('hex');
140
+ const fallbackBucketName = `${bucketName}-${randomSuffix}`;
141
+ await logAndProgress(
142
+ `Failed to create storage bucket ${bucketName} (${createError.message}). Falling back to randomized bucket ${fallbackBucketName}...`,
143
+ progressCallback,
144
+ 'warn'
145
+ );
146
+ return await ensureStorageBucketExists(
147
+ projectId,
148
+ fallbackBucketName,
149
+ location,
150
+ accessToken,
151
+ labels,
152
+ true,
153
+ progressCallback
154
+ );
155
+ }
69
156
  const errorMessage = `Failed to create storage bucket ${bucketName}. Error details: ${createError.message}`;
70
157
  console.error(
71
158
  `Failed to create storage bucket ${bucketName}. Error details:`,
@@ -84,15 +171,14 @@ export async function ensureStorageBucketExists(
84
171
  }
85
172
 
86
173
  /**
87
- * Uploads a buffer to a specified Google Cloud Storage bucket and blob name.
174
+ * Uploads a buffer to a Cloud Storage bucket.
88
175
  *
89
176
  * @async
90
- * @param {import('@google-cloud/storage').Bucket} bucket - The Google Cloud Storage bucket object.
91
- * @param {Buffer} buffer - The buffer containing the data to upload.
92
- * @param {string} destinationBlobName - The name for the blob in the bucket.
177
+ * @param {import('@google-cloud/storage').Bucket} bucket - The GCS Bucket object.
178
+ * @param {Buffer} buffer - The buffer content to upload.
179
+ * @param {string} destinationBlobName - The name of the file in the bucket.
93
180
  * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
94
- * @returns {Promise<import('@google-cloud/storage').File>} A promise that resolves with the GCS File object representing the uploaded blob.
95
- * @throws {Error} If the upload fails.
181
+ * @returns {Promise<import('@google-cloud/storage').File>} A promise that resolves with the File object.
96
182
  */
97
183
  export async function uploadToStorageBucket(
98
184
  bucket,
@@ -105,15 +191,21 @@ export async function uploadToStorageBucket(
105
191
  `Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
106
192
  progressCallback
107
193
  );
194
+ const file = bucket.file(destinationBlobName);
108
195
  await callWithRetry(
109
- () => bucket.file(destinationBlobName).save(buffer),
196
+ () => file.save(buffer),
110
197
  `storage.bucket.file.save ${destinationBlobName}`
111
198
  );
199
+ const [metadata] = await callWithRetry(
200
+ () => file.getMetadata(),
201
+ `storage.bucket.file.getMetadata ${destinationBlobName}`
202
+ );
203
+ file.metadata = metadata;
112
204
  await logAndProgress(
113
205
  `File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
114
206
  progressCallback
115
207
  );
116
- return bucket.file(destinationBlobName);
208
+ return file;
117
209
  } catch (error) {
118
210
  const errorMessage = `Error uploading buffer: ${error.message}`;
119
211
  console.error(`Error uploading buffer:`, error);
@@ -121,3 +213,59 @@ export async function uploadToStorageBucket(
121
213
  throw error;
122
214
  }
123
215
  }
216
+
217
+ /**
218
+ * Grants a specific role to a member on a GCS bucket.
219
+ *
220
+ * @async
221
+ * @param {import('@google-cloud/storage').Bucket} bucket - The GCS Bucket object.
222
+ * @param {string} role - The role to grant (e.g., 'roles/storage.objectAdmin').
223
+ * @param {string} member - The member to grant the role to (e.g., 'serviceAccount:xyz@example.com').
224
+ * @param {function(object): void} [progressCallback] - Optional callback for progress updates.
225
+ * @returns {Promise<void>}
226
+ */
227
+ export async function grantBucketAccess(
228
+ bucket,
229
+ role,
230
+ member,
231
+ progressCallback
232
+ ) {
233
+ try {
234
+ await logAndProgress(
235
+ `Granting ${role} to ${member} on bucket ${bucket.name}...`,
236
+ progressCallback
237
+ );
238
+ const [policy] = await callWithRetry(
239
+ () => bucket.iam.getPolicy({ requestedPolicyVersion: 3 }),
240
+ `storage.bucket.iam.getPolicy ${bucket.name}`
241
+ );
242
+
243
+ // Check if member already has the role
244
+ const binding = policy.bindings.find((b) => b.role === role);
245
+ if (binding && binding.members.includes(member)) {
246
+ await logAndProgress(
247
+ `${member} already has ${role} on bucket ${bucket.name}.`,
248
+ progressCallback
249
+ );
250
+ return;
251
+ }
252
+
253
+ if (binding) {
254
+ binding.members.push(member);
255
+ } else {
256
+ policy.bindings.push({ role: role, members: [member] });
257
+ }
258
+
259
+ await callWithRetry(
260
+ () => bucket.iam.setPolicy(policy),
261
+ `storage.bucket.iam.setPolicy ${bucket.name}`
262
+ );
263
+ await logAndProgress(
264
+ `Successfully granted ${role} to ${member} on bucket ${bucket.name}.`,
265
+ progressCallback
266
+ );
267
+ } catch (error) {
268
+ const errorMessage = `Error granting access to bucket ${bucket.name}: ${error.message}`;
269
+ await logAndProgress(errorMessage, progressCallback, 'warn');
270
+ }
271
+ }