@google-cloud/cloud-run-mcp 1.3.0 → 1.6.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/.c8rc.json +9 -0
- package/.github/images/deploy_from_apps.gif +0 -0
- package/.github/images/deploy_from_ide.gif +0 -0
- package/.github/images/deploycli.gif +0 -0
- package/.github/workflows/lint-checker.yml +17 -0
- package/.github/workflows/local-tests.yml +1 -1
- package/.kokoro/kokoro_build.sh +22 -0
- package/.kokoro/presubmit.cfg +21 -0
- package/.kokoro/run_tests.sh +31 -0
- package/CONTRIBUTING.md +29 -8
- package/README.md +65 -19
- package/REVIEWING.md +20 -0
- package/gemini-extension/GEMINI.md +3 -3
- package/lib/cloud-api/build.js +200 -0
- package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +12 -8
- package/lib/{gcp-projects.js → cloud-api/projects.js} +58 -28
- package/lib/cloud-api/registry.js +98 -0
- package/lib/{cloud-run-services.js → cloud-api/run.js} +103 -21
- package/lib/cloud-api/storage.js +123 -0
- package/lib/deployment/deployer.js +474 -0
- package/lib/util/archive.js +95 -0
- package/lib/util/helpers.js +42 -0
- package/mcp-server.js +29 -11
- package/package.json +9 -6
- package/test/local/cloud-api/build.test.js +436 -0
- package/test/local/cloud-api/projects.test.js +18 -0
- package/test/local/mcp-server-streamable-http.test.js +57 -0
- package/test/local/mcp-server.test.js +0 -1
- package/test/local/notifications.test.js +48 -0
- package/test/local/tools.test.js +143 -124
- package/test/need-gcp/cloud-run-services.test.js +57 -77
- package/test/need-gcp/deployer.test.js +71 -0
- package/test/need-gcp/gcp-auth-check.test.js +1 -1
- package/test/need-gcp/gcp-projects.test.js +6 -60
- package/test/need-gcp/test-helpers.js +175 -0
- package/test/need-gcp/workflows/deployment-workflows.test.js +100 -0
- package/tools/register-tools.js +612 -0
- package/tools/tools.js +48 -0
- package/lib/cloud-run-deploy.js +0 -917
- package/test/need-gcp/cloud-run-deploy.test.js +0 -158
- package/tools.js +0 -969
- /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
- /package/lib/{gcp-billing-accounts.js → cloud-api/billing.js} +0 -0
- /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
|
@@ -14,64 +14,10 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import { createProjectAndAttachBilling } from '../../lib/gcp-projects.js';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import { setupProject } from './test-helpers.js';
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
async function getOptionalProjectId() {
|
|
27
|
-
let projectIdInput = process.argv[2]; // Get the third element (index 2) which is the first argument
|
|
28
|
-
|
|
29
|
-
if (projectIdInput && projectIdInput.trim() !== '') {
|
|
30
|
-
console.log(
|
|
31
|
-
`Using Project ID from command line argument: ${projectIdInput.trim()}`
|
|
32
|
-
);
|
|
33
|
-
return projectIdInput.trim();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const rl = readline.createInterface({ input, output });
|
|
37
|
-
projectIdInput = await rl.question(
|
|
38
|
-
'Enter a specific Project ID to use (or press Enter to auto-generate): '
|
|
39
|
-
);
|
|
40
|
-
rl.close();
|
|
41
|
-
|
|
42
|
-
if (projectIdInput && projectIdInput.trim() !== '') {
|
|
43
|
-
console.log(
|
|
44
|
-
`Attempting to create project with specified ID: ${projectIdInput.trim()}`
|
|
45
|
-
);
|
|
46
|
-
return projectIdInput.trim();
|
|
47
|
-
}
|
|
48
|
-
console.log('No Project ID specified. An ID will be auto-generated.');
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function main() {
|
|
53
|
-
try {
|
|
54
|
-
const optionalProjectId = await getOptionalProjectId();
|
|
55
|
-
console.log('Attempting to create a new project and attach billing...');
|
|
56
|
-
const newProjectResult =
|
|
57
|
-
await createProjectAndAttachBilling(optionalProjectId);
|
|
58
|
-
|
|
59
|
-
if (newProjectResult && newProjectResult.projectId) {
|
|
60
|
-
console.log(
|
|
61
|
-
`Successfully created project: ${newProjectResult.projectId}`
|
|
62
|
-
);
|
|
63
|
-
console.log(newProjectResult.billingMessage);
|
|
64
|
-
console.log('\nProject creation test completed successfully.');
|
|
65
|
-
} else {
|
|
66
|
-
console.error(
|
|
67
|
-
'Failed to create a new project or retrieve project details.'
|
|
68
|
-
);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error('Error during project creation test:', error.message);
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
main();
|
|
20
|
+
test('should create a new project and attach billing', async (t) => {
|
|
21
|
+
console.log('Attempting to create a new project and attach billing...');
|
|
22
|
+
await setupProject(t);
|
|
23
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
import assert from 'node:assert';
|
|
17
|
+
import {
|
|
18
|
+
createProjectAndAttachBilling,
|
|
19
|
+
deleteProject,
|
|
20
|
+
generateProjectId,
|
|
21
|
+
} from '../../lib/cloud-api/projects.js';
|
|
22
|
+
import {
|
|
23
|
+
callWithRetry,
|
|
24
|
+
ensureApisEnabled,
|
|
25
|
+
} from '../../lib/cloud-api/helpers.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets project number from project ID.
|
|
29
|
+
* @param {string} projectId
|
|
30
|
+
* @returns {Promise<string>} project number
|
|
31
|
+
*/
|
|
32
|
+
export async function getProjectNumber(projectId) {
|
|
33
|
+
const { ProjectsClient } = await import('@google-cloud/resource-manager');
|
|
34
|
+
const client = new ProjectsClient();
|
|
35
|
+
try {
|
|
36
|
+
const [project] = await client.getProject({
|
|
37
|
+
name: `projects/${projectId}`,
|
|
38
|
+
});
|
|
39
|
+
// project.name is in format projects/123456
|
|
40
|
+
return project.name.split('/')[1];
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(
|
|
43
|
+
`Error getting project number for project ${projectId}:`,
|
|
44
|
+
error.message
|
|
45
|
+
);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Adds an IAM policy binding to a project.
|
|
52
|
+
* @param {string} projectId The project ID.
|
|
53
|
+
* @param {string} member The member to add, e.g., 'user:foo@example.com'.
|
|
54
|
+
* @param {string} role The role to grant, e.g., 'roles/viewer'.
|
|
55
|
+
*/
|
|
56
|
+
export async function addIamPolicyBinding(projectId, member, role) {
|
|
57
|
+
const { ProjectsClient } = await import('@google-cloud/resource-manager');
|
|
58
|
+
const client = new ProjectsClient();
|
|
59
|
+
|
|
60
|
+
console.log(
|
|
61
|
+
`Adding IAM binding for ${member} with role ${role} to project ${projectId}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const [policy] = await client.getIamPolicy({
|
|
66
|
+
resource: `projects/${projectId}`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log('Current IAM Policy:', JSON.stringify(policy, null, 2));
|
|
70
|
+
|
|
71
|
+
// Check if the binding already exists
|
|
72
|
+
const binding = policy.bindings.find((b) => b.role === role);
|
|
73
|
+
if (binding) {
|
|
74
|
+
if (!binding.members.includes(member)) {
|
|
75
|
+
binding.members.push(member);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
policy.bindings.push({
|
|
79
|
+
role: role,
|
|
80
|
+
members: [member],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('Updated IAM Policy:', JSON.stringify(policy, null, 2));
|
|
85
|
+
|
|
86
|
+
// Set the updated policy
|
|
87
|
+
await client.setIamPolicy({
|
|
88
|
+
resource: `projects/${projectId}`,
|
|
89
|
+
policy: policy,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
console.log(
|
|
93
|
+
`Successfully added IAM binding for ${member} with role ${
|
|
94
|
+
role
|
|
95
|
+
} to project ${projectId}`
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(
|
|
99
|
+
`Error adding IAM policy binding to project ${projectId}:`,
|
|
100
|
+
error.message
|
|
101
|
+
);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create project, attach billing.
|
|
108
|
+
* @returns {Promise<string>} projectId
|
|
109
|
+
*/
|
|
110
|
+
export async function setupProject() {
|
|
111
|
+
const projectId = 'test-' + generateProjectId();
|
|
112
|
+
console.log(`Generated project ID: ${projectId}`);
|
|
113
|
+
const parent = process.env.GCP_PARENT || process.argv[2];
|
|
114
|
+
const newProjectResult = await createProjectAndAttachBilling(
|
|
115
|
+
projectId,
|
|
116
|
+
parent
|
|
117
|
+
);
|
|
118
|
+
assert(newProjectResult, 'newProjectResult should not be null');
|
|
119
|
+
assert(
|
|
120
|
+
newProjectResult.projectId,
|
|
121
|
+
'newProjectResult.projectId should not be null'
|
|
122
|
+
);
|
|
123
|
+
assert(
|
|
124
|
+
newProjectResult.billingMessage,
|
|
125
|
+
'newProjectResult.billingMessage should not be null'
|
|
126
|
+
);
|
|
127
|
+
assert(
|
|
128
|
+
newProjectResult.billingMessage.startsWith(
|
|
129
|
+
`Project ${newProjectResult.projectId} created successfully.`
|
|
130
|
+
),
|
|
131
|
+
'newProjectResult.billingMessage should start with success message'
|
|
132
|
+
);
|
|
133
|
+
console.log(`Successfully created project: ${newProjectResult.projectId}`);
|
|
134
|
+
console.log(newProjectResult.billingMessage);
|
|
135
|
+
|
|
136
|
+
return projectId;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Delete project
|
|
141
|
+
* @param {string} projectId
|
|
142
|
+
*/
|
|
143
|
+
export async function cleanupProject(projectId) {
|
|
144
|
+
try {
|
|
145
|
+
await deleteProject(projectId);
|
|
146
|
+
console.log(`Successfully deleted project: ${projectId}`);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error(`Failed to delete project ${projectId}:`, e.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Enable APIs and set IAM permissions for source deployments.
|
|
154
|
+
* Note: This function is only needed for Cloud Build since it uses the compute
|
|
155
|
+
* default service account. The compute service account needs the editor role to
|
|
156
|
+
* deploy to Cloud Run, which is usually granted by default, but in this case we
|
|
157
|
+
* ensure it due to restrictions in some organizations.
|
|
158
|
+
* @param {string} projectId
|
|
159
|
+
*/
|
|
160
|
+
export async function setSourceDeployProjectPermissions(projectId) {
|
|
161
|
+
const { ServiceUsageClient } = await import('@google-cloud/service-usage');
|
|
162
|
+
const serviceUsageClient = new ServiceUsageClient({ projectId });
|
|
163
|
+
const context = {
|
|
164
|
+
serviceUsageClient: serviceUsageClient,
|
|
165
|
+
};
|
|
166
|
+
await ensureApisEnabled(context, projectId, ['run.googleapis.com']);
|
|
167
|
+
console.log('Adding editor role to Compute SA...');
|
|
168
|
+
const projectNumber = await getProjectNumber(projectId);
|
|
169
|
+
const member = `serviceAccount:${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
170
|
+
await callWithRetry(
|
|
171
|
+
() => addIamPolicyBinding(projectId, member, 'roles/editor'),
|
|
172
|
+
`addIamPolicyBinding roles/editor to ${member}`
|
|
173
|
+
);
|
|
174
|
+
console.log('Compute SA editor role added.');
|
|
175
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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 fs from 'fs/promises';
|
|
18
|
+
import assert from 'node:assert';
|
|
19
|
+
import { test, describe, before, after } from 'node:test';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
import { deploy, deployImage } from '../../../lib/deployment/deployer.js';
|
|
23
|
+
import {
|
|
24
|
+
cleanupProject,
|
|
25
|
+
setSourceDeployProjectPermissions,
|
|
26
|
+
setupProject,
|
|
27
|
+
} from '../test-helpers.js';
|
|
28
|
+
|
|
29
|
+
describe('Deployment workflows', () => {
|
|
30
|
+
let projectId;
|
|
31
|
+
|
|
32
|
+
before(async () => {
|
|
33
|
+
try {
|
|
34
|
+
projectId = await setupProject();
|
|
35
|
+
await setSourceDeployProjectPermissions(projectId);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('Error during project creation and setup:', err);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('Scenario-1: Starting deployment of hello image...', async () => {
|
|
43
|
+
const configImageDeploy = {
|
|
44
|
+
projectId: projectId,
|
|
45
|
+
serviceName: 'hello-scenario',
|
|
46
|
+
region: 'europe-west1',
|
|
47
|
+
imageUrl: 'gcr.io/cloudrun/hello',
|
|
48
|
+
};
|
|
49
|
+
await deployImage(configImageDeploy);
|
|
50
|
+
|
|
51
|
+
console.log('Scenario-1: Deployment completed.');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('Scenario-2: Starting deployment with invalid files...', async () => {
|
|
55
|
+
const configFailingBuild = {
|
|
56
|
+
projectId: projectId,
|
|
57
|
+
serviceName: 'example-failing-app',
|
|
58
|
+
region: 'europe-west1',
|
|
59
|
+
files: [
|
|
60
|
+
{
|
|
61
|
+
filename: 'main.txt',
|
|
62
|
+
content:
|
|
63
|
+
'This is not a valid application source file and should cause a build failure.',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
await assert.rejects(
|
|
68
|
+
deploy(configFailingBuild),
|
|
69
|
+
{ message: /ERROR: failed to detect: no buildpacks participating/ },
|
|
70
|
+
'Deployment should have failed with a buildpack detection error'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Scenario-3: Starting deployment of Go app with file content...', async () => {
|
|
75
|
+
const mainGoContent = await fs.readFile(
|
|
76
|
+
path.resolve('example-sources-to-deploy/main.go'),
|
|
77
|
+
'utf-8'
|
|
78
|
+
);
|
|
79
|
+
const goModContent = await fs.readFile(
|
|
80
|
+
path.resolve('example-sources-to-deploy/go.mod'),
|
|
81
|
+
'utf-8'
|
|
82
|
+
);
|
|
83
|
+
const configGoWithContent = {
|
|
84
|
+
projectId: projectId,
|
|
85
|
+
serviceName: 'example-go-app-content',
|
|
86
|
+
region: 'europe-west1',
|
|
87
|
+
files: [
|
|
88
|
+
{ filename: 'main.go', content: mainGoContent },
|
|
89
|
+
{ filename: 'go.mod', content: goModContent },
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
await deploy(configGoWithContent);
|
|
93
|
+
console.log('Scenario-3: Deployment completed.');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
after(async () => {
|
|
97
|
+
// Clean up: delete the project created for tests
|
|
98
|
+
cleanupProject(projectId);
|
|
99
|
+
});
|
|
100
|
+
});
|