@google-cloud/cloud-run-mcp 1.8.0 → 1.10.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/.env.example +13 -0
- package/.env.gcloud-sdk-oauth +13 -0
- package/README.md +38 -0
- package/constants.js +8 -0
- package/gemini-extension.json +12 -0
- package/lib/clients.js +72 -2
- package/lib/cloud-api/auth.js +1 -0
- package/lib/cloud-api/billing.js +16 -10
- package/lib/cloud-api/build.js +6 -4
- package/lib/cloud-api/helpers.js +19 -6
- package/lib/cloud-api/projects.js +15 -10
- package/lib/cloud-api/registry.js +5 -1
- package/lib/cloud-api/run.js +12 -11
- package/lib/cloud-api/storage.js +2 -1
- package/lib/deployment/constants.js +3 -0
- package/lib/deployment/deployer.js +75 -9
- package/lib/deployment/helpers.js +41 -48
- package/lib/deployment/source-processor.js +0 -48
- package/lib/deployment/universal-maker.js +274 -0
- package/lib/middleware/oauth.js +92 -0
- package/lib/util/helpers.js +12 -0
- package/mcp-server.js +51 -7
- package/package.json +2 -2
- package/test/local/clients.test.js +33 -3
- package/test/local/deployment-helpers.test.js +34 -86
- package/test/local/mcp-server-stdio.test.js +0 -1
- package/test/local/mcp-server-streamable-http.test.js +69 -0
- package/test/local/mcp-server.test.js +1 -0
- package/test/local/middleware/oauth.test.js +156 -0
- package/test/local/source-processor.test.js +0 -165
- package/test/local/tools.test.js +2 -2
- package/test/local/universal-maker.test.js +248 -0
- package/tools/register-tools.js +18 -5
- package/.kokoro/kokoro_build.sh +0 -22
- package/.kokoro/presubmit.cfg +0 -21
- package/.kokoro/run_tests.sh +0 -31
|
@@ -32,15 +32,16 @@ import {
|
|
|
32
32
|
} from './helpers.js';
|
|
33
33
|
import {
|
|
34
34
|
prepareSourceDirectory,
|
|
35
|
-
installDependencies,
|
|
36
35
|
cleanupTempDirectory,
|
|
37
36
|
} from './source-processor.js';
|
|
37
|
+
import { runUniversalMaker } from './universal-maker.js';
|
|
38
38
|
|
|
39
39
|
import {
|
|
40
40
|
DEPLOYMENT_CONFIG,
|
|
41
41
|
DEPLOYMENT_TYPES,
|
|
42
42
|
MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES,
|
|
43
43
|
REQUIRED_APIS,
|
|
44
|
+
RUNTIMES,
|
|
44
45
|
} from './constants.js';
|
|
45
46
|
|
|
46
47
|
/**
|
|
@@ -66,10 +67,11 @@ async function deployToCloudRun(
|
|
|
66
67
|
progressCallback,
|
|
67
68
|
skipIamCheck,
|
|
68
69
|
deploymentType,
|
|
70
|
+
accessToken,
|
|
69
71
|
zippedSourceContainer,
|
|
70
72
|
runtime
|
|
71
73
|
) {
|
|
72
|
-
const runClient = await getRunClient(projectId);
|
|
74
|
+
const runClient = await getRunClient(projectId, accessToken);
|
|
73
75
|
const parent = runClient.locationPath(projectId, location);
|
|
74
76
|
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
75
77
|
const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
|
|
@@ -101,6 +103,7 @@ async function deployToCloudRun(
|
|
|
101
103
|
projectId,
|
|
102
104
|
location,
|
|
103
105
|
serviceId,
|
|
106
|
+
accessToken,
|
|
104
107
|
progressCallback
|
|
105
108
|
);
|
|
106
109
|
|
|
@@ -245,7 +248,8 @@ async function deployWithZip(
|
|
|
245
248
|
bucketName,
|
|
246
249
|
deploymentAttrs,
|
|
247
250
|
progressCallback,
|
|
248
|
-
skipIamCheck
|
|
251
|
+
skipIamCheck,
|
|
252
|
+
accessToken
|
|
249
253
|
) {
|
|
250
254
|
await logAndProgress(
|
|
251
255
|
`Attempting direct source deployment...`,
|
|
@@ -256,7 +260,18 @@ async function deployWithZip(
|
|
|
256
260
|
|
|
257
261
|
try {
|
|
258
262
|
tempDir = await prepareSourceDirectory(files);
|
|
259
|
-
|
|
263
|
+
|
|
264
|
+
if (
|
|
265
|
+
deploymentAttrs.runtime === RUNTIMES.NODEJS ||
|
|
266
|
+
deploymentAttrs.runtime === RUNTIMES.PYTHON
|
|
267
|
+
) {
|
|
268
|
+
deploymentAttrs = await updateDeploymentAttrsWithUniversalMaker(
|
|
269
|
+
tempDir,
|
|
270
|
+
deploymentAttrs,
|
|
271
|
+
accessToken,
|
|
272
|
+
progressCallback
|
|
273
|
+
);
|
|
274
|
+
}
|
|
260
275
|
|
|
261
276
|
const archiveBuffer = await zipFiles([tempDir], true, progressCallback);
|
|
262
277
|
const archiveName = DEPLOYMENT_CONFIG.TARGZ_FILE_NAME;
|
|
@@ -276,6 +291,7 @@ async function deployWithZip(
|
|
|
276
291
|
projectId,
|
|
277
292
|
bucketName,
|
|
278
293
|
region,
|
|
294
|
+
accessToken,
|
|
279
295
|
progressCallback
|
|
280
296
|
);
|
|
281
297
|
|
|
@@ -301,6 +317,7 @@ async function deployWithZip(
|
|
|
301
317
|
progressCallback,
|
|
302
318
|
skipIamCheck,
|
|
303
319
|
DEPLOYMENT_TYPES.NO_BUILD,
|
|
320
|
+
accessToken,
|
|
304
321
|
container,
|
|
305
322
|
deploymentAttrs.runtime
|
|
306
323
|
);
|
|
@@ -311,6 +328,43 @@ async function deployWithZip(
|
|
|
311
328
|
}
|
|
312
329
|
}
|
|
313
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Updates deployment attributes using result from Universal Maker.
|
|
333
|
+
*
|
|
334
|
+
* @async
|
|
335
|
+
* @param {string} tempDir - Path to the temporary source directory.
|
|
336
|
+
* @param {object} deploymentAttrs - The initial deployment attributes.
|
|
337
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
338
|
+
* @returns {Promise<object>} A promise that resolves with the updated deployment attributes.
|
|
339
|
+
*/
|
|
340
|
+
async function updateDeploymentAttrsWithUniversalMaker(
|
|
341
|
+
tempDir,
|
|
342
|
+
deploymentAttrs,
|
|
343
|
+
accessToken,
|
|
344
|
+
progressCallback
|
|
345
|
+
) {
|
|
346
|
+
const umResult = await runUniversalMaker(
|
|
347
|
+
tempDir,
|
|
348
|
+
accessToken,
|
|
349
|
+
progressCallback
|
|
350
|
+
);
|
|
351
|
+
if (umResult) {
|
|
352
|
+
await logAndProgress(
|
|
353
|
+
'Updating deployment attributes using Universal Maker results.',
|
|
354
|
+
progressCallback
|
|
355
|
+
);
|
|
356
|
+
return {
|
|
357
|
+
...deploymentAttrs,
|
|
358
|
+
baseImage: umResult.runtime || deploymentAttrs.baseImage,
|
|
359
|
+
runtime: umResult.runtime || deploymentAttrs.runtime,
|
|
360
|
+
cmd: umResult.command ? [umResult.command] : deploymentAttrs.cmd,
|
|
361
|
+
args: umResult.args || deploymentAttrs.args,
|
|
362
|
+
envVars: umResult.envVars || {},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return deploymentAttrs;
|
|
366
|
+
}
|
|
367
|
+
|
|
314
368
|
/**
|
|
315
369
|
* Handles deployment using Google Cloud Build.
|
|
316
370
|
* This function zips the source code, uploads it to Google Cloud Storage,
|
|
@@ -337,7 +391,8 @@ async function deployWithBuild(
|
|
|
337
391
|
hasDockerfile,
|
|
338
392
|
bucketName,
|
|
339
393
|
progressCallback,
|
|
340
|
-
skipIamCheck
|
|
394
|
+
skipIamCheck,
|
|
395
|
+
accessToken
|
|
341
396
|
) {
|
|
342
397
|
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
|
|
343
398
|
|
|
@@ -345,6 +400,7 @@ async function deployWithBuild(
|
|
|
345
400
|
projectId,
|
|
346
401
|
bucketName,
|
|
347
402
|
region,
|
|
403
|
+
accessToken,
|
|
348
404
|
progressCallback
|
|
349
405
|
);
|
|
350
406
|
|
|
@@ -361,6 +417,7 @@ async function deployWithBuild(
|
|
|
361
417
|
|
|
362
418
|
await ensureArtifactRegistryRepoExists(
|
|
363
419
|
projectId,
|
|
420
|
+
accessToken,
|
|
364
421
|
region,
|
|
365
422
|
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
366
423
|
'DOCKER',
|
|
@@ -377,6 +434,7 @@ async function deployWithBuild(
|
|
|
377
434
|
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
378
435
|
imageUrl,
|
|
379
436
|
hasDockerfile,
|
|
437
|
+
accessToken,
|
|
380
438
|
progressCallback
|
|
381
439
|
);
|
|
382
440
|
|
|
@@ -389,7 +447,8 @@ async function deployWithBuild(
|
|
|
389
447
|
builtImageUrl,
|
|
390
448
|
progressCallback,
|
|
391
449
|
skipIamCheck,
|
|
392
|
-
DEPLOYMENT_TYPES.WITH_BUILD
|
|
450
|
+
DEPLOYMENT_TYPES.WITH_BUILD,
|
|
451
|
+
accessToken
|
|
393
452
|
);
|
|
394
453
|
|
|
395
454
|
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
@@ -414,6 +473,7 @@ export async function deploy({
|
|
|
414
473
|
files,
|
|
415
474
|
progressCallback,
|
|
416
475
|
skipIamCheck,
|
|
476
|
+
accessToken,
|
|
417
477
|
}) {
|
|
418
478
|
if (!projectId) {
|
|
419
479
|
const errorMsg =
|
|
@@ -444,6 +504,7 @@ export async function deploy({
|
|
|
444
504
|
await ensureApisEnabled(
|
|
445
505
|
projectId,
|
|
446
506
|
REQUIRED_APIS.SOURCE_DEPLOY,
|
|
507
|
+
accessToken,
|
|
447
508
|
progressCallback
|
|
448
509
|
);
|
|
449
510
|
|
|
@@ -470,7 +531,8 @@ export async function deploy({
|
|
|
470
531
|
bucketName,
|
|
471
532
|
deploymentAttrs,
|
|
472
533
|
progressCallback,
|
|
473
|
-
skipIamCheck
|
|
534
|
+
skipIamCheck,
|
|
535
|
+
accessToken
|
|
474
536
|
);
|
|
475
537
|
} catch (error) {
|
|
476
538
|
await logAndProgress(
|
|
@@ -489,7 +551,8 @@ export async function deploy({
|
|
|
489
551
|
hasDockerfile,
|
|
490
552
|
bucketName,
|
|
491
553
|
progressCallback,
|
|
492
|
-
skipIamCheck
|
|
554
|
+
skipIamCheck,
|
|
555
|
+
accessToken
|
|
493
556
|
);
|
|
494
557
|
} catch (error) {
|
|
495
558
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
@@ -518,6 +581,7 @@ export async function deployImage({
|
|
|
518
581
|
imageUrl,
|
|
519
582
|
progressCallback,
|
|
520
583
|
skipIamCheck,
|
|
584
|
+
accessToken,
|
|
521
585
|
}) {
|
|
522
586
|
if (!projectId) {
|
|
523
587
|
const errorMsg =
|
|
@@ -547,6 +611,7 @@ export async function deployImage({
|
|
|
547
611
|
await ensureApisEnabled(
|
|
548
612
|
projectId,
|
|
549
613
|
REQUIRED_APIS.IMAGE_DEPLOY,
|
|
614
|
+
accessToken,
|
|
550
615
|
progressCallback
|
|
551
616
|
);
|
|
552
617
|
|
|
@@ -562,7 +627,8 @@ export async function deployImage({
|
|
|
562
627
|
imageUrl,
|
|
563
628
|
progressCallback,
|
|
564
629
|
skipIamCheck,
|
|
565
|
-
DEPLOYMENT_TYPES.IMAGE
|
|
630
|
+
DEPLOYMENT_TYPES.IMAGE,
|
|
631
|
+
accessToken
|
|
566
632
|
);
|
|
567
633
|
|
|
568
634
|
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
@@ -72,6 +72,18 @@ function checkIfNodeJsRuntime(files) {
|
|
|
72
72
|
return fs.existsSync(packageJsonPath);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Checks if the provided folder appears to be a Python project.
|
|
77
|
+
* It looks for a `requirements.txt` or `pyproject.toml` file in the root of the first path in the `files` array.
|
|
78
|
+
* @param {string[]} files - Array of file paths
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
function checkIfPythonRuntime(files) {
|
|
82
|
+
const requirementsTxtPath = path.join(files[0], 'requirements.txt');
|
|
83
|
+
const pyprojectTomlPath = path.join(files[0], 'pyproject.toml');
|
|
84
|
+
return fs.existsSync(requirementsTxtPath) || fs.existsSync(pyprojectTomlPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
/**
|
|
76
88
|
* Returns an empty deployment attributes object.
|
|
77
89
|
* This is used as a default or when attributes cannot be determined.
|
|
@@ -86,55 +98,30 @@ function getEmptyDeploymentAttrs() {
|
|
|
86
98
|
};
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
/**
|
|
90
|
-
* Extracts deployment attributes (runtime, command, args, baseImage) from a Node.js project's `package.json`.
|
|
91
|
-
* This function is used for direct source deployments, bypassing Cloud Build.
|
|
92
|
-
* It returns empty attributes if a `start` script is missing, invalid, or if `engines.node` is specified (indicating a need for Cloud Build).
|
|
93
|
-
* @param {string[]} files - Array of file paths
|
|
94
|
-
* @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
|
|
95
|
-
*/
|
|
96
|
-
function getNodeJsDeploymentAttrs(files) {
|
|
97
|
-
const packageJsonPath = path.join(files[0], 'package.json');
|
|
98
|
-
let packageJson;
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
102
|
-
} catch (error) {
|
|
103
|
-
// If package.json exists but is invalid, we can't extract attributes.
|
|
104
|
-
return getEmptyDeploymentAttrs();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const startScript = packageJson?.scripts?.start;
|
|
108
|
-
const hasNodeEngine = packageJson?.engines?.node;
|
|
109
|
-
|
|
110
|
-
if (typeof startScript !== 'string' || !startScript.trim() || hasNodeEngine) {
|
|
111
|
-
return getEmptyDeploymentAttrs();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Handle multiple spaces properly
|
|
115
|
-
const parts = startScript.trim().split(/\s+/);
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
runtime: RUNTIMES.NODEJS,
|
|
119
|
-
cmd: [parts[0]],
|
|
120
|
-
args: parts.slice(1),
|
|
121
|
-
baseImage: DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE, // latest default base image
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
101
|
/**
|
|
126
102
|
* Determines deployment attributes based on the file list/folder content.
|
|
127
103
|
* @param {Array<string|Object>} files - Array of file paths or file objects.
|
|
128
|
-
* @returns {{runtime: string|undefined,
|
|
104
|
+
* @returns {{runtime: string|undefined, baseImage: string|undefined}} Deployment attributes object.
|
|
129
105
|
*/
|
|
130
106
|
function getDeploymentAttrs(files) {
|
|
107
|
+
let deploymentAttrs = getEmptyDeploymentAttrs();
|
|
131
108
|
// Currently only support detection of NodeJs runtime
|
|
132
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
!isFolder(files) ||
|
|
111
|
+
!(checkIfNodeJsRuntime(files) || checkIfPythonRuntime(files))
|
|
112
|
+
) {
|
|
133
113
|
// TODO: support file list deployment runtime detection
|
|
134
|
-
return
|
|
114
|
+
return deploymentAttrs;
|
|
135
115
|
}
|
|
136
116
|
|
|
137
|
-
|
|
117
|
+
if (checkIfPythonRuntime(files)) {
|
|
118
|
+
deploymentAttrs.runtime = RUNTIMES.PYTHON;
|
|
119
|
+
deploymentAttrs.baseImage = DEPLOYMENT_CONFIG.DEFAULT_PYTHON_BASE_IMAGE;
|
|
120
|
+
} else {
|
|
121
|
+
deploymentAttrs.runtime = RUNTIMES.NODEJS;
|
|
122
|
+
deploymentAttrs.baseImage = DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE;
|
|
123
|
+
}
|
|
124
|
+
return deploymentAttrs;
|
|
138
125
|
}
|
|
139
126
|
|
|
140
127
|
/**
|
|
@@ -157,13 +144,7 @@ export function makeFileDeploymentMetadata(files) {
|
|
|
157
144
|
* @returns {boolean} True if zip deployment is feasible.
|
|
158
145
|
*/
|
|
159
146
|
export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
|
|
160
|
-
return !!(
|
|
161
|
-
!hasDockerfile &&
|
|
162
|
-
deploymentAttrs &&
|
|
163
|
-
deploymentAttrs.runtime &&
|
|
164
|
-
deploymentAttrs.cmd &&
|
|
165
|
-
deploymentAttrs.args
|
|
166
|
-
);
|
|
147
|
+
return !!(!hasDockerfile && deploymentAttrs && deploymentAttrs.runtime);
|
|
167
148
|
}
|
|
168
149
|
|
|
169
150
|
/**
|
|
@@ -179,7 +160,7 @@ export function createDirectSourceDeploymentContainer({
|
|
|
179
160
|
fileName,
|
|
180
161
|
deploymentAttrs,
|
|
181
162
|
}) {
|
|
182
|
-
|
|
163
|
+
const container = {
|
|
183
164
|
image: DEPLOYMENT_CONFIG.NO_BUILD_IMAGE_TYPE,
|
|
184
165
|
baseImageUri: deploymentAttrs.baseImage,
|
|
185
166
|
sourceCode: {
|
|
@@ -191,4 +172,16 @@ export function createDirectSourceDeploymentContainer({
|
|
|
191
172
|
command: deploymentAttrs.cmd,
|
|
192
173
|
args: deploymentAttrs.args,
|
|
193
174
|
};
|
|
175
|
+
|
|
176
|
+
// Add environment variables if provided
|
|
177
|
+
if (deploymentAttrs.envVars && typeof deploymentAttrs.envVars === 'object') {
|
|
178
|
+
container.env = Object.entries(deploymentAttrs.envVars).map(
|
|
179
|
+
([name, value]) => ({
|
|
180
|
+
name,
|
|
181
|
+
value: String(value),
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return container;
|
|
194
187
|
}
|
|
@@ -58,54 +58,6 @@ export async function prepareSourceDirectory(files, progressCallback) {
|
|
|
58
58
|
return tempDir;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/**
|
|
62
|
-
* Installs Node.js dependencies in the target directory if a package.json exists.
|
|
63
|
-
* Skips if node_modules already exists (assumed pre-installed or copied).
|
|
64
|
-
*
|
|
65
|
-
* @param {string} targetDir - The directory containing the source code.
|
|
66
|
-
*/
|
|
67
|
-
async function installNodeDependencies(targetDir) {
|
|
68
|
-
const nodeModulesPath = path.join(targetDir, 'node_modules');
|
|
69
|
-
|
|
70
|
-
if (fs.existsSync(nodeModulesPath)) {
|
|
71
|
-
console.log(
|
|
72
|
-
`Existing node_modules detected. Skipping dependency installation.`
|
|
73
|
-
);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check for lock file to decide between install and ci
|
|
78
|
-
const hasLockFile = fs.existsSync(path.join(targetDir, 'package-lock.json'));
|
|
79
|
-
const npmCommand = hasLockFile
|
|
80
|
-
? 'npm ci --omit=dev'
|
|
81
|
-
: 'npm install --omit=dev';
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
await execAsync(npmCommand, { cwd: targetDir });
|
|
85
|
-
console.log(`${npmCommand} completed successfully`);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.log(`Dependency installation failed: ${e.message}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Generic wrapper to install dependencies for the detected runtime.
|
|
93
|
-
*
|
|
94
|
-
* @param {string} targetDir - The directory containing the source code.
|
|
95
|
-
* @param {Object} deploymentAttrs - Deployment attributes containing runtime info.
|
|
96
|
-
*/
|
|
97
|
-
export async function installDependencies(targetDir, deploymentAttrs) {
|
|
98
|
-
switch (deploymentAttrs?.runtime) {
|
|
99
|
-
case RUNTIMES.NODEJS:
|
|
100
|
-
await installNodeDependencies(targetDir);
|
|
101
|
-
break;
|
|
102
|
-
default:
|
|
103
|
-
console.log(
|
|
104
|
-
`No specific dependency installation logic for runtime: ${deploymentAttrs?.runtime}`
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
61
|
/**
|
|
110
62
|
* Cleans up the temporary directory.
|
|
111
63
|
*
|
|
@@ -0,0 +1,274 @@
|
|
|
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 { exec } from 'child_process';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import util from 'util';
|
|
22
|
+
import crypto from 'crypto';
|
|
23
|
+
import { logAndProgress } from '../util/helpers.js';
|
|
24
|
+
import { TEMP_PATHS } from './constants.js';
|
|
25
|
+
import { getArtifactRegistryClient } from '../clients.js';
|
|
26
|
+
|
|
27
|
+
const execAsync = util.promisify(exec);
|
|
28
|
+
|
|
29
|
+
const UNIVERSAL_MAKER_BIN = 'universal_maker';
|
|
30
|
+
const UM_VERSION = '1.0.0';
|
|
31
|
+
|
|
32
|
+
const AR_HOST = 'https://artifactregistry.googleapis.com';
|
|
33
|
+
const AR_PROJECT = 'serverless-runtimes';
|
|
34
|
+
const AR_LOCATION = 'us-central1';
|
|
35
|
+
const AR_REPOSITORY = 'universal-maker';
|
|
36
|
+
const AR_DOWNLOAD_PATH_PREFIX = '/download/v1/';
|
|
37
|
+
const AR_DOWNLOAD_URL_SUFFIX = ':download?alt=media';
|
|
38
|
+
|
|
39
|
+
const ARCH_MAPPING = {
|
|
40
|
+
linux_x64: 'x86-64',
|
|
41
|
+
darwin_arm64: 'darwin-arm64',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Gets the architecture key for the current platform.
|
|
46
|
+
* @returns {string|null} The architecture key or null if not supported.
|
|
47
|
+
*/
|
|
48
|
+
function getUniversalMakerArchitectureKey() {
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
const arch = process.arch;
|
|
51
|
+
|
|
52
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
53
|
+
return 'linux_x64';
|
|
54
|
+
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
55
|
+
return 'darwin_arm64';
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks if the existing binary is up to date by comparing its SHA256 mash with the remote metadata.
|
|
62
|
+
* If the binary is out of date, it removes it.
|
|
63
|
+
* @param {string} binPath - Path to the local binary.
|
|
64
|
+
* @param {string} arch - Current architecture.
|
|
65
|
+
* @param {string} accessToken - Access token for authentication.
|
|
66
|
+
* @param {function} progressCallback - Progress callback.
|
|
67
|
+
* @returns {Promise<boolean>} True if the binary is up to date (or if updating fails), false otherwise.
|
|
68
|
+
*/
|
|
69
|
+
async function isBinaryUpToDate(binPath, arch, accessToken, progressCallback) {
|
|
70
|
+
if (!fs.existsSync(binPath)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await logAndProgress(
|
|
76
|
+
'Checking for Universal Maker updates...',
|
|
77
|
+
progressCallback,
|
|
78
|
+
'debug'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const artifactRegistryClient = await getArtifactRegistryClient(
|
|
82
|
+
AR_PROJECT,
|
|
83
|
+
accessToken
|
|
84
|
+
);
|
|
85
|
+
const resourceName = artifactRegistryClient.filePath(
|
|
86
|
+
AR_PROJECT,
|
|
87
|
+
AR_LOCATION,
|
|
88
|
+
AR_REPOSITORY,
|
|
89
|
+
`${arch}:${UM_VERSION}:${UNIVERSAL_MAKER_BIN}`
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const [file] = await artifactRegistryClient.getFile({ name: resourceName });
|
|
93
|
+
const remoteSha256Base64 = file.hashes?.find(
|
|
94
|
+
(h) => h.type === 'SHA256'
|
|
95
|
+
)?.value;
|
|
96
|
+
|
|
97
|
+
if (!remoteSha256Base64) {
|
|
98
|
+
return false; // Cannot verify, download new binary
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const remoteSha256Hex = Buffer.from(remoteSha256Base64, 'base64').toString(
|
|
102
|
+
'hex'
|
|
103
|
+
);
|
|
104
|
+
const localFileContent = fs.readFileSync(binPath);
|
|
105
|
+
const localSha256Hex = crypto
|
|
106
|
+
.createHash('sha256')
|
|
107
|
+
.update(localFileContent)
|
|
108
|
+
.digest('hex');
|
|
109
|
+
|
|
110
|
+
if (remoteSha256Hex === localSha256Hex) {
|
|
111
|
+
await logAndProgress(
|
|
112
|
+
'Universal Maker binary is up to date.',
|
|
113
|
+
progressCallback,
|
|
114
|
+
'debug'
|
|
115
|
+
);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await logAndProgress(
|
|
120
|
+
'Universal Maker binary is out of date. Removing existing binary.',
|
|
121
|
+
progressCallback,
|
|
122
|
+
'debug'
|
|
123
|
+
);
|
|
124
|
+
fs.unlinkSync(binPath);
|
|
125
|
+
return false;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
await logAndProgress(
|
|
128
|
+
`Could not check for Universal Maker updates: ${error.message}. Downloading the new binary.`,
|
|
129
|
+
progressCallback,
|
|
130
|
+
'debug'
|
|
131
|
+
);
|
|
132
|
+
return false; // On error, fallback to downloading the new binary
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Downloads the universal maker binary if it doesn't exist.
|
|
138
|
+
* @param {string} binDir - Directory to store the binary.
|
|
139
|
+
* @param {string} accessToken - Access token for authentication.
|
|
140
|
+
* @param {function} progressCallback - Progress callback.
|
|
141
|
+
* @returns {Promise<string|null>} Path to the binary or null if not supported.
|
|
142
|
+
*/
|
|
143
|
+
async function ensureUniversalMaker(binDir, accessToken, progressCallback) {
|
|
144
|
+
const key = getUniversalMakerArchitectureKey();
|
|
145
|
+
|
|
146
|
+
if (!key) {
|
|
147
|
+
await logAndProgress(
|
|
148
|
+
`Universal Maker is not supported on ${process.platform} ${process.arch}.`,
|
|
149
|
+
progressCallback,
|
|
150
|
+
'debug'
|
|
151
|
+
);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const binPath = path.join(binDir, UNIVERSAL_MAKER_BIN);
|
|
156
|
+
const arch = ARCH_MAPPING[key];
|
|
157
|
+
|
|
158
|
+
if (await isBinaryUpToDate(binPath, arch, accessToken, progressCallback)) {
|
|
159
|
+
return binPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await logAndProgress(`Downloading Universal Maker...`, progressCallback);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
if (!fs.existsSync(binDir)) {
|
|
166
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const artifactRegistryClient = await getArtifactRegistryClient(
|
|
170
|
+
AR_PROJECT,
|
|
171
|
+
accessToken
|
|
172
|
+
);
|
|
173
|
+
const resourceName = artifactRegistryClient.filePath(
|
|
174
|
+
AR_PROJECT,
|
|
175
|
+
AR_LOCATION,
|
|
176
|
+
AR_REPOSITORY,
|
|
177
|
+
`${arch}:${UM_VERSION}:${UNIVERSAL_MAKER_BIN}`
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const url = `${AR_HOST}${AR_DOWNLOAD_PATH_PREFIX}${resourceName}${AR_DOWNLOAD_URL_SUFFIX}`;
|
|
181
|
+
const response = await artifactRegistryClient.auth.request({
|
|
182
|
+
url,
|
|
183
|
+
responseType: 'stream',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const dest = fs.createWriteStream(binPath);
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
response.data.pipe(dest).on('finish', resolve).on('error', reject);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
fs.chmodSync(binPath, '755');
|
|
192
|
+
return binPath;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
await logAndProgress(
|
|
195
|
+
`Failed to download Universal Maker: ${error.message}`,
|
|
196
|
+
progressCallback,
|
|
197
|
+
'debug'
|
|
198
|
+
);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Runs Universal Maker on the given application directory.
|
|
205
|
+
* @param {string} appDir - Directory containing the application source.
|
|
206
|
+
* @param {function} progressCallback - Progress callback.
|
|
207
|
+
* @param {string} accessToken - Access token for authentication.
|
|
208
|
+
* @returns {Promise<object|null>} The parsed build_output.json or null.
|
|
209
|
+
*/
|
|
210
|
+
export async function runUniversalMaker(appDir, accessToken, progressCallback) {
|
|
211
|
+
const binDir = path.join(
|
|
212
|
+
os.homedir(),
|
|
213
|
+
TEMP_PATHS.BASE,
|
|
214
|
+
TEMP_PATHS.BIN_SUBDIR
|
|
215
|
+
);
|
|
216
|
+
const binPath = await ensureUniversalMaker(
|
|
217
|
+
binDir,
|
|
218
|
+
accessToken,
|
|
219
|
+
progressCallback
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (!binPath) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const outputDir = path.join(os.tmpdir(), `um-output-${Date.now()}`);
|
|
227
|
+
if (!fs.existsSync(outputDir)) {
|
|
228
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// ./universal_maker -application_dir <source_code_path> -output_dir <output_results_path> -output_format json
|
|
233
|
+
await logAndProgress(
|
|
234
|
+
`Running Universal Maker: ${binPath} -application_dir ${appDir} -output_dir ${outputDir} -output_format json`,
|
|
235
|
+
progressCallback,
|
|
236
|
+
'debug'
|
|
237
|
+
);
|
|
238
|
+
const command = `"${binPath}" -application_dir "${appDir}" -output_dir "${outputDir}" -output_format json`;
|
|
239
|
+
await execAsync(command);
|
|
240
|
+
|
|
241
|
+
const outputPath = path.join(outputDir, 'build_output.json');
|
|
242
|
+
if (fs.existsSync(outputPath)) {
|
|
243
|
+
const output = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
|
|
244
|
+
await logAndProgress(
|
|
245
|
+
'Universal Maker completed successfully.',
|
|
246
|
+
progressCallback
|
|
247
|
+
);
|
|
248
|
+
return output;
|
|
249
|
+
} else {
|
|
250
|
+
await logAndProgress(
|
|
251
|
+
'Universal Maker did not produce build_output.json',
|
|
252
|
+
progressCallback,
|
|
253
|
+
'debug'
|
|
254
|
+
);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
await logAndProgress(
|
|
259
|
+
`Universal Maker failed: ${error.message}`,
|
|
260
|
+
progressCallback,
|
|
261
|
+
'debug'
|
|
262
|
+
);
|
|
263
|
+
return null;
|
|
264
|
+
} finally {
|
|
265
|
+
// Cleanup output dir
|
|
266
|
+
try {
|
|
267
|
+
if (fs.existsSync(outputDir)) {
|
|
268
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
// ignore cleanup errors
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|