@google-cloud/cloud-run-mcp 1.6.0 → 1.7.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 +1 -1
- package/CONTRIBUTING.md +1 -1
- package/lib/cloud-api/billing.js +85 -0
- package/lib/cloud-api/helpers.js +73 -31
- package/lib/deployment/constants.js +54 -0
- package/lib/deployment/deployer.js +252 -105
- package/lib/deployment/helpers.js +194 -0
- package/lib/deployment/source-processor.js +124 -0
- package/lib/util/archive.js +15 -6
- package/package.json +2 -2
- package/test/local/deployment-helpers.test.js +231 -0
- package/test/local/mcp-server-stdio.test.js +59 -0
- package/test/local/mcp-server-streamable-http.test.js +25 -21
- package/test/local/source-processor.test.js +290 -0
- package/test/local/test-utils.js +51 -0
- package/test/need-gcp/workflows/deployment-workflows.test.js +3 -3
|
@@ -24,27 +24,23 @@ import { ensureArtifactRegistryRepoExists } from '../cloud-api/registry.js';
|
|
|
24
24
|
import { triggerCloudBuild } from '../cloud-api/build.js';
|
|
25
25
|
import { logAndProgress } from '../util/helpers.js';
|
|
26
26
|
import { checkCloudRunServiceExists } from '../cloud-api/run.js';
|
|
27
|
+
import {
|
|
28
|
+
canDeployWithoutBuild,
|
|
29
|
+
createDirectSourceDeploymentContainer,
|
|
30
|
+
makeFileDeploymentMetadata,
|
|
31
|
+
} from './helpers.js';
|
|
32
|
+
import {
|
|
33
|
+
prepareSourceDirectory,
|
|
34
|
+
installDependencies,
|
|
35
|
+
cleanupTempDirectory,
|
|
36
|
+
} from './source-processor.js';
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const REQUIRED_APIS_FOR_SOURCE_DEPLOY = [
|
|
35
|
-
'iam.googleapis.com',
|
|
36
|
-
'storage.googleapis.com',
|
|
37
|
-
'cloudbuild.googleapis.com',
|
|
38
|
-
'artifactregistry.googleapis.com',
|
|
39
|
-
'run.googleapis.com',
|
|
40
|
-
'serviceusage.googleapis.com',
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
// APIs required for deploying a container image.
|
|
44
|
-
const REQUIRED_APIS_FOR_IMAGE_DEPLOY = [
|
|
45
|
-
'run.googleapis.com',
|
|
46
|
-
'serviceusage.googleapis.com',
|
|
47
|
-
];
|
|
38
|
+
import {
|
|
39
|
+
DEPLOYMENT_CONFIG,
|
|
40
|
+
DEPLOYMENT_TYPES,
|
|
41
|
+
MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES,
|
|
42
|
+
REQUIRED_APIS,
|
|
43
|
+
} from './constants.js';
|
|
48
44
|
|
|
49
45
|
/**
|
|
50
46
|
* Deploys or updates a Cloud Run service with the specified container image.
|
|
@@ -68,7 +64,10 @@ async function deployToCloudRun(
|
|
|
68
64
|
serviceId,
|
|
69
65
|
imgUrl,
|
|
70
66
|
progressCallback,
|
|
71
|
-
skipIamCheck
|
|
67
|
+
skipIamCheck,
|
|
68
|
+
deploymentType,
|
|
69
|
+
zippedSourceContainer,
|
|
70
|
+
runtime
|
|
72
71
|
) {
|
|
73
72
|
const parent = context.runClient.locationPath(projectId, location);
|
|
74
73
|
const servicePath = context.runClient.servicePath(
|
|
@@ -78,13 +77,20 @@ async function deployToCloudRun(
|
|
|
78
77
|
);
|
|
79
78
|
const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
|
|
80
79
|
|
|
80
|
+
const containers =
|
|
81
|
+
deploymentType === DEPLOYMENT_TYPES.NO_BUILD
|
|
82
|
+
? [zippedSourceContainer]
|
|
83
|
+
: [{ image: imgUrl }];
|
|
84
|
+
|
|
81
85
|
const service = {
|
|
82
86
|
template: {
|
|
83
87
|
revision: revisionName,
|
|
84
|
-
containers
|
|
88
|
+
containers,
|
|
85
89
|
},
|
|
86
90
|
labels: {
|
|
87
|
-
'created-by':
|
|
91
|
+
'created-by': DEPLOYMENT_CONFIG.LABEL_CREATED_BY,
|
|
92
|
+
'deployment-type': deploymentType,
|
|
93
|
+
runtime: runtime,
|
|
88
94
|
},
|
|
89
95
|
};
|
|
90
96
|
|
|
@@ -213,6 +219,195 @@ async function deployToCloudRun(
|
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Handles deployment directly from source via a zipped archive (no Cloud Build).
|
|
224
|
+
* This function prepares the source code, installs dependencies, zips the code,
|
|
225
|
+
* uploads it to Google Cloud Storage, and then deploys it to Cloud Run
|
|
226
|
+
* using the direct source deployment mechanism.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} context - The context object containing various GCP clients.
|
|
229
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
230
|
+
* @param {string} region - The Google Cloud region for the deployment.
|
|
231
|
+
* @param {string} serviceName - The name of the Cloud Run service.
|
|
232
|
+
* @param {Array<string|{filename: string, content: Buffer|string}>} files - Files to deploy.
|
|
233
|
+
* @param {string} bucketName - The name of the GCS bucket to use for staging.
|
|
234
|
+
* @param {object} deploymentAttrs - Deployment attributes detected from the source.
|
|
235
|
+
* @param {string} [deploymentAttrs.runtime] - The detected runtime (e.g., 'nodejs').
|
|
236
|
+
* @param {string[]} [deploymentAttrs.cmd] - The command to run the application.
|
|
237
|
+
* @param {string[]} [deploymentAttrs.args] - The arguments for the command.
|
|
238
|
+
* @param {string} [deploymentAttrs.baseImage] - The base image to use for the deployment.
|
|
239
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
240
|
+
* @param {boolean} [skipIamCheck=false] - Whether to skip the IAM check during Cloud Run service creation/update.
|
|
241
|
+
* @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
|
|
242
|
+
* @throws {Error} If any part of the deployment process fails.
|
|
243
|
+
*/
|
|
244
|
+
async function deployWithZip(
|
|
245
|
+
context,
|
|
246
|
+
projectId,
|
|
247
|
+
region,
|
|
248
|
+
serviceName,
|
|
249
|
+
files,
|
|
250
|
+
bucketName,
|
|
251
|
+
deploymentAttrs,
|
|
252
|
+
progressCallback,
|
|
253
|
+
skipIamCheck
|
|
254
|
+
) {
|
|
255
|
+
await logAndProgress(
|
|
256
|
+
`Attempting direct source deployment...`,
|
|
257
|
+
progressCallback
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
let tempDir;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
tempDir = await prepareSourceDirectory(files);
|
|
264
|
+
await installDependencies(tempDir, deploymentAttrs);
|
|
265
|
+
|
|
266
|
+
const archiveBuffer = await zipFiles([tempDir], true, progressCallback);
|
|
267
|
+
const archiveName = DEPLOYMENT_CONFIG.TARGZ_FILE_NAME;
|
|
268
|
+
|
|
269
|
+
if (archiveBuffer.length > MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES) {
|
|
270
|
+
const errorMsg = `Warning: Source archive size (${(
|
|
271
|
+
archiveBuffer.length /
|
|
272
|
+
(1024 * 1024)
|
|
273
|
+
).toFixed(
|
|
274
|
+
2
|
|
275
|
+
)} MiB) exceeds the ${MAX_ALLOWED_DIRECT_SOURCE_SIZE_BYTES / (1024 * 1024)} MiB limit.`;
|
|
276
|
+
await logAndProgress(errorMsg, progressCallback, 'warn');
|
|
277
|
+
throw new Error('Archive size exceeds limit');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const bucket = await ensureStorageBucketExists(
|
|
281
|
+
context,
|
|
282
|
+
bucketName,
|
|
283
|
+
region,
|
|
284
|
+
progressCallback
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
await uploadToStorageBucket(
|
|
288
|
+
context,
|
|
289
|
+
bucket,
|
|
290
|
+
archiveBuffer,
|
|
291
|
+
archiveName,
|
|
292
|
+
progressCallback
|
|
293
|
+
);
|
|
294
|
+
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
295
|
+
|
|
296
|
+
const container = createDirectSourceDeploymentContainer({
|
|
297
|
+
bucketName,
|
|
298
|
+
fileName: archiveName,
|
|
299
|
+
deploymentAttrs,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const service = await deployToCloudRun(
|
|
303
|
+
context,
|
|
304
|
+
projectId,
|
|
305
|
+
region,
|
|
306
|
+
serviceName,
|
|
307
|
+
undefined,
|
|
308
|
+
progressCallback,
|
|
309
|
+
skipIamCheck,
|
|
310
|
+
DEPLOYMENT_TYPES.NO_BUILD,
|
|
311
|
+
container,
|
|
312
|
+
deploymentAttrs.runtime
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return service;
|
|
316
|
+
} finally {
|
|
317
|
+
await cleanupTempDirectory(tempDir);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Handles deployment using Google Cloud Build.
|
|
323
|
+
* This function zips the source code, uploads it to Google Cloud Storage,
|
|
324
|
+
* triggers a Cloud Build job to build a container image, pushes the image
|
|
325
|
+
* to Artifact Registry, and then deploys the image to Cloud Run.
|
|
326
|
+
*
|
|
327
|
+
* @param {object} context - The context object containing various GCP clients.
|
|
328
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
329
|
+
* @param {string} region - The Google Cloud region for the deployment.
|
|
330
|
+
* @param {string} serviceName - The name of the Cloud Run service.
|
|
331
|
+
* @param {Array<string|{filename: string, content: Buffer|string}>} files - Files to deploy.
|
|
332
|
+
* @param {boolean} hasDockerfile - Whether a Dockerfile is present in the files.
|
|
333
|
+
* @param {string} bucketName - The name of the GCS bucket to use for staging.
|
|
334
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
335
|
+
* @param {boolean} [skipIamCheck=false] - Whether to skip the IAM check during Cloud Run service creation/update.
|
|
336
|
+
* @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
|
|
337
|
+
* @throws {Error} If any part of the build or deployment process fails.
|
|
338
|
+
*/
|
|
339
|
+
async function deployWithBuild(
|
|
340
|
+
context,
|
|
341
|
+
projectId,
|
|
342
|
+
region,
|
|
343
|
+
serviceName,
|
|
344
|
+
files,
|
|
345
|
+
hasDockerfile,
|
|
346
|
+
bucketName,
|
|
347
|
+
progressCallback,
|
|
348
|
+
skipIamCheck
|
|
349
|
+
) {
|
|
350
|
+
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
|
|
351
|
+
|
|
352
|
+
const bucket = await ensureStorageBucketExists(
|
|
353
|
+
context,
|
|
354
|
+
bucketName,
|
|
355
|
+
region,
|
|
356
|
+
progressCallback
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const archiveBuffer = await zipFiles(files, false, progressCallback);
|
|
360
|
+
const archiveName = DEPLOYMENT_CONFIG.ZIP_FILE_NAME;
|
|
361
|
+
|
|
362
|
+
await uploadToStorageBucket(
|
|
363
|
+
context,
|
|
364
|
+
bucket,
|
|
365
|
+
archiveBuffer,
|
|
366
|
+
archiveName,
|
|
367
|
+
progressCallback
|
|
368
|
+
);
|
|
369
|
+
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
370
|
+
|
|
371
|
+
await ensureArtifactRegistryRepoExists(
|
|
372
|
+
context,
|
|
373
|
+
projectId,
|
|
374
|
+
region,
|
|
375
|
+
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
376
|
+
'DOCKER',
|
|
377
|
+
progressCallback
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
381
|
+
|
|
382
|
+
const buildResult = await triggerCloudBuild(
|
|
383
|
+
context,
|
|
384
|
+
projectId,
|
|
385
|
+
region,
|
|
386
|
+
bucketName,
|
|
387
|
+
archiveName,
|
|
388
|
+
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
389
|
+
imageUrl,
|
|
390
|
+
hasDockerfile,
|
|
391
|
+
progressCallback
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const builtImageUrl = buildResult.results.images[0].name;
|
|
395
|
+
|
|
396
|
+
const service = await deployToCloudRun(
|
|
397
|
+
context,
|
|
398
|
+
projectId,
|
|
399
|
+
region,
|
|
400
|
+
serviceName,
|
|
401
|
+
builtImageUrl,
|
|
402
|
+
progressCallback,
|
|
403
|
+
skipIamCheck,
|
|
404
|
+
DEPLOYMENT_TYPES.WITH_BUILD
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
408
|
+
return service;
|
|
409
|
+
}
|
|
410
|
+
|
|
216
411
|
/**
|
|
217
412
|
* Deploys a service to Google Cloud Run.
|
|
218
413
|
* @param {object} config - The deployment configuration.
|
|
@@ -257,8 +452,6 @@ export async function deploy({
|
|
|
257
452
|
}
|
|
258
453
|
}
|
|
259
454
|
|
|
260
|
-
const path = await import('path');
|
|
261
|
-
const fs = await import('fs');
|
|
262
455
|
const { Storage } = await import('@google-cloud/storage');
|
|
263
456
|
const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
|
|
264
457
|
const { ArtifactRegistryClient } = await import(
|
|
@@ -282,103 +475,56 @@ export async function deploy({
|
|
|
282
475
|
await ensureApisEnabled(
|
|
283
476
|
context,
|
|
284
477
|
projectId,
|
|
285
|
-
|
|
478
|
+
REQUIRED_APIS.SOURCE_DEPLOY,
|
|
286
479
|
progressCallback
|
|
287
480
|
);
|
|
288
481
|
|
|
289
482
|
const bucketName = `${projectId}-source-bucket`;
|
|
290
|
-
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
|
|
291
483
|
|
|
292
484
|
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
293
485
|
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
294
486
|
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
295
487
|
await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
296
488
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
files
|
|
300
|
-
typeof files[0] === 'string' &&
|
|
301
|
-
fs.statSync(files[0]).isDirectory()
|
|
302
|
-
) {
|
|
303
|
-
// Handle folder deployment: check for Dockerfile inside the folder
|
|
304
|
-
const dockerfilePath = path.join(files[0], 'Dockerfile');
|
|
305
|
-
const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
|
|
306
|
-
if (
|
|
307
|
-
fs.existsSync(dockerfilePath) ||
|
|
308
|
-
fs.existsSync(dockerfilePathLowerCase)
|
|
309
|
-
) {
|
|
310
|
-
hasDockerfile = true;
|
|
311
|
-
}
|
|
312
|
-
} else {
|
|
313
|
-
// Handle file list deployment or file content deployment
|
|
314
|
-
for (const file of files) {
|
|
315
|
-
if (typeof file === 'string') {
|
|
316
|
-
if (path.basename(file).toLowerCase() === 'dockerfile') {
|
|
317
|
-
hasDockerfile = true;
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
} else if (typeof file === 'object' && file.filename) {
|
|
321
|
-
if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
|
|
322
|
-
hasDockerfile = true;
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
329
|
-
|
|
330
|
-
const bucket = await ensureStorageBucketExists(
|
|
331
|
-
context,
|
|
332
|
-
bucketName,
|
|
333
|
-
region,
|
|
334
|
-
progressCallback
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
const zipBuffer = await zipFiles(files, progressCallback);
|
|
338
|
-
await uploadToStorageBucket(
|
|
339
|
-
context,
|
|
340
|
-
bucket,
|
|
341
|
-
zipBuffer,
|
|
342
|
-
ZIP_FILE_NAME,
|
|
343
|
-
progressCallback
|
|
344
|
-
);
|
|
345
|
-
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
346
|
-
|
|
347
|
-
await ensureArtifactRegistryRepoExists(
|
|
348
|
-
context,
|
|
349
|
-
projectId,
|
|
350
|
-
region,
|
|
351
|
-
REPO_NAME,
|
|
352
|
-
'DOCKER',
|
|
353
|
-
progressCallback
|
|
354
|
-
);
|
|
489
|
+
// Determines if we can use the direct source deployment (no Cloud Build)
|
|
490
|
+
const { hasDockerfile, deploymentAttrs } =
|
|
491
|
+
makeFileDeploymentMetadata(files);
|
|
355
492
|
|
|
356
|
-
|
|
357
|
-
context,
|
|
358
|
-
projectId,
|
|
359
|
-
region,
|
|
360
|
-
bucketName,
|
|
361
|
-
ZIP_FILE_NAME,
|
|
362
|
-
REPO_NAME,
|
|
363
|
-
imageUrl,
|
|
364
|
-
hasDockerfile,
|
|
365
|
-
progressCallback
|
|
366
|
-
);
|
|
493
|
+
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
367
494
|
|
|
368
|
-
|
|
495
|
+
if (canDeployWithoutBuild({ hasDockerfile, deploymentAttrs })) {
|
|
496
|
+
try {
|
|
497
|
+
return await deployWithZip(
|
|
498
|
+
context,
|
|
499
|
+
projectId,
|
|
500
|
+
region,
|
|
501
|
+
serviceName,
|
|
502
|
+
files,
|
|
503
|
+
bucketName,
|
|
504
|
+
deploymentAttrs,
|
|
505
|
+
progressCallback,
|
|
506
|
+
skipIamCheck
|
|
507
|
+
);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
await logAndProgress(
|
|
510
|
+
`Failed to deploy directly from source: ${error.message}. Retrying to deploy using Cloud Build...`,
|
|
511
|
+
progressCallback,
|
|
512
|
+
'warn'
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
369
516
|
|
|
370
|
-
|
|
517
|
+
return await deployWithBuild(
|
|
371
518
|
context,
|
|
372
519
|
projectId,
|
|
373
520
|
region,
|
|
374
521
|
serviceName,
|
|
375
|
-
|
|
522
|
+
files,
|
|
523
|
+
hasDockerfile,
|
|
524
|
+
bucketName,
|
|
376
525
|
progressCallback,
|
|
377
526
|
skipIamCheck
|
|
378
527
|
);
|
|
379
|
-
|
|
380
|
-
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
381
|
-
return service;
|
|
382
528
|
} catch (error) {
|
|
383
529
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
384
530
|
console.error(`Deployment Failed`, error);
|
|
@@ -444,7 +590,7 @@ export async function deployImage({
|
|
|
444
590
|
await ensureApisEnabled(
|
|
445
591
|
context,
|
|
446
592
|
projectId,
|
|
447
|
-
|
|
593
|
+
REQUIRED_APIS.IMAGE_DEPLOY,
|
|
448
594
|
progressCallback
|
|
449
595
|
);
|
|
450
596
|
|
|
@@ -460,10 +606,11 @@ export async function deployImage({
|
|
|
460
606
|
serviceName,
|
|
461
607
|
imageUrl,
|
|
462
608
|
progressCallback,
|
|
463
|
-
skipIamCheck
|
|
609
|
+
skipIamCheck,
|
|
610
|
+
DEPLOYMENT_TYPES.IMAGE
|
|
464
611
|
);
|
|
465
612
|
|
|
466
|
-
await logAndProgress(`Deployment
|
|
613
|
+
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
467
614
|
return service;
|
|
468
615
|
} catch (error) {
|
|
469
616
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
@@ -0,0 +1,194 @@
|
|
|
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 fs from 'fs';
|
|
19
|
+
import { DEPLOYMENT_CONFIG, RUNTIMES } from './constants.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the input is a single folder path.
|
|
23
|
+
* @param {Array<string|Object>} files - Array of file paths or file objects.
|
|
24
|
+
* @returns {boolean} - true if files contains exactly one string which is a directory.
|
|
25
|
+
*/
|
|
26
|
+
function isFolder(files) {
|
|
27
|
+
if (files.length !== 1 || typeof files[0] !== 'string') return false;
|
|
28
|
+
try {
|
|
29
|
+
return fs.statSync(files[0]).isDirectory();
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a Dockerfile exists in the provided files array.
|
|
37
|
+
* @param {Array<string|Object>} files - An array of file paths or objects with filename properties.
|
|
38
|
+
* @returns {boolean} - Returns true if a Dockerfile is found, false otherwise.
|
|
39
|
+
*/
|
|
40
|
+
function checkIfDockerFileExists(files) {
|
|
41
|
+
if (isFolder(files)) {
|
|
42
|
+
// Handle folder deployment: check for Dockerfile inside the folder
|
|
43
|
+
return (
|
|
44
|
+
fs.existsSync(path.join(files[0], 'Dockerfile')) ||
|
|
45
|
+
fs.existsSync(path.join(files[0], 'dockerfile'))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle file list deployment or file content deployment
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (typeof file === 'string') {
|
|
52
|
+
if (path.basename(file).toLowerCase() === 'dockerfile') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
} else if (typeof file === 'object' && file.filename) {
|
|
56
|
+
if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Checks if the provided folder appears to be a Node.js project.
|
|
66
|
+
* It looks for a `package.json` file in the root of the first path in the `files` array.
|
|
67
|
+
* @param {string[]} files - Array of file paths
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function checkIfNodeJsRuntime(files) {
|
|
71
|
+
const packageJsonPath = path.join(files[0], 'package.json');
|
|
72
|
+
return fs.existsSync(packageJsonPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns an empty deployment attributes object.
|
|
77
|
+
* This is used as a default or when attributes cannot be determined.
|
|
78
|
+
* @returns {{runtime: undefined, cmd: undefined, args: undefined, baseImage: undefined}}
|
|
79
|
+
*/
|
|
80
|
+
function getEmptyDeploymentAttrs() {
|
|
81
|
+
return {
|
|
82
|
+
runtime: undefined,
|
|
83
|
+
cmd: undefined,
|
|
84
|
+
args: undefined,
|
|
85
|
+
baseImage: undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
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
|
+
/**
|
|
126
|
+
* Determines deployment attributes based on the file list/folder content.
|
|
127
|
+
* @param {Array<string|Object>} files - Array of file paths or file objects.
|
|
128
|
+
* @returns {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}} Deployment attributes object.
|
|
129
|
+
*/
|
|
130
|
+
function getDeploymentAttrs(files) {
|
|
131
|
+
// Currently only support detection of NodeJs runtime
|
|
132
|
+
if (!isFolder(files) || !checkIfNodeJsRuntime(files)) {
|
|
133
|
+
// TODO: support file list deployment runtime detection
|
|
134
|
+
return getEmptyDeploymentAttrs();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return getNodeJsDeploymentAttrs(files);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates metadata for the file deployment, including Dockerfile presence and runtime attributes.
|
|
142
|
+
* @param {Array<string|Object>} files - Array of file paths or file objects to be deployed.
|
|
143
|
+
* @returns {{hasDockerfile: boolean, deploymentAttrs: {runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined, baseImage: string|undefined}}} Metadata object.
|
|
144
|
+
*/
|
|
145
|
+
export function makeFileDeploymentMetadata(files) {
|
|
146
|
+
return {
|
|
147
|
+
hasDockerfile: checkIfDockerFileExists(files),
|
|
148
|
+
deploymentAttrs: getDeploymentAttrs(files),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Checks if a zip-based source deployment (no-build) is feasible.
|
|
154
|
+
* @param {object} metadata - The deployment metadata.
|
|
155
|
+
* @param {boolean} metadata.hasDockerfile - Whether a Dockerfile is present.
|
|
156
|
+
* @param {{runtime: string|undefined, cmd: string[]|undefined, args: string[]|undefined}} metadata.deploymentAttrs - Deployment attributes.
|
|
157
|
+
* @returns {boolean} True if zip deployment is feasible.
|
|
158
|
+
*/
|
|
159
|
+
export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
|
|
160
|
+
return !!(
|
|
161
|
+
!hasDockerfile &&
|
|
162
|
+
deploymentAttrs &&
|
|
163
|
+
deploymentAttrs.runtime &&
|
|
164
|
+
deploymentAttrs.cmd &&
|
|
165
|
+
deploymentAttrs.args
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Creates the container spec for a direct source deployment (no Cloud Build).
|
|
171
|
+
* @param {object} params - The parameters for creating the container spec.
|
|
172
|
+
* @param {string} params.bucketName - The GCS bucket name where the source is uploaded.
|
|
173
|
+
* @param {string} params.fileName - The GCS object name (e.g., source.tar.gz).
|
|
174
|
+
* @param {{cmd: string[], args: string[], baseImage: string}} params.deploymentAttrs - Deployment attributes including command, args, and the base image to use.
|
|
175
|
+
* @returns {object} The container specification object for the Cloud Run service.
|
|
176
|
+
*/
|
|
177
|
+
export function createDirectSourceDeploymentContainer({
|
|
178
|
+
bucketName,
|
|
179
|
+
fileName,
|
|
180
|
+
deploymentAttrs,
|
|
181
|
+
}) {
|
|
182
|
+
return {
|
|
183
|
+
image: DEPLOYMENT_CONFIG.NO_BUILD_IMAGE_TYPE,
|
|
184
|
+
baseImageUri: deploymentAttrs.baseImage,
|
|
185
|
+
sourceCode: {
|
|
186
|
+
cloudStorageSource: {
|
|
187
|
+
bucket: bucketName,
|
|
188
|
+
object: fileName,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
command: deploymentAttrs.cmd,
|
|
192
|
+
args: deploymentAttrs.args,
|
|
193
|
+
};
|
|
194
|
+
}
|