@google-cloud/cloud-run-mcp 1.0.0 → 1.3.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/local-tests.yml +17 -0
- package/.github/workflows/npm-publish.yml +23 -0
- package/CONTRIBUTING.md +14 -0
- package/README.md +8 -7
- package/gemini-extension/GEMINI.md +57 -0
- package/gemini-extension.json +3 -2
- package/lib/cloud-run-deploy.js +90 -107
- package/lib/cloud-run-services.js +16 -4
- package/lib/gcp-api-helpers.js +143 -0
- package/lib/gcp-auth-check.js +4 -4
- package/lib/gcp-projects.js +1 -1
- package/mcp-server.js +5 -2
- package/package.json +9 -9
- package/test/local/gemini-extension.test.js +35 -0
- package/test/local/npx.test.js +36 -0
- package/test/{test-prompts.js → local/prompts.test.js} +1 -1
- package/test/{test-tools.js → local/tools.test.js} +62 -31
- package/test/{test-deploy.js → need-gcp/cloud-run-deploy.test.js} +1 -1
- package/test/{test-service-logs.js → need-gcp/cloud-run-services.test.js} +1 -1
- package/test/{test-gcp-auth.js → need-gcp/gcp-auth-check.test.js} +17 -106
- package/test/{test-create-project.js → need-gcp/gcp-projects.test.js} +1 -1
- package/tools.js +215 -182
- package/test/run-all-tests.js +0 -19
- /package/test/{test-mcp-server.js → local/mcp-server.test.js} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Run Local Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [ main ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: 20
|
|
16
|
+
- run: npm ci
|
|
17
|
+
- run: npm test
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish-npm:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
|
+
with:
|
|
14
|
+
node-version: 20
|
|
15
|
+
- run: npm ci
|
|
16
|
+
# Now configure with the publish service for install.
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 20
|
|
20
|
+
registry-url: https://wombat-dressing-room.appspot.com/
|
|
21
|
+
- run: npm publish
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
|
package/CONTRIBUTING.md
CHANGED
|
@@ -105,3 +105,17 @@ npm run test:deploy
|
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
This script requires an existing Google Cloud Project ID to be provided when prompted or as a command-line argument.
|
|
108
|
+
|
|
109
|
+
## Publishing to npm
|
|
110
|
+
|
|
111
|
+
To update the [npm package]https://www.npmjs.com/package/@google-cloud/cloud-run-mcp)
|
|
112
|
+
|
|
113
|
+
Run the following:
|
|
114
|
+
|
|
115
|
+
* `git checkout -b new-release`
|
|
116
|
+
* `npm test`
|
|
117
|
+
* `npm version minor`
|
|
118
|
+
* `git push --follow-tags`
|
|
119
|
+
* On GitHub, [create a new release](https://github.com/GoogleCloudPlatform/cloud-run-mcp/releases/new) for this tag.
|
|
120
|
+
|
|
121
|
+
Then a [GitHub Action]https://github.com/GoogleCloudPlatform/cloud-run-mcp/blob/main/.github/workflows/npm-publish.yml) automatically publishes to npm when new releases are pushes.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Enable MCP-compatible AI agents to deploy apps to Cloud Run.
|
|
|
6
6
|
"mcpServers":{
|
|
7
7
|
"cloud-run": {
|
|
8
8
|
"command": "npx",
|
|
9
|
-
"args": ["-y", "
|
|
9
|
+
"args": ["-y", "@google-cloud/cloud-run-mcp"]
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
```
|
|
@@ -72,7 +72,7 @@ Then configure the MCP server using either Node.js or Docker:
|
|
|
72
72
|
```json
|
|
73
73
|
"cloud-run": {
|
|
74
74
|
"command": "npx",
|
|
75
|
-
"args": ["-y", "
|
|
75
|
+
"args": ["-y", "@google-cloud/cloud-run-mcp"]
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
@@ -81,7 +81,7 @@ Then configure the MCP server using either Node.js or Docker:
|
|
|
81
81
|
```json
|
|
82
82
|
"cloud-run": {
|
|
83
83
|
"command": "npx",
|
|
84
|
-
"args": ["-y", "
|
|
84
|
+
"args": ["-y", "@google-cloud/cloud-run-mcp"],
|
|
85
85
|
"env": {
|
|
86
86
|
"GOOGLE_CLOUD_PROJECT": "PROJECT_NAME",
|
|
87
87
|
"GOOGLE_CLOUD_REGION": "PROJECT_REGION",
|
|
@@ -118,13 +118,14 @@ See Docker's [MCP catalog](https://hub.docker.com/mcp/server/cloud-run-mcp/overv
|
|
|
118
118
|
}
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
## Use as a
|
|
121
|
+
## Use as a Gemini CLI extension
|
|
122
122
|
|
|
123
|
-
To install this as a
|
|
123
|
+
To install this as a [Gemini CLI](https://github.com/google-gemini/gemini-cli) extension, run the following command:
|
|
124
124
|
|
|
125
125
|
```bash
|
|
126
|
-
mkdir -p ~/.gemini/extensions/cloud-run && \
|
|
127
|
-
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json
|
|
126
|
+
mkdir -p ~/.gemini/extensions/cloud-run/gemini-extension && \
|
|
127
|
+
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension.json > ~/.gemini/extensions/cloud-run/gemini-extension.json && \
|
|
128
|
+
curl -s -L https://raw.githubusercontent.com/GoogleCloudPlatform/cloud-run-mcp/main/gemini-extension/Gemini.md > ~/.gemini/extensions/cloud-run/gemini-extension/Gemini.md
|
|
128
129
|
```
|
|
129
130
|
|
|
130
131
|
## Use as remote MCP server
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Cloud Run MCP Server
|
|
2
|
+
|
|
3
|
+
## Code that can be deployed
|
|
4
|
+
|
|
5
|
+
Only web servers can be deployed using this MCP server.
|
|
6
|
+
The code needs to listen for HTTP reqeusts on the port defined by the $PORT environment variable or 8080.
|
|
7
|
+
|
|
8
|
+
### Supported languages
|
|
9
|
+
|
|
10
|
+
- If the code is in Node.js, Python, Go, Java, .NET, PHP, Ruby, a Dockerfile is not needed.
|
|
11
|
+
- If the code is in another language, or has any custom dependency needs, a Dockerfile is needed.
|
|
12
|
+
|
|
13
|
+
### Static-only apps
|
|
14
|
+
|
|
15
|
+
To deploy static-only applications, create a Dockerfile that serves these static files. For example using `nginx`:
|
|
16
|
+
|
|
17
|
+
`Dockerfile`
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
FROM nginx:stable
|
|
21
|
+
|
|
22
|
+
COPY ./static /var/www
|
|
23
|
+
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
|
24
|
+
|
|
25
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`nginx.conf`:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
server {
|
|
32
|
+
listen 8080;
|
|
33
|
+
server_name _;
|
|
34
|
+
|
|
35
|
+
root /var/www/;
|
|
36
|
+
index index.html;
|
|
37
|
+
|
|
38
|
+
# Force all paths to load either itself (js files) or go through index.html.
|
|
39
|
+
location / {
|
|
40
|
+
try_files $uri /index.html;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Google Cloud pre-requisities
|
|
46
|
+
|
|
47
|
+
The user must have an existing Google Cloud account with billing set up, and ideally an existing Google Cloud project.
|
|
48
|
+
|
|
49
|
+
If deployment fails because of an access or IAM error, it is likely that the user doesn't have Google Cloud credentials on the local machine.
|
|
50
|
+
The user mush follow these steps:
|
|
51
|
+
|
|
52
|
+
1. Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) and authenticate with their Google account.
|
|
53
|
+
|
|
54
|
+
2. Set up application credentials using the command:
|
|
55
|
+
```bash
|
|
56
|
+
gcloud auth application-default login
|
|
57
|
+
```
|
package/gemini-extension.json
CHANGED
package/lib/cloud-run-deploy.js
CHANGED
|
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
|
|
18
|
+
|
|
17
19
|
// Configuration
|
|
18
20
|
const REPO_NAME = 'mcp-cloud-run-deployments';
|
|
19
21
|
const ZIP_FILE_NAME = 'source.zip';
|
|
@@ -63,50 +65,6 @@ function logAndProgress(message, progressCallback, severity = 'info') {
|
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
|
|
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
68
|
/**
|
|
111
69
|
* Checks if a Cloud Run service already exists.
|
|
112
70
|
*
|
|
@@ -127,7 +85,10 @@ async function checkCloudRunServiceExists(
|
|
|
127
85
|
const parent = runClient.locationPath(projectId, location);
|
|
128
86
|
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
129
87
|
try {
|
|
130
|
-
await
|
|
88
|
+
await callWithRetry(
|
|
89
|
+
() => runClient.getService({ name: servicePath }),
|
|
90
|
+
`getService ${serviceId}`
|
|
91
|
+
);
|
|
131
92
|
logAndProgress(
|
|
132
93
|
`Cloud Run service ${serviceId} already exists.`,
|
|
133
94
|
progressCallback
|
|
@@ -208,17 +169,25 @@ async function deployToCloudRun(
|
|
|
208
169
|
|
|
209
170
|
if (exists) {
|
|
210
171
|
dryRunServiceConfig.name = servicePath;
|
|
211
|
-
await
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
172
|
+
await callWithRetry(
|
|
173
|
+
() =>
|
|
174
|
+
runClient.updateService({
|
|
175
|
+
service: dryRunServiceConfig,
|
|
176
|
+
validateOnly: true,
|
|
177
|
+
}),
|
|
178
|
+
`updateService (dry run) ${serviceId}`
|
|
179
|
+
);
|
|
215
180
|
} else {
|
|
216
|
-
await
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
181
|
+
await callWithRetry(
|
|
182
|
+
() =>
|
|
183
|
+
runClient.createService({
|
|
184
|
+
parent: parent,
|
|
185
|
+
service: dryRunServiceConfig,
|
|
186
|
+
serviceId: serviceId,
|
|
187
|
+
validateOnly: true,
|
|
188
|
+
}),
|
|
189
|
+
`createService (dry run) ${serviceId}`
|
|
190
|
+
);
|
|
222
191
|
}
|
|
223
192
|
logAndProgress(
|
|
224
193
|
`Dry run successful for ${serviceId} with current configuration.`,
|
|
@@ -261,14 +230,21 @@ async function deployToCloudRun(
|
|
|
261
230
|
progressCallback
|
|
262
231
|
);
|
|
263
232
|
service.name = servicePath;
|
|
264
|
-
[operation] = await
|
|
233
|
+
[operation] = await callWithRetry(
|
|
234
|
+
() => runClient.updateService({ service }),
|
|
235
|
+
`updateService ${serviceId}`
|
|
236
|
+
);
|
|
265
237
|
} else {
|
|
266
238
|
logAndProgress(`Creating new service ${serviceId}...`, progressCallback);
|
|
267
|
-
[operation] = await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
239
|
+
[operation] = await callWithRetry(
|
|
240
|
+
() =>
|
|
241
|
+
runClient.createService({
|
|
242
|
+
parent: parent,
|
|
243
|
+
service: service, // 'service' object might have invokerIamDisabled removed
|
|
244
|
+
serviceId: serviceId,
|
|
245
|
+
}),
|
|
246
|
+
`createService ${serviceId}`
|
|
247
|
+
);
|
|
272
248
|
}
|
|
273
249
|
|
|
274
250
|
logAndProgress(`Deploying ${serviceId} to Cloud Run...`, progressCallback);
|
|
@@ -305,7 +281,10 @@ async function ensureStorageBucketExists(
|
|
|
305
281
|
) {
|
|
306
282
|
const bucket = storage.bucket(bucketName);
|
|
307
283
|
try {
|
|
308
|
-
const [exists] = await
|
|
284
|
+
const [exists] = await callWithRetry(
|
|
285
|
+
() => bucket.exists(),
|
|
286
|
+
`storage.bucket.exists ${bucketName}`
|
|
287
|
+
);
|
|
309
288
|
if (exists) {
|
|
310
289
|
logAndProgress(`Bucket ${bucketName} already exists.`, progressCallback);
|
|
311
290
|
return bucket;
|
|
@@ -315,9 +294,10 @@ async function ensureStorageBucketExists(
|
|
|
315
294
|
progressCallback
|
|
316
295
|
);
|
|
317
296
|
try {
|
|
318
|
-
const [createdBucket] = await
|
|
319
|
-
location: location,
|
|
320
|
-
|
|
297
|
+
const [createdBucket] = await callWithRetry(
|
|
298
|
+
() => storage.createBucket(bucketName, { location: location }),
|
|
299
|
+
`storage.createBucket ${bucketName}`
|
|
300
|
+
);
|
|
321
301
|
logAndProgress(
|
|
322
302
|
`Storage bucket ${createdBucket.name} created successfully in ${location}.`,
|
|
323
303
|
progressCallback
|
|
@@ -441,7 +421,10 @@ async function uploadToStorageBucket(
|
|
|
441
421
|
`Uploading buffer to gs://${bucket.name}/${destinationBlobName}...`,
|
|
442
422
|
progressCallback
|
|
443
423
|
);
|
|
444
|
-
await
|
|
424
|
+
await callWithRetry(
|
|
425
|
+
() => bucket.file(destinationBlobName).save(buffer),
|
|
426
|
+
`storage.bucket.file.save ${destinationBlobName}`
|
|
427
|
+
);
|
|
445
428
|
logAndProgress(
|
|
446
429
|
`File ${destinationBlobName} uploaded successfully to gs://${bucket.name}/${destinationBlobName}.`,
|
|
447
430
|
progressCallback
|
|
@@ -483,9 +466,10 @@ async function ensureArtifactRegistryRepoExists(
|
|
|
483
466
|
);
|
|
484
467
|
|
|
485
468
|
try {
|
|
486
|
-
const [repository] = await
|
|
487
|
-
name: repoPath,
|
|
488
|
-
|
|
469
|
+
const [repository] = await callWithRetry(
|
|
470
|
+
() => artifactRegistryClient.getRepository({ name: repoPath }),
|
|
471
|
+
`artifactRegistry.getRepository ${repositoryId}`
|
|
472
|
+
);
|
|
489
473
|
logAndProgress(
|
|
490
474
|
`Repository ${repositoryId} already exists in ${location}.`,
|
|
491
475
|
progressCallback
|
|
@@ -500,31 +484,26 @@ async function ensureArtifactRegistryRepoExists(
|
|
|
500
484
|
const repositoryToCreate = {
|
|
501
485
|
format: format,
|
|
502
486
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
createError
|
|
524
|
-
);
|
|
525
|
-
logAndProgress(errorMessage, progressCallback, 'error');
|
|
526
|
-
throw createError;
|
|
527
|
-
}
|
|
487
|
+
|
|
488
|
+
const [operation] = await callWithRetry(
|
|
489
|
+
() =>
|
|
490
|
+
artifactRegistryClient.createRepository({
|
|
491
|
+
parent: parent,
|
|
492
|
+
repository: repositoryToCreate,
|
|
493
|
+
repositoryId: repositoryId,
|
|
494
|
+
}),
|
|
495
|
+
`artifactRegistry.createRepository ${repositoryId}`
|
|
496
|
+
);
|
|
497
|
+
logAndProgress(
|
|
498
|
+
`Creating Artifact Registry repository ${repositoryId}...`,
|
|
499
|
+
progressCallback
|
|
500
|
+
);
|
|
501
|
+
const [result] = await operation.promise();
|
|
502
|
+
logAndProgress(
|
|
503
|
+
`Artifact Registry repository ${result.name} created successfully.`,
|
|
504
|
+
progressCallback
|
|
505
|
+
);
|
|
506
|
+
return result;
|
|
528
507
|
} else {
|
|
529
508
|
const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
|
|
530
509
|
console.error(
|
|
@@ -605,19 +584,19 @@ async function triggerCloudBuild(
|
|
|
605
584
|
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
606
585
|
progressCallback
|
|
607
586
|
);
|
|
608
|
-
const [operation] = await
|
|
609
|
-
projectId: projectId,
|
|
610
|
-
|
|
611
|
-
|
|
587
|
+
const [operation] = await callWithRetry(
|
|
588
|
+
() => cloudBuildClient.createBuild({ projectId: projectId, build: build }),
|
|
589
|
+
'cloudBuild.createBuild'
|
|
590
|
+
);
|
|
612
591
|
|
|
613
592
|
logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
614
593
|
const buildId = operation.metadata.build.id;
|
|
615
594
|
let completedBuild;
|
|
616
595
|
while (true) {
|
|
617
|
-
const [getBuildOperation] = await
|
|
618
|
-
projectId: projectId,
|
|
619
|
-
|
|
620
|
-
|
|
596
|
+
const [getBuildOperation] = await callWithRetry(
|
|
597
|
+
() => cloudBuildClient.getBuild({ projectId: projectId, id: buildId }),
|
|
598
|
+
`cloudBuild.getBuild ${buildId}`
|
|
599
|
+
);
|
|
621
600
|
if (
|
|
622
601
|
['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
|
|
623
602
|
getBuildOperation.status
|
|
@@ -668,11 +647,15 @@ async function triggerCloudBuild(
|
|
|
668
647
|
);
|
|
669
648
|
|
|
670
649
|
// Fetch the most recent N log entries
|
|
671
|
-
const [entries] = await
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
650
|
+
const [entries] = await callWithRetry(
|
|
651
|
+
() =>
|
|
652
|
+
loggingClient.getEntries({
|
|
653
|
+
filter: logFilter,
|
|
654
|
+
orderBy: 'timestamp desc', // Get latest logs first
|
|
655
|
+
pageSize: BUILD_LOGS_LINES_TO_FETCH,
|
|
656
|
+
}),
|
|
657
|
+
`logging.getEntries for build ${buildId}`
|
|
658
|
+
);
|
|
676
659
|
|
|
677
660
|
if (entries && entries.length > 0) {
|
|
678
661
|
// Entries are newest first, reverse for chronological order of the snippet
|
|
@@ -15,6 +15,7 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import protofiles from 'google-proto-files';
|
|
18
|
+
import { callWithRetry, ensureApisEnabled } from './gcp-api-helpers.js';
|
|
18
19
|
|
|
19
20
|
let runClient;
|
|
20
21
|
let loggingClient;
|
|
@@ -32,13 +33,19 @@ export async function listServices(projectId, location) {
|
|
|
32
33
|
const { ServicesClient } = v2;
|
|
33
34
|
runClient = new ServicesClient({ projectId });
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
await ensureApisEnabled(projectId, ['run.googleapis.com']);
|
|
38
|
+
|
|
35
39
|
const parent = runClient.locationPath(projectId, location);
|
|
36
40
|
|
|
37
41
|
try {
|
|
38
42
|
console.log(
|
|
39
43
|
`Listing Cloud Run services in project ${projectId}, location ${location}...`
|
|
40
44
|
);
|
|
41
|
-
const [services] = await
|
|
45
|
+
const [services] = await callWithRetry(
|
|
46
|
+
() => runClient.listServices({ parent }),
|
|
47
|
+
'listServices'
|
|
48
|
+
);
|
|
42
49
|
return services;
|
|
43
50
|
} catch (error) {
|
|
44
51
|
console.error(`Error listing Cloud Run services:`, error);
|
|
@@ -66,7 +73,10 @@ export async function getService(projectId, location, serviceId) {
|
|
|
66
73
|
console.log(
|
|
67
74
|
`Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
|
|
68
75
|
);
|
|
69
|
-
const [service] = await
|
|
76
|
+
const [service] = await callWithRetry(
|
|
77
|
+
() => runClient.getService({ name: servicePath }),
|
|
78
|
+
'getService'
|
|
79
|
+
);
|
|
70
80
|
return service;
|
|
71
81
|
} catch (error) {
|
|
72
82
|
console.error(
|
|
@@ -123,8 +133,10 @@ export async function getServiceLogs(
|
|
|
123
133
|
console.log(`Request options: ${JSON.stringify(options)}`);
|
|
124
134
|
|
|
125
135
|
// getEntries returns the entries and the full API response
|
|
126
|
-
const [entries, nextRequestOptions, apiResponse] =
|
|
127
|
-
|
|
136
|
+
const [entries, nextRequestOptions, apiResponse] = await callWithRetry(
|
|
137
|
+
() => loggingClient.getEntries(options),
|
|
138
|
+
'getEntries'
|
|
139
|
+
);
|
|
128
140
|
|
|
129
141
|
const formattedLogLines = entries
|
|
130
142
|
.map((entry) => formatLogEntry(entry))
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* Calls a function with retry logic for GCP API calls.
|
|
19
|
+
* Retries on gRPC error code 7 (PERMISSION_DENIED).
|
|
20
|
+
* @param {Function} fn The function to call.
|
|
21
|
+
* @param {string} description A description of the function being called, for logging.
|
|
22
|
+
* @returns {Promise<any>} The result of the function.
|
|
23
|
+
*/
|
|
24
|
+
export async function callWithRetry(fn, description) {
|
|
25
|
+
const maxRetries = 7;
|
|
26
|
+
const initialBackoff = 1000; // 1 second
|
|
27
|
+
let retries = 0;
|
|
28
|
+
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 7 && retries < maxRetries) {
|
|
34
|
+
retries++;
|
|
35
|
+
let backoff;
|
|
36
|
+
if (retries === 1) {
|
|
37
|
+
backoff = 15000; // 15 seconds for the first retry
|
|
38
|
+
} else {
|
|
39
|
+
backoff = initialBackoff * Math.pow(2, retries - 2);
|
|
40
|
+
}
|
|
41
|
+
console.warn(
|
|
42
|
+
`API call "${description}" failed with PERMISSION_DENIED. Retrying in ${
|
|
43
|
+
backoff / 1000
|
|
44
|
+
}s... (attempt ${retries}/${maxRetries})`
|
|
45
|
+
);
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
47
|
+
} else {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a single Google Cloud API is enabled and enables it if not.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} serviceUsageClient - The Service Usage client.
|
|
58
|
+
* @param {string} serviceName - The full name of the service (e.g., 'projects/my-project/services/run.googleapis.com').
|
|
59
|
+
* @param {string} api - The API identifier (e.g., 'run.googleapis.com').
|
|
60
|
+
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
61
|
+
* @returns {Promise<void>} A promise that resolves when the API is enabled.
|
|
62
|
+
* @throws {Error} If the API fails to enable or if there's an issue checking its status.
|
|
63
|
+
*/
|
|
64
|
+
async function checkAndEnableApi(
|
|
65
|
+
serviceUsageClient,
|
|
66
|
+
serviceName,
|
|
67
|
+
api,
|
|
68
|
+
progressCallback
|
|
69
|
+
) {
|
|
70
|
+
const [service] = await callWithRetry(
|
|
71
|
+
() => serviceUsageClient.getService({ name: serviceName }),
|
|
72
|
+
`getService ${api}`
|
|
73
|
+
);
|
|
74
|
+
if (service.state !== 'ENABLED') {
|
|
75
|
+
const message = `API [${api}] is not enabled. Enabling...`;
|
|
76
|
+
console.log(message);
|
|
77
|
+
if (progressCallback) progressCallback({ level: 'info', data: message });
|
|
78
|
+
|
|
79
|
+
const [operation] = await callWithRetry(
|
|
80
|
+
() => serviceUsageClient.enableService({ name: serviceName }),
|
|
81
|
+
`enableService ${api}`
|
|
82
|
+
);
|
|
83
|
+
await operation.promise();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Ensures that the specified Google Cloud APIs are enabled for the given project.
|
|
89
|
+
* If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
|
|
90
|
+
* Throws an error if an API cannot be enabled.
|
|
91
|
+
*
|
|
92
|
+
* @async
|
|
93
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
94
|
+
* @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
|
|
95
|
+
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
96
|
+
* The first argument is the message, the optional second argument is the type ('error', 'warning', 'info').
|
|
97
|
+
* @throws {Error} If an API fails to enable or if there's an issue checking its status.
|
|
98
|
+
* @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
|
|
99
|
+
*/
|
|
100
|
+
export async function ensureApisEnabled(projectId, apis, progressCallback) {
|
|
101
|
+
const { ServiceUsageClient } = await import('@google-cloud/service-usage');
|
|
102
|
+
const serviceUsageClient = new ServiceUsageClient({ projectId });
|
|
103
|
+
const message = 'Checking and enabling required APIs...';
|
|
104
|
+
console.log(message);
|
|
105
|
+
if (progressCallback) progressCallback({ level: 'info', data: message });
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
for (const api of apis) {
|
|
109
|
+
const serviceName = `projects/${projectId}/services/${api}`;
|
|
110
|
+
try {
|
|
111
|
+
await checkAndEnableApi(
|
|
112
|
+
serviceUsageClient,
|
|
113
|
+
serviceName,
|
|
114
|
+
api,
|
|
115
|
+
progressCallback
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// First attempt failed, log a warning and retry once after a delay.
|
|
119
|
+
const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
|
|
120
|
+
console.warn(warnMsg);
|
|
121
|
+
if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
|
|
122
|
+
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
124
|
+
try {
|
|
125
|
+
await checkAndEnableApi(
|
|
126
|
+
serviceUsageClient,
|
|
127
|
+
serviceName,
|
|
128
|
+
api,
|
|
129
|
+
progressCallback
|
|
130
|
+
);
|
|
131
|
+
} catch (retryError) {
|
|
132
|
+
// If the retry also fails, throw an error.
|
|
133
|
+
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
134
|
+
console.error(errorMessage, retryError);
|
|
135
|
+
if (progressCallback) progressCallback({ level: 'error', data: errorMessage });
|
|
136
|
+
throw new Error(errorMessage);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const successMsg = 'All required APIs are enabled.';
|
|
141
|
+
console.log(successMsg);
|
|
142
|
+
if (progressCallback) progressCallback({ level: 'info', data: successMsg });
|
|
143
|
+
}
|