@google-cloud/cloud-run-mcp 1.9.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/gemini-extension.json +12 -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 +11 -0
- package/lib/deployment/deployer.js +334 -30
- package/lib/deployment/helpers.js +99 -49
- package/lib/deployment/source-processor.js +0 -48
- package/lib/deployment/universal-maker.js +147 -0
- 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 +126 -79
- package/test/local/source-processor.test.js +0 -165
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +157 -0
- 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
|
@@ -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.
|
|
@@ -72,6 +97,18 @@ function checkIfNodeJsRuntime(files) {
|
|
|
72
97
|
return fs.existsSync(packageJsonPath);
|
|
73
98
|
}
|
|
74
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Checks if the provided folder appears to be a Python project.
|
|
102
|
+
* It looks for a `requirements.txt` or `pyproject.toml` file in the root of the first path in the `files` array.
|
|
103
|
+
* @param {string[]} files - Array of file paths
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function checkIfPythonRuntime(files) {
|
|
107
|
+
const requirementsTxtPath = path.join(files[0], 'requirements.txt');
|
|
108
|
+
const pyprojectTomlPath = path.join(files[0], 'pyproject.toml');
|
|
109
|
+
return fs.existsSync(requirementsTxtPath) || fs.existsSync(pyprojectTomlPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
/**
|
|
76
113
|
* Returns an empty deployment attributes object.
|
|
77
114
|
* This is used as a default or when attributes cannot be determined.
|
|
@@ -86,55 +123,30 @@ function getEmptyDeploymentAttrs() {
|
|
|
86
123
|
};
|
|
87
124
|
}
|
|
88
125
|
|
|
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
|
/**
|
|
126
127
|
* Determines deployment attributes based on the file list/folder content.
|
|
127
128
|
* @param {Array<string|Object>} files - Array of file paths or file objects.
|
|
128
|
-
* @returns {{runtime: string|undefined,
|
|
129
|
+
* @returns {{runtime: string|undefined, baseImage: string|undefined}} Deployment attributes object.
|
|
129
130
|
*/
|
|
130
131
|
function getDeploymentAttrs(files) {
|
|
132
|
+
let deploymentAttrs = getEmptyDeploymentAttrs();
|
|
131
133
|
// Currently only support detection of NodeJs runtime
|
|
132
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
!isFolder(files) ||
|
|
136
|
+
!(checkIfNodeJsRuntime(files) || checkIfPythonRuntime(files))
|
|
137
|
+
) {
|
|
133
138
|
// TODO: support file list deployment runtime detection
|
|
134
|
-
return
|
|
139
|
+
return deploymentAttrs;
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
|
|
142
|
+
if (checkIfPythonRuntime(files)) {
|
|
143
|
+
deploymentAttrs.runtime = RUNTIMES.PYTHON;
|
|
144
|
+
deploymentAttrs.baseImage = DEPLOYMENT_CONFIG.DEFAULT_PYTHON_BASE_IMAGE;
|
|
145
|
+
} else {
|
|
146
|
+
deploymentAttrs.runtime = RUNTIMES.NODEJS;
|
|
147
|
+
deploymentAttrs.baseImage = DEPLOYMENT_CONFIG.DEFAULT_NODE_BASE_IMAGE;
|
|
148
|
+
}
|
|
149
|
+
return deploymentAttrs;
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
/**
|
|
@@ -145,6 +157,7 @@ function getDeploymentAttrs(files) {
|
|
|
145
157
|
export function makeFileDeploymentMetadata(files) {
|
|
146
158
|
return {
|
|
147
159
|
hasDockerfile: checkIfDockerFileExists(files),
|
|
160
|
+
composeFilePath: checkIfComposeFileExists(files),
|
|
148
161
|
deploymentAttrs: getDeploymentAttrs(files),
|
|
149
162
|
};
|
|
150
163
|
}
|
|
@@ -157,13 +170,7 @@ export function makeFileDeploymentMetadata(files) {
|
|
|
157
170
|
* @returns {boolean} True if zip deployment is feasible.
|
|
158
171
|
*/
|
|
159
172
|
export function canDeployWithoutBuild({ hasDockerfile, deploymentAttrs }) {
|
|
160
|
-
return !!(
|
|
161
|
-
!hasDockerfile &&
|
|
162
|
-
deploymentAttrs &&
|
|
163
|
-
deploymentAttrs.runtime &&
|
|
164
|
-
deploymentAttrs.cmd &&
|
|
165
|
-
deploymentAttrs.args
|
|
166
|
-
);
|
|
173
|
+
return !!(!hasDockerfile && deploymentAttrs && deploymentAttrs.runtime);
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
/**
|
|
@@ -179,7 +186,7 @@ export function createDirectSourceDeploymentContainer({
|
|
|
179
186
|
fileName,
|
|
180
187
|
deploymentAttrs,
|
|
181
188
|
}) {
|
|
182
|
-
|
|
189
|
+
const container = {
|
|
183
190
|
image: DEPLOYMENT_CONFIG.NO_BUILD_IMAGE_TYPE,
|
|
184
191
|
baseImageUri: deploymentAttrs.baseImage,
|
|
185
192
|
sourceCode: {
|
|
@@ -191,4 +198,47 @@ export function createDirectSourceDeploymentContainer({
|
|
|
191
198
|
command: deploymentAttrs.cmd,
|
|
192
199
|
args: deploymentAttrs.args,
|
|
193
200
|
};
|
|
201
|
+
|
|
202
|
+
// Add environment variables if provided
|
|
203
|
+
if (deploymentAttrs.envVars && typeof deploymentAttrs.envVars === 'object') {
|
|
204
|
+
container.env = Object.entries(deploymentAttrs.envVars).map(
|
|
205
|
+
([name, value]) => ({
|
|
206
|
+
name,
|
|
207
|
+
value: String(value),
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return container;
|
|
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
|
+
}
|
|
194
244
|
}
|
|
@@ -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,147 @@
|
|
|
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 { logAndProgress } from '../util/helpers.js';
|
|
23
|
+
import { TEMP_PATHS } from './constants.js';
|
|
24
|
+
import { ensureRepositoryDownloaded } from '../util/artifacts.js';
|
|
25
|
+
|
|
26
|
+
const execAsync = util.promisify(exec);
|
|
27
|
+
|
|
28
|
+
const UNIVERSAL_MAKER_BIN = 'universal_maker';
|
|
29
|
+
const UM_VERSION = '1.0.0';
|
|
30
|
+
|
|
31
|
+
const AR_PROJECT = 'serverless-runtimes';
|
|
32
|
+
const AR_LOCATION = 'us-central1';
|
|
33
|
+
const AR_REPOSITORY = 'universal-maker';
|
|
34
|
+
|
|
35
|
+
const ARCH_MAPPING = {
|
|
36
|
+
linux_x64: 'x86-64',
|
|
37
|
+
darwin_arm64: 'darwin-arm64',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gets the architecture key for the current platform.
|
|
42
|
+
* @returns {string|null} The architecture key or null if not supported.
|
|
43
|
+
*/
|
|
44
|
+
function getUniversalMakerArchitectureKey() {
|
|
45
|
+
const platform = process.platform;
|
|
46
|
+
const arch = process.arch;
|
|
47
|
+
|
|
48
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
49
|
+
return 'linux_x64';
|
|
50
|
+
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
51
|
+
return 'darwin_arm64';
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Runs Universal Maker on the given application directory.
|
|
58
|
+
* @param {string} appDir - Directory containing the application source.
|
|
59
|
+
* @param {function} progressCallback - Progress callback.
|
|
60
|
+
* @param {string} accessToken - Access token for authentication.
|
|
61
|
+
* @returns {Promise<object|null>} The parsed build_output.json or null.
|
|
62
|
+
*/
|
|
63
|
+
export async function runUniversalMaker(appDir, accessToken, progressCallback) {
|
|
64
|
+
const binDir = path.join(
|
|
65
|
+
os.homedir(),
|
|
66
|
+
TEMP_PATHS.BASE,
|
|
67
|
+
TEMP_PATHS.BIN_SUBDIR
|
|
68
|
+
);
|
|
69
|
+
const key = getUniversalMakerArchitectureKey();
|
|
70
|
+
if (!key) {
|
|
71
|
+
await logAndProgress(
|
|
72
|
+
`Universal Maker is not supported on ${process.platform} ${process.arch}.`,
|
|
73
|
+
progressCallback,
|
|
74
|
+
'debug'
|
|
75
|
+
);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const arch = ARCH_MAPPING[key];
|
|
80
|
+
const binPath = path.join(binDir, UNIVERSAL_MAKER_BIN);
|
|
81
|
+
|
|
82
|
+
const binPathResult = await ensureRepositoryDownloaded(
|
|
83
|
+
binPath,
|
|
84
|
+
{
|
|
85
|
+
project: AR_PROJECT,
|
|
86
|
+
location: AR_LOCATION,
|
|
87
|
+
repository: AR_REPOSITORY,
|
|
88
|
+
artifactPath: `${arch}:${UM_VERSION}:${UNIVERSAL_MAKER_BIN}`,
|
|
89
|
+
displayName: 'Universal Maker',
|
|
90
|
+
},
|
|
91
|
+
accessToken,
|
|
92
|
+
progressCallback
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!binPathResult) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const outputDir = path.join(os.tmpdir(), `um-output-${Date.now()}`);
|
|
100
|
+
if (!fs.existsSync(outputDir)) {
|
|
101
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// ./universal_maker -application_dir <source_code_path> -output_dir <output_results_path> -output_format json
|
|
106
|
+
await logAndProgress(
|
|
107
|
+
`Running Universal Maker: ${binPath} -application_dir ${appDir} -output_dir ${outputDir} -output_format json`,
|
|
108
|
+
progressCallback,
|
|
109
|
+
'debug'
|
|
110
|
+
);
|
|
111
|
+
const command = `"${binPath}" -application_dir "${appDir}" -output_dir "${outputDir}" -output_format json`;
|
|
112
|
+
await execAsync(command);
|
|
113
|
+
|
|
114
|
+
const outputPath = path.join(outputDir, 'build_output.json');
|
|
115
|
+
if (fs.existsSync(outputPath)) {
|
|
116
|
+
const output = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
|
|
117
|
+
await logAndProgress(
|
|
118
|
+
'Universal Maker completed successfully.',
|
|
119
|
+
progressCallback
|
|
120
|
+
);
|
|
121
|
+
return output;
|
|
122
|
+
} else {
|
|
123
|
+
await logAndProgress(
|
|
124
|
+
'Universal Maker did not produce build_output.json',
|
|
125
|
+
progressCallback,
|
|
126
|
+
'debug'
|
|
127
|
+
);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await logAndProgress(
|
|
132
|
+
`Universal Maker failed: ${error.message}`,
|
|
133
|
+
progressCallback,
|
|
134
|
+
'debug'
|
|
135
|
+
);
|
|
136
|
+
return null;
|
|
137
|
+
} finally {
|
|
138
|
+
// Cleanup output dir
|
|
139
|
+
try {
|
|
140
|
+
if (fs.existsSync(outputDir)) {
|
|
141
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// ignore cleanup errors
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/lib/util/archive.js
CHANGED
|
@@ -31,7 +31,7 @@ import { logAndProgress } from './helpers.js';
|
|
|
31
31
|
export async function zipFiles(files, useTarGz, progressCallback) {
|
|
32
32
|
const path = await import('path');
|
|
33
33
|
const fs = await import('fs');
|
|
34
|
-
const
|
|
34
|
+
const { TarArchive, ZipArchive } = await import('archiver');
|
|
35
35
|
|
|
36
36
|
return new Promise((resolve, reject) => {
|
|
37
37
|
logAndProgress('Creating in-memory archive...', progressCallback);
|
|
@@ -39,11 +39,11 @@ export async function zipFiles(files, useTarGz, progressCallback) {
|
|
|
39
39
|
|
|
40
40
|
let archive;
|
|
41
41
|
if (useTarGz) {
|
|
42
|
-
archive =
|
|
42
|
+
archive = new TarArchive({
|
|
43
43
|
gzip: { level: 9 },
|
|
44
44
|
});
|
|
45
45
|
} else {
|
|
46
|
-
archive =
|
|
46
|
+
archive = new ZipArchive({
|
|
47
47
|
zlib: { level: 9 },
|
|
48
48
|
});
|
|
49
49
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
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 fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import crypto from 'crypto';
|
|
20
|
+
import { logAndProgress } from './helpers.js';
|
|
21
|
+
import { getArtifactRegistryClient } from '../clients.js';
|
|
22
|
+
|
|
23
|
+
const AR_HOST = 'https://artifactregistry.googleapis.com';
|
|
24
|
+
const AR_DOWNLOAD_PATH_PREFIX = '/download/v1/';
|
|
25
|
+
const AR_DOWNLOAD_URL_SUFFIX = ':download?alt=media';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if the existing binary is up to date by comparing its SHA256 hash with the remote metadata.
|
|
29
|
+
* If the binary is out of date, it removes it.
|
|
30
|
+
* @param {string} binPath - Path to the local binary.
|
|
31
|
+
* @param {object} artifactParams - Parameters for the remote artifact.
|
|
32
|
+
* @param {string} artifactParams.project - Artifact Registry project.
|
|
33
|
+
* @param {string} artifactParams.location - Artifact Registry location.
|
|
34
|
+
* @param {string} artifactParams.repository - Artifact Registry repository.
|
|
35
|
+
* @param {string} artifactParams.artifactPath - Path to the artifact within the repository.
|
|
36
|
+
* @param {string} artifactParams.displayName - A friendly name for logging.
|
|
37
|
+
* @param {string} accessToken - Access token for authentication.
|
|
38
|
+
* @param {function} progressCallback - Progress callback.
|
|
39
|
+
* @returns {Promise<boolean>} True if the binary is up to date, false otherwise.
|
|
40
|
+
*/
|
|
41
|
+
export async function isBinaryUpToDate(
|
|
42
|
+
binPath,
|
|
43
|
+
artifactParams,
|
|
44
|
+
accessToken,
|
|
45
|
+
progressCallback
|
|
46
|
+
) {
|
|
47
|
+
const { project, location, repository, artifactPath, displayName } =
|
|
48
|
+
artifactParams;
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(binPath)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await logAndProgress(
|
|
56
|
+
`Checking for ${displayName} updates...`,
|
|
57
|
+
progressCallback,
|
|
58
|
+
'debug'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const artifactRegistryClient = await getArtifactRegistryClient(
|
|
62
|
+
project,
|
|
63
|
+
accessToken
|
|
64
|
+
);
|
|
65
|
+
const resourceName = artifactRegistryClient.filePath(
|
|
66
|
+
project,
|
|
67
|
+
location,
|
|
68
|
+
repository,
|
|
69
|
+
artifactPath
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const [file] = await artifactRegistryClient.getFile({ name: resourceName });
|
|
73
|
+
const remoteSha256Base64 = file.hashes?.find(
|
|
74
|
+
(h) => h.type === 'SHA256'
|
|
75
|
+
)?.value;
|
|
76
|
+
|
|
77
|
+
if (!remoteSha256Base64) {
|
|
78
|
+
return false; // Cannot verify, download new binary
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const remoteSha256Hex = Buffer.from(remoteSha256Base64, 'base64').toString(
|
|
82
|
+
'hex'
|
|
83
|
+
);
|
|
84
|
+
const localFileContent = fs.readFileSync(binPath);
|
|
85
|
+
const localSha256Hex = crypto
|
|
86
|
+
.createHash('sha256')
|
|
87
|
+
.update(localFileContent)
|
|
88
|
+
.digest('hex');
|
|
89
|
+
|
|
90
|
+
if (remoteSha256Hex === localSha256Hex) {
|
|
91
|
+
await logAndProgress(
|
|
92
|
+
`${displayName} binary is up to date.`,
|
|
93
|
+
progressCallback,
|
|
94
|
+
'debug'
|
|
95
|
+
);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await logAndProgress(
|
|
100
|
+
`${displayName} binary is out of date. Removing existing binary.`,
|
|
101
|
+
progressCallback,
|
|
102
|
+
'debug'
|
|
103
|
+
);
|
|
104
|
+
fs.unlinkSync(binPath);
|
|
105
|
+
return false;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await logAndProgress(
|
|
108
|
+
`Could not check for ${displayName} updates: ${error.message}. Downloading the new binary.`,
|
|
109
|
+
progressCallback,
|
|
110
|
+
'debug'
|
|
111
|
+
);
|
|
112
|
+
return false; // On error, fallback to downloading the new binary
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Downloads a binary from Artifact Registry if it doesn't exist or is out of date.
|
|
118
|
+
* @param {string} binPath - Path where the binary should be stored.
|
|
119
|
+
* @param {object} artifactParams - Parameters for the remote artifact.
|
|
120
|
+
* @param {string} artifactParams.project - Artifact Registry project.
|
|
121
|
+
* @param {string} artifactParams.location - Artifact Registry location.
|
|
122
|
+
* @param {string} artifactParams.repository - Artifact Registry repository.
|
|
123
|
+
* @param {string} artifactParams.artifactPath - Path to the artifact within the repository.
|
|
124
|
+
* @param {string} artifactParams.displayName - A friendly name for logging.
|
|
125
|
+
* @param {string} accessToken - Access token for authentication.
|
|
126
|
+
* @param {function} progressCallback - Progress callback.
|
|
127
|
+
* @returns {Promise<string|null>} Path to the binary or null if it fails.
|
|
128
|
+
*/
|
|
129
|
+
export async function ensureRepositoryDownloaded(
|
|
130
|
+
binPath,
|
|
131
|
+
artifactParams,
|
|
132
|
+
accessToken,
|
|
133
|
+
progressCallback
|
|
134
|
+
) {
|
|
135
|
+
const { project, location, repository, artifactPath, displayName } =
|
|
136
|
+
artifactParams;
|
|
137
|
+
const binDir = path.dirname(binPath);
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
await isBinaryUpToDate(
|
|
141
|
+
binPath,
|
|
142
|
+
artifactParams,
|
|
143
|
+
accessToken,
|
|
144
|
+
progressCallback
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
return binPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await logAndProgress(`Downloading ${displayName}...`, progressCallback);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
if (!fs.existsSync(binDir)) {
|
|
154
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const artifactRegistryClient = await getArtifactRegistryClient(
|
|
158
|
+
project,
|
|
159
|
+
accessToken
|
|
160
|
+
);
|
|
161
|
+
const resourceName = artifactRegistryClient.filePath(
|
|
162
|
+
project,
|
|
163
|
+
location,
|
|
164
|
+
repository,
|
|
165
|
+
artifactPath
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const url = `${AR_HOST}${AR_DOWNLOAD_PATH_PREFIX}${resourceName}${AR_DOWNLOAD_URL_SUFFIX}`;
|
|
169
|
+
const response = await artifactRegistryClient.auth.request({
|
|
170
|
+
url,
|
|
171
|
+
responseType: 'stream',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const dest = fs.createWriteStream(binPath);
|
|
175
|
+
await new Promise((resolve, reject) => {
|
|
176
|
+
response.data.pipe(dest).on('finish', resolve).on('error', reject);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
fs.chmodSync(binPath, '755');
|
|
180
|
+
return binPath;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
await logAndProgress(
|
|
183
|
+
`Failed to download ${displayName}: ${error.message}`,
|
|
184
|
+
progressCallback,
|
|
185
|
+
'debug'
|
|
186
|
+
);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|