@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
package/lib/cloud-api/storage.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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,
|
|
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
|
|
174
|
+
* Uploads a buffer to a Cloud Storage bucket.
|
|
88
175
|
*
|
|
89
176
|
* @async
|
|
90
|
-
* @param {import('@google-cloud/storage').Bucket} bucket - The
|
|
91
|
-
* @param {Buffer} buffer - The buffer
|
|
92
|
-
* @param {string} destinationBlobName - The name
|
|
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
|
|
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
|
-
() =>
|
|
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
|
|
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
|
+
}
|