@google-cloud/cloud-run-mcp 1.10.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/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 +8 -0
- package/lib/deployment/deployer.js +283 -28
- package/lib/deployment/helpers.js +58 -1
- package/lib/deployment/universal-maker.js +21 -148
- 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 +100 -1
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +23 -114
- 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
|
@@ -26,6 +26,27 @@ import {
|
|
|
26
26
|
setupProject,
|
|
27
27
|
} from '../test-helpers.js';
|
|
28
28
|
|
|
29
|
+
const GCP_REGION = 'asia-southeast1';
|
|
30
|
+
const RUN_INGRESS_POLICY = process.env.RUN_INGRESS_POLICY || undefined;
|
|
31
|
+
const ZIP_DEPLOY_SUCCESS_MESSAGE =
|
|
32
|
+
'Deployment completed successfully with zip deploy';
|
|
33
|
+
const SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE =
|
|
34
|
+
'Deployment completed successfully with source build deploy';
|
|
35
|
+
|
|
36
|
+
async function assertDeploymentSuccess(config, expectedMessage) {
|
|
37
|
+
let successMessage = '';
|
|
38
|
+
config.progressCallback = (p) => {
|
|
39
|
+
if (p.data === expectedMessage) {
|
|
40
|
+
successMessage = p.data;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
if (RUN_INGRESS_POLICY) {
|
|
44
|
+
config.ingress = RUN_INGRESS_POLICY;
|
|
45
|
+
}
|
|
46
|
+
await deploy(config);
|
|
47
|
+
assert.strictEqual(successMessage, expectedMessage);
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
describe('Deployment workflows', () => {
|
|
30
51
|
let projectId;
|
|
31
52
|
|
|
@@ -33,6 +54,8 @@ describe('Deployment workflows', () => {
|
|
|
33
54
|
try {
|
|
34
55
|
projectId = await setupProject();
|
|
35
56
|
await setSourceDeployProjectPermissions(projectId);
|
|
57
|
+
console.log('Waiting 2 minutes for IAM propagation...');
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 120000));
|
|
36
59
|
} catch (err) {
|
|
37
60
|
console.error('Error during project creation and setup:', err);
|
|
38
61
|
throw err;
|
|
@@ -43,9 +66,12 @@ describe('Deployment workflows', () => {
|
|
|
43
66
|
const configImageDeploy = {
|
|
44
67
|
projectId: projectId,
|
|
45
68
|
serviceName: 'hello-scenario',
|
|
46
|
-
region:
|
|
69
|
+
region: GCP_REGION,
|
|
47
70
|
imageUrl: 'gcr.io/cloudrun/hello',
|
|
48
71
|
};
|
|
72
|
+
if (RUN_INGRESS_POLICY) {
|
|
73
|
+
configImageDeploy.ingress = RUN_INGRESS_POLICY;
|
|
74
|
+
}
|
|
49
75
|
await deployImage(configImageDeploy);
|
|
50
76
|
|
|
51
77
|
console.log('Scenario-1: Deployment completed.');
|
|
@@ -55,7 +81,7 @@ describe('Deployment workflows', () => {
|
|
|
55
81
|
const configFailingBuild = {
|
|
56
82
|
projectId: projectId,
|
|
57
83
|
serviceName: 'example-failing-app',
|
|
58
|
-
region:
|
|
84
|
+
region: GCP_REGION,
|
|
59
85
|
files: [
|
|
60
86
|
{
|
|
61
87
|
filename: 'main.txt',
|
|
@@ -64,6 +90,9 @@ describe('Deployment workflows', () => {
|
|
|
64
90
|
},
|
|
65
91
|
],
|
|
66
92
|
};
|
|
93
|
+
if (RUN_INGRESS_POLICY) {
|
|
94
|
+
configFailingBuild.ingress = RUN_INGRESS_POLICY;
|
|
95
|
+
}
|
|
67
96
|
await assert.rejects(
|
|
68
97
|
deploy(configFailingBuild),
|
|
69
98
|
{ message: /ERROR: failed to detect: no buildpacks participating/ },
|
|
@@ -73,26 +102,163 @@ describe('Deployment workflows', () => {
|
|
|
73
102
|
|
|
74
103
|
test('Scenario-3: Starting deployment of Go app with file content...', async () => {
|
|
75
104
|
const mainGoContent = await fs.readFile(
|
|
76
|
-
path.resolve('example-sources-to-deploy/main.go'),
|
|
105
|
+
path.resolve('example-sources-to-deploy/golang/main.go'),
|
|
77
106
|
'utf-8'
|
|
78
107
|
);
|
|
79
108
|
const goModContent = await fs.readFile(
|
|
80
|
-
path.resolve('example-sources-to-deploy/go.mod'),
|
|
109
|
+
path.resolve('example-sources-to-deploy/golang/go.mod'),
|
|
81
110
|
'utf-8'
|
|
82
111
|
);
|
|
83
112
|
const configGoWithContent = {
|
|
84
113
|
projectId: projectId,
|
|
85
114
|
serviceName: 'example-go-app-content',
|
|
86
|
-
region:
|
|
115
|
+
region: GCP_REGION,
|
|
87
116
|
files: [
|
|
88
117
|
{ filename: 'main.go', content: mainGoContent },
|
|
89
118
|
{ filename: 'go.mod', content: goModContent },
|
|
90
119
|
],
|
|
91
120
|
};
|
|
92
|
-
await
|
|
121
|
+
await assertDeploymentSuccess(
|
|
122
|
+
configGoWithContent,
|
|
123
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
124
|
+
);
|
|
93
125
|
console.log('Scenario-3: Deployment completed.');
|
|
94
126
|
});
|
|
95
127
|
|
|
128
|
+
test('Scenario-4: Starting deployment of pip-based Python app with folder path... uses zip deploy', async () => {
|
|
129
|
+
const configPipProject = {
|
|
130
|
+
projectId: projectId,
|
|
131
|
+
serviceName: 'example-pip-project-folder-path',
|
|
132
|
+
region: GCP_REGION,
|
|
133
|
+
files: ['example-sources-to-deploy/python/pip-project'],
|
|
134
|
+
};
|
|
135
|
+
await assertDeploymentSuccess(configPipProject, ZIP_DEPLOY_SUCCESS_MESSAGE);
|
|
136
|
+
console.log('Scenario-4: Deployment completed.');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('Scenario-5: Starting deployment of pip-based Python app with file-based content... uses source build deploy', async () => {
|
|
140
|
+
const mainPyContent = await fs.readFile(
|
|
141
|
+
path.resolve('example-sources-to-deploy/python/pip-project/main.py'),
|
|
142
|
+
'utf-8'
|
|
143
|
+
);
|
|
144
|
+
const requirementsTxtContent = await fs.readFile(
|
|
145
|
+
path.resolve(
|
|
146
|
+
'example-sources-to-deploy/python/pip-project/requirements.txt'
|
|
147
|
+
),
|
|
148
|
+
'utf-8'
|
|
149
|
+
);
|
|
150
|
+
const configPipProject = {
|
|
151
|
+
projectId: projectId,
|
|
152
|
+
serviceName: 'example-pip-project-file-content',
|
|
153
|
+
region: GCP_REGION,
|
|
154
|
+
files: [
|
|
155
|
+
{ filename: 'main.py', content: mainPyContent },
|
|
156
|
+
{ filename: 'requirements.txt', content: requirementsTxtContent },
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
await assertDeploymentSuccess(
|
|
160
|
+
configPipProject,
|
|
161
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
162
|
+
);
|
|
163
|
+
console.log('Scenario-5: Deployment completed.');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('Scenario-6: Starting deployment of pyproject-based Python app with folder path... uses zip deploy', async () => {
|
|
167
|
+
const configPyprojectProject = {
|
|
168
|
+
projectId: projectId,
|
|
169
|
+
serviceName: 'example-pyproject-project-folder-path',
|
|
170
|
+
region: GCP_REGION,
|
|
171
|
+
files: ['example-sources-to-deploy/python/pyproject-project'],
|
|
172
|
+
};
|
|
173
|
+
await assertDeploymentSuccess(
|
|
174
|
+
configPyprojectProject,
|
|
175
|
+
ZIP_DEPLOY_SUCCESS_MESSAGE
|
|
176
|
+
);
|
|
177
|
+
console.log('Scenario-6: Deployment completed.');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('Scenario-7: Starting deployment of pyproject-based Python app with file-based content... uses source build deploy', async () => {
|
|
181
|
+
const mainPyContent = await fs.readFile(
|
|
182
|
+
path.resolve(
|
|
183
|
+
'example-sources-to-deploy/python/pyproject-project/main.py'
|
|
184
|
+
),
|
|
185
|
+
'utf-8'
|
|
186
|
+
);
|
|
187
|
+
const pyprojectContent = await fs.readFile(
|
|
188
|
+
path.resolve(
|
|
189
|
+
'example-sources-to-deploy/python/pyproject-project/pyproject.toml'
|
|
190
|
+
),
|
|
191
|
+
'utf-8'
|
|
192
|
+
);
|
|
193
|
+
const configPyprojectProject = {
|
|
194
|
+
projectId: projectId,
|
|
195
|
+
serviceName: 'example-pyproject-project-file-content',
|
|
196
|
+
region: GCP_REGION,
|
|
197
|
+
files: [
|
|
198
|
+
{ filename: 'main.py', content: mainPyContent },
|
|
199
|
+
{ filename: 'pyproject.toml', content: pyprojectContent },
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
await assertDeploymentSuccess(
|
|
203
|
+
configPyprojectProject,
|
|
204
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
205
|
+
);
|
|
206
|
+
console.log('Scenario-7: Deployment completed.');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('Scenario-8: Starting deployment of Node.js app with folder path... uses zip deploy', async () => {
|
|
210
|
+
const configNodeProject = {
|
|
211
|
+
projectId: projectId,
|
|
212
|
+
serviceName: 'example-node-project-folder-path',
|
|
213
|
+
region: GCP_REGION,
|
|
214
|
+
files: ['example-sources-to-deploy/nodejs'],
|
|
215
|
+
};
|
|
216
|
+
await assertDeploymentSuccess(
|
|
217
|
+
configNodeProject,
|
|
218
|
+
ZIP_DEPLOY_SUCCESS_MESSAGE
|
|
219
|
+
);
|
|
220
|
+
console.log('Scenario-8: Deployment completed.');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Scenario-9: Starting deployment of Node.js app with file content... uses build deploy', async () => {
|
|
224
|
+
const packageJsonContent = await fs.readFile(
|
|
225
|
+
path.resolve('example-sources-to-deploy/nodejs/package.json'),
|
|
226
|
+
'utf-8'
|
|
227
|
+
);
|
|
228
|
+
const indexJsContent = await fs.readFile(
|
|
229
|
+
path.resolve('example-sources-to-deploy/nodejs/index.js'),
|
|
230
|
+
'utf-8'
|
|
231
|
+
);
|
|
232
|
+
const configNodeWithContent = {
|
|
233
|
+
projectId: projectId,
|
|
234
|
+
serviceName: 'example-node-project-file-content',
|
|
235
|
+
region: GCP_REGION,
|
|
236
|
+
files: [
|
|
237
|
+
{ filename: 'package.json', content: packageJsonContent },
|
|
238
|
+
{ filename: 'index.js', content: indexJsContent },
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
await assertDeploymentSuccess(
|
|
242
|
+
configNodeWithContent,
|
|
243
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
244
|
+
);
|
|
245
|
+
console.log('Scenario-9: Deployment completed.');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('Scenario-10: Starting deployment of Java app with folder path... uses source build deploy', async () => {
|
|
249
|
+
const configJavaProject = {
|
|
250
|
+
projectId: projectId,
|
|
251
|
+
serviceName: 'example-java-project-folder-path',
|
|
252
|
+
region: GCP_REGION,
|
|
253
|
+
files: ['example-sources-to-deploy/java'],
|
|
254
|
+
};
|
|
255
|
+
await assertDeploymentSuccess(
|
|
256
|
+
configJavaProject,
|
|
257
|
+
SOURCE_BUILD_DEPLOY_SUCCESS_MESSAGE
|
|
258
|
+
);
|
|
259
|
+
console.log('Scenario-10: Deployment completed.');
|
|
260
|
+
});
|
|
261
|
+
|
|
96
262
|
after(async () => {
|
|
97
263
|
// Clean up: delete the project created for tests
|
|
98
264
|
cleanupProject(projectId);
|
package/tools/register-tools.js
CHANGED
|
@@ -241,11 +241,21 @@ function registerGetServiceTool(server, options) {
|
|
|
241
241
|
options.accessToken
|
|
242
242
|
);
|
|
243
243
|
if (serviceDetails) {
|
|
244
|
+
// Format last deployment date, handling both Protobuf Timestamp (with
|
|
245
|
+
// seconds as string or number) and standard Date/ISO strings.
|
|
246
|
+
const lastDeployedDate = serviceDetails.updateTime
|
|
247
|
+
? new Date(
|
|
248
|
+
serviceDetails.updateTime.seconds
|
|
249
|
+
? Number(serviceDetails.updateTime.seconds) * 1000
|
|
250
|
+
: serviceDetails.updateTime
|
|
251
|
+
).toUTCString()
|
|
252
|
+
: 'N/A';
|
|
253
|
+
const consoleUrl = `https://console.cloud.google.com/run/detail/${region}/${service}?project=${project}`;
|
|
244
254
|
return {
|
|
245
255
|
content: [
|
|
246
256
|
{
|
|
247
257
|
type: 'text',
|
|
248
|
-
text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nLast deployed by: ${serviceDetails.lastModifier}`,
|
|
258
|
+
text: `Name: ${service}\nRegion: ${region}\nProject: ${project}\nURL: ${serviceDetails.uri}\nConsole URL: ${consoleUrl}\nLast deployed by: ${serviceDetails.lastModifier}\nLast deployed date: ${lastDeployedDate}`,
|
|
249
259
|
},
|
|
250
260
|
],
|
|
251
261
|
};
|
|
@@ -366,7 +376,9 @@ function registerDeployLocalFolderTool(server, options) {
|
|
|
366
376
|
.string()
|
|
367
377
|
.optional()
|
|
368
378
|
.default(options.defaultServiceName)
|
|
369
|
-
.describe(
|
|
379
|
+
.describe(
|
|
380
|
+
'Name of the Cloud Run service to deploy to. If not provided, it will be inferred.'
|
|
381
|
+
),
|
|
370
382
|
folderPath: z
|
|
371
383
|
.string()
|
|
372
384
|
.describe(
|
|
@@ -405,13 +417,15 @@ function registerDeployLocalFolderTool(server, options) {
|
|
|
405
417
|
files: [folderPath],
|
|
406
418
|
skipIamCheck: options.skipIamCheck, // Pass the new flag
|
|
407
419
|
accessToken: options.accessToken,
|
|
420
|
+
ingress: options.ingress,
|
|
408
421
|
progressCallback,
|
|
409
422
|
});
|
|
423
|
+
const serviceName = service || response.service;
|
|
410
424
|
return {
|
|
411
425
|
content: [
|
|
412
426
|
{
|
|
413
427
|
type: 'text',
|
|
414
|
-
text: `Cloud Run service ${
|
|
428
|
+
text: `Cloud Run service ${serviceName} deployed from folder ${folderPath} in project ${project}\nCloud Console: https://console.cloud.google.com/run/detail/${region}/${serviceName}?project=${project}\nService URL: ${response.uri}`,
|
|
415
429
|
},
|
|
416
430
|
],
|
|
417
431
|
};
|
|
@@ -507,6 +521,7 @@ function registerDeployFileContentsTool(server, options) {
|
|
|
507
521
|
files: files,
|
|
508
522
|
skipIamCheck: options.skipIamCheck, // Pass the new flag
|
|
509
523
|
accessToken: options.accessToken,
|
|
524
|
+
ingress: options.ingress,
|
|
510
525
|
progressCallback,
|
|
511
526
|
});
|
|
512
527
|
return {
|
|
@@ -588,6 +603,7 @@ function registerDeployContainerImageTool(server, options) {
|
|
|
588
603
|
imageUrl: imageUrl,
|
|
589
604
|
skipIamCheck: options.skipIamCheck,
|
|
590
605
|
accessToken: options.accessToken,
|
|
606
|
+
ingress: options.ingress,
|
|
591
607
|
progressCallback,
|
|
592
608
|
});
|
|
593
609
|
return {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|