@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/build.js
CHANGED
|
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs';
|
|
17
19
|
import {
|
|
18
20
|
getCloudBuildClient,
|
|
19
21
|
getLoggingClient,
|
|
@@ -21,8 +23,16 @@ import {
|
|
|
21
23
|
getBuildsClient,
|
|
22
24
|
} from '../clients.js';
|
|
23
25
|
import { callWithRetry } from './helpers.js';
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
logAndProgress,
|
|
28
|
+
calculateSourceFingerprint,
|
|
29
|
+
sanitizeCloudRunServiceName,
|
|
30
|
+
} from '../util/helpers.js';
|
|
25
31
|
import { checkCloudRunServiceExists } from './run.js';
|
|
32
|
+
import { ensureArtifactRegistryRepoExists } from './registry.js';
|
|
33
|
+
import { ensureStorageBucketExists, uploadToStorageBucket } from './storage.js';
|
|
34
|
+
import { zipFiles } from '../util/archive.js';
|
|
35
|
+
import { DEPLOYMENT_CONFIG } from '../deployment/constants.js';
|
|
26
36
|
|
|
27
37
|
const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
|
|
28
38
|
const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
|
|
@@ -54,9 +64,11 @@ export async function triggerCloudBuild(
|
|
|
54
64
|
targetImageUrl,
|
|
55
65
|
hasDockerfile,
|
|
56
66
|
accessToken,
|
|
57
|
-
progressCallback
|
|
67
|
+
progressCallback,
|
|
68
|
+
ingress
|
|
58
69
|
) {
|
|
59
|
-
const
|
|
70
|
+
const rawServiceName = targetImageUrl.split('/').pop().split(/[:@]/)[0];
|
|
71
|
+
const serviceName = sanitizeCloudRunServiceName(rawServiceName);
|
|
60
72
|
const parent = `projects/${projectId}/locations/${location}`;
|
|
61
73
|
const serviceFullName = `${parent}/services/${serviceName}`;
|
|
62
74
|
const buildsClient = await getBuildsClient(projectId, accessToken);
|
|
@@ -78,6 +90,10 @@ export async function triggerCloudBuild(
|
|
|
78
90
|
},
|
|
79
91
|
};
|
|
80
92
|
|
|
93
|
+
if (ingress) {
|
|
94
|
+
servicePatch.ingress = ingress;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
try {
|
|
82
98
|
if (serviceExists) {
|
|
83
99
|
await logAndProgress(
|
|
@@ -123,36 +139,76 @@ export async function triggerCloudBuild(
|
|
|
123
139
|
throw new Error(`Dry-run deployment failed: ${err.message}`);
|
|
124
140
|
}
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
bucket: sourceBucketName,
|
|
130
|
-
object: sourceBlobName,
|
|
131
|
-
},
|
|
132
|
-
imageUri: targetImageUrl,
|
|
133
|
-
client: 'cloud-run-mcp',
|
|
134
|
-
};
|
|
142
|
+
let buildId;
|
|
143
|
+
let completedBuild;
|
|
144
|
+
|
|
135
145
|
if (hasDockerfile) {
|
|
136
|
-
|
|
146
|
+
// Docker builds need manual control via v1 createBuild to enable BuildKit support.
|
|
147
|
+
// This resolves issues with advanced Dockerfile instructions like heredocs (RUN <<EOF).
|
|
148
|
+
const build = {
|
|
149
|
+
images: [targetImageUrl],
|
|
150
|
+
source: {
|
|
151
|
+
storageSource: {
|
|
152
|
+
bucket: sourceBucketName,
|
|
153
|
+
object: sourceBlobName,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
steps: [
|
|
157
|
+
{
|
|
158
|
+
name: 'gcr.io/cloud-builders/docker',
|
|
159
|
+
args: ['build', '-t', targetImageUrl, '.'],
|
|
160
|
+
env: ['DOCKER_BUILDKIT=1'],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
options: {
|
|
164
|
+
logging: 'CLOUD_LOGGING_ONLY',
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await logAndProgress(
|
|
169
|
+
`Initiating Cloud Build (BuildKit enabled) for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
170
|
+
progressCallback
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const [operation] = await callWithRetry(
|
|
174
|
+
() =>
|
|
175
|
+
cloudBuildClient.createBuild({
|
|
176
|
+
parent: `projects/${projectId}/locations/${location}`,
|
|
177
|
+
build,
|
|
178
|
+
}),
|
|
179
|
+
'cloudBuild.createBuild'
|
|
180
|
+
);
|
|
181
|
+
buildId = operation.metadata.build.id;
|
|
137
182
|
} else {
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
// For simple buildpacks, use the high-level submitBuild API if possible or if preferred for simplicity.
|
|
184
|
+
const request = {
|
|
185
|
+
parent: `projects/${projectId}/locations/${location}`,
|
|
186
|
+
storageSource: {
|
|
187
|
+
bucket: sourceBucketName,
|
|
188
|
+
object: sourceBlobName,
|
|
189
|
+
},
|
|
190
|
+
imageUri: targetImageUrl,
|
|
191
|
+
client: 'cloud-run-mcp',
|
|
192
|
+
buildpackBuild: {},
|
|
193
|
+
};
|
|
140
194
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
195
|
+
await logAndProgress(
|
|
196
|
+
`Initiating Cloud Build (Buildpacks) for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
197
|
+
progressCallback
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const [submitBuildResponse] = await callWithRetry(
|
|
201
|
+
() => buildsClient.submitBuild(request),
|
|
202
|
+
'builds.submitBuild'
|
|
203
|
+
);
|
|
204
|
+
const encodedBuildId = submitBuildResponse.buildOperation.name
|
|
205
|
+
.split('/')
|
|
206
|
+
.pop();
|
|
207
|
+
buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
|
|
208
|
+
}
|
|
145
209
|
|
|
146
|
-
const [submitBuildResponse] = await callWithRetry(
|
|
147
|
-
() => buildsClient.submitBuild(request),
|
|
148
|
-
'builds.submitBuild'
|
|
149
|
-
);
|
|
150
210
|
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
151
|
-
|
|
152
|
-
.split('/')
|
|
153
|
-
.pop();
|
|
154
|
-
const buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
|
|
155
|
-
let completedBuild;
|
|
211
|
+
|
|
156
212
|
while (true) {
|
|
157
213
|
const [getBuildOperation] = await callWithRetry(
|
|
158
214
|
() =>
|
|
@@ -247,3 +303,111 @@ export async function triggerCloudBuild(
|
|
|
247
303
|
throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
|
|
248
304
|
}
|
|
249
305
|
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Build source for services defined in resourcesConfig.source_builds
|
|
309
|
+
* @param {*} resourcesConfig - resourcesConfig received from `run-compose resource` call
|
|
310
|
+
* @param {*} accessToken - access token for authentication
|
|
311
|
+
* @param {*} projectId - GCP project id
|
|
312
|
+
* @param {*} region - GCP region
|
|
313
|
+
* @param {*} folderPath - Path to the folder containing the source code
|
|
314
|
+
* @param {*} progressCallback - Callback function to report progress
|
|
315
|
+
* @returns - Completed resourcesConfig object
|
|
316
|
+
*/
|
|
317
|
+
export async function composeBuild(
|
|
318
|
+
resourcesConfig,
|
|
319
|
+
accessToken,
|
|
320
|
+
projectId,
|
|
321
|
+
region,
|
|
322
|
+
folderPath,
|
|
323
|
+
progressCallback
|
|
324
|
+
) {
|
|
325
|
+
if (
|
|
326
|
+
!resourcesConfig.source_builds ||
|
|
327
|
+
Object.keys(resourcesConfig.source_builds).length === 0
|
|
328
|
+
) {
|
|
329
|
+
return resourcesConfig;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Ensure AR Repo exists once
|
|
333
|
+
await ensureArtifactRegistryRepoExists(
|
|
334
|
+
projectId,
|
|
335
|
+
accessToken,
|
|
336
|
+
region,
|
|
337
|
+
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
338
|
+
'DOCKER',
|
|
339
|
+
progressCallback
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const bucketName = `${projectId}_cloudbuild`;
|
|
343
|
+
|
|
344
|
+
// Ensure Storage Bucket exists once
|
|
345
|
+
const bucket = await ensureStorageBucketExists(
|
|
346
|
+
projectId,
|
|
347
|
+
bucketName,
|
|
348
|
+
region,
|
|
349
|
+
accessToken,
|
|
350
|
+
undefined,
|
|
351
|
+
false,
|
|
352
|
+
progressCallback
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const buildPromises = Object.entries(resourcesConfig.source_builds).map(
|
|
356
|
+
async ([serviceName, buildInfo]) => {
|
|
357
|
+
const contextDir = path.join(folderPath, buildInfo.context);
|
|
358
|
+
await logAndProgress(
|
|
359
|
+
`Building source for service ${serviceName} in ${contextDir}...`,
|
|
360
|
+
progressCallback
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Check for Dockerfile in context
|
|
364
|
+
const hasDockerfile = fs.existsSync(path.join(contextDir, 'Dockerfile'));
|
|
365
|
+
|
|
366
|
+
// Generate Fingerprint
|
|
367
|
+
const fingerprint = await calculateSourceFingerprint(contextDir);
|
|
368
|
+
const composeProject = resourcesConfig.project;
|
|
369
|
+
|
|
370
|
+
// Generate image URL
|
|
371
|
+
// Use format: {repo}/{project_name}_{container}:{tag} matching builder.py
|
|
372
|
+
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${composeProject}_${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
|
|
373
|
+
|
|
374
|
+
// Create tar.gz archive in memory
|
|
375
|
+
const archiveBuffer = await zipFiles(
|
|
376
|
+
[contextDir],
|
|
377
|
+
true,
|
|
378
|
+
progressCallback
|
|
379
|
+
);
|
|
380
|
+
// Use format: {epoch}.{fingerprint}.tgz in source/ folder
|
|
381
|
+
const archiveName = `source/${Date.now()}.${fingerprint}.tgz`;
|
|
382
|
+
|
|
383
|
+
const file = await uploadToStorageBucket(
|
|
384
|
+
bucket,
|
|
385
|
+
archiveBuffer,
|
|
386
|
+
archiveName,
|
|
387
|
+
progressCallback
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Trigger build
|
|
391
|
+
const buildResult = await triggerCloudBuild(
|
|
392
|
+
projectId,
|
|
393
|
+
region,
|
|
394
|
+
bucket.name,
|
|
395
|
+
archiveName,
|
|
396
|
+
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
397
|
+
imageUrl,
|
|
398
|
+
hasDockerfile,
|
|
399
|
+
accessToken,
|
|
400
|
+
progressCallback,
|
|
401
|
+
undefined
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const builtImageUrl = buildResult.results.images[0].name;
|
|
405
|
+
// Update resourcesConfig with image_id
|
|
406
|
+
resourcesConfig.source_builds[serviceName].image_id = builtImageUrl;
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
await Promise.all(buildPromises);
|
|
411
|
+
|
|
412
|
+
return resourcesConfig;
|
|
413
|
+
}
|
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
|
+
}
|