@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.
- package/README.md +24 -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/gemini-extension.json +12 -0
- package/lib/clients.js +104 -3
- 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 +11 -0
- package/lib/deployment/deployer.js +334 -30
- package/lib/deployment/helpers.js +99 -49
- package/lib/deployment/source-processor.js +0 -48
- package/lib/deployment/universal-maker.js +147 -0
- 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 +4 -0
- package/package.json +6 -3
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +224 -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 +126 -79
- package/test/local/source-processor.test.js +0 -165
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +157 -0
- package/test/local/util.test.js +361 -0
- package/test/need-gcp/deployer.test.js +1 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +19 -3
- /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/run.js
CHANGED
|
@@ -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
|
+
}
|
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
|
+
}
|