@google-cloud/cloud-run-mcp 1.0.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/.prettierignore +2 -0
- package/.prettierrc.json +8 -0
- package/CONTRIBUTING.md +107 -0
- package/Dockerfile +7 -0
- package/LICENSE +201 -0
- package/README.md +191 -0
- package/example-sources-to-deploy/Dockerfile +49 -0
- package/example-sources-to-deploy/go.mod +3 -0
- package/example-sources-to-deploy/main.go +52 -0
- package/gemini-extension.json +10 -0
- package/lib/cloud-run-deploy.js +934 -0
- package/lib/cloud-run-services.js +181 -0
- package/lib/gcp-auth-check.js +79 -0
- package/lib/gcp-billing-accounts.js +98 -0
- package/lib/gcp-metadata.js +62 -0
- package/lib/gcp-projects.js +156 -0
- package/mcp-server.js +221 -0
- package/package.json +60 -0
- package/prompts.js +96 -0
- package/test/run-all-tests.js +19 -0
- package/test/test-create-project.js +77 -0
- package/test/test-deploy.js +158 -0
- package/test/test-gcp-auth.js +317 -0
- package/test/test-mcp-server.js +56 -0
- package/test/test-prompts.js +174 -0
- package/test/test-service-logs.js +109 -0
- package/test/test-tools.js +393 -0
- package/tools.js +936 -0
package/mcp-server.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Copyright 2025 Google LLC
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
you may not use this file except in compliance with the License.
|
|
8
|
+
You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
See the License for the specific language governing permissions and
|
|
16
|
+
limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import express from 'express';
|
|
20
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
21
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
22
|
+
// Support SSE for backward compatibility
|
|
23
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
24
|
+
// Support stdio, as it is easier to use locally
|
|
25
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
26
|
+
import { registerTools, registerToolsRemote } from './tools.js';
|
|
27
|
+
import { registerPrompts } from './prompts.js';
|
|
28
|
+
import { checkGCP } from './lib/gcp-metadata.js';
|
|
29
|
+
import { ensureGCPCredentials } from './lib/gcp-auth-check.js';
|
|
30
|
+
import 'dotenv/config';
|
|
31
|
+
|
|
32
|
+
const gcpInfo = await checkGCP();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ensure that console.log and console.error are compatible with stdio.
|
|
36
|
+
* (Right now, it just disables them)
|
|
37
|
+
*/
|
|
38
|
+
function makeLoggingCompatibleWithStdio() {
|
|
39
|
+
// redirect all console.log (which usually go to to stdout) to stderr.
|
|
40
|
+
console.log = console.error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldStartStdio() {
|
|
44
|
+
if (process.env.GCP_STDIO) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (gcpInfo && gcpInfo.project) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (shouldStartStdio()) {
|
|
54
|
+
makeLoggingCompatibleWithStdio();
|
|
55
|
+
await ensureGCPCredentials();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Read default configurations from environment variables
|
|
59
|
+
const envProjectId = process.env.GOOGLE_CLOUD_PROJECT || undefined;
|
|
60
|
+
const envRegion = process.env.GOOGLE_CLOUD_REGION;
|
|
61
|
+
const defaultServiceName = process.env.DEFAULT_SERVICE_NAME;
|
|
62
|
+
const skipIamCheck = process.env.SKIP_IAM_CHECK !== 'false';
|
|
63
|
+
|
|
64
|
+
async function getServer() {
|
|
65
|
+
// Create an MCP server with implementation details
|
|
66
|
+
const server = new McpServer(
|
|
67
|
+
{
|
|
68
|
+
name: 'cloud-run',
|
|
69
|
+
version: '1.0.0',
|
|
70
|
+
},
|
|
71
|
+
{ capabilities: { logging: {} } }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Get GCP metadata info once
|
|
75
|
+
const gcpInfo = await checkGCP();
|
|
76
|
+
|
|
77
|
+
// Determine the effective project and region based on priority: Env Var > GCP Metadata > Hardcoded default
|
|
78
|
+
const effectiveProjectId =
|
|
79
|
+
envProjectId || (gcpInfo && gcpInfo.project) || undefined;
|
|
80
|
+
const effectiveRegion =
|
|
81
|
+
envRegion || (gcpInfo && gcpInfo.region) || 'europe-west1';
|
|
82
|
+
|
|
83
|
+
if (shouldStartStdio() || !(gcpInfo && gcpInfo.project)) {
|
|
84
|
+
console.log('Using tools optimized for local or stdio mode.');
|
|
85
|
+
// Pass the determined defaults to the local tool registration
|
|
86
|
+
await registerTools(server, {
|
|
87
|
+
defaultProjectId: effectiveProjectId,
|
|
88
|
+
defaultRegion: effectiveRegion,
|
|
89
|
+
defaultServiceName,
|
|
90
|
+
skipIamCheck,
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
console.log(
|
|
94
|
+
`Running on GCP project: ${effectiveProjectId}, region: ${effectiveRegion}. Using tools optimized for remote use.`
|
|
95
|
+
);
|
|
96
|
+
// Pass the determined defaults to the remote tool registration
|
|
97
|
+
await registerToolsRemote(server, {
|
|
98
|
+
defaultProjectId: effectiveProjectId,
|
|
99
|
+
defaultRegion: effectiveRegion,
|
|
100
|
+
defaultServiceName,
|
|
101
|
+
skipIamCheck,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Register prompts with the server
|
|
106
|
+
registerPrompts(server);
|
|
107
|
+
|
|
108
|
+
return server;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// stdio
|
|
112
|
+
if (shouldStartStdio()) {
|
|
113
|
+
const stdioTransport = new StdioServerTransport();
|
|
114
|
+
const server = await getServer();
|
|
115
|
+
await server.connect(stdioTransport);
|
|
116
|
+
console.log('Cloud Run MCP server stdio transport connected');
|
|
117
|
+
} else {
|
|
118
|
+
console.log('Running on GCP, stdio transport will not be started.');
|
|
119
|
+
await ensureGCPCredentials();
|
|
120
|
+
const app = express();
|
|
121
|
+
app.use(express.json());
|
|
122
|
+
|
|
123
|
+
app.post('/mcp', async (req, res) => {
|
|
124
|
+
console.log('/mcp Received:', req.body);
|
|
125
|
+
const server = await getServer();
|
|
126
|
+
try {
|
|
127
|
+
const transport = new StreamableHTTPServerTransport({
|
|
128
|
+
sessionIdGenerator: undefined,
|
|
129
|
+
});
|
|
130
|
+
await server.connect(transport);
|
|
131
|
+
await transport.handleRequest(req, res, req.body);
|
|
132
|
+
res.on('close', () => {
|
|
133
|
+
console.log('Request closed');
|
|
134
|
+
transport.close();
|
|
135
|
+
server.close();
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error handling MCP request:', error);
|
|
139
|
+
if (!res.headersSent) {
|
|
140
|
+
res.status(500).json({
|
|
141
|
+
jsonrpc: '2.0',
|
|
142
|
+
error: {
|
|
143
|
+
code: -32603,
|
|
144
|
+
message: 'Internal server error',
|
|
145
|
+
},
|
|
146
|
+
id: null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
app.get('/mcp', async (req, res) => {
|
|
153
|
+
console.log('Received GET MCP request');
|
|
154
|
+
res.writeHead(405).end(
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
jsonrpc: '2.0',
|
|
157
|
+
error: {
|
|
158
|
+
code: -32000,
|
|
159
|
+
message: 'Method not allowed.',
|
|
160
|
+
},
|
|
161
|
+
id: null,
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
app.delete('/mcp', async (req, res) => {
|
|
167
|
+
console.log('Received DELETE MCP request');
|
|
168
|
+
res.writeHead(405).end(
|
|
169
|
+
JSON.stringify({
|
|
170
|
+
jsonrpc: '2.0',
|
|
171
|
+
error: {
|
|
172
|
+
code: -32000,
|
|
173
|
+
message: 'Method not allowed.',
|
|
174
|
+
},
|
|
175
|
+
id: null,
|
|
176
|
+
})
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Support SSE for baackward compatibility
|
|
181
|
+
const sseTransports = {};
|
|
182
|
+
|
|
183
|
+
// Legacy SSE endpoint for older clients
|
|
184
|
+
app.get('/sse', async (req, res) => {
|
|
185
|
+
console.log('/sse Received:', req.body);
|
|
186
|
+
const server = await getServer();
|
|
187
|
+
// Create SSE transport for legacy clients
|
|
188
|
+
const transport = new SSEServerTransport('/messages', res);
|
|
189
|
+
sseTransports[transport.sessionId] = transport;
|
|
190
|
+
|
|
191
|
+
res.on('close', () => {
|
|
192
|
+
delete sseTransports[transport.sessionId];
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await server.connect(transport);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Legacy message endpoint for older clients
|
|
199
|
+
app.post('/messages', async (req, res) => {
|
|
200
|
+
console.log('/messages Received:', req.body);
|
|
201
|
+
const sessionId = req.query.sessionId;
|
|
202
|
+
const transport = sseTransports[sessionId];
|
|
203
|
+
if (transport) {
|
|
204
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
205
|
+
} else {
|
|
206
|
+
res.status(400).send('No transport found for sessionId');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Start the server
|
|
211
|
+
const PORT = process.env.PORT || 3000;
|
|
212
|
+
app.listen(PORT, () => {
|
|
213
|
+
console.log(`Cloud Run MCP server listening on port ${PORT}`);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Handle server shutdown
|
|
218
|
+
process.on('SIGINT', async () => {
|
|
219
|
+
console.log('Shutting down server...');
|
|
220
|
+
process.exit(0);
|
|
221
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@google-cloud/cloud-run-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cloud Run MCP deployment tool",
|
|
6
|
+
"main": "mcp-server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cloud-run-mcp": "mcp-server.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"deploy": "gcloud run deploy cloud-run-mcp --source . --no-invoker-iam-check",
|
|
12
|
+
"test:deploy": "node test/test-deploy.js",
|
|
13
|
+
"test:create-project": "node test/test-create-project.js",
|
|
14
|
+
"test:service-logs": "node test/test-service-logs.js",
|
|
15
|
+
"test:gcp-auth": "node test/test-gcp-auth.js",
|
|
16
|
+
"test:prompts": "node --test test/test-prompts.js",
|
|
17
|
+
"test:mcp": "node --test test/test-mcp-server.js",
|
|
18
|
+
"test:tools": "node --test test/test-tools.js",
|
|
19
|
+
"test": "node test/run-all-tests.js",
|
|
20
|
+
"mcp-inspector": "npx @modelcontextprotocol/inspector node mcp-server.js",
|
|
21
|
+
"start": "node mcp-server.js",
|
|
22
|
+
"lint:check": "prettier --check .",
|
|
23
|
+
"lint": "prettier --write ."
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/GoogleCloudPlatform/cloud-run-mcp.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"cloud-run",
|
|
32
|
+
"gcp"
|
|
33
|
+
],
|
|
34
|
+
"author": "steren@google.com",
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/GoogleCloudPlatform/cloud-run-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/GoogleCloudPlatform/cloud-run-mcp#readme",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@google-cloud/artifact-registry": "^4.0.1",
|
|
42
|
+
"@google-cloud/billing": "^5.0.1",
|
|
43
|
+
"@google-cloud/cloudbuild": "^5.0.1",
|
|
44
|
+
"@google-cloud/logging": "^11.2.0",
|
|
45
|
+
"@google-cloud/resource-manager": "^6.0.1",
|
|
46
|
+
"@google-cloud/run": "^2.0.1",
|
|
47
|
+
"@google-cloud/service-usage": "^4.1.0",
|
|
48
|
+
"@google-cloud/storage": "^7.16.0",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
50
|
+
"archiver": "^7.0.1",
|
|
51
|
+
"dotenv": "^17.2.1",
|
|
52
|
+
"express": "^5.1.0",
|
|
53
|
+
"google-proto-files": "^5.0.0",
|
|
54
|
+
"zod": "^3.25.76"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"esmock": "^2.7.1",
|
|
58
|
+
"prettier": "3.6.2"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/prompts.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 { z } from 'zod';
|
|
17
|
+
|
|
18
|
+
export const registerPrompts = (server) => {
|
|
19
|
+
// Prompts will be registered here.
|
|
20
|
+
server.registerPrompt(
|
|
21
|
+
'deploy',
|
|
22
|
+
{
|
|
23
|
+
description: 'Deploys the current working directory to Cloud Run.',
|
|
24
|
+
argsSchema: {
|
|
25
|
+
name: z
|
|
26
|
+
.string()
|
|
27
|
+
.describe(
|
|
28
|
+
'Name of the Cloud Run service to deploy to. Defaults to the name of the current directory'
|
|
29
|
+
)
|
|
30
|
+
.optional(),
|
|
31
|
+
project: z.string().describe('Google Cloud project ID').optional(),
|
|
32
|
+
region: z
|
|
33
|
+
.string()
|
|
34
|
+
.describe('Region where the services are located')
|
|
35
|
+
.optional(),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async ({ name, project, region }) => {
|
|
39
|
+
const serviceNamePrompt =
|
|
40
|
+
name ||
|
|
41
|
+
'a name for the application based on the current working directory.';
|
|
42
|
+
const projectPrompt = project ? ` in project ${project}` : '';
|
|
43
|
+
const regionPrompt = region ? ` in region ${region}` : '';
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
messages: [
|
|
47
|
+
{
|
|
48
|
+
role: 'user',
|
|
49
|
+
content: {
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: `Use the deploy_local_folder tool to deploy the current folder${projectPrompt}${regionPrompt}. The service name should be ${serviceNamePrompt}`,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
server.registerPrompt(
|
|
60
|
+
'logs',
|
|
61
|
+
{
|
|
62
|
+
description: 'Gets the logs for a Cloud Run service.',
|
|
63
|
+
argsSchema: {
|
|
64
|
+
service: z
|
|
65
|
+
.string()
|
|
66
|
+
.describe(
|
|
67
|
+
'Name of the Cloud Run service. Defaults to the name of the current directory.'
|
|
68
|
+
)
|
|
69
|
+
.optional(),
|
|
70
|
+
project: z.string().describe('Google Cloud project ID').optional(),
|
|
71
|
+
region: z
|
|
72
|
+
.string()
|
|
73
|
+
.describe('Region where the services are located')
|
|
74
|
+
.optional(),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
async ({ service, project, region }) => {
|
|
78
|
+
const serviceNamePrompt =
|
|
79
|
+
service || 'named for the current working directory';
|
|
80
|
+
const projectPrompt = project ? ` in project ${project}` : '';
|
|
81
|
+
const regionPrompt = region ? ` in region ${region}` : '';
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: {
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: `Use get_service_log to get logs${projectPrompt}${regionPrompt} for the service ${serviceNamePrompt}`,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
const tests = [
|
|
4
|
+
'npm run test:deploy',
|
|
5
|
+
'npm run test:service-logs',
|
|
6
|
+
'npm run test:gcp-auth',
|
|
7
|
+
'npm run test:create-project',
|
|
8
|
+
'npm run test:prompts',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
for (const test of tests) {
|
|
12
|
+
try {
|
|
13
|
+
console.log(`Running: ${test}`);
|
|
14
|
+
execSync(test, { stdio: 'inherit' });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error(`Failed to run: ${test}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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 readline from 'readline/promises';
|
|
18
|
+
import { stdin as input, stdout as output } from 'process';
|
|
19
|
+
import { createProjectAndAttachBilling } from '../lib/gcp-projects.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Prompts the user for an optional project ID or allows them to proceed with auto-generation.
|
|
23
|
+
* Checks for a project ID from command line arguments first.
|
|
24
|
+
* @returns {Promise<string|undefined>} The Google Cloud project ID or undefined if auto-generation is chosen.
|
|
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();
|
|
@@ -0,0 +1,158 @@
|
|
|
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 { deploy, deployImage } from '../lib/cloud-run-deploy.js';
|
|
18
|
+
import readline from 'readline/promises';
|
|
19
|
+
import { stdin as input, stdout as output } from 'process';
|
|
20
|
+
import fs from 'fs/promises';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the project ID from command line arguments or prompts the user.
|
|
25
|
+
* If "new" is provided, it returns undefined to trigger new project creation.
|
|
26
|
+
* @returns {Promise<string|undefined>} The Google Cloud project ID or undefined.
|
|
27
|
+
*/
|
|
28
|
+
async function getProjectId() {
|
|
29
|
+
let projectIdInput = process.argv[2]; // Get the third element (index 2) which is the first argument
|
|
30
|
+
|
|
31
|
+
if (!projectIdInput) {
|
|
32
|
+
const rl = readline.createInterface({ input, output });
|
|
33
|
+
projectIdInput = await rl.question(
|
|
34
|
+
'Please enter your Google Cloud project ID: '
|
|
35
|
+
);
|
|
36
|
+
rl.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!projectIdInput || projectIdInput.trim() === '') {
|
|
40
|
+
console.error('No project ID provided. Exiting.');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (projectIdInput.toLowerCase() === 'new') {
|
|
44
|
+
console.error(
|
|
45
|
+
'Project creation ("new") is now handled by test-create-project.js. Please provide an existing project ID. Exiting.'
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
console.log(`Using Project ID: ${projectIdInput}`);
|
|
50
|
+
return projectIdInput;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const projectIdToUse = await getProjectId();
|
|
54
|
+
|
|
55
|
+
// Configuration for Go deployment with Dockerfile
|
|
56
|
+
const configGoWithDockerfile = {
|
|
57
|
+
projectId: projectIdToUse, // Use the obtained project ID
|
|
58
|
+
serviceName: 'example-go-app-docker', // Name of the Cloud Run service
|
|
59
|
+
region: 'europe-west1', // Google Cloud region
|
|
60
|
+
files: [
|
|
61
|
+
'example-sources-to-deploy/main.go',
|
|
62
|
+
'example-sources-to-deploy/go.mod',
|
|
63
|
+
'example-sources-to-deploy/Dockerfile',
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Configuration for Go deployment without Dockerfile (using buildpacks)
|
|
68
|
+
const configGoWithoutDockerfile = {
|
|
69
|
+
projectId: projectIdToUse,
|
|
70
|
+
serviceName: 'example-go-app-buildpack',
|
|
71
|
+
region: 'europe-west1',
|
|
72
|
+
files: [
|
|
73
|
+
'example-sources-to-deploy/main.go',
|
|
74
|
+
'example-sources-to-deploy/go.mod',
|
|
75
|
+
// Dockerfile is intentionally omitted here
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Configuration for Go deployment with file content (using buildpacks)
|
|
80
|
+
const configGoWithContent = {
|
|
81
|
+
projectId: projectIdToUse,
|
|
82
|
+
serviceName: 'example-go-app-content',
|
|
83
|
+
region: 'europe-west1',
|
|
84
|
+
files: [], // To be populated with file content
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Configuration for a deployment that is expected to fail
|
|
88
|
+
const configFailingBuild = {
|
|
89
|
+
projectId: projectIdToUse,
|
|
90
|
+
serviceName: 'example-failing-app',
|
|
91
|
+
region: 'europe-west1',
|
|
92
|
+
files: [
|
|
93
|
+
{
|
|
94
|
+
filename: 'main.txt',
|
|
95
|
+
content:
|
|
96
|
+
'This is not a valid application source file and should cause a build failure.',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Configuration for deploying a container image
|
|
102
|
+
const configImageDeploy = {
|
|
103
|
+
projectId: projectIdToUse,
|
|
104
|
+
serviceName: 'hello-from-image',
|
|
105
|
+
region: 'europe-west1',
|
|
106
|
+
imageUrl: 'gcr.io/cloudrun/hello',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
console.log('\n--- Testing container image deployment ---');
|
|
111
|
+
await deployImage(configImageDeploy);
|
|
112
|
+
console.log('--- Container image deployment test completed ---');
|
|
113
|
+
|
|
114
|
+
console.log('\n--- Testing intentionally failing build ---');
|
|
115
|
+
try {
|
|
116
|
+
await deploy(configFailingBuild);
|
|
117
|
+
// If deploy doesn't throw an error, then the test has failed because it was expected to fail.
|
|
118
|
+
console.error(
|
|
119
|
+
'Deployment test failed: The build with invalid files succeeded when it was expected to fail.'
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.log('Intentionally failing build test completed as expected.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log('--- Testing Go deployment with Dockerfile ---');
|
|
127
|
+
await deploy(configGoWithDockerfile);
|
|
128
|
+
console.log('--- Go deployment with Dockerfile test completed ---');
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
'\n--- Testing Go deployment without Dockerfile (Buildpacks) ---'
|
|
132
|
+
);
|
|
133
|
+
await deploy(configGoWithoutDockerfile);
|
|
134
|
+
console.log(
|
|
135
|
+
'--- Go deployment without Dockerfile (Buildpacks) test completed ---'
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
console.log('\n--- Testing Go deployment with file content (Buildpacks) ---');
|
|
139
|
+
const mainGoContent = await fs.readFile(
|
|
140
|
+
path.resolve('example-sources-to-deploy/main.go'),
|
|
141
|
+
'utf-8'
|
|
142
|
+
);
|
|
143
|
+
const goModContent = await fs.readFile(
|
|
144
|
+
path.resolve('example-sources-to-deploy/go.mod'),
|
|
145
|
+
'utf-8'
|
|
146
|
+
);
|
|
147
|
+
configGoWithContent.files = [
|
|
148
|
+
{ filename: 'main.go', content: mainGoContent },
|
|
149
|
+
{ filename: 'go.mod', content: goModContent },
|
|
150
|
+
];
|
|
151
|
+
await deploy(configGoWithContent);
|
|
152
|
+
console.log(
|
|
153
|
+
'--- Go deployment with file content (Buildpacks) test completed ---'
|
|
154
|
+
);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('An unexpected error occurred in the test script:', error);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|