@google-cloud/cloud-run-mcp 1.7.0 → 1.9.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/.env.example +13 -0
- package/.env.gcloud-sdk-oauth +13 -0
- package/README.md +38 -0
- package/constants.js +8 -0
- package/lib/clients.js +292 -0
- package/lib/cloud-api/auth.js +1 -0
- package/lib/cloud-api/billing.js +21 -14
- package/lib/cloud-api/build.js +98 -49
- package/lib/cloud-api/helpers.js +10 -17
- package/lib/cloud-api/projects.js +16 -13
- package/lib/cloud-api/registry.js +9 -5
- package/lib/cloud-api/run.js +20 -38
- package/lib/cloud-api/storage.js +7 -7
- package/lib/deployment/deployer.js +33 -61
- package/lib/deployment/helpers.js +1 -1
- package/lib/middleware/oauth.js +92 -0
- package/lib/util/helpers.js +12 -0
- package/mcp-server.js +51 -7
- package/package.json +3 -3
- package/test/local/clients.test.js +176 -0
- package/test/local/cloud-api/build.test.js +293 -309
- package/test/local/mcp-server-stdio.test.js +0 -1
- package/test/local/mcp-server-streamable-http.test.js +69 -0
- package/test/local/mcp-server.test.js +1 -0
- package/test/local/middleware/oauth.test.js +156 -0
- package/test/local/mock-clients.js +35 -0
- package/test/local/tools.test.js +2 -2
- package/test/need-gcp/test-helpers.js +1 -1
- package/tools/register-tools.js +18 -5
- package/.kokoro/kokoro_build.sh +0 -22
- package/.kokoro/presubmit.cfg +0 -21
- package/.kokoro/run_tests.sh +0 -31
package/.env.example
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
PORT = 3000
|
|
2
|
+
GCP_STDIO = false
|
|
3
|
+
|
|
4
|
+
OAUTH_ENABLED = true
|
|
5
|
+
GOOGLE_OAUTH_CLIENT_ID = YOUR_CLIENT_ID
|
|
6
|
+
GOOGLE_OAUTH_CLIENT_SECRET = YOUR_CLIENT_SECRET
|
|
7
|
+
GOOGLE_OAUTH_AUDIENCE = YOUR_AUDIENCE # generally, same as your client id
|
|
8
|
+
GOOGLE_OAUTH_REDIRECT_URI = http://localhost:7777/oauth/callback
|
|
9
|
+
|
|
10
|
+
OAUTH_PROTECTED_RESOURCE = http://localhost:${PORT}/mcp
|
|
11
|
+
OAUTH_AUTHORIZATION_SERVER = http://localhost:${PORT}/auth/google
|
|
12
|
+
OAUTH_AUTHORIZATION_ENDPOINT = https://accounts.google.com/o/oauth2/v2/auth
|
|
13
|
+
OAUTH_TOKEN_ENDPOINT = https://oauth2.googleapis.com/token
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
PORT = 3000
|
|
2
|
+
GCP_STDIO = false
|
|
3
|
+
|
|
4
|
+
OAUTH_ENABLED = true
|
|
5
|
+
GOOGLE_OAUTH_CLIENT_ID = 32555940559.apps.googleusercontent.com
|
|
6
|
+
GOOGLE_OAUTH_CLIENT_SECRET = ZmssLNjJy2998hD4CTg2ejr2
|
|
7
|
+
GOOGLE_OAUTH_AUDIENCE = 32555940559.apps.googleusercontent.com
|
|
8
|
+
GOOGLE_OAUTH_REDIRECT_URI = http://localhost:7777/oauth/callback
|
|
9
|
+
|
|
10
|
+
OAUTH_PROTECTED_RESOURCE = http://localhost:${PORT}/mcp
|
|
11
|
+
OAUTH_AUTHORIZATION_SERVER = http://localhost:${PORT}/auth/google
|
|
12
|
+
OAUTH_AUTHORIZATION_ENDPOINT = https://accounts.google.com/o/oauth2/v2/auth
|
|
13
|
+
OAUTH_TOKEN_ENDPOINT = https://oauth2.googleapis.com/token
|
package/README.md
CHANGED
|
@@ -235,4 +235,42 @@ With this option, you will only be able to deploy code to the same Google Cloud
|
|
|
235
235
|
}
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
+
## Using MCP Server with OAuth
|
|
239
|
+
|
|
240
|
+
Cloud Run MCP server supports OAuth as an authentication mechanism. In order to use OAuth, create the OAuth client, and configure a `.env` file with the appropriate values pertaining to your OAuth client. A `.env.example` is provided for reference.
|
|
241
|
+
|
|
242
|
+
The Cloud Run MCP server works seamlessly with Google Cloud SDK OAuth client. In order to leverage the Google Cloud SDK OAuth client, you can use the `.env.gcloud-sdk-oauth` file as your `.env` file as follows:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
cp .env.gcloud-sdk-oauth .env
|
|
246
|
+
node mcp-server.js
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Configure MCP Server on Gemini CLI to use OAuth
|
|
250
|
+
|
|
251
|
+
When the Cloud Run MCP server is started in the OAuth mode, the MCP client should also be configured to use OAuth. You can setup the MCP server in OAuth mode in the Gemini CLI by using the following JSON in the `~/.gemini/settings.json` file:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"mcpServers": {
|
|
256
|
+
"cloud-run": {
|
|
257
|
+
"httpUrl": "http://localhost:3000/mcp",
|
|
258
|
+
"oauth": {
|
|
259
|
+
"enabled": true,
|
|
260
|
+
"clientId": "<OAUTH_CLIENT_ID>",
|
|
261
|
+
"clientSecret": "<OAUTH_CLIENT_SECRET>"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Post the configuration changes as shown above, start the Gemini CLI. You should authenticate the Cloud Run MCP server using the following prompt in the Gemini CLI:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
/mcp auth cloud-run
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
This will take you to the authentication page on your browser, wherein you need to sign in using the appropriate gmail id, and accept the terms and conditions. Once the authentication is succcessful, you can come back to the Gemini CLI, and the Cloud Run MCP server will be ready to use.
|
|
275
|
+
|
|
238
276
|
The Google Cloud Platform Terms of Service (available at https://cloud.google.com/terms/) and the Data Processing and Security Terms (available at https://cloud.google.com/terms/data-processing-terms) do not apply to any component of the Cloud Run MCP Server software.
|
package/constants.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const SCOPES = {
|
|
2
|
+
OPENID: 'openid',
|
|
3
|
+
EMAIL: 'https://www.googleapis.com/auth/userinfo.email',
|
|
4
|
+
CLOUD_PLATFORM: 'https://www.googleapis.com/auth/cloud-platform',
|
|
5
|
+
};
|
|
6
|
+
export const BEARER_METHODS_SUPPORTED = ['header'];
|
|
7
|
+
export const RESPONSE_TYPES_SUPPORTED = ['code'];
|
|
8
|
+
export const GCLOUD_AUTH = 'gcloud_auth';
|
package/lib/clients.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
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 { OAuth2Client } from 'google-auth-library';
|
|
18
|
+
import { GCLOUD_AUTH } from '../constants.js';
|
|
19
|
+
|
|
20
|
+
const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID;
|
|
21
|
+
const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
|
|
22
|
+
const REDIRECT_URI = process.env.GOOGLE_OAUTH_REDIRECT_URI;
|
|
23
|
+
const AUTHORIZATION_HEADER = 'Authorization';
|
|
24
|
+
const BEARER_PREFIX = 'Bearer';
|
|
25
|
+
|
|
26
|
+
function keyGenerator(projectId, accessToken) {
|
|
27
|
+
return accessToken !== GCLOUD_AUTH ? projectId + accessToken : projectId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const clients = {
|
|
31
|
+
run: new Map(),
|
|
32
|
+
builds: new Map(),
|
|
33
|
+
serviceUsage: new Map(),
|
|
34
|
+
storage: new Map(),
|
|
35
|
+
cloudBuild: new Map(),
|
|
36
|
+
artifactRegistry: new Map(),
|
|
37
|
+
logging: new Map(),
|
|
38
|
+
billing: new Map(),
|
|
39
|
+
projects: new Map(),
|
|
40
|
+
oauth: new Map(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function getAuthClient(accessToken) {
|
|
44
|
+
return {
|
|
45
|
+
getRequestHeaders: () => {
|
|
46
|
+
const headers = new Map();
|
|
47
|
+
headers.set(AUTHORIZATION_HEADER, `${BEARER_PREFIX} ${accessToken}`);
|
|
48
|
+
return headers;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wraps an OAuth2Client to be compatible with gRPC-based Google Cloud clients.
|
|
55
|
+
*
|
|
56
|
+
* Some Google Cloud SDK clients (like Cloud Run, Service Usage, etc.) use gRPC
|
|
57
|
+
* and expect the `getRequestHeaders()` method of the auth client to return a
|
|
58
|
+
* `Map` of headers. However, the standard `OAuth2Client` from `google-auth-library`
|
|
59
|
+
* returns a plain Javascript object (e.g., `{ Authorization: 'Bearer ...' }`).
|
|
60
|
+
*
|
|
61
|
+
* This wrapper uses a Proxy to intercept calls to `getRequestHeaders`. It calls
|
|
62
|
+
* the original method and converts the result from a plain object to a `Map`
|
|
63
|
+
* if necessary, ensuring compatibility with gRPC-based clients while maintaining
|
|
64
|
+
* the original behavior for other properties.
|
|
65
|
+
*
|
|
66
|
+
* @param {OAuth2Client} authClient - The original OAuth2Client instance.
|
|
67
|
+
* @returns {Proxy<OAuth2Client>} A proxy compatible with gRPC clients.
|
|
68
|
+
*/
|
|
69
|
+
function wrapForGrpc(authClient) {
|
|
70
|
+
return new Proxy(authClient, {
|
|
71
|
+
get(target, prop, receiver) {
|
|
72
|
+
if (prop === 'getRequestHeaders') {
|
|
73
|
+
return async (...args) => {
|
|
74
|
+
const headers = await target.getRequestHeaders(...args);
|
|
75
|
+
if (headers instanceof Map) {
|
|
76
|
+
return headers;
|
|
77
|
+
}
|
|
78
|
+
// Convert plain object (from OAuth2Client) to Map (expected by grpc-js)
|
|
79
|
+
const headerMap = new Map();
|
|
80
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
81
|
+
headerMap.set(k, v);
|
|
82
|
+
}
|
|
83
|
+
return headerMap;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return Reflect.get(target, prop, receiver);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Services that use HTTP/REST (or prefer Object headers) instead of gRPC (Map headers)
|
|
92
|
+
const HTTP_SERVICES = ['storage', 'logging'];
|
|
93
|
+
|
|
94
|
+
export async function getClient(
|
|
95
|
+
service,
|
|
96
|
+
key,
|
|
97
|
+
loadClient,
|
|
98
|
+
options = {},
|
|
99
|
+
accessToken = null
|
|
100
|
+
) {
|
|
101
|
+
if (!clients[service].has(key)) {
|
|
102
|
+
const ClientClass = await loadClient();
|
|
103
|
+
const finalOptions = { ...options };
|
|
104
|
+
if (accessToken && accessToken !== GCLOUD_AUTH) {
|
|
105
|
+
const oauthClient = await getOAuthClient(accessToken);
|
|
106
|
+
// Storage and Logging use HTTP and expect the native OAuth2Client (headers as Object).
|
|
107
|
+
// Other services (Run, ServiceUsage, etc.) use gRPC and expect headers as Map.
|
|
108
|
+
if (HTTP_SERVICES.includes(service)) {
|
|
109
|
+
finalOptions.authClient = oauthClient;
|
|
110
|
+
} else {
|
|
111
|
+
finalOptions.authClient = wrapForGrpc(oauthClient);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
clients[service].set(key, new ClientClass(finalOptions));
|
|
115
|
+
}
|
|
116
|
+
return clients[service].get(key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Gets a Cloud Run Services Client for the specified project.
|
|
121
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
122
|
+
* @returns {Promise<import('@google-cloud/run').v2.ServicesClient>}
|
|
123
|
+
*/
|
|
124
|
+
export async function getRunClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
125
|
+
const key = keyGenerator(projectId, accessToken);
|
|
126
|
+
return getClient(
|
|
127
|
+
'run',
|
|
128
|
+
key,
|
|
129
|
+
async () => (await import('@google-cloud/run')).v2.ServicesClient,
|
|
130
|
+
{ projectId },
|
|
131
|
+
accessToken
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Gets a Cloud Run Build Client for the specified project.
|
|
137
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
138
|
+
* @returns {Promise<import('@google-cloud/run').v2.BuildsClient>}
|
|
139
|
+
*/
|
|
140
|
+
export async function getBuildsClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
141
|
+
const key = keyGenerator(projectId, accessToken);
|
|
142
|
+
return getClient(
|
|
143
|
+
'builds',
|
|
144
|
+
key,
|
|
145
|
+
async () => (await import('@google-cloud/run')).v2.BuildsClient,
|
|
146
|
+
{ projectId },
|
|
147
|
+
accessToken
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Gets a Service Usage Client for the specified project.
|
|
153
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
154
|
+
* @returns {Promise<import('@google-cloud/service-usage').ServiceUsageClient>}
|
|
155
|
+
*/
|
|
156
|
+
export async function getServiceUsageClient(
|
|
157
|
+
projectId,
|
|
158
|
+
accessToken = GCLOUD_AUTH
|
|
159
|
+
) {
|
|
160
|
+
const key = keyGenerator(projectId, accessToken);
|
|
161
|
+
return getClient(
|
|
162
|
+
'serviceUsage',
|
|
163
|
+
key,
|
|
164
|
+
async () =>
|
|
165
|
+
(await import('@google-cloud/service-usage')).ServiceUsageClient,
|
|
166
|
+
{ projectId },
|
|
167
|
+
accessToken
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Gets a Storage Client for the specified project.
|
|
173
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
174
|
+
* @returns {Promise<import('@google-cloud/storage').Storage>}
|
|
175
|
+
*/
|
|
176
|
+
export async function getStorageClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
177
|
+
const key = keyGenerator(projectId, accessToken);
|
|
178
|
+
return getClient(
|
|
179
|
+
'storage',
|
|
180
|
+
key,
|
|
181
|
+
async () => (await import('@google-cloud/storage')).Storage,
|
|
182
|
+
{ projectId },
|
|
183
|
+
accessToken
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Gets a Cloud Build Client for the specified project.
|
|
189
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
190
|
+
* @returns {Promise<import('@google-cloud/cloudbuild').CloudBuildClient>}
|
|
191
|
+
*/
|
|
192
|
+
export async function getCloudBuildClient(
|
|
193
|
+
projectId,
|
|
194
|
+
accessToken = GCLOUD_AUTH
|
|
195
|
+
) {
|
|
196
|
+
const key = keyGenerator(projectId, accessToken);
|
|
197
|
+
return getClient(
|
|
198
|
+
'cloudBuild',
|
|
199
|
+
key,
|
|
200
|
+
async () => (await import('@google-cloud/cloudbuild')).CloudBuildClient,
|
|
201
|
+
{ projectId },
|
|
202
|
+
accessToken
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets an Artifact Registry Client for the specified project.
|
|
208
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
209
|
+
* @returns {Promise<import('@google-cloud/artifact-registry').ArtifactRegistryClient>}
|
|
210
|
+
*/
|
|
211
|
+
export async function getArtifactRegistryClient(
|
|
212
|
+
projectId,
|
|
213
|
+
accessToken = GCLOUD_AUTH
|
|
214
|
+
) {
|
|
215
|
+
const key = keyGenerator(projectId, accessToken);
|
|
216
|
+
return getClient(
|
|
217
|
+
'artifactRegistry',
|
|
218
|
+
key,
|
|
219
|
+
async () =>
|
|
220
|
+
(await import('@google-cloud/artifact-registry')).ArtifactRegistryClient,
|
|
221
|
+
{ projectId },
|
|
222
|
+
accessToken
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Gets a Logging Client for the specified project.
|
|
228
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
229
|
+
* @returns {Promise<import('@google-cloud/logging').Logging>}
|
|
230
|
+
*/
|
|
231
|
+
export async function getLoggingClient(projectId, accessToken = GCLOUD_AUTH) {
|
|
232
|
+
const key = keyGenerator(projectId, accessToken);
|
|
233
|
+
return getClient(
|
|
234
|
+
'logging',
|
|
235
|
+
key,
|
|
236
|
+
async () => (await import('@google-cloud/logging')).Logging,
|
|
237
|
+
{ projectId },
|
|
238
|
+
accessToken
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Gets a Billing Client for the specified project.
|
|
244
|
+
* Note: BillingClient usually doesn't take projectId in constructor for listing accounts,
|
|
245
|
+
* but might for project billing info. We will cache by projectId anyway or 'global' if projectId is null.
|
|
246
|
+
* @param {string} [projectId] - The Google Cloud project ID (optional).
|
|
247
|
+
* @returns {Promise<import('@google-cloud/billing').CloudBillingClient>}
|
|
248
|
+
*/
|
|
249
|
+
export async function getBillingClient(
|
|
250
|
+
projectId = 'global',
|
|
251
|
+
accessToken = GCLOUD_AUTH
|
|
252
|
+
) {
|
|
253
|
+
const key = keyGenerator(projectId, accessToken);
|
|
254
|
+
return getClient(
|
|
255
|
+
'billing',
|
|
256
|
+
key,
|
|
257
|
+
async () => (await import('@google-cloud/billing')).CloudBillingClient,
|
|
258
|
+
projectId !== 'global' ? { projectId } : {},
|
|
259
|
+
accessToken
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Gets a Projects Client (Resource Manager).
|
|
265
|
+
* @returns {Promise<import('@google-cloud/resource-manager').ProjectsClient>}
|
|
266
|
+
*/
|
|
267
|
+
export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
|
|
268
|
+
const key = keyGenerator('global', accessToken);
|
|
269
|
+
return getClient(
|
|
270
|
+
'projects',
|
|
271
|
+
key,
|
|
272
|
+
async () => (await import('@google-cloud/resource-manager')).ProjectsClient,
|
|
273
|
+
{},
|
|
274
|
+
accessToken
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Gets an OAuth2 Client for the specified access token.
|
|
280
|
+
* @param {string} accessToken - The access token.
|
|
281
|
+
* @returns {Promise<OAuth2Client>}
|
|
282
|
+
*/
|
|
283
|
+
export async function getOAuthClient(accessToken) {
|
|
284
|
+
// Use the access token itself as the key since it's unique per session/user
|
|
285
|
+
if (!clients.oauth.has(accessToken)) {
|
|
286
|
+
const client = new OAuth2Client(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
|
|
287
|
+
client.setCredentials({ access_token: accessToken });
|
|
288
|
+
clients.oauth.set(accessToken, client);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return clients.oauth.get(accessToken);
|
|
292
|
+
}
|
package/lib/cloud-api/auth.js
CHANGED
|
@@ -23,6 +23,7 @@ import { GoogleAuth } from 'google-auth-library';
|
|
|
23
23
|
* @async
|
|
24
24
|
* @returns {Promise<boolean>} A promise that resolves to true if GCP auth are found, and false otherwise.
|
|
25
25
|
*/
|
|
26
|
+
//TODO: Rename ensureGCPCredentials to ensureADCCredentials
|
|
26
27
|
export async function ensureGCPCredentials() {
|
|
27
28
|
console.error('Checking for Google Cloud Application Default Credentials...');
|
|
28
29
|
try {
|
package/lib/cloud-api/billing.js
CHANGED
|
@@ -21,9 +21,17 @@ limitations under the License.
|
|
|
21
21
|
* @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
|
|
22
22
|
* each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
import { getBillingClient } from '../clients.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Lists all accessible Google Cloud Billing Accounts.
|
|
28
|
+
* @async
|
|
29
|
+
* @function listBillingAccounts
|
|
30
|
+
* @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
|
|
31
|
+
* each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
|
|
32
|
+
*/
|
|
33
|
+
export async function listBillingAccounts(accessToken) {
|
|
34
|
+
const client = await getBillingClient('global', accessToken);
|
|
27
35
|
try {
|
|
28
36
|
const [accounts] = await client.listBillingAccounts();
|
|
29
37
|
if (!accounts || accounts.length === 0) {
|
|
@@ -51,10 +59,10 @@ export async function listBillingAccounts() {
|
|
|
51
59
|
*/
|
|
52
60
|
export async function attachProjectToBillingAccount(
|
|
53
61
|
projectId,
|
|
54
|
-
billingAccountName
|
|
62
|
+
billingAccountName,
|
|
63
|
+
accessToken
|
|
55
64
|
) {
|
|
56
|
-
const
|
|
57
|
-
const client = new CloudBillingClient();
|
|
65
|
+
const client = await getBillingClient(projectId, accessToken);
|
|
58
66
|
const projectName = `projects/${projectId}`;
|
|
59
67
|
|
|
60
68
|
if (!projectId) {
|
|
@@ -104,9 +112,8 @@ export async function attachProjectToBillingAccount(
|
|
|
104
112
|
* @param {string} projectId - The ID of the project to check.
|
|
105
113
|
* @returns {Promise<boolean>} A promise that resolves to true if billing is enabled, false otherwise.
|
|
106
114
|
*/
|
|
107
|
-
export async function isBillingEnabled(projectId) {
|
|
108
|
-
const
|
|
109
|
-
const client = new CloudBillingClient();
|
|
115
|
+
export async function isBillingEnabled(projectId, accessToken) {
|
|
116
|
+
const client = await getBillingClient(projectId, accessToken);
|
|
110
117
|
const projectName = `projects/${projectId}`;
|
|
111
118
|
try {
|
|
112
119
|
// getProjectBillingInfo requires cloudbilling.googleapis.com API to check billing status
|
|
@@ -127,18 +134,17 @@ export async function isBillingEnabled(projectId) {
|
|
|
127
134
|
/**
|
|
128
135
|
* Ensures billing is enabled for a project. If not, attempts to enable it
|
|
129
136
|
* by attaching the single open billing account if one is found.
|
|
130
|
-
* @param {object} context - The context object containing clients and other parameters.
|
|
131
137
|
* @param {string} projectId The project ID to check billing for.
|
|
132
138
|
* @param {function} progressCallback A callback function for progress updates.
|
|
133
139
|
*/
|
|
134
140
|
export async function ensureBillingEnabled(
|
|
135
|
-
context,
|
|
136
141
|
projectId,
|
|
142
|
+
accessToken,
|
|
137
143
|
progressCallback
|
|
138
144
|
) {
|
|
139
|
-
if (!(await isBillingEnabled(projectId))) {
|
|
145
|
+
if (!(await isBillingEnabled(projectId, accessToken))) {
|
|
140
146
|
// Billing is disabled, try to fix it.
|
|
141
|
-
const accounts = await listBillingAccounts();
|
|
147
|
+
const accounts = await listBillingAccounts(accessToken);
|
|
142
148
|
|
|
143
149
|
if (accounts && accounts.length === 1 && accounts[0].open) {
|
|
144
150
|
// Exactly one open account found, try to attach it.
|
|
@@ -150,7 +156,8 @@ export async function ensureBillingEnabled(
|
|
|
150
156
|
|
|
151
157
|
const attachmentResult = await attachProjectToBillingAccount(
|
|
152
158
|
projectId,
|
|
153
|
-
account.name
|
|
159
|
+
account.name,
|
|
160
|
+
accessToken
|
|
154
161
|
);
|
|
155
162
|
|
|
156
163
|
if (!attachmentResult || !attachmentResult.billingEnabled) {
|
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,
|
|
@@ -47,67 +53,111 @@ export async function triggerCloudBuild(
|
|
|
47
53
|
targetRepoName,
|
|
48
54
|
targetImageUrl,
|
|
49
55
|
hasDockerfile,
|
|
56
|
+
accessToken,
|
|
50
57
|
progressCallback
|
|
51
58
|
) {
|
|
59
|
+
const serviceName = targetImageUrl.split('/').pop().split(/[:@]/)[0];
|
|
60
|
+
const parent = `projects/${projectId}/locations/${location}`;
|
|
61
|
+
const serviceFullName = `${parent}/services/${serviceName}`;
|
|
62
|
+
const buildsClient = await getBuildsClient(projectId, accessToken);
|
|
63
|
+
const runClient = await getRunClient(projectId, accessToken);
|
|
64
|
+
const serviceExists = await checkCloudRunServiceExists(
|
|
65
|
+
projectId,
|
|
66
|
+
location,
|
|
67
|
+
serviceName,
|
|
68
|
+
accessToken,
|
|
69
|
+
progressCallback
|
|
70
|
+
);
|
|
71
|
+
const cloudBuildClient = await getCloudBuildClient(projectId, accessToken);
|
|
72
|
+
const loggingClient = await getLoggingClient(projectId, accessToken);
|
|
52
73
|
let buildSteps;
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const servicePatch = {
|
|
76
|
+
template: {
|
|
77
|
+
containers: [{ image: targetImageUrl }],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
if (serviceExists) {
|
|
83
|
+
await logAndProgress(
|
|
84
|
+
`Performing dry-run update for service ${serviceName}...`,
|
|
85
|
+
progressCallback
|
|
86
|
+
);
|
|
87
|
+
servicePatch.name = serviceFullName;
|
|
88
|
+
await callWithRetry(
|
|
89
|
+
() =>
|
|
90
|
+
runClient.updateService({
|
|
91
|
+
service: servicePatch,
|
|
92
|
+
validateOnly: true,
|
|
93
|
+
}),
|
|
94
|
+
'run.updateService-dryrun'
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
await logAndProgress(
|
|
98
|
+
`Performing dry-run creation for service ${serviceName}...`,
|
|
99
|
+
progressCallback
|
|
100
|
+
);
|
|
101
|
+
await callWithRetry(
|
|
102
|
+
() =>
|
|
103
|
+
runClient.createService({
|
|
104
|
+
parent: parent,
|
|
105
|
+
service: servicePatch,
|
|
106
|
+
serviceId: serviceName,
|
|
107
|
+
validateOnly: true,
|
|
108
|
+
}),
|
|
109
|
+
'run.createService-dryrun'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
await logAndProgress(
|
|
113
|
+
`Dry-run validation successful.`,
|
|
114
|
+
progressCallback,
|
|
115
|
+
'debug'
|
|
116
|
+
);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
await logAndProgress(
|
|
119
|
+
`Dry-run validation failed for service ${serviceName}.`,
|
|
120
|
+
progressCallback,
|
|
121
|
+
'error'
|
|
122
|
+
);
|
|
123
|
+
throw new Error(`Dry-run deployment failed: ${err.message}`);
|
|
76
124
|
}
|
|
77
125
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
126
|
+
const request = {
|
|
127
|
+
parent: `projects/${projectId}/locations/${location}`,
|
|
128
|
+
storageSource: {
|
|
129
|
+
bucket: sourceBucketName,
|
|
130
|
+
object: sourceBlobName,
|
|
84
131
|
},
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
imageUri: targetImageUrl,
|
|
133
|
+
client: 'cloud-run-mcp',
|
|
87
134
|
};
|
|
135
|
+
if (hasDockerfile) {
|
|
136
|
+
request.dockerBuild = {};
|
|
137
|
+
} else {
|
|
138
|
+
request.buildpackBuild = {};
|
|
139
|
+
}
|
|
88
140
|
|
|
89
141
|
await logAndProgress(
|
|
90
142
|
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
91
143
|
progressCallback
|
|
92
144
|
);
|
|
93
|
-
const [operation] = await callWithRetry(
|
|
94
|
-
() =>
|
|
95
|
-
context.cloudBuildClient.createBuild({
|
|
96
|
-
projectId: projectId,
|
|
97
|
-
build: build,
|
|
98
|
-
}),
|
|
99
|
-
'cloudBuild.createBuild'
|
|
100
|
-
);
|
|
101
145
|
|
|
146
|
+
const [submitBuildResponse] = await callWithRetry(
|
|
147
|
+
() => buildsClient.submitBuild(request),
|
|
148
|
+
'builds.submitBuild'
|
|
149
|
+
);
|
|
102
150
|
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
103
|
-
const
|
|
151
|
+
const encodedBuildId = submitBuildResponse.buildOperation.name
|
|
152
|
+
.split('/')
|
|
153
|
+
.pop();
|
|
154
|
+
const buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
|
|
104
155
|
let completedBuild;
|
|
105
156
|
while (true) {
|
|
106
157
|
const [getBuildOperation] = await callWithRetry(
|
|
107
158
|
() =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
id: buildId,
|
|
159
|
+
cloudBuildClient.getBuild({
|
|
160
|
+
name: `projects/${projectId}/locations/${location}/builds/${buildId}`,
|
|
111
161
|
}),
|
|
112
162
|
`cloudBuild.getBuild ${buildId}`
|
|
113
163
|
);
|
|
@@ -132,10 +182,9 @@ export async function triggerCloudBuild(
|
|
|
132
182
|
`Cloud Build job ${buildId} completed successfully.`,
|
|
133
183
|
progressCallback
|
|
134
184
|
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
185
|
+
const builtImage = completedBuild.results.images[0].name;
|
|
186
|
+
await logAndProgress(`Image built: ${builtImage}`, progressCallback);
|
|
187
|
+
|
|
139
188
|
return completedBuild;
|
|
140
189
|
} else {
|
|
141
190
|
const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
|
|
@@ -160,7 +209,7 @@ export async function triggerCloudBuild(
|
|
|
160
209
|
// Fetch the most recent N log entries
|
|
161
210
|
const [entries] = await callWithRetry(
|
|
162
211
|
() =>
|
|
163
|
-
|
|
212
|
+
loggingClient.getEntries({
|
|
164
213
|
filter: logFilter,
|
|
165
214
|
orderBy: 'timestamp desc', // Get latest logs first
|
|
166
215
|
pageSize: BUILD_LOGS_LINES_TO_FETCH,
|