@google-cloud/cloud-run-mcp 1.9.0 → 1.11.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/README.md +24 -0
- package/SECURITY.md +7 -0
- package/cloudbuild.yaml +37 -0
- package/example-sources-to-deploy/java/pom.xml +41 -0
- package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
- package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
- package/example-sources-to-deploy/nodejs/index.js +15 -0
- package/example-sources-to-deploy/nodejs/package.json +12 -0
- package/example-sources-to-deploy/python/pip-project/main.py +11 -0
- package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
- package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
- package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
- package/gemini-extension.json +12 -0
- package/lib/clients.js +104 -3
- package/lib/cloud-api/build.js +192 -28
- package/lib/cloud-api/run.js +62 -0
- package/lib/cloud-api/secrets.js +196 -0
- package/lib/cloud-api/storage.js +164 -16
- package/lib/deployment/compose.js +517 -0
- package/lib/deployment/constants.js +11 -0
- package/lib/deployment/deployer.js +334 -30
- package/lib/deployment/helpers.js +99 -49
- package/lib/deployment/source-processor.js +0 -48
- package/lib/deployment/universal-maker.js +147 -0
- package/lib/util/archive.js +3 -3
- package/lib/util/artifacts.js +189 -0
- package/lib/util/helpers.js +109 -0
- package/mcp-server.js +4 -0
- package/package.json +6 -3
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +224 -2
- package/test/local/cloud-api/build.test.js +330 -25
- package/test/local/cloud-api/run.test.js +129 -0
- package/test/local/cloud-api/secrets.test.js +291 -0
- package/test/local/cloud-api/storage.test.js +405 -0
- package/test/local/compose.test.js +618 -0
- package/test/local/deploy.test.js +408 -0
- package/test/local/deployment-helpers.test.js +126 -79
- package/test/local/source-processor.test.js +0 -165
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +157 -0
- package/test/local/util.test.js +361 -0
- package/test/need-gcp/deployer.test.js +1 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +19 -3
- /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
- /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
- /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
package/README.md
CHANGED
|
@@ -274,3 +274,27 @@ Post the configuration changes as shown above, start the Gemini CLI. You should
|
|
|
274
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
275
|
|
|
276
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.
|
|
277
|
+
|
|
278
|
+
# Cloud Run Skills
|
|
279
|
+
|
|
280
|
+
We introduce Cloud Run skills to enable AI agents to perform actions on Cloud Run. You can use these skills with Gemini CLI and other agentic AI tools. The skills are available at [Cloud Run Skills](https://github.com/GoogleCloudPlatform/cloud-run-mcp/blob/main/skills/cloud-run/SKILL.md).
|
|
281
|
+
|
|
282
|
+
The Cloud Run skills are based on top of gcloud cli for Cloud Run empowering agents to perform all the actions on the Cloud Run using gcloud, as can be performed by the GCP user using gcloud cli.
|
|
283
|
+
|
|
284
|
+
In order to use Cloud Run skills:
|
|
285
|
+
|
|
286
|
+
1. Ensure you have the `gcloud` CLI installed and authenticated with `gcloud auth login` and `gcloud auth application-default login`.
|
|
287
|
+
2. Set your project with `gcloud config set project [PROJECT_ID]`.
|
|
288
|
+
3. Enable the skills on your agentic AI tool. For example, you can enable the skills for Gemini CLI using the following command on your terminal:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
gemini skills install https://github.com/GoogleCloudPlatform/cloud-run-mcp.git --path skills/cloud-run
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
4. Once the skills are enabled, you can use them to perform actions on Cloud Run. Here are some of the prompts for you to get started:
|
|
295
|
+
|
|
296
|
+
- List the Cloud Run services in the project test-gcp-project in the region us-west1.
|
|
297
|
+
|
|
298
|
+
- Deploy the folder /home/username/workspace/hello-world as Cloud Run service hello-world to the project test-gcp-project in the region us-west1.
|
|
299
|
+
|
|
300
|
+
- Describe the Cloud Run job hello-job in the project test-gcp-project in the region europe-west1.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
To report a security issue, please use [g.co/vulnz](https://g.co/vulnz).
|
|
4
|
+
|
|
5
|
+
The Google Security Team will respond within 5 working days of your report on [g.co/vulnz](https://g.co/vulnz).
|
|
6
|
+
|
|
7
|
+
We use [g.co/vulnz](https://g.co/vulnz) for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue.
|
package/cloudbuild.yaml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# Default cloudbuild.yaml for Cloud Shell custom images.
|
|
16
|
+
# This file is used by the Cloud Build trigger that builds this image.
|
|
17
|
+
|
|
18
|
+
substitutions:
|
|
19
|
+
_IMAGE_TAG: '${COMMIT_SHA}'
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: 'gcr.io/cloud-builders/docker'
|
|
23
|
+
args:
|
|
24
|
+
[
|
|
25
|
+
'build',
|
|
26
|
+
'-t',
|
|
27
|
+
'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:latest',
|
|
28
|
+
'-t',
|
|
29
|
+
'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:public-image-${_IMAGE_TAG}',
|
|
30
|
+
'.',
|
|
31
|
+
]
|
|
32
|
+
images:
|
|
33
|
+
- 'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:latest'
|
|
34
|
+
- 'us-docker.pkg.dev/${PROJECT_ID}/container/mcp:public-image-${_IMAGE_TAG}'
|
|
35
|
+
|
|
36
|
+
options:
|
|
37
|
+
dynamicSubstitutions: true
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
3
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
4
|
+
<modelVersion>4.0.0</modelVersion>
|
|
5
|
+
<parent>
|
|
6
|
+
<groupId>org.springframework.boot</groupId>
|
|
7
|
+
<artifactId>spring-boot-starter-parent</artifactId>
|
|
8
|
+
<version>3.2.2</version>
|
|
9
|
+
<relativePath/> <!-- lookup parent from repository -->
|
|
10
|
+
</parent>
|
|
11
|
+
<groupId>com.example</groupId>
|
|
12
|
+
<artifactId>helloworld</artifactId>
|
|
13
|
+
<version>0.0.1-SNAPSHOT</version>
|
|
14
|
+
<name>helloworld</name>
|
|
15
|
+
<description>Demo project for Spring Boot</description>
|
|
16
|
+
<properties>
|
|
17
|
+
<java.version>17</java.version>
|
|
18
|
+
</properties>
|
|
19
|
+
<dependencies>
|
|
20
|
+
<dependency>
|
|
21
|
+
<groupId>org.springframework.boot</groupId>
|
|
22
|
+
<artifactId>spring-boot-starter-web</artifactId>
|
|
23
|
+
</dependency>
|
|
24
|
+
|
|
25
|
+
<dependency>
|
|
26
|
+
<groupId>org.springframework.boot</groupId>
|
|
27
|
+
<artifactId>spring-boot-starter-test</artifactId>
|
|
28
|
+
<scope>test</scope>
|
|
29
|
+
</dependency>
|
|
30
|
+
</dependencies>
|
|
31
|
+
|
|
32
|
+
<build>
|
|
33
|
+
<plugins>
|
|
34
|
+
<plugin>
|
|
35
|
+
<groupId>org.springframework.boot</groupId>
|
|
36
|
+
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
37
|
+
</plugin>
|
|
38
|
+
</plugins>
|
|
39
|
+
</build>
|
|
40
|
+
|
|
41
|
+
</project>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.example.helloworld;
|
|
2
|
+
|
|
3
|
+
import org.springframework.boot.SpringApplication;
|
|
4
|
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|
5
|
+
import org.springframework.web.bind.annotation.GetMapping;
|
|
6
|
+
import org.springframework.web.bind.annotation.RestController;
|
|
7
|
+
|
|
8
|
+
@SpringBootApplication
|
|
9
|
+
@RestController
|
|
10
|
+
public class HelloworldApplication {
|
|
11
|
+
|
|
12
|
+
public static void main(String[] args) {
|
|
13
|
+
SpringApplication.run(HelloworldApplication.class, args);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@GetMapping("/")
|
|
17
|
+
public String hello() {
|
|
18
|
+
return "Hello from Spring Boot on Cloud Run!";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
server.port=${PORT:8080}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const app = express();
|
|
3
|
+
const port = process.env.PORT || 8080;
|
|
4
|
+
|
|
5
|
+
app.get('/', (req, res) => {
|
|
6
|
+
res.send('Hello from Node.js on Cloud Run!');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
app.get('/health', (req, res) => {
|
|
10
|
+
res.status(200).send('OK');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
app.listen(port, () => {
|
|
14
|
+
console.log(`Nodejs sample app listening at http://localhost:${port}`);
|
|
15
|
+
});
|
package/gemini-extension.json
CHANGED
|
@@ -7,5 +7,17 @@
|
|
|
7
7
|
"args": ["-y", "@google-cloud/cloud-run-mcp"]
|
|
8
8
|
}
|
|
9
9
|
},
|
|
10
|
+
"settings": [
|
|
11
|
+
{
|
|
12
|
+
"name": "Project ID",
|
|
13
|
+
"description": "Google Cloud project ID to use for Cloud Run operations.",
|
|
14
|
+
"envVar": "GOOGLE_CLOUD_PROJECT"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "Region",
|
|
18
|
+
"description": "Google Cloud region where Cloud Run services will be managed.",
|
|
19
|
+
"envVar": "GOOGLE_CLOUD_REGION"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
10
22
|
"contextFileName": "gemini-extension/GEMINI.md"
|
|
11
23
|
}
|
package/lib/clients.js
CHANGED
|
@@ -22,12 +22,15 @@ const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET;
|
|
|
22
22
|
const REDIRECT_URI = process.env.GOOGLE_OAUTH_REDIRECT_URI;
|
|
23
23
|
const AUTHORIZATION_HEADER = 'Authorization';
|
|
24
24
|
const BEARER_PREFIX = 'Bearer';
|
|
25
|
+
const CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
|
|
25
26
|
|
|
26
|
-
function keyGenerator(projectId, accessToken) {
|
|
27
|
-
|
|
27
|
+
function keyGenerator(projectId, accessToken, region = '') {
|
|
28
|
+
const baseKey =
|
|
29
|
+
accessToken !== GCLOUD_AUTH ? projectId + accessToken : projectId;
|
|
30
|
+
return baseKey + (region || '');
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const clients = {
|
|
33
|
+
export const clients = {
|
|
31
34
|
run: new Map(),
|
|
32
35
|
builds: new Map(),
|
|
33
36
|
serviceUsage: new Map(),
|
|
@@ -37,7 +40,9 @@ const clients = {
|
|
|
37
40
|
logging: new Map(),
|
|
38
41
|
billing: new Map(),
|
|
39
42
|
projects: new Map(),
|
|
43
|
+
runV1: new Map(),
|
|
40
44
|
oauth: new Map(),
|
|
45
|
+
secretManager: new Map(),
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
function getAuthClient(accessToken) {
|
|
@@ -275,6 +280,26 @@ export async function getProjectsClient(accessToken = GCLOUD_AUTH) {
|
|
|
275
280
|
);
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Gets a Secret Manager Client for the specified project.
|
|
285
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
286
|
+
* @returns {Promise<import('@google-cloud/secret-manager').SecretManagerServiceClient>}
|
|
287
|
+
*/
|
|
288
|
+
export async function getSecretManagerClient(
|
|
289
|
+
projectId,
|
|
290
|
+
accessToken = GCLOUD_AUTH
|
|
291
|
+
) {
|
|
292
|
+
const key = keyGenerator(projectId, accessToken);
|
|
293
|
+
return getClient(
|
|
294
|
+
'secretManager',
|
|
295
|
+
key,
|
|
296
|
+
async () =>
|
|
297
|
+
(await import('@google-cloud/secret-manager')).SecretManagerServiceClient,
|
|
298
|
+
{ projectId },
|
|
299
|
+
accessToken
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
278
303
|
/**
|
|
279
304
|
* Gets an OAuth2 Client for the specified access token.
|
|
280
305
|
* @param {string} accessToken - The access token.
|
|
@@ -290,3 +315,79 @@ export async function getOAuthClient(accessToken) {
|
|
|
290
315
|
|
|
291
316
|
return clients.oauth.get(accessToken);
|
|
292
317
|
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Gets a Cloud Run v1 Client (from googleapis).
|
|
321
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
322
|
+
* @param {string} accessToken - Access token for authentication.
|
|
323
|
+
* @param {string} [region] - The Google Cloud region (optional).
|
|
324
|
+
* @returns {Promise<import('googleapis').run_v1.Run>}
|
|
325
|
+
*/
|
|
326
|
+
export async function getRunV1Client(
|
|
327
|
+
projectId,
|
|
328
|
+
accessToken = GCLOUD_AUTH,
|
|
329
|
+
region = null
|
|
330
|
+
) {
|
|
331
|
+
const key = keyGenerator(projectId, accessToken, region);
|
|
332
|
+
if (!clients.runV1.has(key)) {
|
|
333
|
+
const { google } = await import('googleapis');
|
|
334
|
+
const options = { version: 'v1' };
|
|
335
|
+
|
|
336
|
+
if (accessToken && accessToken !== GCLOUD_AUTH) {
|
|
337
|
+
options.auth = await getOAuthClient(accessToken);
|
|
338
|
+
} else {
|
|
339
|
+
// Use Application Default Credentials for GCLOUD_AUTH
|
|
340
|
+
const auth = new google.auth.GoogleAuth({
|
|
341
|
+
scopes: [CLOUD_PLATFORM_SCOPE],
|
|
342
|
+
projectId: projectId,
|
|
343
|
+
});
|
|
344
|
+
options.auth = auth;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (region) {
|
|
348
|
+
const availableRegions = await getCloudRunRegions(projectId, accessToken);
|
|
349
|
+
if (!availableRegions.includes(region)) {
|
|
350
|
+
throw new Error(`Invalid region: ${region}.`);
|
|
351
|
+
}
|
|
352
|
+
options.rootUrl = `https://${region}-run.googleapis.com/`;
|
|
353
|
+
}
|
|
354
|
+
clients.runV1.set(key, google.run(options));
|
|
355
|
+
}
|
|
356
|
+
return clients.runV1.get(key);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let cachedRegions = null;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Resets the cached regions. Used for testing.
|
|
363
|
+
*/
|
|
364
|
+
export function resetCachedRegions() {
|
|
365
|
+
cachedRegions = null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Returns a list of supported Cloud Run regions by fetching them from Compute Engine API.
|
|
370
|
+
* This is the SDK equivalent of `gcloud compute regions list`.
|
|
371
|
+
* Note: This is not moved to utils as it is required by getRunV1Client. Moving it to utils
|
|
372
|
+
* would lead to circular dependency.
|
|
373
|
+
* @param {string} projectId - The project ID to fetch regions for.
|
|
374
|
+
* @param {string} accessToken - The access token for authentication.
|
|
375
|
+
* @returns {Promise<string[]>}
|
|
376
|
+
*/
|
|
377
|
+
export async function getCloudRunRegions(projectId, accessToken) {
|
|
378
|
+
if (cachedRegions) {
|
|
379
|
+
return cachedRegions;
|
|
380
|
+
}
|
|
381
|
+
const runClient = await getRunClient(projectId, accessToken);
|
|
382
|
+
const iterable = runClient.listLocationsAsync({
|
|
383
|
+
name: `projects/${projectId}`,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const regions = [];
|
|
387
|
+
for await (const region of iterable) {
|
|
388
|
+
regions.push(region.locationId);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
cachedRegions = regions;
|
|
392
|
+
return cachedRegions;
|
|
393
|
+
}
|
package/lib/cloud-api/build.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 path from 'node:path';
|
|
18
|
+
import fs from 'node:fs';
|
|
17
19
|
import {
|
|
18
20
|
getCloudBuildClient,
|
|
19
21
|
getLoggingClient,
|
|
@@ -21,8 +23,16 @@ import {
|
|
|
21
23
|
getBuildsClient,
|
|
22
24
|
} from '../clients.js';
|
|
23
25
|
import { callWithRetry } from './helpers.js';
|
|
24
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
logAndProgress,
|
|
28
|
+
calculateSourceFingerprint,
|
|
29
|
+
sanitizeCloudRunServiceName,
|
|
30
|
+
} from '../util/helpers.js';
|
|
25
31
|
import { checkCloudRunServiceExists } from './run.js';
|
|
32
|
+
import { ensureArtifactRegistryRepoExists } from './registry.js';
|
|
33
|
+
import { ensureStorageBucketExists, uploadToStorageBucket } from './storage.js';
|
|
34
|
+
import { zipFiles } from '../util/archive.js';
|
|
35
|
+
import { DEPLOYMENT_CONFIG } from '../deployment/constants.js';
|
|
26
36
|
|
|
27
37
|
const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
|
|
28
38
|
const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
|
|
@@ -54,9 +64,11 @@ export async function triggerCloudBuild(
|
|
|
54
64
|
targetImageUrl,
|
|
55
65
|
hasDockerfile,
|
|
56
66
|
accessToken,
|
|
57
|
-
progressCallback
|
|
67
|
+
progressCallback,
|
|
68
|
+
ingress
|
|
58
69
|
) {
|
|
59
|
-
const
|
|
70
|
+
const rawServiceName = targetImageUrl.split('/').pop().split(/[:@]/)[0];
|
|
71
|
+
const serviceName = sanitizeCloudRunServiceName(rawServiceName);
|
|
60
72
|
const parent = `projects/${projectId}/locations/${location}`;
|
|
61
73
|
const serviceFullName = `${parent}/services/${serviceName}`;
|
|
62
74
|
const buildsClient = await getBuildsClient(projectId, accessToken);
|
|
@@ -78,6 +90,10 @@ export async function triggerCloudBuild(
|
|
|
78
90
|
},
|
|
79
91
|
};
|
|
80
92
|
|
|
93
|
+
if (ingress) {
|
|
94
|
+
servicePatch.ingress = ingress;
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
try {
|
|
82
98
|
if (serviceExists) {
|
|
83
99
|
await logAndProgress(
|
|
@@ -123,36 +139,76 @@ export async function triggerCloudBuild(
|
|
|
123
139
|
throw new Error(`Dry-run deployment failed: ${err.message}`);
|
|
124
140
|
}
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
bucket: sourceBucketName,
|
|
130
|
-
object: sourceBlobName,
|
|
131
|
-
},
|
|
132
|
-
imageUri: targetImageUrl,
|
|
133
|
-
client: 'cloud-run-mcp',
|
|
134
|
-
};
|
|
142
|
+
let buildId;
|
|
143
|
+
let completedBuild;
|
|
144
|
+
|
|
135
145
|
if (hasDockerfile) {
|
|
136
|
-
|
|
146
|
+
// Docker builds need manual control via v1 createBuild to enable BuildKit support.
|
|
147
|
+
// This resolves issues with advanced Dockerfile instructions like heredocs (RUN <<EOF).
|
|
148
|
+
const build = {
|
|
149
|
+
images: [targetImageUrl],
|
|
150
|
+
source: {
|
|
151
|
+
storageSource: {
|
|
152
|
+
bucket: sourceBucketName,
|
|
153
|
+
object: sourceBlobName,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
steps: [
|
|
157
|
+
{
|
|
158
|
+
name: 'gcr.io/cloud-builders/docker',
|
|
159
|
+
args: ['build', '-t', targetImageUrl, '.'],
|
|
160
|
+
env: ['DOCKER_BUILDKIT=1'],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
options: {
|
|
164
|
+
logging: 'CLOUD_LOGGING_ONLY',
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await logAndProgress(
|
|
169
|
+
`Initiating Cloud Build (BuildKit enabled) for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
170
|
+
progressCallback
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const [operation] = await callWithRetry(
|
|
174
|
+
() =>
|
|
175
|
+
cloudBuildClient.createBuild({
|
|
176
|
+
parent: `projects/${projectId}/locations/${location}`,
|
|
177
|
+
build,
|
|
178
|
+
}),
|
|
179
|
+
'cloudBuild.createBuild'
|
|
180
|
+
);
|
|
181
|
+
buildId = operation.metadata.build.id;
|
|
137
182
|
} else {
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
// For simple buildpacks, use the high-level submitBuild API if possible or if preferred for simplicity.
|
|
184
|
+
const request = {
|
|
185
|
+
parent: `projects/${projectId}/locations/${location}`,
|
|
186
|
+
storageSource: {
|
|
187
|
+
bucket: sourceBucketName,
|
|
188
|
+
object: sourceBlobName,
|
|
189
|
+
},
|
|
190
|
+
imageUri: targetImageUrl,
|
|
191
|
+
client: 'cloud-run-mcp',
|
|
192
|
+
buildpackBuild: {},
|
|
193
|
+
};
|
|
140
194
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
195
|
+
await logAndProgress(
|
|
196
|
+
`Initiating Cloud Build (Buildpacks) for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
197
|
+
progressCallback
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const [submitBuildResponse] = await callWithRetry(
|
|
201
|
+
() => buildsClient.submitBuild(request),
|
|
202
|
+
'builds.submitBuild'
|
|
203
|
+
);
|
|
204
|
+
const encodedBuildId = submitBuildResponse.buildOperation.name
|
|
205
|
+
.split('/')
|
|
206
|
+
.pop();
|
|
207
|
+
buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
|
|
208
|
+
}
|
|
145
209
|
|
|
146
|
-
const [submitBuildResponse] = await callWithRetry(
|
|
147
|
-
() => buildsClient.submitBuild(request),
|
|
148
|
-
'builds.submitBuild'
|
|
149
|
-
);
|
|
150
210
|
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
151
|
-
|
|
152
|
-
.split('/')
|
|
153
|
-
.pop();
|
|
154
|
-
const buildId = Buffer.from(encodedBuildId, 'base64').toString('ascii');
|
|
155
|
-
let completedBuild;
|
|
211
|
+
|
|
156
212
|
while (true) {
|
|
157
213
|
const [getBuildOperation] = await callWithRetry(
|
|
158
214
|
() =>
|
|
@@ -247,3 +303,111 @@ export async function triggerCloudBuild(
|
|
|
247
303
|
throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
|
|
248
304
|
}
|
|
249
305
|
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Build source for services defined in resourcesConfig.source_builds
|
|
309
|
+
* @param {*} resourcesConfig - resourcesConfig received from `run-compose resource` call
|
|
310
|
+
* @param {*} accessToken - access token for authentication
|
|
311
|
+
* @param {*} projectId - GCP project id
|
|
312
|
+
* @param {*} region - GCP region
|
|
313
|
+
* @param {*} folderPath - Path to the folder containing the source code
|
|
314
|
+
* @param {*} progressCallback - Callback function to report progress
|
|
315
|
+
* @returns - Completed resourcesConfig object
|
|
316
|
+
*/
|
|
317
|
+
export async function composeBuild(
|
|
318
|
+
resourcesConfig,
|
|
319
|
+
accessToken,
|
|
320
|
+
projectId,
|
|
321
|
+
region,
|
|
322
|
+
folderPath,
|
|
323
|
+
progressCallback
|
|
324
|
+
) {
|
|
325
|
+
if (
|
|
326
|
+
!resourcesConfig.source_builds ||
|
|
327
|
+
Object.keys(resourcesConfig.source_builds).length === 0
|
|
328
|
+
) {
|
|
329
|
+
return resourcesConfig;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Ensure AR Repo exists once
|
|
333
|
+
await ensureArtifactRegistryRepoExists(
|
|
334
|
+
projectId,
|
|
335
|
+
accessToken,
|
|
336
|
+
region,
|
|
337
|
+
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
338
|
+
'DOCKER',
|
|
339
|
+
progressCallback
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const bucketName = `${projectId}_cloudbuild`;
|
|
343
|
+
|
|
344
|
+
// Ensure Storage Bucket exists once
|
|
345
|
+
const bucket = await ensureStorageBucketExists(
|
|
346
|
+
projectId,
|
|
347
|
+
bucketName,
|
|
348
|
+
region,
|
|
349
|
+
accessToken,
|
|
350
|
+
undefined,
|
|
351
|
+
false,
|
|
352
|
+
progressCallback
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const buildPromises = Object.entries(resourcesConfig.source_builds).map(
|
|
356
|
+
async ([serviceName, buildInfo]) => {
|
|
357
|
+
const contextDir = path.join(folderPath, buildInfo.context);
|
|
358
|
+
await logAndProgress(
|
|
359
|
+
`Building source for service ${serviceName} in ${contextDir}...`,
|
|
360
|
+
progressCallback
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Check for Dockerfile in context
|
|
364
|
+
const hasDockerfile = fs.existsSync(path.join(contextDir, 'Dockerfile'));
|
|
365
|
+
|
|
366
|
+
// Generate Fingerprint
|
|
367
|
+
const fingerprint = await calculateSourceFingerprint(contextDir);
|
|
368
|
+
const composeProject = resourcesConfig.project;
|
|
369
|
+
|
|
370
|
+
// Generate image URL
|
|
371
|
+
// Use format: {repo}/{project_name}_{container}:{tag} matching builder.py
|
|
372
|
+
const imageUrl = `${region}-docker.pkg.dev/${projectId}/${DEPLOYMENT_CONFIG.REPO_NAME}/${composeProject}_${serviceName}:${DEPLOYMENT_CONFIG.IMAGE_TAG}`;
|
|
373
|
+
|
|
374
|
+
// Create tar.gz archive in memory
|
|
375
|
+
const archiveBuffer = await zipFiles(
|
|
376
|
+
[contextDir],
|
|
377
|
+
true,
|
|
378
|
+
progressCallback
|
|
379
|
+
);
|
|
380
|
+
// Use format: {epoch}.{fingerprint}.tgz in source/ folder
|
|
381
|
+
const archiveName = `source/${Date.now()}.${fingerprint}.tgz`;
|
|
382
|
+
|
|
383
|
+
const file = await uploadToStorageBucket(
|
|
384
|
+
bucket,
|
|
385
|
+
archiveBuffer,
|
|
386
|
+
archiveName,
|
|
387
|
+
progressCallback
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Trigger build
|
|
391
|
+
const buildResult = await triggerCloudBuild(
|
|
392
|
+
projectId,
|
|
393
|
+
region,
|
|
394
|
+
bucket.name,
|
|
395
|
+
archiveName,
|
|
396
|
+
DEPLOYMENT_CONFIG.REPO_NAME,
|
|
397
|
+
imageUrl,
|
|
398
|
+
hasDockerfile,
|
|
399
|
+
accessToken,
|
|
400
|
+
progressCallback,
|
|
401
|
+
undefined
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const builtImageUrl = buildResult.results.images[0].name;
|
|
405
|
+
// Update resourcesConfig with image_id
|
|
406
|
+
resourcesConfig.source_builds[serviceName].image_id = builtImageUrl;
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
await Promise.all(buildPromises);
|
|
411
|
+
|
|
412
|
+
return resourcesConfig;
|
|
413
|
+
}
|