@google-cloud/cloud-run-mcp 1.10.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/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 +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/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 +100 -1
- 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/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
|
@@ -0,0 +1,517 @@
|
|
|
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 path from 'path';
|
|
18
|
+
import { promises as fsPromises } from 'fs';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import util from 'util';
|
|
21
|
+
import { execFile } from 'child_process';
|
|
22
|
+
import crypto from 'crypto';
|
|
23
|
+
import { logAndProgress } from '../util/helpers.js';
|
|
24
|
+
import { uploadDirectory } from './helpers.js';
|
|
25
|
+
import { TEMP_PATHS, RUN_COMPOSE } from './constants.js';
|
|
26
|
+
import { ensureRepositoryDownloaded } from '../util/artifacts.js';
|
|
27
|
+
import {
|
|
28
|
+
ensureStorageBucketExists,
|
|
29
|
+
uploadToStorageBucket,
|
|
30
|
+
grantBucketAccess,
|
|
31
|
+
} from '../cloud-api/storage.js';
|
|
32
|
+
import { getProjectNumber, sanitizeLabelValue } from '../util/helpers.js';
|
|
33
|
+
import { ensureApisEnabled } from '../cloud-api/helpers.js';
|
|
34
|
+
import {
|
|
35
|
+
getSecret,
|
|
36
|
+
createSecret,
|
|
37
|
+
addSecretVersion,
|
|
38
|
+
addSecretAccessorBinding,
|
|
39
|
+
} from '../cloud-api/secrets.js';
|
|
40
|
+
|
|
41
|
+
const execFileAsync = util.promisify(execFile);
|
|
42
|
+
|
|
43
|
+
// TODO: Move to production project
|
|
44
|
+
const AR_PROJECT = 'serverless-runtimes-qa';
|
|
45
|
+
const AR_LOCATION = 'us-central1';
|
|
46
|
+
const AR_REPOSITORY = 'run-compose';
|
|
47
|
+
|
|
48
|
+
const ARCH_MAPPING = {
|
|
49
|
+
darwin_amd64: 'darwin_amd64',
|
|
50
|
+
darwin_arm64: 'darwin_arm64',
|
|
51
|
+
linux_386: 'linux_386',
|
|
52
|
+
linux_aarch64: 'linux_aarch64',
|
|
53
|
+
linux_amd64: 'linux_amd64',
|
|
54
|
+
windows_386: 'windows_386',
|
|
55
|
+
windows_amd64: 'windows_amd64',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const _MAX_BUCKET_NAME_LENGTH = 63;
|
|
59
|
+
const _HASH_LENGTH = 8;
|
|
60
|
+
const SHA1 = 'sha1';
|
|
61
|
+
const COMPOSE_SUFFIX = 'compose';
|
|
62
|
+
const HEX_ENCODING = 'hex';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets the architecture key for the current platform.
|
|
66
|
+
* @returns {string|null} The architecture key or null if not supported.
|
|
67
|
+
*/
|
|
68
|
+
function getRunComposeArchitectureKey() {
|
|
69
|
+
const platform = process.platform;
|
|
70
|
+
const arch = process.arch;
|
|
71
|
+
|
|
72
|
+
if (platform === 'darwin') {
|
|
73
|
+
if (arch === 'x64') return 'darwin_amd64';
|
|
74
|
+
if (arch === 'arm64') return 'darwin_arm64';
|
|
75
|
+
} else if (platform === 'linux') {
|
|
76
|
+
if (arch === 'x64') return 'linux_amd64';
|
|
77
|
+
if (arch === 'arm64') return 'linux_aarch64';
|
|
78
|
+
if (arch === 'ia32') return 'linux_386';
|
|
79
|
+
} else if (platform === 'win32') {
|
|
80
|
+
if (arch === 'x64') return 'windows_amd64';
|
|
81
|
+
if (arch === 'ia32') return 'windows_386';
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ensures run-compose binary is downloaded and returns its path.
|
|
88
|
+
* @param {string} accessToken - Access token for authentication.
|
|
89
|
+
* @param {function} progressCallback - Progress callback.
|
|
90
|
+
* @returns {Promise<string|null>} The path to the downloaded binary or null if it fails.
|
|
91
|
+
*/
|
|
92
|
+
export async function runCompose(accessToken, progressCallback) {
|
|
93
|
+
const binDir = path.join(
|
|
94
|
+
os.homedir(),
|
|
95
|
+
TEMP_PATHS.BASE,
|
|
96
|
+
TEMP_PATHS.BIN_SUBDIR
|
|
97
|
+
);
|
|
98
|
+
const key = getRunComposeArchitectureKey();
|
|
99
|
+
if (!key) {
|
|
100
|
+
await logAndProgress(
|
|
101
|
+
`run-compose is not supported on ${process.platform} ${process.arch}.`,
|
|
102
|
+
progressCallback,
|
|
103
|
+
'debug'
|
|
104
|
+
);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const arch = ARCH_MAPPING[key];
|
|
109
|
+
const binPath = path.join(binDir, RUN_COMPOSE.BIN);
|
|
110
|
+
|
|
111
|
+
const binPathResult = await ensureRepositoryDownloaded(
|
|
112
|
+
binPath,
|
|
113
|
+
{
|
|
114
|
+
project: AR_PROJECT,
|
|
115
|
+
location: AR_LOCATION,
|
|
116
|
+
repository: AR_REPOSITORY,
|
|
117
|
+
artifactPath: `${arch}:${RUN_COMPOSE.VERSION}:${RUN_COMPOSE.BIN}`,
|
|
118
|
+
displayName: 'run-compose',
|
|
119
|
+
},
|
|
120
|
+
accessToken,
|
|
121
|
+
progressCallback
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!binPathResult) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return binPathResult;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Gets resource configuration for a compose file using the run-compose binary.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} binPath - Path to the run-compose binary.
|
|
135
|
+
* @param {string} composeFilePath - Path to the compose.yaml file.
|
|
136
|
+
* @param {string} region - The Google Cloud region.
|
|
137
|
+
* @param {function} progressCallback - Optional callback for progress updates.
|
|
138
|
+
* @returns {Promise<string>} The output from the resource command.
|
|
139
|
+
*/
|
|
140
|
+
export async function resourceCompose(
|
|
141
|
+
binPath,
|
|
142
|
+
composeFilePath,
|
|
143
|
+
region,
|
|
144
|
+
progressCallback
|
|
145
|
+
) {
|
|
146
|
+
try {
|
|
147
|
+
const parentDir = path.dirname(composeFilePath);
|
|
148
|
+
await logAndProgress(
|
|
149
|
+
`Running command: ${binPath} resource ${composeFilePath} --region ${region} --out .`,
|
|
150
|
+
progressCallback,
|
|
151
|
+
'debug'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const { stdout, stderr } = await execFileAsync(
|
|
155
|
+
binPath,
|
|
156
|
+
['resource', composeFilePath, '--region', region, '--out', '.'],
|
|
157
|
+
{ cwd: parentDir }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (stderr) {
|
|
161
|
+
await logAndProgress(
|
|
162
|
+
`run-compose resource stderr: ${stderr}`,
|
|
163
|
+
progressCallback,
|
|
164
|
+
'warn'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return stdout;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const errorMsg = `Failed to get resources for compose file: ${error.message}`;
|
|
170
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
171
|
+
throw new Error(errorMsg);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Translates a compose file using the run-compose binary.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} binPath - Path to the run-compose binary.
|
|
179
|
+
* @param {string} composeFilePath - Path to the compose.yaml file.
|
|
180
|
+
* @param {string} region - The Google Cloud region.
|
|
181
|
+
* @param {string} projectNumber - The Google Cloud project number.
|
|
182
|
+
* @param {function} progressCallback - Optional callback for progress updates.
|
|
183
|
+
* @param {object} [resourcesConfig] - Optional resource configuration.
|
|
184
|
+
* @returns {Promise<string>} The output from the translation command.
|
|
185
|
+
*/
|
|
186
|
+
export async function translateCompose(
|
|
187
|
+
binPath,
|
|
188
|
+
composeFilePath,
|
|
189
|
+
region,
|
|
190
|
+
projectNumber,
|
|
191
|
+
progressCallback,
|
|
192
|
+
resourcesConfig = null
|
|
193
|
+
) {
|
|
194
|
+
try {
|
|
195
|
+
const parentDir = path.dirname(composeFilePath);
|
|
196
|
+
await logAndProgress(
|
|
197
|
+
`Translating compose file ${composeFilePath}...`,
|
|
198
|
+
progressCallback
|
|
199
|
+
);
|
|
200
|
+
const args = [
|
|
201
|
+
'translate',
|
|
202
|
+
composeFilePath,
|
|
203
|
+
'--region',
|
|
204
|
+
region,
|
|
205
|
+
'--project-number',
|
|
206
|
+
projectNumber,
|
|
207
|
+
'--out',
|
|
208
|
+
'.',
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
if (resourcesConfig) {
|
|
212
|
+
const configStr = JSON.stringify(resourcesConfig);
|
|
213
|
+
args.push('--resources-config', configStr);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await logAndProgress(
|
|
217
|
+
`Running command: "${binPath}" ${args.join(' ')}`,
|
|
218
|
+
progressCallback,
|
|
219
|
+
'debug'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const { stdout, stderr } = await execFileAsync(binPath, args, {
|
|
223
|
+
cwd: parentDir,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (stderr) {
|
|
227
|
+
await logAndProgress(
|
|
228
|
+
`run-compose translate stderr: ${stderr}`,
|
|
229
|
+
progressCallback,
|
|
230
|
+
'warn'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await logAndProgress(
|
|
235
|
+
'Compose file translated successfully.',
|
|
236
|
+
progressCallback
|
|
237
|
+
);
|
|
238
|
+
return stdout;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const errorMsg = `Failed to translate compose file: ${error.message}`;
|
|
241
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
242
|
+
throw new Error(errorMsg);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Handle volumes defined in resourcesConfig (bind volumes and named volumes)
|
|
248
|
+
* @param {Object} resourcesConfig - resourcesConfig received from `run-compose resource` call
|
|
249
|
+
* @param {string} accessToken - access token for authentication
|
|
250
|
+
* @param {string} projectId - GCP project id
|
|
251
|
+
* @param {string} region - GCP region
|
|
252
|
+
* @param {string} folderPath - Path to the folder containing the source code
|
|
253
|
+
* @param {function} progressCallback - Callback function to report progress
|
|
254
|
+
* @returns {Promise<Object>} - Completed resourcesConfig object
|
|
255
|
+
*/
|
|
256
|
+
export async function composeVolumes(
|
|
257
|
+
resourcesConfig,
|
|
258
|
+
accessToken,
|
|
259
|
+
projectId,
|
|
260
|
+
region,
|
|
261
|
+
folderPath,
|
|
262
|
+
progressCallback
|
|
263
|
+
) {
|
|
264
|
+
const hasBindVolumes =
|
|
265
|
+
resourcesConfig.volumes && resourcesConfig.volumes.bind_mount;
|
|
266
|
+
const hasNamedVolumes =
|
|
267
|
+
resourcesConfig.volumes && resourcesConfig.volumes.named_volume;
|
|
268
|
+
|
|
269
|
+
if (hasBindVolumes || hasNamedVolumes) {
|
|
270
|
+
const projectNumber = await getProjectNumber(projectId, accessToken);
|
|
271
|
+
const projectName = resourcesConfig.project;
|
|
272
|
+
const sanitizedComposeProjectName = projectName
|
|
273
|
+
.toLowerCase()
|
|
274
|
+
.replace(/[^a-z0-9]/g, '-')
|
|
275
|
+
.replace(/-+/g, '-');
|
|
276
|
+
|
|
277
|
+
const bucketNameCandidate = `${projectNumber}-${sanitizedComposeProjectName}-${region}-${COMPOSE_SUFFIX}`;
|
|
278
|
+
let bucketName = bucketNameCandidate;
|
|
279
|
+
if (bucketNameCandidate.length > _MAX_BUCKET_NAME_LENGTH) {
|
|
280
|
+
const projectHash = crypto
|
|
281
|
+
.createHash(SHA1)
|
|
282
|
+
.update(sanitizedComposeProjectName)
|
|
283
|
+
.digest(HEX_ENCODING)
|
|
284
|
+
.slice(0, _HASH_LENGTH);
|
|
285
|
+
bucketName = `${projectNumber}-${projectHash}-${region}-${COMPOSE_SUFFIX}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
resourcesConfig.volumes = resourcesConfig.volumes || {};
|
|
289
|
+
resourcesConfig.volumes.bucket_name = bucketName;
|
|
290
|
+
|
|
291
|
+
const computeServiceAccount = `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
292
|
+
|
|
293
|
+
const labels = {
|
|
294
|
+
[RUN_COMPOSE.LABEL_MANAGED_BY]: RUN_COMPOSE.LABEL_MANAGED_BY_VALUE,
|
|
295
|
+
[RUN_COMPOSE.LABEL_PROJECT]: sanitizeLabelValue(projectName),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Explicitly ensure the bucket exists in the project
|
|
299
|
+
const bucket = await ensureStorageBucketExists(
|
|
300
|
+
projectId,
|
|
301
|
+
bucketName,
|
|
302
|
+
region,
|
|
303
|
+
accessToken,
|
|
304
|
+
labels,
|
|
305
|
+
false,
|
|
306
|
+
progressCallback
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
await grantBucketAccess(
|
|
310
|
+
bucket,
|
|
311
|
+
'roles/storage.objectAdmin',
|
|
312
|
+
computeServiceAccount,
|
|
313
|
+
progressCallback
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (hasBindVolumes) {
|
|
317
|
+
await handleBindMounts(
|
|
318
|
+
resourcesConfig,
|
|
319
|
+
bucket,
|
|
320
|
+
folderPath,
|
|
321
|
+
progressCallback
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//Nothing special needs to be done for named volume.
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return resourcesConfig;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Handles uploading and configuring bind mounts for Cloud Run Compose.
|
|
333
|
+
* @param {Object} resourcesConfig - The resource configuration to update.
|
|
334
|
+
* @param {string} bucket - Volume bucket.
|
|
335
|
+
* @param {string} folderPath - Local path to the compose project.
|
|
336
|
+
* @param {Function} progressCallback - Callback for progress updates.
|
|
337
|
+
*/
|
|
338
|
+
async function handleBindMounts(
|
|
339
|
+
resourcesConfig,
|
|
340
|
+
bucket,
|
|
341
|
+
folderPath,
|
|
342
|
+
progressCallback
|
|
343
|
+
) {
|
|
344
|
+
const bucketName = bucket.name;
|
|
345
|
+
const bindMountsData = resourcesConfig.volumes.bind_mount;
|
|
346
|
+
|
|
347
|
+
// Handle both Map (serviceName -> mounts) and potential direct mounts list
|
|
348
|
+
const entries = Array.isArray(bindMountsData)
|
|
349
|
+
? [['direct', bindMountsData]]
|
|
350
|
+
: Object.entries(bindMountsData);
|
|
351
|
+
|
|
352
|
+
for (const [serviceName, mounts] of entries) {
|
|
353
|
+
// Ensure mounts is an array
|
|
354
|
+
const mountsArray = Array.isArray(mounts) ? mounts : [mounts];
|
|
355
|
+
|
|
356
|
+
for (const mount of mountsArray) {
|
|
357
|
+
const source = mount.source || mount.path; // handle both naming conventions
|
|
358
|
+
if (!source) continue;
|
|
359
|
+
|
|
360
|
+
const fullSourcePath = path.resolve(folderPath, source);
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
await fsPromises.access(fullSourcePath);
|
|
364
|
+
} catch {
|
|
365
|
+
const errorMsg = `Bind mount source '${source}' for service '${serviceName}' does not exist.`;
|
|
366
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
367
|
+
throw new Error(errorMsg);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let gcsPath;
|
|
371
|
+
const relativeSourcePath = path.relative(folderPath, fullSourcePath);
|
|
372
|
+
|
|
373
|
+
if (relativeSourcePath === '' || relativeSourcePath === '.') {
|
|
374
|
+
gcsPath = `bind_mounts/${serviceName}`;
|
|
375
|
+
} else {
|
|
376
|
+
const sourceBasename = path.basename(fullSourcePath);
|
|
377
|
+
gcsPath = `bind_mounts/${serviceName}/${sourceBasename}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
mount.mount_source = gcsPath;
|
|
381
|
+
// Also update volume_id for backward compatibility if needed
|
|
382
|
+
mount.volume_id = `gs://${bucketName}/${gcsPath}`;
|
|
383
|
+
|
|
384
|
+
const stats = await fsPromises.stat(fullSourcePath);
|
|
385
|
+
if (stats.isDirectory()) {
|
|
386
|
+
await logAndProgress(
|
|
387
|
+
`Uploading bind mount directory ${source} for service ${serviceName} to gs://${bucketName}/${gcsPath}/...`,
|
|
388
|
+
progressCallback
|
|
389
|
+
);
|
|
390
|
+
await uploadDirectory(
|
|
391
|
+
bucket,
|
|
392
|
+
fullSourcePath,
|
|
393
|
+
gcsPath,
|
|
394
|
+
progressCallback
|
|
395
|
+
);
|
|
396
|
+
} else {
|
|
397
|
+
await logAndProgress(
|
|
398
|
+
`Uploading bind mount file ${source} for service ${serviceName} to gs://${bucketName}/${gcsPath}...`,
|
|
399
|
+
progressCallback
|
|
400
|
+
);
|
|
401
|
+
const fileBuffer = await fsPromises.readFile(fullSourcePath);
|
|
402
|
+
await uploadToStorageBucket(
|
|
403
|
+
bucket,
|
|
404
|
+
fileBuffer,
|
|
405
|
+
gcsPath,
|
|
406
|
+
progressCallback
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Handle secrets defined in resourcesConfig.
|
|
415
|
+
* @param {Object} resourcesConfig - resourcesConfig received from `run-compose resource` call.
|
|
416
|
+
* @param {string} accessToken - access token for authentication.
|
|
417
|
+
* @param {string} projectId - GCP project ID.
|
|
418
|
+
* @param {string} folderPath - Path to the folder containing the source code.
|
|
419
|
+
* @param {function} progressCallback - Callback function to report progress.
|
|
420
|
+
* @returns {Promise<Object>} - Completed resourcesConfig object.
|
|
421
|
+
*/
|
|
422
|
+
export async function composeSecrets(
|
|
423
|
+
resourcesConfig,
|
|
424
|
+
accessToken,
|
|
425
|
+
projectId,
|
|
426
|
+
folderPath,
|
|
427
|
+
progressCallback
|
|
428
|
+
) {
|
|
429
|
+
if (
|
|
430
|
+
!resourcesConfig.secrets ||
|
|
431
|
+
Object.keys(resourcesConfig.secrets).length === 0
|
|
432
|
+
) {
|
|
433
|
+
return resourcesConfig;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Ensure Secret Manager API is enabled
|
|
437
|
+
await ensureApisEnabled(
|
|
438
|
+
projectId,
|
|
439
|
+
['secretmanager.googleapis.com'],
|
|
440
|
+
accessToken,
|
|
441
|
+
progressCallback
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const projectNumber = await getProjectNumber(projectId, accessToken);
|
|
445
|
+
const computeServiceAccount = `${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
446
|
+
const projectName = resourcesConfig.project;
|
|
447
|
+
const labels = {
|
|
448
|
+
[RUN_COMPOSE.LABEL_MANAGED_BY]: RUN_COMPOSE.LABEL_MANAGED_BY_VALUE,
|
|
449
|
+
[RUN_COMPOSE.LABEL_PROJECT]: sanitizeLabelValue(projectName),
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const secretEntries = Object.entries(resourcesConfig.secrets);
|
|
453
|
+
|
|
454
|
+
for (const [secretName, secretConfig] of secretEntries) {
|
|
455
|
+
// Expected structure in secretConfig: { name, file, mount }
|
|
456
|
+
if (!secretConfig.name || !secretConfig.file || !secretConfig.mount) {
|
|
457
|
+
await logAndProgress(
|
|
458
|
+
`Secret configuration for ${secretName} is incomplete. 'name', 'file' and 'mount' are required.`,
|
|
459
|
+
progressCallback,
|
|
460
|
+
'warn'
|
|
461
|
+
);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const fullFilePath = path.resolve(folderPath, secretConfig.file);
|
|
466
|
+
try {
|
|
467
|
+
await fsPromises.access(fullFilePath);
|
|
468
|
+
} catch {
|
|
469
|
+
const errorMsg = `Secret file not found: ${fullFilePath}`;
|
|
470
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
471
|
+
throw new Error(errorMsg);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const secretId = secretConfig.name;
|
|
475
|
+
|
|
476
|
+
// Check if secret exists
|
|
477
|
+
let secret = await getSecret(projectId, secretId, accessToken);
|
|
478
|
+
if (!secret) {
|
|
479
|
+
secret = await createSecret(
|
|
480
|
+
projectId,
|
|
481
|
+
secretId,
|
|
482
|
+
accessToken,
|
|
483
|
+
labels,
|
|
484
|
+
progressCallback
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Add IAM policy binding
|
|
489
|
+
await addSecretAccessorBinding(
|
|
490
|
+
projectId,
|
|
491
|
+
secretId,
|
|
492
|
+
`serviceAccount:${computeServiceAccount}`,
|
|
493
|
+
accessToken,
|
|
494
|
+
progressCallback
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
// Add secret version
|
|
498
|
+
const secretContent = await fsPromises.readFile(fullFilePath);
|
|
499
|
+
const version = await addSecretVersion(
|
|
500
|
+
projectId,
|
|
501
|
+
secretId,
|
|
502
|
+
secretContent,
|
|
503
|
+
accessToken,
|
|
504
|
+
progressCallback
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// Update secret_version in config
|
|
508
|
+
secretConfig.secret_version = version.name;
|
|
509
|
+
|
|
510
|
+
await logAndProgress(
|
|
511
|
+
`Secret '${secretId}' version '${version.name}' prepared for usage.`,
|
|
512
|
+
progressCallback
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return resourcesConfig;
|
|
517
|
+
}
|
|
@@ -55,3 +55,11 @@ export const RUNTIMES = {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
export const MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES = 250 * 1024 * 1024; // 250 MiB
|
|
58
|
+
|
|
59
|
+
export const RUN_COMPOSE = {
|
|
60
|
+
BIN: 'run-compose',
|
|
61
|
+
VERSION: '1.0.0',
|
|
62
|
+
LABEL_MANAGED_BY: 'managed-by',
|
|
63
|
+
LABEL_MANAGED_BY_VALUE: 'runcompose',
|
|
64
|
+
LABEL_PROJECT: 'run-compose-project',
|
|
65
|
+
};
|