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