@google-cloud/cloud-run-mcp 1.5.0 → 1.7.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/workflows/lint-checker.yml +17 -0
- package/.github/workflows/npm-publish.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 +24 -9
- package/README.md +52 -11
- package/REVIEWING.md +20 -0
- package/gemini-extension/GEMINI.md +1 -1
- package/lib/cloud-api/billing.js +183 -0
- package/lib/cloud-api/build.js +200 -0
- package/lib/{gcp-api-helpers.js → cloud-api/helpers.js} +81 -35
- 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/constants.js +54 -0
- package/lib/deployment/deployer.js +621 -0
- package/lib/deployment/helpers.js +194 -0
- package/lib/deployment/source-processor.js +124 -0
- package/lib/util/archive.js +104 -0
- package/lib/util/helpers.js +42 -0
- package/mcp-server.js +21 -10
- package/package.json +8 -5
- package/test/local/cloud-api/build.test.js +436 -0
- package/test/local/cloud-api/projects.test.js +18 -0
- package/test/local/deployment-helpers.test.js +231 -0
- package/test/local/mcp-server-stdio.test.js +59 -0
- package/test/local/mcp-server-streamable-http.test.js +61 -0
- package/test/local/notifications.test.js +9 -6
- package/test/local/source-processor.test.js +290 -0
- package/test/local/test-utils.js +51 -0
- package/test/local/tools.test.js +114 -70
- package/test/need-gcp/cloud-run-services.test.js +44 -44
- package/test/need-gcp/{cloud-run-deploy.test.js → deployer.test.js} +3 -56
- package/test/need-gcp/gcp-auth-check.test.js +1 -1
- package/test/need-gcp/gcp-projects.test.js +3 -15
- 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 +106 -97
- package/tools/tools.js +2 -9
- package/lib/cloud-run-deploy.js +0 -937
- package/lib/gcp-billing-accounts.js +0 -98
- /package/lib/{gcp-auth-check.js → cloud-api/auth.js} +0 -0
- /package/lib/{gcp-metadata.js → cloud-api/metadata.js} +0 -0
package/test/local/tools.test.js
CHANGED
|
@@ -37,12 +37,16 @@ describe('registerTools', () => {
|
|
|
37
37
|
registerTool: mock.fn(),
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
'../../
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
const { registerTools } = await esmock(
|
|
41
|
+
'../../tools/tools.js',
|
|
42
|
+
{},
|
|
43
|
+
{
|
|
44
|
+
'../../lib/cloud-api/projects.js': {
|
|
45
|
+
listProjects: () =>
|
|
46
|
+
Promise.resolve([{ id: 'project1' }, { id: 'project2' }]),
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
);
|
|
46
50
|
|
|
47
51
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
48
52
|
|
|
@@ -68,15 +72,19 @@ describe('registerTools', () => {
|
|
|
68
72
|
registerTool: mock.fn(),
|
|
69
73
|
};
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
'../../
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const { registerTools } = await esmock(
|
|
76
|
+
'../../tools/tools.js',
|
|
77
|
+
{},
|
|
78
|
+
{
|
|
79
|
+
'../../lib/cloud-api/projects.js': {
|
|
80
|
+
createProjectAndAttachBilling: (projectId) =>
|
|
81
|
+
Promise.resolve({
|
|
82
|
+
projectId: projectId,
|
|
83
|
+
billingMessage: `Project ${projectId} created successfully. Billing attached.`,
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
80
88
|
|
|
81
89
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
82
90
|
|
|
@@ -89,7 +97,7 @@ describe('registerTools', () => {
|
|
|
89
97
|
content: [
|
|
90
98
|
{
|
|
91
99
|
type: 'text',
|
|
92
|
-
text: '
|
|
100
|
+
text: 'Project my-project created successfully. Billing attached.',
|
|
93
101
|
},
|
|
94
102
|
],
|
|
95
103
|
});
|
|
@@ -100,15 +108,20 @@ describe('registerTools', () => {
|
|
|
100
108
|
registerTool: mock.fn(),
|
|
101
109
|
};
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
'../../
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
const { registerTools } = await esmock(
|
|
112
|
+
'../../tools/tools.js',
|
|
113
|
+
{},
|
|
114
|
+
{
|
|
115
|
+
'../../lib/cloud-api/projects.js': {
|
|
116
|
+
createProjectAndAttachBilling: () =>
|
|
117
|
+
Promise.resolve({
|
|
118
|
+
projectId: 'generated-project',
|
|
119
|
+
billingMessage:
|
|
120
|
+
'Project generated-project created successfully. Billing attached.',
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
);
|
|
112
125
|
|
|
113
126
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
114
127
|
|
|
@@ -121,7 +134,7 @@ describe('registerTools', () => {
|
|
|
121
134
|
content: [
|
|
122
135
|
{
|
|
123
136
|
type: 'text',
|
|
124
|
-
text: '
|
|
137
|
+
text: 'Project generated-project created successfully. Billing attached.',
|
|
125
138
|
},
|
|
126
139
|
],
|
|
127
140
|
});
|
|
@@ -134,15 +147,25 @@ describe('registerTools', () => {
|
|
|
134
147
|
registerTool: mock.fn(),
|
|
135
148
|
};
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
'../../
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
const { registerTools } = await esmock(
|
|
151
|
+
'../../tools/tools.js',
|
|
152
|
+
{},
|
|
153
|
+
{
|
|
154
|
+
'../../lib/cloud-api/run.js': {
|
|
155
|
+
listServices: () =>
|
|
156
|
+
Promise.resolve({
|
|
157
|
+
'my-region1': [
|
|
158
|
+
{ name: 'service1', uri: 'uri1' },
|
|
159
|
+
{ name: 'service2', uri: 'uri2' },
|
|
160
|
+
],
|
|
161
|
+
'my-region2': [
|
|
162
|
+
{ name: 'service3', uri: 'uri3' },
|
|
163
|
+
{ name: 'service4', uri: 'uri4' },
|
|
164
|
+
],
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
);
|
|
146
169
|
|
|
147
170
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
148
171
|
|
|
@@ -151,14 +174,17 @@ describe('registerTools', () => {
|
|
|
151
174
|
).arguments[2];
|
|
152
175
|
const result = await handler({
|
|
153
176
|
project: 'my-project',
|
|
154
|
-
region: 'my-region',
|
|
155
177
|
});
|
|
156
178
|
|
|
157
179
|
assert.deepStrictEqual(result, {
|
|
158
180
|
content: [
|
|
159
181
|
{
|
|
160
182
|
type: 'text',
|
|
161
|
-
text: 'Services in project my-project (location my-
|
|
183
|
+
text: 'Services in project my-project (location my-region1):\n- service1 (URL: uri1)\n- service2 (URL: uri2)',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: 'Services in project my-project (location my-region2):\n- service3 (URL: uri3)\n- service4 (URL: uri4)',
|
|
162
188
|
},
|
|
163
189
|
],
|
|
164
190
|
});
|
|
@@ -171,16 +197,20 @@ describe('registerTools', () => {
|
|
|
171
197
|
registerTool: mock.fn(),
|
|
172
198
|
};
|
|
173
199
|
|
|
174
|
-
|
|
175
|
-
'../../
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
const { registerTools } = await esmock(
|
|
201
|
+
'../../tools/tools.js',
|
|
202
|
+
{},
|
|
203
|
+
{
|
|
204
|
+
'../../lib/cloud-api/run.js': {
|
|
205
|
+
getService: () =>
|
|
206
|
+
Promise.resolve({
|
|
207
|
+
name: 'my-service',
|
|
208
|
+
uri: 'my-uri',
|
|
209
|
+
lastModifier: 'me',
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
);
|
|
184
214
|
|
|
185
215
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
186
216
|
|
|
@@ -222,11 +252,15 @@ describe('registerTools', () => {
|
|
|
222
252
|
return Promise.resolve({ logs: 'log3\nlog4', requestOptions: null });
|
|
223
253
|
};
|
|
224
254
|
|
|
225
|
-
|
|
226
|
-
'../../
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
255
|
+
const { registerTools } = await esmock(
|
|
256
|
+
'../../tools/tools.js',
|
|
257
|
+
{},
|
|
258
|
+
{
|
|
259
|
+
'../../lib/cloud-api/run.js': {
|
|
260
|
+
getServiceLogs: getServiceLogs,
|
|
261
|
+
},
|
|
262
|
+
}
|
|
263
|
+
);
|
|
230
264
|
|
|
231
265
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
232
266
|
|
|
@@ -250,19 +284,21 @@ describe('registerTools', () => {
|
|
|
250
284
|
});
|
|
251
285
|
});
|
|
252
286
|
|
|
253
|
-
|
|
254
|
-
|
|
255
287
|
describe('deploy_local_folder', () => {
|
|
256
288
|
it('should deploy local folder', async () => {
|
|
257
289
|
const server = {
|
|
258
290
|
registerTool: mock.fn(),
|
|
259
291
|
};
|
|
260
292
|
|
|
261
|
-
|
|
262
|
-
'../../
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
293
|
+
const { registerTools } = await esmock(
|
|
294
|
+
'../../tools/tools.js',
|
|
295
|
+
{},
|
|
296
|
+
{
|
|
297
|
+
'../../lib/deployment/deployer.js': {
|
|
298
|
+
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
);
|
|
266
302
|
|
|
267
303
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
268
304
|
|
|
@@ -296,11 +332,15 @@ describe('registerTools', () => {
|
|
|
296
332
|
registerTool: mock.fn(),
|
|
297
333
|
};
|
|
298
334
|
|
|
299
|
-
|
|
300
|
-
'../../
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
335
|
+
const { registerTools } = await esmock(
|
|
336
|
+
'../../tools/tools.js',
|
|
337
|
+
{},
|
|
338
|
+
{
|
|
339
|
+
'../../lib/deployment/deployer.js': {
|
|
340
|
+
deploy: () => Promise.resolve({ uri: 'my-uri' }),
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
);
|
|
304
344
|
|
|
305
345
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
306
346
|
|
|
@@ -334,11 +374,15 @@ describe('registerTools', () => {
|
|
|
334
374
|
registerTool: mock.fn(),
|
|
335
375
|
};
|
|
336
376
|
|
|
337
|
-
|
|
338
|
-
'../../
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
377
|
+
const { registerTools } = await esmock(
|
|
378
|
+
'../../tools/tools.js',
|
|
379
|
+
{},
|
|
380
|
+
{
|
|
381
|
+
'../../lib/deployment/deployer.js': {
|
|
382
|
+
deployImage: () => Promise.resolve({ uri: 'my-uri' }),
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
);
|
|
342
386
|
|
|
343
387
|
registerTools(server, { gcpCredentialsAvailable: true });
|
|
344
388
|
|
|
@@ -372,7 +416,7 @@ describe('registerTools', () => {
|
|
|
372
416
|
registerTool: mock.fn(),
|
|
373
417
|
};
|
|
374
418
|
|
|
375
|
-
|
|
419
|
+
const { registerTools } = await esmock('../../tools/tools.js', {});
|
|
376
420
|
|
|
377
421
|
registerTools(server, { gcpCredentialsAvailable: false });
|
|
378
422
|
|
|
@@ -15,66 +15,68 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { test } from 'node:test';
|
|
18
|
+
import { getServiceLogs, listServices } from '../../lib/cloud-api/run.js';
|
|
18
19
|
import assert from 'node:assert';
|
|
19
|
-
import {
|
|
20
|
-
getServiceLogs,
|
|
21
|
-
listServices,
|
|
22
|
-
} from '../../lib/cloud-run-services.js';
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
|
-
*
|
|
26
|
-
* @returns {
|
|
22
|
+
* Get the GCP project ID from GOOGLE_CLOUD_PROJECT or command line argument.
|
|
23
|
+
* @returns {string} The GCP project ID
|
|
27
24
|
*/
|
|
28
|
-
function
|
|
25
|
+
function getProjectId() {
|
|
29
26
|
const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.argv[2];
|
|
30
|
-
const [, , arg1, arg2, arg3] = process.argv;
|
|
31
|
-
|
|
32
|
-
let region = 'europe-west1';
|
|
33
|
-
let serviceId;
|
|
34
|
-
|
|
35
|
-
if (process.env.GOOGLE_CLOUD_PROJECT) {
|
|
36
|
-
region = arg1 || 'europe-west1';
|
|
37
|
-
serviceId = arg2;
|
|
38
|
-
} else {
|
|
39
|
-
region = arg2 || 'europe-west1';
|
|
40
|
-
serviceId = arg3;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
27
|
if (!projectId) {
|
|
44
28
|
console.error(
|
|
45
|
-
'Usage: node <script> <projectId>
|
|
29
|
+
'Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT'
|
|
46
30
|
);
|
|
47
31
|
process.exit(1);
|
|
48
32
|
}
|
|
33
|
+
console.log(`Using Project ID: ${projectId}`);
|
|
34
|
+
return projectId;
|
|
35
|
+
}
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Gets service details from GOOGLE_CLOUD_PROJECT or command line arguments.
|
|
39
|
+
* @returns {{projectId: string, region: string, serviceId: string}} The service details
|
|
40
|
+
*/
|
|
41
|
+
async function getServiceDetails() {
|
|
42
|
+
const projectId = getProjectId();
|
|
43
|
+
let region, serviceId;
|
|
56
44
|
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
try {
|
|
46
|
+
const allServices = await listServices(projectId);
|
|
47
|
+
|
|
48
|
+
if (!allServices || Object.keys(allServices).length === 0) {
|
|
49
|
+
assert.fail('No services found for the given project.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const regions = Object.keys(allServices);
|
|
53
|
+
region = regions[0];
|
|
54
|
+
|
|
55
|
+
if (!allServices[region] || allServices[region].length === 0) {
|
|
56
|
+
assert.fail(`No services found in region: ${region}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const serviceName = allServices[region][0].name;
|
|
60
|
+
serviceId = serviceName.split('/').pop();
|
|
61
|
+
console.log(`Using region - ${region} and service ID - ${serviceId}`);
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
return { projectId, region, serviceId };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Better error handling for the API call itself
|
|
66
|
+
console.error('Error fetching services:', error.message);
|
|
67
|
+
throw error; // Re-throw the error to fail the test
|
|
68
|
+
}
|
|
69
|
+
}
|
|
61
70
|
|
|
62
71
|
test('should list services', async () => {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
console.log('Services found:', services.
|
|
72
|
+
const projectId = getProjectId();
|
|
73
|
+
const services = await listServices(projectId);
|
|
74
|
+
console.log('Services found:', services ? Object.keys(services) : 'None');
|
|
75
|
+
console.log('All Services', services);
|
|
66
76
|
});
|
|
67
77
|
|
|
68
78
|
test('should fetch service logs', async () => {
|
|
69
|
-
|
|
70
|
-
console.log('Skipping service log test: no serviceId provided.');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
console.log(
|
|
75
|
-
`
|
|
76
|
-
Fetching logs for service "${serviceId}" in project "${projectId}" (region: ${region})...`
|
|
77
|
-
);
|
|
79
|
+
const { projectId, region, serviceId } = await getServiceDetails();
|
|
78
80
|
|
|
79
81
|
const result = await getServiceLogs(projectId, region, serviceId);
|
|
80
82
|
|
|
@@ -85,5 +87,3 @@ Fetching logs for service "${serviceId}" in project "${projectId}" (region: ${re
|
|
|
85
87
|
console.log('No logs found for this service.');
|
|
86
88
|
}
|
|
87
89
|
});
|
|
88
|
-
|
|
89
|
-
|
|
@@ -16,68 +16,15 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
import { describe, test } from 'node:test';
|
|
18
18
|
import assert from 'node:assert';
|
|
19
|
-
import { deploy, deployImage } from '../../lib/
|
|
20
|
-
import fs from 'fs/promises';
|
|
21
|
-
import path from 'path';
|
|
19
|
+
import { deploy, deployImage } from '../../lib/deployment/deployer.js';
|
|
22
20
|
|
|
23
21
|
const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.argv[2];
|
|
24
22
|
if (!projectId) {
|
|
25
|
-
console.error(
|
|
26
|
-
'Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT'
|
|
27
|
-
);
|
|
23
|
+
console.error('Usage: node <script> <projectId> or set GOOGLE_CLOUD_PROJECT');
|
|
28
24
|
process.exit(1);
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
describe('Cloud Run Deployments', () => {
|
|
32
|
-
test('should deploy a container image', async () => {
|
|
33
|
-
const configImageDeploy = {
|
|
34
|
-
projectId: projectId,
|
|
35
|
-
serviceName: 'hello-from-image',
|
|
36
|
-
region: 'europe-west1',
|
|
37
|
-
imageUrl: 'gcr.io/cloudrun/hello',
|
|
38
|
-
};
|
|
39
|
-
await deployImage(configImageDeploy);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('should fail to deploy with invalid files', async () => {
|
|
43
|
-
const configFailingBuild = {
|
|
44
|
-
projectId: projectId,
|
|
45
|
-
serviceName: 'example-failing-app',
|
|
46
|
-
region: 'europe-west1',
|
|
47
|
-
files: [
|
|
48
|
-
{
|
|
49
|
-
filename: 'main.txt',
|
|
50
|
-
content:
|
|
51
|
-
'This is not a valid application source file and should cause a build failure.',
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
await assert.rejects(deploy(configFailingBuild));
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
test('should deploy a Go app with file content (Buildpacks)', async () => {
|
|
61
|
-
const mainGoContent = await fs.readFile(
|
|
62
|
-
path.resolve('example-sources-to-deploy/main.go'),
|
|
63
|
-
'utf-8'
|
|
64
|
-
);
|
|
65
|
-
const goModContent = await fs.readFile(
|
|
66
|
-
path.resolve('example-sources-to-deploy/go.mod'),
|
|
67
|
-
'utf-8'
|
|
68
|
-
);
|
|
69
|
-
const configGoWithContent = {
|
|
70
|
-
projectId: projectId,
|
|
71
|
-
serviceName: 'example-go-app-content',
|
|
72
|
-
region: 'europe-west1',
|
|
73
|
-
files: [
|
|
74
|
-
{ filename: 'main.go', content: mainGoContent },
|
|
75
|
-
{ filename: 'go.mod', content: goModContent },
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
await deploy(configGoWithContent);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
28
|
test('should fail to deploy without a service name', async () => {
|
|
82
29
|
const config = {
|
|
83
30
|
projectId: projectId,
|
|
@@ -121,4 +68,4 @@ describe('Cloud Run Deployments', () => {
|
|
|
121
68
|
message: 'Error: projectId is required in the configuration object.',
|
|
122
69
|
});
|
|
123
70
|
});
|
|
124
|
-
});
|
|
71
|
+
});
|
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|
|
17
17
|
import assert from 'node:assert/strict';
|
|
18
18
|
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
19
19
|
import { GoogleAuth } from 'google-auth-library';
|
|
20
|
-
import { ensureGCPCredentials } from '../../lib/
|
|
20
|
+
import { ensureGCPCredentials } from '../../lib/cloud-api/auth.js';
|
|
21
21
|
|
|
22
22
|
describe('ensureGCPCredentials', () => {
|
|
23
23
|
let originalConsoleLog;
|
|
@@ -15,21 +15,9 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { test } from 'node:test';
|
|
18
|
-
import
|
|
19
|
-
import { createProjectAndAttachBilling } from '../../lib/gcp-projects.js';
|
|
18
|
+
import { setupProject } from './test-helpers.js';
|
|
20
19
|
|
|
21
|
-
test('should create a new project and attach billing', async () => {
|
|
20
|
+
test('should create a new project and attach billing', async (t) => {
|
|
22
21
|
console.log('Attempting to create a new project and attach billing...');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
assert(newProjectResult, 'newProjectResult should not be null');
|
|
26
|
-
assert(
|
|
27
|
-
newProjectResult.projectId,
|
|
28
|
-
'newProjectResult.projectId should not be null'
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
console.log(
|
|
32
|
-
`Successfully created project: ${newProjectResult.projectId}`
|
|
33
|
-
);
|
|
34
|
-
console.log(newProjectResult.billingMessage);
|
|
22
|
+
await setupProject(t);
|
|
35
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
|
+
}
|