@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.
Files changed (52) hide show
  1. package/.github/workflows/npm-publish.yml +10 -6
  2. package/README.md +26 -0
  3. package/SECURITY.md +7 -0
  4. package/cloudbuild.yaml +37 -0
  5. package/example-sources-to-deploy/java/pom.xml +41 -0
  6. package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
  7. package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
  8. package/example-sources-to-deploy/nodejs/index.js +15 -0
  9. package/example-sources-to-deploy/nodejs/package.json +12 -0
  10. package/example-sources-to-deploy/python/pip-project/main.py +11 -0
  11. package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
  12. package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
  13. package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
  14. package/lib/clients.js +142 -12
  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 +8 -0
  21. package/lib/deployment/deployer.js +283 -28
  22. package/lib/deployment/helpers.js +58 -1
  23. package/lib/deployment/universal-maker.js +21 -148
  24. package/lib/middleware/oauth.js +26 -15
  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 +83 -20
  29. package/package.json +7 -4
  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 +307 -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 +100 -1
  40. package/test/local/mcp-server-streamable-http.test.js +132 -12
  41. package/test/local/middleware/oauth.test.js +106 -5
  42. package/test/local/test-utils.js +2 -18
  43. package/test/local/tools.test.js +43 -1
  44. package/test/local/universal-maker.test.js +23 -114
  45. package/test/local/util.test.js +361 -0
  46. package/test/need-gcp/deployer.test.js +1 -1
  47. package/test/need-gcp/test-helpers.js +5 -1
  48. package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
  49. package/tools/register-tools.js +98 -78
  50. /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
  51. /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
  52. /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
@@ -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
+ }