@google-cloud/cloud-run-mcp 1.10.0 → 1.12.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 +10 -6
- package/README.md +26 -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 +142 -12
- 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/middleware/oauth.js +26 -15
- 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 +83 -20
- package/package.json +7 -4
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +307 -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/mcp-server-streamable-http.test.js +132 -12
- package/test/local/middleware/oauth.test.js +106 -5
- 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/test-helpers.js +5 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +98 -78
- /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
|
@@ -19,22 +19,18 @@ import fs from 'fs';
|
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import os from 'os';
|
|
21
21
|
import util from 'util';
|
|
22
|
-
import crypto from 'crypto';
|
|
23
22
|
import { logAndProgress } from '../util/helpers.js';
|
|
24
23
|
import { TEMP_PATHS } from './constants.js';
|
|
25
|
-
import {
|
|
24
|
+
import { ensureRepositoryDownloaded } from '../util/artifacts.js';
|
|
26
25
|
|
|
27
26
|
const execAsync = util.promisify(exec);
|
|
28
27
|
|
|
29
28
|
const UNIVERSAL_MAKER_BIN = 'universal_maker';
|
|
30
29
|
const UM_VERSION = '1.0.0';
|
|
31
30
|
|
|
32
|
-
const AR_HOST = 'https://artifactregistry.googleapis.com';
|
|
33
31
|
const AR_PROJECT = 'serverless-runtimes';
|
|
34
32
|
const AR_LOCATION = 'us-central1';
|
|
35
33
|
const AR_REPOSITORY = 'universal-maker';
|
|
36
|
-
const AR_DOWNLOAD_PATH_PREFIX = '/download/v1/';
|
|
37
|
-
const AR_DOWNLOAD_URL_SUFFIX = ':download?alt=media';
|
|
38
34
|
|
|
39
35
|
const ARCH_MAPPING = {
|
|
40
36
|
linux_x64: 'x86-64',
|
|
@@ -58,91 +54,19 @@ function getUniversalMakerArchitectureKey() {
|
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param {string} binPath - Path to the local binary.
|
|
64
|
-
* @param {string} arch - Current architecture.
|
|
65
|
-
* @param {string} accessToken - Access token for authentication.
|
|
57
|
+
* Runs Universal Maker on the given application directory.
|
|
58
|
+
* @param {string} appDir - Directory containing the application source.
|
|
66
59
|
* @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
60
|
* @param {string} accessToken - Access token for authentication.
|
|
140
|
-
* @
|
|
141
|
-
* @returns {Promise<string|null>} Path to the binary or null if not supported.
|
|
61
|
+
* @returns {Promise<object|null>} The parsed build_output.json or null.
|
|
142
62
|
*/
|
|
143
|
-
async function
|
|
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
|
+
);
|
|
144
69
|
const key = getUniversalMakerArchitectureKey();
|
|
145
|
-
|
|
146
70
|
if (!key) {
|
|
147
71
|
await logAndProgress(
|
|
148
72
|
`Universal Maker is not supported on ${process.platform} ${process.arch}.`,
|
|
@@ -152,74 +76,23 @@ async function ensureUniversalMaker(binDir, accessToken, progressCallback) {
|
|
|
152
76
|
return null;
|
|
153
77
|
}
|
|
154
78
|
|
|
155
|
-
const binPath = path.join(binDir, UNIVERSAL_MAKER_BIN);
|
|
156
79
|
const arch = ARCH_MAPPING[key];
|
|
80
|
+
const binPath = path.join(binDir, UNIVERSAL_MAKER_BIN);
|
|
157
81
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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,
|
|
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
|
+
},
|
|
218
91
|
accessToken,
|
|
219
92
|
progressCallback
|
|
220
93
|
);
|
|
221
94
|
|
|
222
|
-
if (!
|
|
95
|
+
if (!binPathResult) {
|
|
223
96
|
return null;
|
|
224
97
|
}
|
|
225
98
|
|
package/lib/middleware/oauth.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { getOAuthClient } from '../clients.js';
|
|
1
|
+
import { clients, getOAuthClient } from '../clients.js';
|
|
2
2
|
import { extractAccessToken } from '../util/helpers.js';
|
|
3
3
|
|
|
4
|
-
const TOOLS_CALL_METHOD = 'tools/call';
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* Verifies the validity of an access token and checks the audience.
|
|
8
6
|
* @param {string} accessToken - The access token to verify.
|
|
@@ -16,13 +14,14 @@ async function verifyAccessToken(accessToken, audience) {
|
|
|
16
14
|
const tokenInfo = await client.getTokenInfo(accessToken);
|
|
17
15
|
|
|
18
16
|
//An expired token will not have audience
|
|
19
|
-
if (audience
|
|
20
|
-
throw new Error(
|
|
17
|
+
if (!audience || tokenInfo.aud !== audience) {
|
|
18
|
+
throw new Error('Invalid token audience');
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
console.log('Access token verified successfully.');
|
|
24
22
|
return tokenInfo;
|
|
25
23
|
} catch (error) {
|
|
24
|
+
clients.oauth.delete(accessToken);
|
|
26
25
|
console.error('Error verifying access token:', error);
|
|
27
26
|
throw error;
|
|
28
27
|
}
|
|
@@ -43,11 +42,19 @@ async function ensureOAuthTokenInEnv(req, res) {
|
|
|
43
42
|
console.log('No authorization header found in request');
|
|
44
43
|
throw new Error('No authorization header');
|
|
45
44
|
}
|
|
45
|
+
const accessToken = extractAccessToken(req.headers.authorization);
|
|
46
|
+
if (!accessToken) {
|
|
47
|
+
console.log('No access token found in authorization header');
|
|
48
|
+
throw new Error('No access token found in authorization header');
|
|
49
|
+
}
|
|
50
|
+
if (!audience) {
|
|
51
|
+
console.log('GOOGLE_OAUTH_AUDIENCE environment variable is missing');
|
|
52
|
+
throw new Error(
|
|
53
|
+
'GOOGLE_OAUTH_AUDIENCE environment variable is required when OAuth is enabled.'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
46
56
|
console.log('Authorization header found.. Verifying token');
|
|
47
|
-
await verifyAccessToken(
|
|
48
|
-
extractAccessToken(req.headers.authorization),
|
|
49
|
-
audience
|
|
50
|
-
);
|
|
57
|
+
await verifyAccessToken(accessToken, audience);
|
|
51
58
|
console.log('Token verified');
|
|
52
59
|
} catch (error) {
|
|
53
60
|
console.error('Authentication failed: ', error);
|
|
@@ -64,11 +71,8 @@ async function ensureOAuthTokenInEnv(req, res) {
|
|
|
64
71
|
* @param {import('express').NextFunction} next
|
|
65
72
|
*/
|
|
66
73
|
export const oauthMiddleware = async (req, res, next) => {
|
|
67
|
-
//If OAUTH_ENABLED is not true
|
|
68
|
-
if (
|
|
69
|
-
process.env.OAUTH_ENABLED !== 'true' ||
|
|
70
|
-
req.body.method !== TOOLS_CALL_METHOD
|
|
71
|
-
) {
|
|
74
|
+
//If OAUTH_ENABLED is not true, skip the middleware
|
|
75
|
+
if (process.env.OAUTH_ENABLED !== 'true') {
|
|
72
76
|
return next();
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -79,12 +83,19 @@ export const oauthMiddleware = async (req, res, next) => {
|
|
|
79
83
|
console.error('OAuth Middleware Error:', error);
|
|
80
84
|
// ensureOAuthTokenInEnv throws an error if auth fails.
|
|
81
85
|
// We catch it and send a 401 response.
|
|
86
|
+
if (process.env.OAUTH_PROTECTED_RESOURCE) {
|
|
87
|
+
const metadataUrl = new URL(
|
|
88
|
+
'/.well-known/oauth-protected-resource',
|
|
89
|
+
process.env.OAUTH_PROTECTED_RESOURCE
|
|
90
|
+
).href;
|
|
91
|
+
res.set('WWW-Authenticate', `Bearer resource_metadata="${metadataUrl}"`);
|
|
92
|
+
}
|
|
82
93
|
res.status(401).json({
|
|
83
94
|
jsonrpc: '2.0',
|
|
84
95
|
error: {
|
|
85
96
|
code: -32001, // Custom auth error code or standard -32000
|
|
86
97
|
message: 'Authentication failed',
|
|
87
|
-
data:
|
|
98
|
+
data: 'Invalid or missing access token',
|
|
88
99
|
},
|
|
89
100
|
id: null,
|
|
90
101
|
});
|
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
|
+
}
|
package/lib/util/helpers.js
CHANGED
|
@@ -14,6 +14,11 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { getProjectsClient } from '../clients.js';
|
|
18
|
+
import crypto from 'node:crypto';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
17
22
|
/**
|
|
18
23
|
* Helper function to log a message and call the progress callback.
|
|
19
24
|
* @param {string} message - The message to log.
|
|
@@ -52,3 +57,107 @@ export function extractAccessToken(authorizationHeader) {
|
|
|
52
57
|
}
|
|
53
58
|
return authorizationHeader.split(' ')[1];
|
|
54
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Gets the project number for a given project ID.
|
|
63
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
64
|
+
* @param {string} accessToken - Access token for authentication.
|
|
65
|
+
* @returns {Promise<string>} The project number.
|
|
66
|
+
*/
|
|
67
|
+
export async function getProjectNumber(projectId, accessToken) {
|
|
68
|
+
const projectsClient = await getProjectsClient(accessToken);
|
|
69
|
+
const [project] = await projectsClient.getProject({
|
|
70
|
+
name: `projects/${projectId}`,
|
|
71
|
+
});
|
|
72
|
+
// project.name is in the format "projects/123456"
|
|
73
|
+
const parts = project.name.split('/');
|
|
74
|
+
return parts.length > 1 ? parts[1] : project.projectNumber;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Calculates a SHA256 fingerprint of a directory.
|
|
79
|
+
* Matches the logic in gcloud run compose builder.py.
|
|
80
|
+
* @param {string} dirPath - The directory to fingerprint.
|
|
81
|
+
* @returns {Promise<string>} The SHA256 hex digest.
|
|
82
|
+
*/
|
|
83
|
+
export async function calculateSourceFingerprint(dirPath) {
|
|
84
|
+
const hash = crypto.createHash('sha256');
|
|
85
|
+
|
|
86
|
+
// Helper for recursive walk matching Python's deterministic order
|
|
87
|
+
async function walk(currentDir) {
|
|
88
|
+
const entries = await fs.promises.readdir(currentDir, {
|
|
89
|
+
withFileTypes: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Sort entries to match deterministic order (dirs and files sorted)
|
|
93
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
94
|
+
|
|
95
|
+
// Handle files first (matching filenames.sort() in builder.py's walk)
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (entry.isFile()) {
|
|
98
|
+
const filePath = path.join(currentDir, entry.name);
|
|
99
|
+
const relPath = path.relative(dirPath, filePath);
|
|
100
|
+
|
|
101
|
+
// Update hash with relative path
|
|
102
|
+
hash.update(relPath);
|
|
103
|
+
|
|
104
|
+
// Update hash with file content
|
|
105
|
+
const content = await fs.promises.readFile(filePath);
|
|
106
|
+
hash.update(content);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Then recurse into directories
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
await walk(path.join(currentDir, entry.name));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await walk(dirPath);
|
|
119
|
+
return hash.digest('hex');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sanitizes a string for use as a Cloud Run service name.
|
|
124
|
+
* Only lowercase, digits, and hyphens are allowed.
|
|
125
|
+
* Must begin with a letter and cannot end with a hyphen.
|
|
126
|
+
* @param {string} name - The name to sanitize.
|
|
127
|
+
* @returns {string} The sanitized name.
|
|
128
|
+
*/
|
|
129
|
+
export function sanitizeCloudRunServiceName(name) {
|
|
130
|
+
let sanitized = name
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphen
|
|
133
|
+
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
134
|
+
|
|
135
|
+
// Ensure it starts with a letter if possible
|
|
136
|
+
if (sanitized.length > 0 && !/^[a-z]/.test(sanitized)) {
|
|
137
|
+
sanitized = 's-' + sanitized;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Cap at 49 characters (Cloud Run limit is usually 50-63 depending on API, 49 is safe)
|
|
141
|
+
return sanitized.slice(0, 49).replace(/-$/, '');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sanitizes a string to be a valid GCP label value.
|
|
146
|
+
* Label values must be:
|
|
147
|
+
* - <= 63 characters.
|
|
148
|
+
* - Contain only lowercase letters, numbers, underscores, and dashes.
|
|
149
|
+
* @param {string} value - The string to sanitize.
|
|
150
|
+
* @returns {string} A valid GCP label value string.
|
|
151
|
+
*/
|
|
152
|
+
export function sanitizeLabelValue(value) {
|
|
153
|
+
if (value === undefined || value === null) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
const stringValue = String(value);
|
|
157
|
+
if (stringValue === '') {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
let sanitized = stringValue.toLowerCase();
|
|
161
|
+
sanitized = sanitized.replace(/[^a-z0-9_-]/g, '-');
|
|
162
|
+
return sanitized.slice(0, 63);
|
|
163
|
+
}
|