@google-cloud/cloud-run-mcp 1.6.0 → 1.8.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 +1 -1
- package/CONTRIBUTING.md +1 -1
- package/lib/clients.js +222 -0
- package/lib/cloud-api/billing.js +90 -4
- package/lib/cloud-api/build.js +96 -49
- package/lib/cloud-api/helpers.js +60 -38
- package/lib/cloud-api/projects.js +4 -6
- package/lib/cloud-api/registry.js +5 -5
- package/lib/cloud-api/run.js +14 -33
- package/lib/cloud-api/storage.js +6 -7
- package/lib/deployment/constants.js +54 -0
- package/lib/deployment/deployer.js +251 -149
- package/lib/deployment/helpers.js +194 -0
- package/lib/deployment/source-processor.js +124 -0
- package/lib/util/archive.js +15 -6
- package/package.json +3 -3
- package/test/local/clients.test.js +146 -0
- package/test/local/cloud-api/build.test.js +293 -309
- package/test/local/deployment-helpers.test.js +231 -0
- package/test/local/mcp-server-stdio.test.js +59 -0
- package/test/local/mcp-server-streamable-http.test.js +25 -21
- package/test/local/mock-clients.js +35 -0
- package/test/local/source-processor.test.js +290 -0
- package/test/local/test-utils.js +51 -0
- package/test/need-gcp/test-helpers.js +1 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +3 -3
package/CONTRIBUTING.md
CHANGED
|
@@ -126,7 +126,7 @@ This script requires an existing Google Cloud Project ID to be provided when pro
|
|
|
126
126
|
|
|
127
127
|
## Publishing to npm
|
|
128
128
|
|
|
129
|
-
To update the [npm package]https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
|
|
129
|
+
To update the [npm package](https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
|
|
130
130
|
|
|
131
131
|
Run the following:
|
|
132
132
|
|
package/lib/clients.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
const AUTHORIZATION_HEADER = 'Authorization';
|
|
18
|
+
const BEARER_PREFIX = 'Bearer';
|
|
19
|
+
const GCLOUD_AUTH = 'gcloud_auth';
|
|
20
|
+
|
|
21
|
+
function keyGenerator(projectId, accessToken) {
|
|
22
|
+
return accessToken !== GCLOUD_AUTH ? projectId + accessToken : projectId;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clients = {
|
|
26
|
+
run: new Map(),
|
|
27
|
+
builds: new Map(),
|
|
28
|
+
serviceUsage: new Map(),
|
|
29
|
+
storage: new Map(),
|
|
30
|
+
cloudBuild: new Map(),
|
|
31
|
+
artifactRegistry: new Map(),
|
|
32
|
+
logging: new Map(),
|
|
33
|
+
billing: new Map(),
|
|
34
|
+
projects: new Map(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getAuthClient(accessToken) {
|
|
38
|
+
return {
|
|
39
|
+
getRequestHeaders: () => {
|
|
40
|
+
const headers = new Map();
|
|
41
|
+
headers.set(AUTHORIZATION_HEADER, `${BEARER_PREFIX} ${accessToken}`);
|
|
42
|
+
return headers;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function getClient(
|
|
48
|
+
service,
|
|
49
|
+
key,
|
|
50
|
+
loadClient,
|
|
51
|
+
options = {},
|
|
52
|
+
accessToken = null
|
|
53
|
+
) {
|
|
54
|
+
if (!clients[service].has(key)) {
|
|
55
|
+
const ClientClass = await loadClient();
|
|
56
|
+
const finalOptions = { ...options };
|
|
57
|
+
if (accessToken && accessToken !== GCLOUD_AUTH) {
|
|
58
|
+
finalOptions.authClient = getAuthClient(accessToken);
|
|
59
|
+
}
|
|
60
|
+
clients[service].set(key, new ClientClass(finalOptions));
|
|
61
|
+
}
|
|
62
|
+
return clients[service].get(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gets a Cloud Run Services Client for the specified project.
|
|
67
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
68
|
+
* @returns {Promise<import('@google-cloud/run').v2.ServicesClient>}
|
|
69
|
+
*/
|
|
70
|
+
export async function getRunClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
71
|
+
const key = keyGenerator(projectId, accessToken);
|
|
72
|
+
return getClient(
|
|
73
|
+
'run',
|
|
74
|
+
key,
|
|
75
|
+
async () => (await import('@google-cloud/run')).v2.ServicesClient,
|
|
76
|
+
{ projectId },
|
|
77
|
+
accessToken
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets a Cloud Run Build Client for the specified project.
|
|
83
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
84
|
+
* @returns {Promise<import('@google-cloud/run').v2.BuildsClient>}
|
|
85
|
+
*/
|
|
86
|
+
export async function getBuildsClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
87
|
+
const key = keyGenerator(projectId, accessToken);
|
|
88
|
+
return getClient(
|
|
89
|
+
'builds',
|
|
90
|
+
key,
|
|
91
|
+
async () => (await import('@google-cloud/run')).v2.BuildsClient,
|
|
92
|
+
{ projectId },
|
|
93
|
+
accessToken
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Gets a Service Usage Client for the specified project.
|
|
99
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
100
|
+
* @returns {Promise<import('@google-cloud/service-usage').ServiceUsageClient>}
|
|
101
|
+
*/
|
|
102
|
+
export async function getServiceUsageClient(
|
|
103
|
+
projectId,
|
|
104
|
+
accessToken = GCLOUD_AUTH
|
|
105
|
+
) {
|
|
106
|
+
const key = keyGenerator(projectId, accessToken);
|
|
107
|
+
return getClient(
|
|
108
|
+
'serviceUsage',
|
|
109
|
+
key,
|
|
110
|
+
async () =>
|
|
111
|
+
(await import('@google-cloud/service-usage')).ServiceUsageClient,
|
|
112
|
+
{ projectId },
|
|
113
|
+
accessToken
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets a Storage Client for the specified project.
|
|
119
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
120
|
+
* @returns {Promise<import('@google-cloud/storage').Storage>}
|
|
121
|
+
*/
|
|
122
|
+
export async function getStorageClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
123
|
+
const key = keyGenerator(projectId, accessToken);
|
|
124
|
+
return getClient(
|
|
125
|
+
'storage',
|
|
126
|
+
key,
|
|
127
|
+
async () => (await import('@google-cloud/storage')).Storage,
|
|
128
|
+
{ projectId },
|
|
129
|
+
accessToken
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Gets a Cloud Build Client for the specified project.
|
|
135
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
136
|
+
* @returns {Promise<import('@google-cloud/cloudbuild').CloudBuildClient>}
|
|
137
|
+
*/
|
|
138
|
+
export async function getCloudBuildClient(
|
|
139
|
+
projectId,
|
|
140
|
+
accessToken = GCLOUD_AUTH
|
|
141
|
+
) {
|
|
142
|
+
const key = keyGenerator(projectId, accessToken);
|
|
143
|
+
return getClient(
|
|
144
|
+
'cloudBuild',
|
|
145
|
+
key,
|
|
146
|
+
async () => (await import('@google-cloud/cloudbuild')).CloudBuildClient,
|
|
147
|
+
{ projectId },
|
|
148
|
+
accessToken
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Gets an Artifact Registry Client for the specified project.
|
|
154
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
155
|
+
* @returns {Promise<import('@google-cloud/artifact-registry').ArtifactRegistryClient>}
|
|
156
|
+
*/
|
|
157
|
+
export async function getArtifactRegistryClient(
|
|
158
|
+
projectId,
|
|
159
|
+
accessToken = GCLOUD_AUTH
|
|
160
|
+
) {
|
|
161
|
+
const key = keyGenerator(projectId, accessToken);
|
|
162
|
+
return getClient(
|
|
163
|
+
'artifactRegistry',
|
|
164
|
+
key,
|
|
165
|
+
async () =>
|
|
166
|
+
(await import('@google-cloud/artifact-registry')).ArtifactRegistryClient,
|
|
167
|
+
{ projectId },
|
|
168
|
+
accessToken
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gets a Logging Client for the specified project.
|
|
174
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
175
|
+
* @returns {Promise<import('@google-cloud/logging').Logging>}
|
|
176
|
+
*/
|
|
177
|
+
export async function getLoggingClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
178
|
+
const key = keyGenerator(projectId, accessToken);
|
|
179
|
+
return getClient(
|
|
180
|
+
'logging',
|
|
181
|
+
key,
|
|
182
|
+
async () => (await import('@google-cloud/logging')).Logging,
|
|
183
|
+
{ projectId },
|
|
184
|
+
accessToken
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Gets a Billing Client for the specified project.
|
|
190
|
+
* Note: BillingClient usually doesn't take projectId in constructor for listing accounts,
|
|
191
|
+
* but might for project billing info. We will cache by projectId anyway or 'global' if projectId is null.
|
|
192
|
+
* @param {string} [projectId] - The Google Cloud project ID (optional).
|
|
193
|
+
* @returns {Promise<import('@google-cloud/billing').CloudBillingClient>}
|
|
194
|
+
*/
|
|
195
|
+
export async function getBillingClient(
|
|
196
|
+
projectId = 'global',
|
|
197
|
+
accessToken = GCLOUD_AUTH
|
|
198
|
+
) {
|
|
199
|
+
const key = keyGenerator(projectId, accessToken);
|
|
200
|
+
return getClient(
|
|
201
|
+
'billing',
|
|
202
|
+
key,
|
|
203
|
+
async () => (await import('@google-cloud/billing')).CloudBillingClient,
|
|
204
|
+
projectId !== 'global' ? { projectId } : {},
|
|
205
|
+
accessToken
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Gets a Projects Client (Resource Manager).
|
|
211
|
+
* @returns {Promise<import('@google-cloud/resource-manager').ProjectsClient>}
|
|
212
|
+
*/
|
|
213
|
+
export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
|
|
214
|
+
const key = keyGenerator('global', accessToken);
|
|
215
|
+
return getClient(
|
|
216
|
+
'projects',
|
|
217
|
+
key,
|
|
218
|
+
async () => (await import('@google-cloud/resource-manager')).ProjectsClient,
|
|
219
|
+
{},
|
|
220
|
+
accessToken
|
|
221
|
+
);
|
|
222
|
+
}
|
package/lib/cloud-api/billing.js
CHANGED
|
@@ -14,6 +14,15 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Lists all accessible Google Cloud Billing Accounts.
|
|
19
|
+
* @async
|
|
20
|
+
* @function listBillingAccounts
|
|
21
|
+
* @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
|
|
22
|
+
* each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
|
|
23
|
+
*/
|
|
24
|
+
import { getBillingClient } from '../clients.js';
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* Lists all accessible Google Cloud Billing Accounts.
|
|
19
28
|
* @async
|
|
@@ -22,8 +31,7 @@ limitations under the License.
|
|
|
22
31
|
* each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
|
|
23
32
|
*/
|
|
24
33
|
export async function listBillingAccounts() {
|
|
25
|
-
const
|
|
26
|
-
const client = new CloudBillingClient();
|
|
34
|
+
const client = await getBillingClient();
|
|
27
35
|
try {
|
|
28
36
|
const [accounts] = await client.listBillingAccounts();
|
|
29
37
|
if (!accounts || accounts.length === 0) {
|
|
@@ -53,8 +61,7 @@ export async function attachProjectToBillingAccount(
|
|
|
53
61
|
projectId,
|
|
54
62
|
billingAccountName
|
|
55
63
|
) {
|
|
56
|
-
const
|
|
57
|
-
const client = new CloudBillingClient();
|
|
64
|
+
const client = await getBillingClient();
|
|
58
65
|
const projectName = `projects/${projectId}`;
|
|
59
66
|
|
|
60
67
|
if (!projectId) {
|
|
@@ -96,3 +103,82 @@ export async function attachProjectToBillingAccount(
|
|
|
96
103
|
return null;
|
|
97
104
|
}
|
|
98
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Checks if billing is enabled for a given project.
|
|
109
|
+
* @async
|
|
110
|
+
* @function isBillingEnabled
|
|
111
|
+
* @param {string} projectId - The ID of the project to check.
|
|
112
|
+
* @returns {Promise<boolean>} A promise that resolves to true if billing is enabled, false otherwise.
|
|
113
|
+
*/
|
|
114
|
+
export async function isBillingEnabled(projectId) {
|
|
115
|
+
const client = await getBillingClient();
|
|
116
|
+
const projectName = `projects/${projectId}`;
|
|
117
|
+
try {
|
|
118
|
+
// getProjectBillingInfo requires cloudbilling.googleapis.com API to check billing status
|
|
119
|
+
// https://docs.cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo
|
|
120
|
+
const [billingInfo] = await client.getProjectBillingInfo({
|
|
121
|
+
name: projectName,
|
|
122
|
+
});
|
|
123
|
+
return billingInfo.billingEnabled;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error(
|
|
126
|
+
`Error checking billing status for project ${projectId}:`,
|
|
127
|
+
error.message || error
|
|
128
|
+
);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ensures billing is enabled for a project. If not, attempts to enable it
|
|
135
|
+
* by attaching the single open billing account if one is found.
|
|
136
|
+
* @param {string} projectId The project ID to check billing for.
|
|
137
|
+
* @param {function} progressCallback A callback function for progress updates.
|
|
138
|
+
*/
|
|
139
|
+
export async function ensureBillingEnabled(projectId, progressCallback) {
|
|
140
|
+
if (!(await isBillingEnabled(projectId))) {
|
|
141
|
+
// Billing is disabled, try to fix it.
|
|
142
|
+
const accounts = await listBillingAccounts();
|
|
143
|
+
|
|
144
|
+
if (accounts && accounts.length === 1 && accounts[0].open) {
|
|
145
|
+
// Exactly one open account found, try to attach it.
|
|
146
|
+
const account = accounts[0];
|
|
147
|
+
const attemptMessage = `Billing is not enabled for project ${projectId}. Found one open billing account: ${account.displayName} (${account.name}). Attempting to attach it...`;
|
|
148
|
+
console.log(attemptMessage);
|
|
149
|
+
if (progressCallback)
|
|
150
|
+
progressCallback({ level: 'info', data: attemptMessage });
|
|
151
|
+
|
|
152
|
+
const attachmentResult = await attachProjectToBillingAccount(
|
|
153
|
+
projectId,
|
|
154
|
+
account.name
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!attachmentResult || !attachmentResult.billingEnabled) {
|
|
158
|
+
const attachFailMessage = `Failed to automatically attach project ${projectId} to billing account ${account.name}. Please enable billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
159
|
+
if (progressCallback)
|
|
160
|
+
progressCallback({ level: 'error', data: attachFailMessage });
|
|
161
|
+
throw new Error(attachFailMessage);
|
|
162
|
+
}
|
|
163
|
+
const attachSuccessMessage = `Successfully attached project ${projectId} to billing account ${account.name}.`;
|
|
164
|
+
console.log(attachSuccessMessage);
|
|
165
|
+
if (progressCallback)
|
|
166
|
+
progressCallback({ level: 'info', data: attachSuccessMessage });
|
|
167
|
+
// If we get here, billing is now enabled, and we can proceed to API checks.
|
|
168
|
+
} else {
|
|
169
|
+
// Cannot auto-attach. Throw error.
|
|
170
|
+
let reason;
|
|
171
|
+
if (!accounts || accounts.length === 0) {
|
|
172
|
+
reason = 'no billing accounts were found';
|
|
173
|
+
} else if (accounts.length > 1) {
|
|
174
|
+
reason = 'multiple billing accounts were found';
|
|
175
|
+
} else {
|
|
176
|
+
reason = `the only available billing account '${accounts[0].displayName}' is not open`;
|
|
177
|
+
}
|
|
178
|
+
const errorMessage = `Billing is not enabled for project ${projectId}, and it could not be enabled automatically because ${reason}. Please enable billing to use Google Cloud services: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
179
|
+
if (progressCallback)
|
|
180
|
+
progressCallback({ level: 'error', data: errorMessage });
|
|
181
|
+
throw new Error(errorMessage);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
package/lib/cloud-api/build.js
CHANGED
|
@@ -14,8 +14,15 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import {
|
|
18
|
+
getCloudBuildClient,
|
|
19
|
+
getLoggingClient,
|
|
20
|
+
getRunClient,
|
|
21
|
+
getBuildsClient,
|
|
22
|
+
} from '../clients.js';
|
|
17
23
|
import { callWithRetry } from './helpers.js';
|
|
18
24
|
import { logAndProgress } from '../util/helpers.js';
|
|
25
|
+
import { checkCloudRunServiceExists } from './run.js';
|
|
19
26
|
|
|
20
27
|
const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
|
|
21
28
|
const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
|
|
@@ -23,10 +30,10 @@ const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build
|
|
|
23
30
|
/**
|
|
24
31
|
* Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
|
|
25
32
|
* It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
|
|
33
|
+
* It performs a dry-run deployment check before starting the build.
|
|
26
34
|
* Waits for the build to complete and returns the build result.
|
|
27
35
|
*
|
|
28
36
|
* @async
|
|
29
|
-
* @param {object} context - The context object containing clients and other parameters.
|
|
30
37
|
* @param {string} projectId - The Google Cloud project ID.
|
|
31
38
|
* @param {string} location - The Google Cloud region for the build.
|
|
32
39
|
* @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
|
|
@@ -39,7 +46,6 @@ const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build
|
|
|
39
46
|
* @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
|
|
40
47
|
*/
|
|
41
48
|
export async function triggerCloudBuild(
|
|
42
|
-
context,
|
|
43
49
|
projectId,
|
|
44
50
|
location,
|
|
45
51
|
sourceBucketName,
|
|
@@ -49,65 +55,107 @@ export async function triggerCloudBuild(
|
|
|
49
55
|
hasDockerfile,
|
|
50
56
|
progressCallback
|
|
51
57
|
) {
|
|
58
|
+
const serviceName = targetImageUrl.split('/').pop().split(/[:@]/)[0];
|
|
59
|
+
const parent = `projects/${projectId}/locations/${location}`;
|
|
60
|
+
const serviceFullName = `${parent}/services/${serviceName}`;
|
|
61
|
+
const buildsClient = await getBuildsClient(projectId);
|
|
62
|
+
const runClient = await getRunClient(projectId);
|
|
63
|
+
const serviceExists = await checkCloudRunServiceExists(
|
|
64
|
+
projectId,
|
|
65
|
+
location,
|
|
66
|
+
serviceName,
|
|
67
|
+
progressCallback
|
|
68
|
+
);
|
|
69
|
+
const cloudBuildClient = await getCloudBuildClient(projectId);
|
|
70
|
+
const loggingClient = await getLoggingClient(projectId);
|
|
52
71
|
let buildSteps;
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
const servicePatch = {
|
|
74
|
+
template: {
|
|
75
|
+
containers: [{ image: targetImageUrl }],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
if (serviceExists) {
|
|
81
|
+
await logAndProgress(
|
|
82
|
+
`Performing dry-run update for service ${serviceName}...`,
|
|
83
|
+
progressCallback
|
|
84
|
+
);
|
|
85
|
+
servicePatch.name = serviceFullName;
|
|
86
|
+
await callWithRetry(
|
|
87
|
+
() =>
|
|
88
|
+
runClient.updateService({
|
|
89
|
+
service: servicePatch,
|
|
90
|
+
validateOnly: true,
|
|
91
|
+
}),
|
|
92
|
+
'run.updateService-dryrun'
|
|
93
|
+
);
|
|
94
|
+
} else {
|
|
95
|
+
await logAndProgress(
|
|
96
|
+
`Performing dry-run creation for service ${serviceName}...`,
|
|
97
|
+
progressCallback
|
|
98
|
+
);
|
|
99
|
+
await callWithRetry(
|
|
100
|
+
() =>
|
|
101
|
+
runClient.createService({
|
|
102
|
+
parent: parent,
|
|
103
|
+
service: servicePatch,
|
|
104
|
+
serviceId: serviceName,
|
|
105
|
+
validateOnly: true,
|
|
106
|
+
}),
|
|
107
|
+
'run.createService-dryrun'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
await logAndProgress(
|
|
111
|
+
`Dry-run validation successful.`,
|
|
112
|
+
progressCallback,
|
|
113
|
+
'debug'
|
|
114
|
+
);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
await logAndProgress(
|
|
117
|
+
`Dry-run validation failed for service ${serviceName}.`,
|
|
118
|
+
progressCallback,
|
|
119
|
+
'error'
|
|
120
|
+
);
|
|
121
|
+
throw new Error(`Dry-run deployment failed: ${err.message}`);
|
|
76
122
|
}
|
|
77
123
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
124
|
+
const request = {
|
|
125
|
+
parent: `projects/${projectId}/locations/${location}`,
|
|
126
|
+
storageSource: {
|
|
127
|
+
bucket: sourceBucketName,
|
|
128
|
+
object: sourceBlobName,
|
|
84
129
|
},
|
|
85
|
-
|
|
86
|
-
|
|
130
|
+
imageUri: targetImageUrl,
|
|
131
|
+
client: 'cloud-run-mcp',
|
|
87
132
|
};
|
|
133
|
+
if (hasDockerfile) {
|
|
134
|
+
request.dockerBuild = {};
|
|
135
|
+
} else {
|
|
136
|
+
request.buildpackBuild = {};
|
|
137
|
+
}
|
|
88
138
|
|
|
89
139
|
await logAndProgress(
|
|
90
140
|
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
91
141
|
progressCallback
|
|
92
142
|
);
|
|
93
|
-
const [operation] = await callWithRetry(
|
|
94
|
-
() =>
|
|
95
|
-
context.cloudBuildClient.createBuild({
|
|
96
|
-
projectId: projectId,
|
|
97
|
-
build: build,
|
|
98
|
-
}),
|
|
99
|
-
'cloudBuild.createBuild'
|
|
100
|
-
);
|
|
101
143
|
|
|
144
|
+
const [submitBuildResponse] = await callWithRetry(
|
|
145
|
+
() => buildsClient.submitBuild(request),
|
|
146
|
+
'builds.submitBuild'
|
|
147
|
+
);
|
|
102
148
|
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
103
|
-
const
|
|
149
|
+
const encodedBuildId = submitBuildResponse.buildOperation.name
|
|
150
|
+
.split('/')
|
|
151
|
+
.pop();
|
|
152
|
+
const buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
|
|
104
153
|
let completedBuild;
|
|
105
154
|
while (true) {
|
|
106
155
|
const [getBuildOperation] = await callWithRetry(
|
|
107
156
|
() =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
id: buildId,
|
|
157
|
+
cloudBuildClient.getBuild({
|
|
158
|
+
name: `projects/${projectId}/locations/${location}/builds/${buildId}`,
|
|
111
159
|
}),
|
|
112
160
|
`cloudBuild.getBuild ${buildId}`
|
|
113
161
|
);
|
|
@@ -132,10 +180,9 @@ export async function triggerCloudBuild(
|
|
|
132
180
|
`Cloud Build job ${buildId} completed successfully.`,
|
|
133
181
|
progressCallback
|
|
134
182
|
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
183
|
+
const builtImage = completedBuild.results.images[0].name;
|
|
184
|
+
await logAndProgress(`Image built: ${builtImage}`, progressCallback);
|
|
185
|
+
|
|
139
186
|
return completedBuild;
|
|
140
187
|
} else {
|
|
141
188
|
const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
|
|
@@ -160,7 +207,7 @@ export async function triggerCloudBuild(
|
|
|
160
207
|
// Fetch the most recent N log entries
|
|
161
208
|
const [entries] = await callWithRetry(
|
|
162
209
|
() =>
|
|
163
|
-
|
|
210
|
+
loggingClient.getEntries({
|
|
164
211
|
filter: logFilter,
|
|
165
212
|
orderBy: 'timestamp desc', // Get latest logs first
|
|
166
213
|
pageSize: BUILD_LOGS_LINES_TO_FETCH,
|
package/lib/cloud-api/helpers.js
CHANGED
|
@@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { getServiceUsageClient } from '../clients.js';
|
|
18
|
+
import { ensureBillingEnabled } from './billing.js';
|
|
19
|
+
|
|
20
|
+
const PREREQUISITE_APIS = [
|
|
21
|
+
'serviceusage.googleapis.com',
|
|
22
|
+
'cloudbilling.googleapis.com',
|
|
23
|
+
];
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Calls a function with retry logic for GCP API calls.
|
|
19
27
|
* Retries on gRPC error code 7 (PERMISSION_DENIED).
|
|
@@ -84,13 +92,55 @@ async function checkAndEnableApi(
|
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Enables a single Google Cloud API with retry logic.
|
|
97
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
98
|
+
* @param {string} api - The API identifier (e.g., 'run.googleapis.com').
|
|
99
|
+
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
100
|
+
* @returns {Promise<void>} A promise that resolves when the API is enabled.
|
|
101
|
+
* @throws {Error} If the API fails to enable or if there's an issue checking its status.
|
|
102
|
+
*/
|
|
103
|
+
export async function enableApiWithRetry(projectId, api, progressCallback) {
|
|
104
|
+
const serviceUsageClient = await getServiceUsageClient(projectId);
|
|
105
|
+
const serviceName = `projects/${projectId}/services/${api}`;
|
|
106
|
+
try {
|
|
107
|
+
await checkAndEnableApi(
|
|
108
|
+
serviceUsageClient,
|
|
109
|
+
serviceName,
|
|
110
|
+
api,
|
|
111
|
+
progressCallback
|
|
112
|
+
);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
// First attempt failed, log a warning and retry once after a delay.
|
|
115
|
+
const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
|
|
116
|
+
console.warn(warnMsg);
|
|
117
|
+
if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
|
|
118
|
+
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
120
|
+
try {
|
|
121
|
+
await checkAndEnableApi(
|
|
122
|
+
serviceUsageClient,
|
|
123
|
+
serviceName,
|
|
124
|
+
api,
|
|
125
|
+
progressCallback
|
|
126
|
+
);
|
|
127
|
+
} catch (retryError) {
|
|
128
|
+
// If the retry also fails, throw an error.
|
|
129
|
+
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
130
|
+
console.error(errorMessage, retryError);
|
|
131
|
+
if (progressCallback)
|
|
132
|
+
progressCallback({ level: 'error', data: errorMessage });
|
|
133
|
+
throw new Error(errorMessage);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
87
138
|
/**
|
|
88
139
|
* Ensures that the specified Google Cloud APIs are enabled for the given project.
|
|
89
140
|
* If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
|
|
90
141
|
* Throws an error if an API cannot be enabled.
|
|
91
142
|
*
|
|
92
143
|
* @async
|
|
93
|
-
* @param {object} context - The context object containing clients and other parameters.
|
|
94
144
|
* @param {string} projectId - The Google Cloud project ID.
|
|
95
145
|
* @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
|
|
96
146
|
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
@@ -98,48 +148,20 @@ async function checkAndEnableApi(
|
|
|
98
148
|
* @throws {Error} If an API fails to enable or if there's an issue checking its status.
|
|
99
149
|
* @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
|
|
100
150
|
*/
|
|
101
|
-
export async function ensureApisEnabled(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
151
|
+
export async function ensureApisEnabled(projectId, apis, progressCallback) {
|
|
152
|
+
for (const api of PREREQUISITE_APIS) {
|
|
153
|
+
await enableApiWithRetry(projectId, api, progressCallback);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Before enabling other APIs, ensure billing is enabled.
|
|
157
|
+
await ensureBillingEnabled(projectId, progressCallback);
|
|
158
|
+
|
|
107
159
|
const message = 'Checking and enabling required APIs...';
|
|
108
160
|
console.log(message);
|
|
109
161
|
if (progressCallback) progressCallback({ level: 'info', data: message });
|
|
110
162
|
|
|
111
163
|
for (const api of apis) {
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await checkAndEnableApi(
|
|
115
|
-
context.serviceUsageClient,
|
|
116
|
-
serviceName,
|
|
117
|
-
api,
|
|
118
|
-
progressCallback
|
|
119
|
-
);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
// First attempt failed, log a warning and retry once after a delay.
|
|
122
|
-
const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
|
|
123
|
-
console.warn(warnMsg);
|
|
124
|
-
if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
|
|
125
|
-
|
|
126
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
127
|
-
try {
|
|
128
|
-
await checkAndEnableApi(
|
|
129
|
-
context.serviceUsageClient,
|
|
130
|
-
serviceName,
|
|
131
|
-
api,
|
|
132
|
-
progressCallback
|
|
133
|
-
);
|
|
134
|
-
} catch (retryError) {
|
|
135
|
-
// If the retry also fails, throw an error.
|
|
136
|
-
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
137
|
-
console.error(errorMessage, retryError);
|
|
138
|
-
if (progressCallback)
|
|
139
|
-
progressCallback({ level: 'error', data: errorMessage });
|
|
140
|
-
throw new Error(errorMessage);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
164
|
+
await enableApiWithRetry(projectId, api, progressCallback);
|
|
143
165
|
}
|
|
144
166
|
const successMsg = 'All required APIs are enabled.';
|
|
145
167
|
console.log(successMsg);
|