@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
|
@@ -22,9 +22,15 @@ import {
|
|
|
22
22
|
uploadToStorageBucket,
|
|
23
23
|
} from '../cloud-api/storage.js';
|
|
24
24
|
import { ensureArtifactRegistryRepoExists } from '../cloud-api/registry.js';
|
|
25
|
-
import { triggerCloudBuild } from '../cloud-api/build.js';
|
|
26
|
-
import {
|
|
27
|
-
|
|
25
|
+
import { triggerCloudBuild, composeBuild } from '../cloud-api/build.js';
|
|
26
|
+
import {
|
|
27
|
+
logAndProgress,
|
|
28
|
+
sanitizeCloudRunServiceName,
|
|
29
|
+
} from '../util/helpers.js';
|
|
30
|
+
import {
|
|
31
|
+
checkCloudRunServiceExists,
|
|
32
|
+
setServicePublicAccess,
|
|
33
|
+
} from '../cloud-api/run.js';
|
|
28
34
|
import {
|
|
29
35
|
canDeployWithoutBuild,
|
|
30
36
|
createDirectSourceDeploymentContainer,
|
|
@@ -43,6 +49,18 @@ import {
|
|
|
43
49
|
REQUIRED_APIS,
|
|
44
50
|
RUNTIMES,
|
|
45
51
|
} from './constants.js';
|
|
52
|
+
import path from 'path';
|
|
53
|
+
import fs from 'fs';
|
|
54
|
+
import yaml from 'js-yaml';
|
|
55
|
+
import { getRunV1Client } from '../clients.js';
|
|
56
|
+
import {
|
|
57
|
+
runCompose as downloadRunCompose,
|
|
58
|
+
resourceCompose,
|
|
59
|
+
translateCompose,
|
|
60
|
+
composeVolumes,
|
|
61
|
+
composeSecrets,
|
|
62
|
+
} from './compose.js';
|
|
63
|
+
import { getProjectNumber } from '../util/helpers.js';
|
|
46
64
|
|
|
47
65
|
/**
|
|
48
66
|
* Deploys or updates a Cloud Run service with the specified container image.
|
|
@@ -69,7 +87,8 @@ async function deployToCloudRun(
|
|
|
69
87
|
deploymentType,
|
|
70
88
|
accessToken,
|
|
71
89
|
zippedSourceContainer,
|
|
72
|
-
runtime
|
|
90
|
+
runtime,
|
|
91
|
+
ingress
|
|
73
92
|
) {
|
|
74
93
|
const runClient = await getRunClient(projectId, accessToken);
|
|
75
94
|
const parent = runClient.locationPath(projectId, location);
|
|
@@ -91,7 +110,11 @@ async function deployToCloudRun(
|
|
|
91
110
|
'deployment-type': deploymentType,
|
|
92
111
|
runtime: runtime,
|
|
93
112
|
},
|
|
113
|
+
client: DEPLOYMENT_CONFIG.LABEL_CREATED_BY,
|
|
94
114
|
};
|
|
115
|
+
if (ingress) {
|
|
116
|
+
service.ingress = ingress;
|
|
117
|
+
}
|
|
95
118
|
|
|
96
119
|
// Conditionally set invokerIamDisabled based on the skipIamCheck flag
|
|
97
120
|
if (skipIamCheck) {
|
|
@@ -249,7 +272,8 @@ async function deployWithZip(
|
|
|
249
272
|
deploymentAttrs,
|
|
250
273
|
progressCallback,
|
|
251
274
|
skipIamCheck,
|
|
252
|
-
accessToken
|
|
275
|
+
accessToken,
|
|
276
|
+
ingress
|
|
253
277
|
) {
|
|
254
278
|
await logAndProgress(
|
|
255
279
|
`Attempting direct source deployment...`,
|
|
@@ -292,10 +316,12 @@ async function deployWithZip(
|
|
|
292
316
|
bucketName,
|
|
293
317
|
region,
|
|
294
318
|
accessToken,
|
|
319
|
+
undefined,
|
|
320
|
+
false,
|
|
295
321
|
progressCallback
|
|
296
322
|
);
|
|
297
323
|
|
|
298
|
-
await uploadToStorageBucket(
|
|
324
|
+
const file = await uploadToStorageBucket(
|
|
299
325
|
bucket,
|
|
300
326
|
archiveBuffer,
|
|
301
327
|
archiveName,
|
|
@@ -304,7 +330,7 @@ async function deployWithZip(
|
|
|
304
330
|
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
305
331
|
|
|
306
332
|
const container = createDirectSourceDeploymentContainer({
|
|
307
|
-
bucketName,
|
|
333
|
+
bucketName: bucket.name,
|
|
308
334
|
fileName: archiveName,
|
|
309
335
|
deploymentAttrs,
|
|
310
336
|
});
|
|
@@ -319,9 +345,14 @@ async function deployWithZip(
|
|
|
319
345
|
DEPLOYMENT_TYPES.NO_BUILD,
|
|
320
346
|
accessToken,
|
|
321
347
|
container,
|
|
322
|
-
deploymentAttrs.runtime
|
|
348
|
+
deploymentAttrs.runtime,
|
|
349
|
+
ingress
|
|
323
350
|
);
|
|
324
351
|
|
|
352
|
+
await logAndProgress(
|
|
353
|
+
`Deployment completed successfully with zip deploy`,
|
|
354
|
+
progressCallback
|
|
355
|
+
);
|
|
325
356
|
return service;
|
|
326
357
|
} finally {
|
|
327
358
|
await cleanupTempDirectory(tempDir);
|
|
@@ -392,7 +423,8 @@ async function deployWithBuild(
|
|
|
392
423
|
bucketName,
|
|
393
424
|
progressCallback,
|
|
394
425
|
skipIamCheck,
|
|
395
|
-
accessToken
|
|
426
|
+
accessToken,
|
|
427
|
+
ingress
|
|
396
428
|
) {
|
|
397
429
|
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
|
|
398
430
|
|
|
@@ -401,13 +433,15 @@ async function deployWithBuild(
|
|
|
401
433
|
bucketName,
|
|
402
434
|
region,
|
|
403
435
|
accessToken,
|
|
436
|
+
undefined,
|
|
437
|
+
false,
|
|
404
438
|
progressCallback
|
|
405
439
|
);
|
|
406
440
|
|
|
407
441
|
const archiveBuffer = await zipFiles(files, false, progressCallback);
|
|
408
442
|
const archiveName = DEPLOYMENT_CONFIG.ZIP_FILE_NAME;
|
|
409
443
|
|
|
410
|
-
await uploadToStorageBucket(
|
|
444
|
+
const file = await uploadToStorageBucket(
|
|
411
445
|
bucket,
|
|
412
446
|
archiveBuffer,
|
|
413
447
|
archiveName,
|
|
@@ -429,13 +463,14 @@ async function deployWithBuild(
|
|
|
429
463
|
const buildResult = await triggerCloudBuild(
|
|
430
464
|
projectId,
|
|
431
465
|
region,
|
|
432
|
-
|
|
466
|
+
bucket.name,
|
|
433
467
|
archiveName,
|
|
434
468
|
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
435
469
|
imageUrl,
|
|
436
470
|
hasDockerfile,
|
|
437
471
|
accessToken,
|
|
438
|
-
progressCallback
|
|
472
|
+
progressCallback,
|
|
473
|
+
ingress
|
|
439
474
|
);
|
|
440
475
|
|
|
441
476
|
const builtImageUrl = buildResult.results.images[0].name;
|
|
@@ -448,10 +483,16 @@ async function deployWithBuild(
|
|
|
448
483
|
progressCallback,
|
|
449
484
|
skipIamCheck,
|
|
450
485
|
DEPLOYMENT_TYPES.WITH_BUILD,
|
|
451
|
-
accessToken
|
|
486
|
+
accessToken,
|
|
487
|
+
undefined,
|
|
488
|
+
undefined,
|
|
489
|
+
ingress
|
|
452
490
|
);
|
|
453
491
|
|
|
454
|
-
await logAndProgress(
|
|
492
|
+
await logAndProgress(
|
|
493
|
+
`Deployment completed successfully with source build deploy`,
|
|
494
|
+
progressCallback
|
|
495
|
+
);
|
|
455
496
|
return service;
|
|
456
497
|
}
|
|
457
498
|
|
|
@@ -474,6 +515,7 @@ export async function deploy({
|
|
|
474
515
|
progressCallback,
|
|
475
516
|
skipIamCheck,
|
|
476
517
|
accessToken,
|
|
518
|
+
ingress,
|
|
477
519
|
}) {
|
|
478
520
|
if (!projectId) {
|
|
479
521
|
const errorMsg =
|
|
@@ -482,13 +524,6 @@ export async function deploy({
|
|
|
482
524
|
throw new Error(errorMsg);
|
|
483
525
|
}
|
|
484
526
|
|
|
485
|
-
if (!serviceName) {
|
|
486
|
-
const errorMsg =
|
|
487
|
-
'Error: serviceName is required in the configuration object.';
|
|
488
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
489
|
-
throw new Error(errorMsg);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
527
|
if (!files || !Array.isArray(files) || files.length === 0) {
|
|
493
528
|
const errorMsg =
|
|
494
529
|
'Error: files array is required in the configuration object.';
|
|
@@ -500,6 +535,30 @@ export async function deploy({
|
|
|
500
535
|
}
|
|
501
536
|
}
|
|
502
537
|
|
|
538
|
+
// Determines if we can use the direct source deployment (no Cloud Build) or compose deployment
|
|
539
|
+
const { hasDockerfile, composeFilePath, deploymentAttrs } =
|
|
540
|
+
makeFileDeploymentMetadata(files);
|
|
541
|
+
|
|
542
|
+
if (composeFilePath) {
|
|
543
|
+
return await deployCompose({
|
|
544
|
+
projectId,
|
|
545
|
+
serviceName,
|
|
546
|
+
region,
|
|
547
|
+
files,
|
|
548
|
+
composeFilePath,
|
|
549
|
+
progressCallback,
|
|
550
|
+
skipIamCheck,
|
|
551
|
+
accessToken,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!serviceName) {
|
|
556
|
+
const errorMsg =
|
|
557
|
+
'Error: serviceName is required in the configuration object.';
|
|
558
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
559
|
+
throw new Error(errorMsg);
|
|
560
|
+
}
|
|
561
|
+
|
|
503
562
|
try {
|
|
504
563
|
await ensureApisEnabled(
|
|
505
564
|
projectId,
|
|
@@ -515,10 +574,6 @@ export async function deploy({
|
|
|
515
574
|
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
516
575
|
await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
517
576
|
|
|
518
|
-
// Determines if we can use the direct source deployment (no Cloud Build)
|
|
519
|
-
const { hasDockerfile, deploymentAttrs } =
|
|
520
|
-
makeFileDeploymentMetadata(files);
|
|
521
|
-
|
|
522
577
|
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
523
578
|
|
|
524
579
|
if (canDeployWithoutBuild({ hasDockerfile, deploymentAttrs })) {
|
|
@@ -532,7 +587,8 @@ export async function deploy({
|
|
|
532
587
|
deploymentAttrs,
|
|
533
588
|
progressCallback,
|
|
534
589
|
skipIamCheck,
|
|
535
|
-
accessToken
|
|
590
|
+
accessToken,
|
|
591
|
+
ingress
|
|
536
592
|
);
|
|
537
593
|
} catch (error) {
|
|
538
594
|
await logAndProgress(
|
|
@@ -552,7 +608,8 @@ export async function deploy({
|
|
|
552
608
|
bucketName,
|
|
553
609
|
progressCallback,
|
|
554
610
|
skipIamCheck,
|
|
555
|
-
accessToken
|
|
611
|
+
accessToken,
|
|
612
|
+
ingress
|
|
556
613
|
);
|
|
557
614
|
} catch (error) {
|
|
558
615
|
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
@@ -582,6 +639,7 @@ export async function deployImage({
|
|
|
582
639
|
progressCallback,
|
|
583
640
|
skipIamCheck,
|
|
584
641
|
accessToken,
|
|
642
|
+
ingress,
|
|
585
643
|
}) {
|
|
586
644
|
if (!projectId) {
|
|
587
645
|
const errorMsg =
|
|
@@ -628,7 +686,10 @@ export async function deployImage({
|
|
|
628
686
|
progressCallback,
|
|
629
687
|
skipIamCheck,
|
|
630
688
|
DEPLOYMENT_TYPES.IMAGE,
|
|
631
|
-
accessToken
|
|
689
|
+
accessToken,
|
|
690
|
+
undefined,
|
|
691
|
+
undefined,
|
|
692
|
+
ingress
|
|
632
693
|
);
|
|
633
694
|
|
|
634
695
|
await logAndProgress(`Deployment completed successfully`, progressCallback);
|
|
@@ -640,3 +701,197 @@ export async function deployImage({
|
|
|
640
701
|
throw error;
|
|
641
702
|
}
|
|
642
703
|
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Deploys services defined in a compose.yaml file to Cloud Run.
|
|
707
|
+
*
|
|
708
|
+
* @async
|
|
709
|
+
* @param {object} config - The deployment configuration.
|
|
710
|
+
* @param {string} config.projectId - The Google Cloud project ID.
|
|
711
|
+
* @param {string} [config.serviceName] - The name of the Cloud Run service.
|
|
712
|
+
* @param {string} [config.region] - The Google Cloud region for deployment.
|
|
713
|
+
* @param {Array<string>} config.files - An array of paths to include in the source bundle.
|
|
714
|
+
* @param {string} config.composeFilePath - The absolute path to the compose configuration file.
|
|
715
|
+
* @param {function(object): void} [config.progressCallback] - Optional callback for progress updates.
|
|
716
|
+
* @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check (if true, will attempt to set public access).
|
|
717
|
+
* @param {string} [config.accessToken] - Access token for authentication.
|
|
718
|
+
* @returns {Promise<object>} A promise that resolves with the deployment result.
|
|
719
|
+
* @throws {Error} If deployment fails or required configuration is missing.
|
|
720
|
+
*/
|
|
721
|
+
export async function deployCompose({
|
|
722
|
+
projectId,
|
|
723
|
+
serviceName,
|
|
724
|
+
region,
|
|
725
|
+
files,
|
|
726
|
+
composeFilePath,
|
|
727
|
+
progressCallback,
|
|
728
|
+
skipIamCheck,
|
|
729
|
+
accessToken,
|
|
730
|
+
}) {
|
|
731
|
+
let tempDir;
|
|
732
|
+
try {
|
|
733
|
+
tempDir = await prepareSourceDirectory(files, progressCallback);
|
|
734
|
+
const tempComposeFilePath = path.join(
|
|
735
|
+
tempDir,
|
|
736
|
+
path.basename(composeFilePath)
|
|
737
|
+
);
|
|
738
|
+
const folderPath = tempDir;
|
|
739
|
+
const projectNumber = await getProjectNumber(projectId, accessToken);
|
|
740
|
+
const binPath = await downloadRunCompose(accessToken, progressCallback);
|
|
741
|
+
|
|
742
|
+
// Get resource configuration
|
|
743
|
+
const resourceOutput = await resourceCompose(
|
|
744
|
+
binPath,
|
|
745
|
+
tempComposeFilePath,
|
|
746
|
+
region,
|
|
747
|
+
progressCallback
|
|
748
|
+
);
|
|
749
|
+
let resourcesConfig = JSON.parse(resourceOutput);
|
|
750
|
+
if (!serviceName) {
|
|
751
|
+
serviceName = resourcesConfig.project;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Handle source builds if any
|
|
755
|
+
if (resourcesConfig.source_builds) {
|
|
756
|
+
resourcesConfig = await composeBuild(
|
|
757
|
+
resourcesConfig,
|
|
758
|
+
accessToken,
|
|
759
|
+
projectId,
|
|
760
|
+
region,
|
|
761
|
+
folderPath,
|
|
762
|
+
progressCallback
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Handle volumes (bind and named)
|
|
767
|
+
resourcesConfig = await composeVolumes(
|
|
768
|
+
resourcesConfig,
|
|
769
|
+
accessToken,
|
|
770
|
+
projectId,
|
|
771
|
+
region,
|
|
772
|
+
folderPath,
|
|
773
|
+
progressCallback
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
// Handle secrets
|
|
777
|
+
resourcesConfig = await composeSecrets(
|
|
778
|
+
resourcesConfig,
|
|
779
|
+
accessToken,
|
|
780
|
+
projectId,
|
|
781
|
+
folderPath,
|
|
782
|
+
progressCallback
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
const result = await translateCompose(
|
|
786
|
+
binPath,
|
|
787
|
+
tempComposeFilePath,
|
|
788
|
+
region,
|
|
789
|
+
projectNumber,
|
|
790
|
+
progressCallback,
|
|
791
|
+
resourcesConfig
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
const translation = JSON.parse(result);
|
|
795
|
+
const deploymentItems = [];
|
|
796
|
+
|
|
797
|
+
if (translation.models) {
|
|
798
|
+
for (const [name, yamlPath] of Object.entries(translation.models)) {
|
|
799
|
+
deploymentItems.push({ name, yamlPath, type: 'model' });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (translation.services) {
|
|
803
|
+
for (const [name, yamlPath] of Object.entries(translation.services)) {
|
|
804
|
+
deploymentItems.push({ name, yamlPath, type: 'service' });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const deploymentResults = {};
|
|
809
|
+
const runV1Client = await getRunV1Client(projectId, accessToken, region);
|
|
810
|
+
|
|
811
|
+
for (const { name: rawName, yamlPath, type } of deploymentItems) {
|
|
812
|
+
const serviceName = sanitizeCloudRunServiceName(rawName);
|
|
813
|
+
const fullYamlPath = path.join(folderPath, yamlPath);
|
|
814
|
+
await logAndProgress(
|
|
815
|
+
`Deploying ${type} ${serviceName} using configuration from ${fullYamlPath}...`,
|
|
816
|
+
progressCallback
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
const serviceConfig = yaml.load(
|
|
820
|
+
await fs.promises.readFile(fullYamlPath, 'utf8')
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
// Ensure the metadata name is sanitized to match
|
|
824
|
+
serviceConfig.metadata = serviceConfig.metadata || {};
|
|
825
|
+
serviceConfig.metadata.name = serviceName;
|
|
826
|
+
|
|
827
|
+
// The name for replaceService in v1 is namespaces/{project}/services/{service}
|
|
828
|
+
const name = `namespaces/${projectId}/services/${serviceName}`;
|
|
829
|
+
const parent = `namespaces/${projectId}`;
|
|
830
|
+
|
|
831
|
+
let response;
|
|
832
|
+
try {
|
|
833
|
+
response = await runV1Client.namespaces.services.replaceService({
|
|
834
|
+
name,
|
|
835
|
+
requestBody: serviceConfig,
|
|
836
|
+
});
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (error.code === 404) {
|
|
839
|
+
await logAndProgress(
|
|
840
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} ${serviceName} does not exist. Creating it...`,
|
|
841
|
+
progressCallback
|
|
842
|
+
);
|
|
843
|
+
response = await runV1Client.namespaces.services.create({
|
|
844
|
+
parent,
|
|
845
|
+
requestBody: serviceConfig,
|
|
846
|
+
});
|
|
847
|
+
} else {
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
await logAndProgress(
|
|
853
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} ${serviceName} deployed successfully.`,
|
|
854
|
+
progressCallback
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
// Set public access if skipIamCheck is enabled
|
|
858
|
+
try {
|
|
859
|
+
if (skipIamCheck) {
|
|
860
|
+
await setServicePublicAccess(
|
|
861
|
+
projectId,
|
|
862
|
+
region,
|
|
863
|
+
serviceName,
|
|
864
|
+
accessToken,
|
|
865
|
+
progressCallback
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
} catch (error) {
|
|
869
|
+
await logAndProgress(
|
|
870
|
+
`Failed to set public access for ${type} ${serviceName}: ${error.message}`,
|
|
871
|
+
progressCallback,
|
|
872
|
+
'warn'
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (type === 'service') {
|
|
876
|
+
const deployedService = response.data || response;
|
|
877
|
+
const serviceUrl = deployedService.status?.url;
|
|
878
|
+
|
|
879
|
+
deploymentResults['service'] = serviceName;
|
|
880
|
+
deploymentResults['uri'] = serviceUrl;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
await logAndProgress(
|
|
884
|
+
`Deployment (compose) completed successfully. Deployment results: ${JSON.stringify(deploymentResults)}`,
|
|
885
|
+
progressCallback
|
|
886
|
+
);
|
|
887
|
+
return deploymentResults;
|
|
888
|
+
} catch (error) {
|
|
889
|
+
const errorMsg = `Deployment (compose) failed: ${error.message}`;
|
|
890
|
+
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
891
|
+
throw error;
|
|
892
|
+
} finally {
|
|
893
|
+
if (tempDir) {
|
|
894
|
+
await cleanupTempDirectory(tempDir);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
@@ -15,8 +15,9 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import fs from 'fs';
|
|
18
|
+
import fs, { promises as fsPromises } from 'fs';
|
|
19
19
|
import { DEPLOYMENT_CONFIG, RUNTIMES } from './constants.js';
|
|
20
|
+
import { uploadToStorageBucket } from '../cloud-api/storage.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Checks if the input is a single folder path.
|
|
@@ -61,6 +62,30 @@ function checkIfDockerFileExists(files) {
|
|
|
61
62
|
return false;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Checks if a compose file exists in the provided files array.
|
|
67
|
+
* @param {Array<string|Object>} files - An array of file paths or objects with filename properties.
|
|
68
|
+
* @returns {string|null} - Returns the path/filename of the compose file if found, null otherwise.
|
|
69
|
+
*/
|
|
70
|
+
function checkIfComposeFileExists(files) {
|
|
71
|
+
const composeFiles = [
|
|
72
|
+
'compose.yaml',
|
|
73
|
+
'compose.yml',
|
|
74
|
+
'docker-compose.yaml',
|
|
75
|
+
'docker-compose.yml',
|
|
76
|
+
];
|
|
77
|
+
if (isFolder(files)) {
|
|
78
|
+
for (const name of composeFiles) {
|
|
79
|
+
const fullPath = path.join(files[0], name);
|
|
80
|
+
if (fs.existsSync(fullPath)) {
|
|
81
|
+
return fullPath;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// TODO: we do not handle file content deployment at present as run-compose binary does not support it
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
/**
|
|
65
90
|
* Checks if the provided folder appears to be a Node.js project.
|
|
66
91
|
* It looks for a `package.json` file in the root of the first path in the `files` array.
|
|
@@ -132,6 +157,7 @@ function getDeploymentAttrs(files) {
|
|
|
132
157
|
export function makeFileDeploymentMetadata(files) {
|
|
133
158
|
return {
|
|
134
159
|
hasDockerfile: checkIfDockerFileExists(files),
|
|
160
|
+
composeFilePath: checkIfComposeFileExists(files),
|
|
135
161
|
deploymentAttrs: getDeploymentAttrs(files),
|
|
136
162
|
};
|
|
137
163
|
}
|
|
@@ -185,3 +211,34 @@ export function createDirectSourceDeploymentContainer({
|
|
|
185
211
|
|
|
186
212
|
return container;
|
|
187
213
|
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Uploads a local directory to a GCS bucket recursively.
|
|
217
|
+
* @param {*} bucket - GCS bucket object
|
|
218
|
+
* @param {string} localPath - Local directory path
|
|
219
|
+
* @param {string} gcsPrefix - GCS prefix (blob name starting part)
|
|
220
|
+
* @param {function} progressCallback - Callback for progress
|
|
221
|
+
*/
|
|
222
|
+
export async function uploadDirectory(
|
|
223
|
+
bucket,
|
|
224
|
+
localPath,
|
|
225
|
+
gcsPrefix,
|
|
226
|
+
progressCallback
|
|
227
|
+
) {
|
|
228
|
+
const entries = await fsPromises.readdir(localPath, { withFileTypes: true });
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const fullPath = path.join(localPath, entry.name);
|
|
231
|
+
const destination = `${gcsPrefix}/${entry.name}`;
|
|
232
|
+
if (entry.isDirectory()) {
|
|
233
|
+
await uploadDirectory(bucket, fullPath, destination, progressCallback);
|
|
234
|
+
} else {
|
|
235
|
+
const buffer = await fsPromises.readFile(fullPath);
|
|
236
|
+
await uploadToStorageBucket(
|
|
237
|
+
bucket,
|
|
238
|
+
buffer,
|
|
239
|
+
destination,
|
|
240
|
+
progressCallback
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|