@google-cloud/cloud-run-mcp 1.5.0 → 1.6.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/.c8rc.json +9 -0
- package/.github/workflows/lint-checker.yml +17 -0
- package/.kokoro/kokoro_build.sh +22 -0
- package/.kokoro/presubmit.cfg +21 -0
- package/.kokoro/run_tests.sh +31 -0
- package/CONTRIBUTING.md +23 -8
- package/README.md +52 -11
- package/REVIEWING.md +20 -0
- package/gemini-extension/GEMINI.md +1 -1
- package/lib/cloud-api/build.js +200 -0
- package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +12 -8
- package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
- package/lib/cloud-api/registry.js +98 -0
- package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
- package/lib/cloud-api/storage.js +123 -0
- package/lib/deployment/deployer.js +474 -0
- package/lib/util/archive.js +95 -0
- package/lib/util/helpers.js +42 -0
- package/mcp-server.js +21 -10
- package/package.json +7 -4
- package/test/local/cloud-api/build.test.js +436 -0
- package/test/local/cloud-api/projects.test.js +18 -0
- package/test/local/mcp-server-streamable-http.test.js +57 -0
- package/test/local/notifications.test.js +9 -6
- package/test/local/tools.test.js +114 -70
- package/test/need-gcp/cloud-run-services.test.js +44 -44
- package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
- package/test/need-gcp/gcp-auth-check.test.js +1 -1
- package/test/need-gcp/gcp-projects.test.js +3 -15
- package/test/need-gcp/test-helpers.js +175 -0
- package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
- package/tools/register-tools.js +106 -97
- package/tools/tools.js +2 -9
- package/lib/cloud-run-deploy.js +0 -937
- /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
- /package/lib/{gcp-billing-accounts.js → cloud-api/billing.js} +0 -0
- /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
package/lib/cloud-run-deploy.js
DELETED
|
@@ -1,937 +0,0 @@
|
|
|
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 { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
|
|
18
|
-
|
|
19
|
-
// Configuration
|
|
20
|
-
const REPO_NAME = 'mcp-cloud-run-deployments';
|
|
21
|
-
const ZIP_FILE_NAME = 'source.zip';
|
|
22
|
-
const IMAGE_TAG = 'latest';
|
|
23
|
-
|
|
24
|
-
// APIs required for deploying from source code.
|
|
25
|
-
const REQUIRED_APIS_FOR_SOURCE_DEPLOY = [
|
|
26
|
-
'iam.googleapis.com',
|
|
27
|
-
'storage.googleapis.com',
|
|
28
|
-
'cloudbuild.googleapis.com',
|
|
29
|
-
'artifactregistry.googleapis.com',
|
|
30
|
-
'run.googleapis.com',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// APIs required for deploying a container image.
|
|
34
|
-
const REQUIRED_APIS_FOR_IMAGE_DEPLOY = ['run.googleapis.com'];
|
|
35
|
-
|
|
36
|
-
const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
|
|
37
|
-
const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
|
|
38
|
-
|
|
39
|
-
// Initialize Clients
|
|
40
|
-
let storage;
|
|
41
|
-
let cloudBuildClient;
|
|
42
|
-
let artifactRegistryClient;
|
|
43
|
-
let runClient;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Helper function to log a message and call the progress callback.
|
|
47
|
-
* @param {string} message - The message to log.
|
|
48
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
49
|
-
* @param {'debug' | 'info' | 'warn' | 'error'} [severity='info'] - The severity level of the message.
|
|
50
|
-
*/
|
|
51
|
-
async function logAndProgress(message, progressCallback, severity = 'info') {
|
|
52
|
-
switch (severity) {
|
|
53
|
-
case 'error':
|
|
54
|
-
console.error(message);
|
|
55
|
-
break;
|
|
56
|
-
case 'warn':
|
|
57
|
-
case 'info':
|
|
58
|
-
case 'debug':
|
|
59
|
-
default:
|
|
60
|
-
console.log(message);
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
if (progressCallback) {
|
|
64
|
-
progressCallback({ level: severity, data: message });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Checks if a Cloud Run service already exists.
|
|
70
|
-
*
|
|
71
|
-
* @async
|
|
72
|
-
* @param {string} projectId - The Google Cloud project ID.
|
|
73
|
-
* @param {string} location - The Google Cloud region where the service is located.
|
|
74
|
-
* @param {string} serviceId - The ID of the Cloud Run service.
|
|
75
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
76
|
-
* @returns {Promise<boolean>} A promise that resolves to true if the service exists, false otherwise.
|
|
77
|
-
* @throws {Error} If there's an error checking the service (other than not found).
|
|
78
|
-
*/
|
|
79
|
-
async function checkCloudRunServiceExists(
|
|
80
|
-
projectId,
|
|
81
|
-
location,
|
|
82
|
-
serviceId,
|
|
83
|
-
progressCallback
|
|
84
|
-
) {
|
|
85
|
-
const parent = runClient.locationPath(projectId, location);
|
|
86
|
-
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
87
|
-
try {
|
|
88
|
-
await callWithRetry(
|
|
89
|
-
() => runClient.getService({ name: servicePath }),
|
|
90
|
-
`getService ${serviceId}`
|
|
91
|
-
);
|
|
92
|
-
await logAndProgress(
|
|
93
|
-
`Cloud Run service ${serviceId} already exists.`,
|
|
94
|
-
progressCallback
|
|
95
|
-
);
|
|
96
|
-
return true;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (error.code === 5) {
|
|
99
|
-
await logAndProgress(
|
|
100
|
-
`Cloud Run service ${serviceId} does not exist.`,
|
|
101
|
-
progressCallback
|
|
102
|
-
);
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const errorMessage = `Error checking Cloud Run service ${serviceId}: ${error.message}`;
|
|
106
|
-
console.error(`Error checking Cloud Run service ${serviceId}:`, error);
|
|
107
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Deploys or updates a Cloud Run service with the specified container image.
|
|
114
|
-
* If the service exists, it's updated; otherwise, a new service is created.
|
|
115
|
-
* The service is configured to be publicly accessible.
|
|
116
|
-
*
|
|
117
|
-
* @async
|
|
118
|
-
* @param {string} projectId - The Google Cloud project ID.
|
|
119
|
-
* @param {string} location - The Google Cloud region for the deployment.
|
|
120
|
-
* @param {string} serviceId - The ID for the Cloud Run service.
|
|
121
|
-
* @param {string} imgUrl - The URL of the container image to deploy.
|
|
122
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
123
|
-
* @returns {Promise<object>} A promise that resolves with the Cloud Run service object upon successful deployment or update.
|
|
124
|
-
* @throws {Error} If the deployment or update process fails.
|
|
125
|
-
*/
|
|
126
|
-
async function deployToCloudRun(
|
|
127
|
-
projectId,
|
|
128
|
-
location,
|
|
129
|
-
serviceId,
|
|
130
|
-
imgUrl,
|
|
131
|
-
progressCallback,
|
|
132
|
-
skipIamCheck
|
|
133
|
-
) {
|
|
134
|
-
const parent = runClient.locationPath(projectId, location);
|
|
135
|
-
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
136
|
-
const revisionName = `${serviceId}-${Date.now()}`; // Generate a unique revision name
|
|
137
|
-
|
|
138
|
-
const service = {
|
|
139
|
-
template: {
|
|
140
|
-
revision: revisionName,
|
|
141
|
-
containers: [{ image: imgUrl }],
|
|
142
|
-
},
|
|
143
|
-
labels: {
|
|
144
|
-
'created-by': 'cloud-run-mcp',
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Conditionally set invokerIamDisabled based on the skipIamCheck flag
|
|
149
|
-
if (skipIamCheck) {
|
|
150
|
-
service.invokerIamDisabled = true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const exists = await checkCloudRunServiceExists(
|
|
155
|
-
projectId,
|
|
156
|
-
location,
|
|
157
|
-
serviceId,
|
|
158
|
-
progressCallback
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Always perform a dry run first for general validation
|
|
162
|
-
try {
|
|
163
|
-
await logAndProgress(
|
|
164
|
-
`Performing dry run for service ${serviceId}...`,
|
|
165
|
-
progressCallback,
|
|
166
|
-
'debug'
|
|
167
|
-
);
|
|
168
|
-
const dryRunServiceConfig = JSON.parse(JSON.stringify(service)); // Deep copy for dry run
|
|
169
|
-
|
|
170
|
-
if (exists) {
|
|
171
|
-
dryRunServiceConfig.name = servicePath;
|
|
172
|
-
await callWithRetry(
|
|
173
|
-
() =>
|
|
174
|
-
runClient.updateService({
|
|
175
|
-
service: dryRunServiceConfig,
|
|
176
|
-
validateOnly: true,
|
|
177
|
-
}),
|
|
178
|
-
`updateService (dry run) ${serviceId}`
|
|
179
|
-
);
|
|
180
|
-
} else {
|
|
181
|
-
await callWithRetry(
|
|
182
|
-
() =>
|
|
183
|
-
runClient.createService({
|
|
184
|
-
parent: parent,
|
|
185
|
-
service: dryRunServiceConfig,
|
|
186
|
-
serviceId: serviceId,
|
|
187
|
-
validateOnly: true,
|
|
188
|
-
}),
|
|
189
|
-
`createService (dry run) ${serviceId}`
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
await logAndProgress(
|
|
193
|
-
`Dry run successful for ${serviceId} with current configuration.`,
|
|
194
|
-
progressCallback,
|
|
195
|
-
'debug'
|
|
196
|
-
);
|
|
197
|
-
} catch (dryRunError) {
|
|
198
|
-
await logAndProgress(
|
|
199
|
-
`Dry run for ${serviceId} failed: ${dryRunError.message}`,
|
|
200
|
-
progressCallback,
|
|
201
|
-
'warn'
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
// Check if the error is related to invokerIamDisabled (this is a heuristic)
|
|
205
|
-
if (
|
|
206
|
-
skipIamCheck &&
|
|
207
|
-
dryRunError.message &&
|
|
208
|
-
(dryRunError.message.toLowerCase().includes('invokeriamdisabled') ||
|
|
209
|
-
dryRunError.message.toLowerCase().includes('iam policy violation') ||
|
|
210
|
-
dryRunError.code === 3) /* INVALID_ARGUMENT */
|
|
211
|
-
) {
|
|
212
|
-
await logAndProgress(
|
|
213
|
-
`Dry run suggests 'invokerIamDisabled' is not allowed or invalid. Attempting deployment without it.`,
|
|
214
|
-
progressCallback,
|
|
215
|
-
'warn'
|
|
216
|
-
);
|
|
217
|
-
delete service.invokerIamDisabled; // Modify the main service object for actual deployment
|
|
218
|
-
} else {
|
|
219
|
-
// For any other validation errors, rethrow to stop the deployment
|
|
220
|
-
const errorMessage = `Dry run validation failed for service ${serviceId}: ${dryRunError.message}`;
|
|
221
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
222
|
-
throw new Error(errorMessage);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let operation;
|
|
227
|
-
if (exists) {
|
|
228
|
-
await logAndProgress(
|
|
229
|
-
`Updating existing service ${serviceId}...`,
|
|
230
|
-
progressCallback
|
|
231
|
-
);
|
|
232
|
-
service.name = servicePath;
|
|
233
|
-
[operation] = await callWithRetry(
|
|
234
|
-
() => runClient.updateService({ service }),
|
|
235
|
-
`updateService ${serviceId}`
|
|
236
|
-
);
|
|
237
|
-
} else {
|
|
238
|
-
await logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
|
|
239
|
-
[operation] = await callWithRetry(
|
|
240
|
-
() =>
|
|
241
|
-
runClient.createService({
|
|
242
|
-
parent: parent,
|
|
243
|
-
service: service, // 'service' object might have invokerIamDisabled removed
|
|
244
|
-
serviceId: serviceId,
|
|
245
|
-
}),
|
|
246
|
-
`createService ${serviceId}`
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
await logAndProgress(
|
|
251
|
-
`Deploying ${serviceId} to Cloud Run...`,
|
|
252
|
-
progressCallback
|
|
253
|
-
);
|
|
254
|
-
const [response] = await operation.promise();
|
|
255
|
-
|
|
256
|
-
await logAndProgress(
|
|
257
|
-
`Service deployed/updated successfully: ${response.uri}`,
|
|
258
|
-
progressCallback
|
|
259
|
-
);
|
|
260
|
-
return response;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
const errorMessage = `Error deploying/updating service ${serviceId}: ${error.message}`;
|
|
263
|
-
console.error(`Error deploying/updating service ${serviceId}:`, error);
|
|
264
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Ensures that a Google Cloud Storage bucket exists.
|
|
271
|
-
* If the bucket does not exist, it attempts to create it in the specified location.
|
|
272
|
-
*
|
|
273
|
-
* @async
|
|
274
|
-
* @param {string} bucketName - The name of the storage bucket.
|
|
275
|
-
* @param {string} [location='us'] - The location to create the bucket in if it doesn't exist. Defaults to 'us'.
|
|
276
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
277
|
-
* @returns {Promise<import('@google-cloud/storage').Bucket>} A promise that resolves with the GCS Bucket object.
|
|
278
|
-
* @throws {Error} If there's an error checking or creating the bucket.
|
|
279
|
-
*/
|
|
280
|
-
async function ensureStorageBucketExists(
|
|
281
|
-
bucketName,
|
|
282
|
-
location = 'us',
|
|
283
|
-
progressCallback
|
|
284
|
-
) {
|
|
285
|
-
const bucket = storage.bucket(bucketName);
|
|
286
|
-
try {
|
|
287
|
-
const [exists] = await callWithRetry(
|
|
288
|
-
() => bucket.exists(),
|
|
289
|
-
`storage.bucket.exists ${bucketName}`
|
|
290
|
-
);
|
|
291
|
-
if (exists) {
|
|
292
|
-
await logAndProgress(
|
|
293
|
-
`Bucket ${bucketName} already exists.`,
|
|
294
|
-
progressCallback
|
|
295
|
-
);
|
|
296
|
-
return bucket;
|
|
297
|
-
} else {
|
|
298
|
-
await logAndProgress(
|
|
299
|
-
`Bucket ${bucketName} does not exist. Creating in location ${location}...`,
|
|
300
|
-
progressCallback
|
|
301
|
-
);
|
|
302
|
-
try {
|
|
303
|
-
const [createdBucket] = await callWithRetry(
|
|
304
|
-
() => storage.createBucket(bucketName, { location: location }),
|
|
305
|
-
`storage.createBucket ${bucketName}`
|
|
306
|
-
);
|
|
307
|
-
await logAndProgress(
|
|
308
|
-
`Storage bucket ${createdBucket.name} created successfully in ${location}.`,
|
|
309
|
-
progressCallback
|
|
310
|
-
);
|
|
311
|
-
return createdBucket;
|
|
312
|
-
} catch (createError) {
|
|
313
|
-
const errorMessage = `Failed to create storage bucket ${bucketName}. Error details: ${createError.message}`;
|
|
314
|
-
console.error(
|
|
315
|
-
`Failed to create storage bucket ${bucketName}. Error details:`,
|
|
316
|
-
createError
|
|
317
|
-
);
|
|
318
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
319
|
-
throw createError;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
} catch (error) {
|
|
323
|
-
const errorMessage = `Error checking/creating bucket ${bucketName}: ${error.message}`;
|
|
324
|
-
console.error(`Error checking/creating bucket ${bucketName}:`, error);
|
|
325
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Creates a zip archive in memory from a list of file paths and/or file objects.
|
|
332
|
-
* File objects should have `filename` (string) and `content` (Buffer or string) properties.
|
|
333
|
-
*
|
|
334
|
-
* @param {Array<string|{filename: string, content: Buffer|string}>} files - An array of items to zip.
|
|
335
|
-
* Each item can be a string representing a file/directory path, or an object
|
|
336
|
-
* with `filename` and `content` properties for in-memory files.
|
|
337
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
338
|
-
* @returns {Promise<Buffer>} A promise that resolves with a Buffer containing the zip data.
|
|
339
|
-
* @throws {Error} If an input file path is not found, an input item has an invalid format, or an archiver error occurs.
|
|
340
|
-
*/
|
|
341
|
-
async function zipFiles(files, progressCallback) {
|
|
342
|
-
const path = await import('path');
|
|
343
|
-
const fs = await import('fs');
|
|
344
|
-
const archiver = (await import('archiver')).default;
|
|
345
|
-
|
|
346
|
-
return new Promise((resolve, reject) => {
|
|
347
|
-
logAndProgress('Creating in-memory zip archive...', progressCallback);
|
|
348
|
-
const chunks = [];
|
|
349
|
-
const archive = archiver('zip', {
|
|
350
|
-
zlib: { level: 9 },
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
archive.on('data', (chunk) => chunks.push(chunk));
|
|
354
|
-
archive.on('end', () => {
|
|
355
|
-
logAndProgress(
|
|
356
|
-
`Files zipped successfully. Total size: ${archive.pointer()} bytes`,
|
|
357
|
-
progressCallback
|
|
358
|
-
);
|
|
359
|
-
resolve(Buffer.concat(chunks));
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
archive.on('warning', (err) => {
|
|
363
|
-
const warningMessage = `Archiver warning: ${err}`;
|
|
364
|
-
logAndProgress(warningMessage, progressCallback, 'warn');
|
|
365
|
-
if (err.code !== 'ENOENT') {
|
|
366
|
-
// ENOENT is often just a warning, others might be more critical for zip
|
|
367
|
-
reject(err);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
archive.on('error', (err) => {
|
|
372
|
-
const errorMessage = `Archiver error: ${err.message}`;
|
|
373
|
-
console.error(errorMessage, err);
|
|
374
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
375
|
-
reject(err);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
files.forEach((file) => {
|
|
379
|
-
if (typeof file === 'object' && 'filename' in file && 'content' in file) {
|
|
380
|
-
archive.append(file.content, { name: file.filename });
|
|
381
|
-
} else if (typeof file === 'string') {
|
|
382
|
-
let pathInput = file;
|
|
383
|
-
|
|
384
|
-
// This is a "hack" to better support WSL on Windows. AI agents tend to send path that start with '/c' in that case. Re-write it to '/mnt/c'
|
|
385
|
-
if (pathInput.startsWith('/c')) {
|
|
386
|
-
pathInput = `/mnt${pathInput}`;
|
|
387
|
-
}
|
|
388
|
-
const filePath = path.resolve(pathInput);
|
|
389
|
-
if (!fs.existsSync(filePath)) {
|
|
390
|
-
throw new Error(`File or directory not found: ${filePath}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const stats = fs.statSync(filePath);
|
|
394
|
-
if (stats.isDirectory()) {
|
|
395
|
-
archive.directory(filePath, false);
|
|
396
|
-
} else {
|
|
397
|
-
archive.file(filePath, { name: path.basename(filePath) });
|
|
398
|
-
}
|
|
399
|
-
} else {
|
|
400
|
-
throw new Error(`Invalid file format: ${JSON.stringify(file)}`);
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
archive.finalize();
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Uploads a buffer to a specified Google Cloud Storage bucket and blob name.
|
|
410
|
-
*
|
|
411
|
-
* @async
|
|
412
|
-
* @param {import('@google-cloud/storage').Bucket} bucket - The Google Cloud Storage bucket object.
|
|
413
|
-
* @param {Buffer} buffer - The buffer containing the data to upload.
|
|
414
|
-
* @param {string} destinationBlobName - The name for the blob in the bucket.
|
|
415
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
416
|
-
* @returns {Promise<import('@google-cloud/storage').File>} A promise that resolves with the GCS File object representing the uploaded blob.
|
|
417
|
-
* @throws {Error} If the upload fails.
|
|
418
|
-
*/
|
|
419
|
-
async function uploadToStorageBucket(
|
|
420
|
-
bucket,
|
|
421
|
-
buffer,
|
|
422
|
-
destinationBlobName,
|
|
423
|
-
progressCallback
|
|
424
|
-
) {
|
|
425
|
-
try {
|
|
426
|
-
await logAndProgress(
|
|
427
|
-
`Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
|
|
428
|
-
progressCallback
|
|
429
|
-
);
|
|
430
|
-
await callWithRetry(
|
|
431
|
-
() => bucket.file(destinationBlobName).save(buffer),
|
|
432
|
-
`storage.bucket.file.save ${destinationBlobName}`
|
|
433
|
-
);
|
|
434
|
-
await logAndProgress(
|
|
435
|
-
`File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
|
|
436
|
-
progressCallback
|
|
437
|
-
);
|
|
438
|
-
return bucket.file(destinationBlobName);
|
|
439
|
-
} catch (error) {
|
|
440
|
-
const errorMessage = `Error uploading buffer: ${error.message}`;
|
|
441
|
-
console.error(`Error uploading buffer:`, error);
|
|
442
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
443
|
-
throw error;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Ensures that an Artifact Registry repository exists.
|
|
449
|
-
* If the repository does not exist, it attempts to create it with the specified format.
|
|
450
|
-
*
|
|
451
|
-
* @async
|
|
452
|
-
* @param {string} projectId - The Google Cloud project ID.
|
|
453
|
-
* @param {string} location - The Google Cloud region for the repository.
|
|
454
|
-
* @param {string} repositoryId - The ID for the Artifact Registry repository.
|
|
455
|
-
* @param {string} [format='DOCKER'] - The format of the repository (e.g., 'DOCKER', 'NPM'). Defaults to 'DOCKER'.
|
|
456
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
457
|
-
* @returns {Promise<object>} A promise that resolves with the Artifact Registry repository object.
|
|
458
|
-
* @throws {Error} If there's an error checking or creating the repository.
|
|
459
|
-
*/
|
|
460
|
-
async function ensureArtifactRegistryRepoExists(
|
|
461
|
-
projectId,
|
|
462
|
-
location,
|
|
463
|
-
repositoryId,
|
|
464
|
-
format = 'DOCKER',
|
|
465
|
-
progressCallback
|
|
466
|
-
) {
|
|
467
|
-
const parent = `projects/${projectId}/locations/${location}`;
|
|
468
|
-
const repoPath = artifactRegistryClient.repositoryPath(
|
|
469
|
-
projectId,
|
|
470
|
-
location,
|
|
471
|
-
repositoryId
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
const [repository] = await callWithRetry(
|
|
476
|
-
() => artifactRegistryClient.getRepository({ name: repoPath }),
|
|
477
|
-
`artifactRegistry.getRepository ${repositoryId}`
|
|
478
|
-
);
|
|
479
|
-
await logAndProgress(
|
|
480
|
-
`Repository ${repositoryId} already exists in ${location}.`,
|
|
481
|
-
progressCallback
|
|
482
|
-
);
|
|
483
|
-
return repository;
|
|
484
|
-
} catch (error) {
|
|
485
|
-
if (error.code === 5) {
|
|
486
|
-
await logAndProgress(
|
|
487
|
-
`Repository ${repositoryId} does not exist in ${location}. Creating...`,
|
|
488
|
-
progressCallback
|
|
489
|
-
);
|
|
490
|
-
const repositoryToCreate = {
|
|
491
|
-
format: format,
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
const [operation] = await callWithRetry(
|
|
495
|
-
() =>
|
|
496
|
-
artifactRegistryClient.createRepository({
|
|
497
|
-
parent: parent,
|
|
498
|
-
repository: repositoryToCreate,
|
|
499
|
-
repositoryId: repositoryId,
|
|
500
|
-
}),
|
|
501
|
-
`artifactRegistry.createRepository ${repositoryId}`
|
|
502
|
-
);
|
|
503
|
-
await logAndProgress(
|
|
504
|
-
`Creating Artifact Registry repository ${repositoryId}...`,
|
|
505
|
-
progressCallback
|
|
506
|
-
);
|
|
507
|
-
const [result] = await operation.promise();
|
|
508
|
-
await logAndProgress(
|
|
509
|
-
`Artifact Registry repository ${result.name} created successfully.`,
|
|
510
|
-
progressCallback
|
|
511
|
-
);
|
|
512
|
-
return result;
|
|
513
|
-
} else {
|
|
514
|
-
const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
|
|
515
|
-
console.error(
|
|
516
|
-
`Error checking/creating repository ${repositoryId}:`,
|
|
517
|
-
error
|
|
518
|
-
);
|
|
519
|
-
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
520
|
-
throw error;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
|
|
527
|
-
* It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
|
|
528
|
-
* Waits for the build to complete and returns the build result.
|
|
529
|
-
*
|
|
530
|
-
* @async
|
|
531
|
-
* @param {string} projectId - The Google Cloud project ID.
|
|
532
|
-
* @param {string} location - The Google Cloud region for the build.
|
|
533
|
-
* @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
|
|
534
|
-
* @param {string} sourceBlobName - The GCS blob name (the zip file) for the source code.
|
|
535
|
-
* @param {string} targetRepoName - The name of the target Artifact Registry repository (used for context, not directly in build steps).
|
|
536
|
-
* @param {string} targetImageUrl - The full Artifact Registry URL for the image to be built (e.g., `location-docker.pkg.dev/project/repo/image:tag`).
|
|
537
|
-
* @param {boolean} hasDockerfile - Indicates whether a Dockerfile is present in the source to guide the build process.
|
|
538
|
-
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
539
|
-
* @returns {Promise<object>} A promise that resolves with the completed Cloud Build object.
|
|
540
|
-
* @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
|
|
541
|
-
*/
|
|
542
|
-
async function triggerCloudBuild(
|
|
543
|
-
projectId,
|
|
544
|
-
location,
|
|
545
|
-
sourceBucketName,
|
|
546
|
-
sourceBlobName,
|
|
547
|
-
targetRepoName,
|
|
548
|
-
targetImageUrl,
|
|
549
|
-
hasDockerfile,
|
|
550
|
-
progressCallback
|
|
551
|
-
) {
|
|
552
|
-
let buildSteps;
|
|
553
|
-
|
|
554
|
-
if (hasDockerfile) {
|
|
555
|
-
buildSteps = [
|
|
556
|
-
{
|
|
557
|
-
name: 'gcr.io/cloud-builders/docker',
|
|
558
|
-
args: ['build', '-t', targetImageUrl, '.'],
|
|
559
|
-
dir: '/workspace',
|
|
560
|
-
},
|
|
561
|
-
];
|
|
562
|
-
} else {
|
|
563
|
-
buildSteps = [
|
|
564
|
-
{
|
|
565
|
-
name: 'gcr.io/k8s-skaffold/pack',
|
|
566
|
-
entrypoint: 'pack',
|
|
567
|
-
args: [
|
|
568
|
-
'build',
|
|
569
|
-
targetImageUrl,
|
|
570
|
-
'--builder',
|
|
571
|
-
'gcr.io/buildpacks/builder:latest',
|
|
572
|
-
],
|
|
573
|
-
dir: '/workspace',
|
|
574
|
-
},
|
|
575
|
-
];
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const build = {
|
|
579
|
-
source: {
|
|
580
|
-
storageSource: {
|
|
581
|
-
bucket: sourceBucketName,
|
|
582
|
-
object: sourceBlobName,
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
steps: buildSteps,
|
|
586
|
-
images: [targetImageUrl],
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
await logAndProgress(
|
|
590
|
-
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
591
|
-
progressCallback
|
|
592
|
-
);
|
|
593
|
-
const [operation] = await callWithRetry(
|
|
594
|
-
() => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
|
|
595
|
-
'cloudBuild.createBuild'
|
|
596
|
-
);
|
|
597
|
-
|
|
598
|
-
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
599
|
-
const buildId = operation.metadata.build.id;
|
|
600
|
-
let completedBuild;
|
|
601
|
-
while (true) {
|
|
602
|
-
const [getBuildOperation] = await callWithRetry(
|
|
603
|
-
() => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
|
|
604
|
-
`cloudBuild.getBuild ${buildId}`
|
|
605
|
-
);
|
|
606
|
-
if (
|
|
607
|
-
['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
|
|
608
|
-
getBuildOperation.status
|
|
609
|
-
)
|
|
610
|
-
) {
|
|
611
|
-
completedBuild = getBuildOperation;
|
|
612
|
-
break;
|
|
613
|
-
}
|
|
614
|
-
await logAndProgress(
|
|
615
|
-
`Build status: ${getBuildOperation.status}. Waiting...`,
|
|
616
|
-
progressCallback,
|
|
617
|
-
'debug'
|
|
618
|
-
);
|
|
619
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (completedBuild.status === 'SUCCESS') {
|
|
623
|
-
await logAndProgress(
|
|
624
|
-
`Cloud Build job ${buildId} completed successfully.`,
|
|
625
|
-
progressCallback
|
|
626
|
-
);
|
|
627
|
-
await logAndProgress(
|
|
628
|
-
`Image built: ${completedBuild.results.images[0].name}`,
|
|
629
|
-
progressCallback
|
|
630
|
-
);
|
|
631
|
-
return completedBuild;
|
|
632
|
-
} else {
|
|
633
|
-
const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
|
|
634
|
-
await logAndProgress(failureMessage, progressCallback, 'error');
|
|
635
|
-
const logsMessage = `Build logs: ${completedBuild.logUrl}`;
|
|
636
|
-
await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
|
|
637
|
-
|
|
638
|
-
let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
|
|
639
|
-
try {
|
|
640
|
-
const { Logging } = await import('@google-cloud/logging');
|
|
641
|
-
const loggingClient = new Logging({ projectId });
|
|
642
|
-
|
|
643
|
-
const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
|
|
644
|
-
await logAndProgress(
|
|
645
|
-
`Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
|
|
646
|
-
progressCallback,
|
|
647
|
-
'debug'
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
// Wait for a short period to allow logs to propagate
|
|
651
|
-
await new Promise((resolve) =>
|
|
652
|
-
setTimeout(resolve, DELAY_WAIT_FOR_BUILD_LOGS)
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
// Fetch the most recent N log entries
|
|
656
|
-
const [entries] = await callWithRetry(
|
|
657
|
-
() =>
|
|
658
|
-
loggingClient.getEntries({
|
|
659
|
-
filter: logFilter,
|
|
660
|
-
orderBy: 'timestamp desc', // Get latest logs first
|
|
661
|
-
pageSize: BUILD_LOGS_LINES_TO_FETCH,
|
|
662
|
-
}),
|
|
663
|
-
`logging.getEntries for build ${buildId}`
|
|
664
|
-
);
|
|
665
|
-
|
|
666
|
-
if (entries && entries.length > 0) {
|
|
667
|
-
// Entries are newest first, reverse for chronological order of the snippet
|
|
668
|
-
const logLines = entries.reverse().map((entry) => entry.data || '');
|
|
669
|
-
if (logLines.length > 0) {
|
|
670
|
-
buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
|
|
671
|
-
await logAndProgress(
|
|
672
|
-
`Successfully fetched snippet of build logs for ${buildId}.`,
|
|
673
|
-
progressCallback,
|
|
674
|
-
'info'
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
} else {
|
|
678
|
-
await logAndProgress(
|
|
679
|
-
`No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
|
|
680
|
-
progressCallback,
|
|
681
|
-
'warn'
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
} catch (logError) {
|
|
685
|
-
console.error(`Error fetching build logs for ${buildId}:`, logError);
|
|
686
|
-
await logAndProgress(
|
|
687
|
-
`Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
|
|
688
|
-
progressCallback,
|
|
689
|
-
'warn'
|
|
690
|
-
);
|
|
691
|
-
// buildLogsSnippet already contains the Log URL as a fallback
|
|
692
|
-
}
|
|
693
|
-
throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Deploys a service to Google Cloud Run.
|
|
699
|
-
* @param {object} config - The deployment configuration.
|
|
700
|
-
* @param {string} config.projectId - The Google Cloud project ID.
|
|
701
|
-
* @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
|
|
702
|
-
* @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
|
|
703
|
-
* @param {Array<string|{filename: string, content: Buffer|string}>} config.files - An array of file paths or file objects (with `filename` and `content`) to deploy.
|
|
704
|
-
* @param {function(object): void} [config.progressCallback] - Optional callback for progress updates. Receives an object with `level` and `data` properties.
|
|
705
|
-
* @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
|
|
706
|
-
* @throws {Error} If deployment fails or required configuration is missing.
|
|
707
|
-
*/
|
|
708
|
-
export async function deploy({
|
|
709
|
-
projectId,
|
|
710
|
-
serviceName,
|
|
711
|
-
region,
|
|
712
|
-
files,
|
|
713
|
-
progressCallback,
|
|
714
|
-
skipIamCheck,
|
|
715
|
-
}) {
|
|
716
|
-
if (!projectId) {
|
|
717
|
-
const errorMsg =
|
|
718
|
-
'Error: projectId is required in the configuration object.';
|
|
719
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
720
|
-
throw new Error(errorMsg);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (!serviceName) {
|
|
724
|
-
const errorMsg =
|
|
725
|
-
'Error: serviceName is required in the configuration object.';
|
|
726
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
727
|
-
throw new Error(errorMsg);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
if (!files || !Array.isArray(files) || files.length === 0) {
|
|
731
|
-
const errorMsg =
|
|
732
|
-
'Error: files array is required in the configuration object.';
|
|
733
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
734
|
-
if (typeof process !== 'undefined' && process.exit) {
|
|
735
|
-
process.exit(1);
|
|
736
|
-
} else {
|
|
737
|
-
throw new Error(errorMsg);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const path = await import('path');
|
|
742
|
-
const fs = await import('fs');
|
|
743
|
-
const { Storage } = await import('@google-cloud/storage');
|
|
744
|
-
const { CloudBuildClient } = await import('@google-cloud/cloudbuild');
|
|
745
|
-
const { ArtifactRegistryClient } = await import(
|
|
746
|
-
'@google-cloud/artifact-registry'
|
|
747
|
-
);
|
|
748
|
-
const { v2: CloudRunV2Module } = await import('@google-cloud/run');
|
|
749
|
-
const { ServicesClient } = CloudRunV2Module;
|
|
750
|
-
|
|
751
|
-
try {
|
|
752
|
-
await ensureApisEnabled(
|
|
753
|
-
projectId,
|
|
754
|
-
REQUIRED_APIS_FOR_SOURCE_DEPLOY,
|
|
755
|
-
progressCallback
|
|
756
|
-
);
|
|
757
|
-
|
|
758
|
-
storage = new Storage({ projectId });
|
|
759
|
-
cloudBuildClient = new CloudBuildClient({ projectId });
|
|
760
|
-
artifactRegistryClient = new ArtifactRegistryClient({ projectId });
|
|
761
|
-
runClient = new ServicesClient({ projectId });
|
|
762
|
-
|
|
763
|
-
const bucketName = `${projectId}-source-bucket`;
|
|
764
|
-
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${REPO_NAME}/${serviceName}:${IMAGE_TAG}`;
|
|
765
|
-
|
|
766
|
-
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
767
|
-
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
768
|
-
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
769
|
-
await logAndProgress(`Files to deploy: ${files.length}`, progressCallback);
|
|
770
|
-
|
|
771
|
-
let hasDockerfile = false;
|
|
772
|
-
if (
|
|
773
|
-
files.length === 1 &&
|
|
774
|
-
typeof files[0] === 'string' &&
|
|
775
|
-
fs.statSync(files[0]).isDirectory()
|
|
776
|
-
) {
|
|
777
|
-
// Handle folder deployment: check for Dockerfile inside the folder
|
|
778
|
-
const dockerfilePath = path.join(files[0], 'Dockerfile');
|
|
779
|
-
const dockerfilePathLowerCase = path.join(files[0], 'dockerfile');
|
|
780
|
-
if (
|
|
781
|
-
fs.existsSync(dockerfilePath) ||
|
|
782
|
-
fs.existsSync(dockerfilePathLowerCase)
|
|
783
|
-
) {
|
|
784
|
-
hasDockerfile = true;
|
|
785
|
-
}
|
|
786
|
-
} else {
|
|
787
|
-
// Handle file list deployment or file content deployment
|
|
788
|
-
for (const file of files) {
|
|
789
|
-
if (typeof file === 'string') {
|
|
790
|
-
if (path.basename(file).toLowerCase() === 'dockerfile') {
|
|
791
|
-
hasDockerfile = true;
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
} else if (typeof file === 'object' && file.filename) {
|
|
795
|
-
if (path.basename(file.filename).toLowerCase() === 'dockerfile') {
|
|
796
|
-
hasDockerfile = true;
|
|
797
|
-
break;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
await logAndProgress(`Dockerfile: ${hasDockerfile}`, progressCallback);
|
|
803
|
-
|
|
804
|
-
const bucket = await ensureStorageBucketExists(
|
|
805
|
-
bucketName,
|
|
806
|
-
region,
|
|
807
|
-
progressCallback
|
|
808
|
-
);
|
|
809
|
-
|
|
810
|
-
const zipBuffer = await zipFiles(files, progressCallback);
|
|
811
|
-
await uploadToStorageBucket(
|
|
812
|
-
bucket,
|
|
813
|
-
zipBuffer,
|
|
814
|
-
ZIP_FILE_NAME,
|
|
815
|
-
progressCallback
|
|
816
|
-
);
|
|
817
|
-
await logAndProgress('Source code uploaded successfully', progressCallback);
|
|
818
|
-
|
|
819
|
-
await ensureArtifactRegistryRepoExists(
|
|
820
|
-
projectId,
|
|
821
|
-
region,
|
|
822
|
-
REPO_NAME,
|
|
823
|
-
'DOCKER',
|
|
824
|
-
progressCallback
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
const buildResult = await triggerCloudBuild(
|
|
828
|
-
projectId,
|
|
829
|
-
region,
|
|
830
|
-
bucketName,
|
|
831
|
-
ZIP_FILE_NAME,
|
|
832
|
-
REPO_NAME,
|
|
833
|
-
imageUrl,
|
|
834
|
-
hasDockerfile,
|
|
835
|
-
progressCallback
|
|
836
|
-
);
|
|
837
|
-
|
|
838
|
-
const builtImageUrl = buildResult.results.images[0].name;
|
|
839
|
-
|
|
840
|
-
const service = await deployToCloudRun(
|
|
841
|
-
projectId,
|
|
842
|
-
region,
|
|
843
|
-
serviceName,
|
|
844
|
-
builtImageUrl,
|
|
845
|
-
progressCallback,
|
|
846
|
-
skipIamCheck
|
|
847
|
-
);
|
|
848
|
-
|
|
849
|
-
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
850
|
-
return service;
|
|
851
|
-
} catch (error) {
|
|
852
|
-
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
853
|
-
console.error(`Deployment Failed`, error);
|
|
854
|
-
await logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
855
|
-
throw error;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Deploys a container image to Google Cloud Run.
|
|
861
|
-
* @param {object} config - The deployment configuration.
|
|
862
|
-
* @param {string} config.projectId - The Google Cloud project ID.
|
|
863
|
-
* @param {string} [config.serviceName='app'] - The name of the Cloud Run service. Defaults to 'app'.
|
|
864
|
-
* @param {string} [config.region='europe-west1'] - The Google Cloud region for deployment. Defaults to 'europe-west1'.
|
|
865
|
-
* @param {string} config.imageUrl - The URL of the container image to deploy.
|
|
866
|
-
* @param {function(object): void} [config.progressCallback] - Optional callback for progress updates. Receives an object with `level` and `data` properties.
|
|
867
|
-
* @param {boolean} [config.skipIamCheck=false] - Whether to skip the IAM check.
|
|
868
|
-
* @returns {Promise<object>} A promise that resolves with the deployed Cloud Run service object.
|
|
869
|
-
* @throws {Error} If deployment fails or required configuration is missing.
|
|
870
|
-
*/
|
|
871
|
-
export async function deployImage({
|
|
872
|
-
projectId,
|
|
873
|
-
serviceName,
|
|
874
|
-
region,
|
|
875
|
-
imageUrl,
|
|
876
|
-
progressCallback,
|
|
877
|
-
skipIamCheck,
|
|
878
|
-
}) {
|
|
879
|
-
if (!projectId) {
|
|
880
|
-
const errorMsg =
|
|
881
|
-
'Error: projectId is required in the configuration object.';
|
|
882
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
883
|
-
throw new Error(errorMsg);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (!serviceName) {
|
|
887
|
-
const errorMsg =
|
|
888
|
-
'Error: serviceName is required in the configuration object.';
|
|
889
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
890
|
-
throw new Error(errorMsg);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
if (!imageUrl) {
|
|
894
|
-
const errorMsg = 'Error: imageUrl is required in the configuration object.';
|
|
895
|
-
await logAndProgress(errorMsg, progressCallback, 'error');
|
|
896
|
-
if (typeof process !== 'undefined' && process.exit) {
|
|
897
|
-
process.exit(1);
|
|
898
|
-
} else {
|
|
899
|
-
throw new Error(errorMsg);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const { v2: CloudRunV2Module } = await import('@google-cloud/run');
|
|
904
|
-
const { ServicesClient } = CloudRunV2Module;
|
|
905
|
-
|
|
906
|
-
try {
|
|
907
|
-
await ensureApisEnabled(
|
|
908
|
-
projectId,
|
|
909
|
-
REQUIRED_APIS_FOR_IMAGE_DEPLOY,
|
|
910
|
-
progressCallback
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
runClient = new ServicesClient({ projectId });
|
|
914
|
-
|
|
915
|
-
await logAndProgress(`Project: ${projectId}`, progressCallback);
|
|
916
|
-
await logAndProgress(`Region: ${region}`, progressCallback);
|
|
917
|
-
await logAndProgress(`Service Name: ${serviceName}`, progressCallback);
|
|
918
|
-
await logAndProgress(`Image URL: ${imageUrl}`, progressCallback);
|
|
919
|
-
|
|
920
|
-
const service = await deployToCloudRun(
|
|
921
|
-
projectId,
|
|
922
|
-
region,
|
|
923
|
-
serviceName,
|
|
924
|
-
imageUrl,
|
|
925
|
-
progressCallback,
|
|
926
|
-
skipIamCheck
|
|
927
|
-
);
|
|
928
|
-
|
|
929
|
-
await logAndProgress(`Deployment Completed Successfully`, progressCallback);
|
|
930
|
-
return service;
|
|
931
|
-
} catch (error) {
|
|
932
|
-
const deployFailedMessage = `Deployment Failed: ${error.message}`;
|
|
933
|
-
console.error(`Deployment Failed`, error);
|
|
934
|
-
await logAndProgress(deployFailedMessage, progressCallback, 'error');
|
|
935
|
-
throw error;
|
|
936
|
-
}
|
|
937
|
-
}
|