@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
|
@@ -0,0 +1,200 @@
|
|
|
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 { callWithRetry } from './helpers.js';
|
|
18
|
+
import { logAndProgress } from '../util/helpers.js';
|
|
19
|
+
|
|
20
|
+
const DELAY_WAIT_FOR_BUILD_LOGS = 10000; // 10 seconds delay to allow logs to propagate
|
|
21
|
+
const BUILD_LOGS_LINES_TO_FETCH = 100; // Number of log lines to fetch for build logs snippet
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Triggers a Google Cloud Build job to build a container image from source code in a GCS bucket.
|
|
25
|
+
* It uses either a Dockerfile found in the source or Google Cloud Buildpacks if no Dockerfile is present.
|
|
26
|
+
* Waits for the build to complete and returns the build result.
|
|
27
|
+
*
|
|
28
|
+
* @async
|
|
29
|
+
* @param {object} context - The context object containing clients and other parameters.
|
|
30
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
31
|
+
* @param {string} location - The Google Cloud region for the build.
|
|
32
|
+
* @param {string} sourceBucketName - The GCS bucket name where the source code (zip) is stored.
|
|
33
|
+
* @param {string} sourceBlobName - The GCS blob name (the zip file) for the source code.
|
|
34
|
+
* @param {string} targetRepoName - The name of the target Artifact Registry repository (used for context, not directly in build steps).
|
|
35
|
+
* @param {string} targetImageUrl - The full Artifact Registry URL for the image to be built (e.g., `location-docker.pkg.dev/project/repo/image:tag`).
|
|
36
|
+
* @param {boolean} hasDockerfile - Indicates whether a Dockerfile is present in the source to guide the build process.
|
|
37
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
38
|
+
* @returns {Promise<object>} A promise that resolves with the completed Cloud Build object.
|
|
39
|
+
* @throws {Error} If the Cloud Build job fails, times out, or encounters an error during initiation or execution.
|
|
40
|
+
*/
|
|
41
|
+
export async function triggerCloudBuild(
|
|
42
|
+
context,
|
|
43
|
+
projectId,
|
|
44
|
+
location,
|
|
45
|
+
sourceBucketName,
|
|
46
|
+
sourceBlobName,
|
|
47
|
+
targetRepoName,
|
|
48
|
+
targetImageUrl,
|
|
49
|
+
hasDockerfile,
|
|
50
|
+
progressCallback
|
|
51
|
+
) {
|
|
52
|
+
let buildSteps;
|
|
53
|
+
|
|
54
|
+
if (hasDockerfile) {
|
|
55
|
+
buildSteps = [
|
|
56
|
+
{
|
|
57
|
+
name: 'gcr.io/cloud-builders/docker',
|
|
58
|
+
args: ['build', '-t', targetImageUrl, '.'],
|
|
59
|
+
dir: '/workspace',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
} else {
|
|
63
|
+
buildSteps = [
|
|
64
|
+
{
|
|
65
|
+
name: 'gcr.io/k8s-skaffold/pack',
|
|
66
|
+
entrypoint: 'pack',
|
|
67
|
+
args: [
|
|
68
|
+
'build',
|
|
69
|
+
targetImageUrl,
|
|
70
|
+
'--builder',
|
|
71
|
+
'gcr.io/buildpacks/builder:latest',
|
|
72
|
+
],
|
|
73
|
+
dir: '/workspace',
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const build = {
|
|
79
|
+
source: {
|
|
80
|
+
storageSource: {
|
|
81
|
+
bucket: sourceBucketName,
|
|
82
|
+
object: sourceBlobName,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
steps: buildSteps,
|
|
86
|
+
images: [targetImageUrl],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await logAndProgress(
|
|
90
|
+
`Initiating Cloud Build for gs://${sourceBucketName}/${sourceBlobName} in ${location}...`,
|
|
91
|
+
progressCallback
|
|
92
|
+
);
|
|
93
|
+
const [operation] = await callWithRetry(
|
|
94
|
+
() =>
|
|
95
|
+
context.cloudBuildClient.createBuild({
|
|
96
|
+
projectId: projectId,
|
|
97
|
+
build: build,
|
|
98
|
+
}),
|
|
99
|
+
'cloudBuild.createBuild'
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await logAndProgress(`Cloud Build job started...`, progressCallback);
|
|
103
|
+
const buildId = operation.metadata.build.id;
|
|
104
|
+
let completedBuild;
|
|
105
|
+
while (true) {
|
|
106
|
+
const [getBuildOperation] = await callWithRetry(
|
|
107
|
+
() =>
|
|
108
|
+
context.cloudBuildClient.getBuild({
|
|
109
|
+
projectId: projectId,
|
|
110
|
+
id: buildId,
|
|
111
|
+
}),
|
|
112
|
+
`cloudBuild.getBuild ${buildId}`
|
|
113
|
+
);
|
|
114
|
+
if (
|
|
115
|
+
['SUCCESS', 'FAILURE', 'INTERNAL_ERROR', 'TIMEOUT', 'CANCELLED'].includes(
|
|
116
|
+
getBuildOperation.status
|
|
117
|
+
)
|
|
118
|
+
) {
|
|
119
|
+
completedBuild = getBuildOperation;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
await logAndProgress(
|
|
123
|
+
`Build status: ${getBuildOperation.status}. Waiting...`,
|
|
124
|
+
progressCallback,
|
|
125
|
+
'debug'
|
|
126
|
+
);
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (completedBuild.status === 'SUCCESS') {
|
|
131
|
+
await logAndProgress(
|
|
132
|
+
`Cloud Build job ${buildId} completed successfully.`,
|
|
133
|
+
progressCallback
|
|
134
|
+
);
|
|
135
|
+
await logAndProgress(
|
|
136
|
+
`Image built: ${completedBuild.results.images[0].name}`,
|
|
137
|
+
progressCallback
|
|
138
|
+
);
|
|
139
|
+
return completedBuild;
|
|
140
|
+
} else {
|
|
141
|
+
const failureMessage = `Cloud Build job ${buildId} failed with status: ${completedBuild.status}`;
|
|
142
|
+
await logAndProgress(failureMessage, progressCallback, 'error');
|
|
143
|
+
const logsMessage = `Build logs: ${completedBuild.logUrl}`;
|
|
144
|
+
await logAndProgress(logsMessage, progressCallback); // Log URL is info, failure is error
|
|
145
|
+
|
|
146
|
+
let buildLogsSnippet = `\n\nRefer to Log URL for full details: ${completedBuild.logUrl}`; // Default snippet
|
|
147
|
+
try {
|
|
148
|
+
const logFilter = `resource.type="build" AND resource.labels.build_id="${buildId}"`;
|
|
149
|
+
await logAndProgress(
|
|
150
|
+
`Attempting to fetch last ${BUILD_LOGS_LINES_TO_FETCH} log lines for build ${buildId}...`,
|
|
151
|
+
progressCallback,
|
|
152
|
+
'debug'
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Wait for a short period to allow logs to propagate
|
|
156
|
+
await new Promise((resolve) =>
|
|
157
|
+
setTimeout(resolve, DELAY_WAIT_FOR_BUILD_LOGS)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Fetch the most recent N log entries
|
|
161
|
+
const [entries] = await callWithRetry(
|
|
162
|
+
() =>
|
|
163
|
+
context.loggingClient.getEntries({
|
|
164
|
+
filter: logFilter,
|
|
165
|
+
orderBy: 'timestamp desc', // Get latest logs first
|
|
166
|
+
pageSize: BUILD_LOGS_LINES_TO_FETCH,
|
|
167
|
+
}),
|
|
168
|
+
`logging.getEntries for build ${buildId}`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (entries && entries.length > 0) {
|
|
172
|
+
// Entries are newest first, reverse for chronological order of the snippet
|
|
173
|
+
const logLines = entries.reverse().map((entry) => entry.data || '');
|
|
174
|
+
if (logLines.length > 0) {
|
|
175
|
+
buildLogsSnippet = `\n\nLast ${logLines.length} log lines from build ${buildId}:\n${logLines.join('\n')}`;
|
|
176
|
+
await logAndProgress(
|
|
177
|
+
`Successfully fetched snippet of build logs for ${buildId}.`,
|
|
178
|
+
progressCallback,
|
|
179
|
+
'info'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
await logAndProgress(
|
|
184
|
+
`No specific log entries retrieved for build ${buildId}. ${buildLogsSnippet}`,
|
|
185
|
+
progressCallback,
|
|
186
|
+
'warn'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} catch (logError) {
|
|
190
|
+
console.error(`Error fetching build logs for ${buildId}:`, logError);
|
|
191
|
+
await logAndProgress(
|
|
192
|
+
`Failed to fetch build logs snippet: ${logError.message}. ${buildLogsSnippet}`,
|
|
193
|
+
progressCallback,
|
|
194
|
+
'warn'
|
|
195
|
+
);
|
|
196
|
+
// buildLogsSnippet already contains the Log URL as a fallback
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Build ${buildId} failed.${buildLogsSnippet}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -14,6 +14,13 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { ensureBillingEnabled } from './billing.js';
|
|
18
|
+
|
|
19
|
+
const PREREQUISITE_APIS = [
|
|
20
|
+
'serviceusage.googleapis.com',
|
|
21
|
+
'cloudbilling.googleapis.com',
|
|
22
|
+
];
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Calls a function with retry logic for GCP API calls.
|
|
19
26
|
* Retries on gRPC error code 7 (PERMISSION_DENIED).
|
|
@@ -84,12 +91,61 @@ async function checkAndEnableApi(
|
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Enables a single Google Cloud API with retry logic.
|
|
96
|
+
* @param {object} serviceUsageClient - The Service Usage client.
|
|
97
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
98
|
+
* @param {string} api - The API identifier (e.g., 'run.googleapis.com').
|
|
99
|
+
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
100
|
+
* @returns {Promise<void>} A promise that resolves when the API is enabled.
|
|
101
|
+
* @throws {Error} If the API fails to enable or if there's an issue checking its status.
|
|
102
|
+
*/
|
|
103
|
+
export async function enableApiWithRetry(
|
|
104
|
+
serviceUsageClient,
|
|
105
|
+
projectId,
|
|
106
|
+
api,
|
|
107
|
+
progressCallback
|
|
108
|
+
) {
|
|
109
|
+
const serviceName = `projects/${projectId}/services/${api}`;
|
|
110
|
+
try {
|
|
111
|
+
await checkAndEnableApi(
|
|
112
|
+
serviceUsageClient,
|
|
113
|
+
serviceName,
|
|
114
|
+
api,
|
|
115
|
+
progressCallback
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// First attempt failed, log a warning and retry once after a delay.
|
|
119
|
+
const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
|
|
120
|
+
console.warn(warnMsg);
|
|
121
|
+
if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
|
|
122
|
+
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
124
|
+
try {
|
|
125
|
+
await checkAndEnableApi(
|
|
126
|
+
serviceUsageClient,
|
|
127
|
+
serviceName,
|
|
128
|
+
api,
|
|
129
|
+
progressCallback
|
|
130
|
+
);
|
|
131
|
+
} catch (retryError) {
|
|
132
|
+
// If the retry also fails, throw an error.
|
|
133
|
+
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
134
|
+
console.error(errorMessage, retryError);
|
|
135
|
+
if (progressCallback)
|
|
136
|
+
progressCallback({ level: 'error', data: errorMessage });
|
|
137
|
+
throw new Error(errorMessage);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
87
142
|
/**
|
|
88
143
|
* Ensures that the specified Google Cloud APIs are enabled for the given project.
|
|
89
144
|
* If an API is not enabled, it attempts to enable it. Retries any failure once after 1s.
|
|
90
145
|
* Throws an error if an API cannot be enabled.
|
|
91
146
|
*
|
|
92
147
|
* @async
|
|
148
|
+
* @param {object} context - The context object containing clients and other parameters.
|
|
93
149
|
* @param {string} projectId - The Google Cloud project ID.
|
|
94
150
|
* @param {string[]} apis - An array of API identifiers to check and enable (e.g., 'run.googleapis.com').
|
|
95
151
|
* @param {function(string, string=): void} progressCallback - A function to call with progress updates.
|
|
@@ -97,47 +153,37 @@ async function checkAndEnableApi(
|
|
|
97
153
|
* @throws {Error} If an API fails to enable or if there's an issue checking its status.
|
|
98
154
|
* @returns {Promise<void>} A promise that resolves when all specified APIs are enabled.
|
|
99
155
|
*/
|
|
100
|
-
export async function ensureApisEnabled(
|
|
101
|
-
|
|
102
|
-
|
|
156
|
+
export async function ensureApisEnabled(
|
|
157
|
+
context,
|
|
158
|
+
projectId,
|
|
159
|
+
apis,
|
|
160
|
+
progressCallback
|
|
161
|
+
) {
|
|
162
|
+
for (const api of PREREQUISITE_APIS) {
|
|
163
|
+
await enableApiWithRetry(
|
|
164
|
+
context.serviceUsageClient,
|
|
165
|
+
projectId,
|
|
166
|
+
api,
|
|
167
|
+
progressCallback
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Before enabling other APIs, ensure billing is enabled.
|
|
172
|
+
await ensureBillingEnabled(context, projectId, progressCallback);
|
|
173
|
+
|
|
103
174
|
const message = 'Checking and enabling required APIs...';
|
|
104
175
|
console.log(message);
|
|
105
176
|
if (progressCallback) progressCallback({ level: 'info', data: message });
|
|
106
177
|
|
|
107
|
-
|
|
108
178
|
for (const api of apis) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
progressCallback
|
|
116
|
-
);
|
|
117
|
-
} catch (error) {
|
|
118
|
-
// First attempt failed, log a warning and retry once after a delay.
|
|
119
|
-
const warnMsg = `Failed to check/enable ${api}, retrying in 1s...`;
|
|
120
|
-
console.warn(warnMsg);
|
|
121
|
-
if (progressCallback) progressCallback({ level: 'warn', data: warnMsg });
|
|
122
|
-
|
|
123
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
124
|
-
try {
|
|
125
|
-
await checkAndEnableApi(
|
|
126
|
-
serviceUsageClient,
|
|
127
|
-
serviceName,
|
|
128
|
-
api,
|
|
129
|
-
progressCallback
|
|
130
|
-
);
|
|
131
|
-
} catch (retryError) {
|
|
132
|
-
// If the retry also fails, throw an error.
|
|
133
|
-
const errorMessage = `Failed to ensure API [${api}] is enabled after retry. Please check manually.`;
|
|
134
|
-
console.error(errorMessage, retryError);
|
|
135
|
-
if (progressCallback) progressCallback({ level: 'error', data: errorMessage });
|
|
136
|
-
throw new Error(errorMessage);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
179
|
+
await enableApiWithRetry(
|
|
180
|
+
context.serviceUsageClient,
|
|
181
|
+
projectId,
|
|
182
|
+
api,
|
|
183
|
+
progressCallback
|
|
184
|
+
);
|
|
139
185
|
}
|
|
140
186
|
const successMsg = 'All required APIs are enabled.';
|
|
141
187
|
console.log(successMsg);
|
|
142
188
|
if (progressCallback) progressCallback({ level: 'info', data: successMsg });
|
|
143
|
-
}
|
|
189
|
+
}
|
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|
|
17
17
|
import {
|
|
18
18
|
listBillingAccounts,
|
|
19
19
|
attachProjectToBillingAccount,
|
|
20
|
-
} from './
|
|
20
|
+
} from './billing.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Lists all accessible Google Cloud Platform projects.
|
|
@@ -39,40 +39,50 @@ export async function listProjects() {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Creates a compliant GCP project ID in the format 'mcp-cvc-cvc', where 'c' is a consonant and 'v' is a vowel.
|
|
44
|
+
* @function generateProjectId
|
|
45
|
+
* @returns {string} A randomly generated, compliant GCP project ID in the format 'mcp-cvc-cvc'.
|
|
46
|
+
*/
|
|
47
|
+
export function generateProjectId() {
|
|
48
|
+
const consonants = 'bcdfghjklmnpqrstvwxyz';
|
|
49
|
+
const vowels = 'aeiou';
|
|
50
|
+
|
|
51
|
+
const getRandomChar = (source) =>
|
|
52
|
+
source.charAt(Math.floor(Math.random() * source.length));
|
|
53
|
+
|
|
54
|
+
const generateCVC = () => {
|
|
55
|
+
const c1 = getRandomChar(consonants);
|
|
56
|
+
const v = getRandomChar(vowels);
|
|
57
|
+
const c2 = getRandomChar(consonants);
|
|
58
|
+
return `${c1}${v}${c2}`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const cvc1 = generateCVC();
|
|
62
|
+
const cvc2 = generateCVC();
|
|
63
|
+
return `mcp-${cvc1}-${cvc2}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
/**
|
|
43
67
|
* Creates a new Google Cloud Platform project.
|
|
44
68
|
* @async
|
|
45
69
|
* @function createProject
|
|
46
70
|
* @param {string} [projectId] - Optional. The desired ID for the new project. If not provided, a compliant ID will be generated automatically (e.g., app-cvc-cvc).
|
|
71
|
+
* @param {string} [parent] - Optional. The resource name of the parent under which the project is to be created. e.g., "organizations/123" or "folders/456".
|
|
47
72
|
* @returns {Promise<{projectId: string}|null>} A promise that resolves to an object containing the new project's ID.
|
|
48
73
|
*/
|
|
49
|
-
export async function createProject(projectId) {
|
|
74
|
+
export async function createProject(projectId, parent) {
|
|
50
75
|
const { ProjectsClient } = await import('@google-cloud/resource-manager');
|
|
51
76
|
const client = new ProjectsClient();
|
|
52
77
|
let projectIdToUse = projectId;
|
|
53
78
|
|
|
54
79
|
if (!projectIdToUse) {
|
|
55
|
-
|
|
56
|
-
const vowels = 'aeiou';
|
|
57
|
-
|
|
58
|
-
const getRandomChar = (source) =>
|
|
59
|
-
source.charAt(Math.floor(Math.random() * source.length));
|
|
60
|
-
|
|
61
|
-
const generateCVC = () => {
|
|
62
|
-
const c1 = getRandomChar(consonants);
|
|
63
|
-
const v = getRandomChar(vowels);
|
|
64
|
-
const c2 = getRandomChar(consonants);
|
|
65
|
-
return `${c1}${v}${c2}`;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const cvc1 = generateCVC();
|
|
69
|
-
const cvc2 = generateCVC();
|
|
70
|
-
projectIdToUse = `mcp-${cvc1}-${cvc2}`;
|
|
80
|
+
projectIdToUse = generateProjectId();
|
|
71
81
|
console.log(`Project ID not provided, generated ID: ${projectIdToUse}`);
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
try {
|
|
75
|
-
const projectPayload = { projectId: projectIdToUse };
|
|
85
|
+
const projectPayload = { projectId: projectIdToUse, parent };
|
|
76
86
|
|
|
77
87
|
console.log(`Attempting to create project with ID: ${projectIdToUse}`);
|
|
78
88
|
|
|
@@ -100,12 +110,13 @@ export async function createProject(projectId) {
|
|
|
100
110
|
* @async
|
|
101
111
|
* @function createProjectAndAttachBilling
|
|
102
112
|
* @param {string} [projectIdParam] - Optional. The desired ID for the new project.
|
|
113
|
+
* @param {string} [parent] - Optional. The resource name of the parent under which the project is to be created. e.g., "organizations/123" or "folders/456".
|
|
103
114
|
* @returns {Promise<{projectId: string, billingMessage: string}>} A promise that resolves to an object containing the project ID and a billing status message.
|
|
104
115
|
*/
|
|
105
|
-
export async function createProjectAndAttachBilling(projectIdParam) {
|
|
116
|
+
export async function createProjectAndAttachBilling(projectIdParam, parent) {
|
|
106
117
|
let newProject;
|
|
107
118
|
try {
|
|
108
|
-
newProject = await createProject(projectIdParam);
|
|
119
|
+
newProject = await createProject(projectIdParam, parent);
|
|
109
120
|
} catch (error) {
|
|
110
121
|
throw new Error(`Failed to create project: ${error.message}`);
|
|
111
122
|
}
|
|
@@ -115,7 +126,7 @@ export async function createProjectAndAttachBilling(projectIdParam) {
|
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
const { projectId } = newProject;
|
|
118
|
-
let billingMessage = `Project ${projectId} created.`;
|
|
129
|
+
let billingMessage = `Project ${projectId} created successfully.`;
|
|
119
130
|
|
|
120
131
|
try {
|
|
121
132
|
const billingAccounts = await listBillingAccounts();
|
|
@@ -130,27 +141,46 @@ export async function createProjectAndAttachBilling(projectIdParam) {
|
|
|
130
141
|
firstBillingAccount.name
|
|
131
142
|
);
|
|
132
143
|
if (billingInfo && billingInfo.billingEnabled) {
|
|
133
|
-
billingMessage += `
|
|
144
|
+
billingMessage += ` It has been attached to billing account ${firstBillingAccount.displayName}.`;
|
|
134
145
|
} else {
|
|
135
|
-
billingMessage += `
|
|
146
|
+
billingMessage += ` However, it could not be attached to billing account ${firstBillingAccount.displayName} or billing not enabled. Please check manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
136
147
|
}
|
|
137
148
|
} else {
|
|
138
149
|
const allBillingAccounts = billingAccounts
|
|
139
150
|
.map((b) => `${b.displayName} (Open: ${b.open})`)
|
|
140
151
|
.join(', ');
|
|
141
|
-
billingMessage += `
|
|
152
|
+
billingMessage += ` However, no open billing accounts were found. Available (may not be usable): ${allBillingAccounts || 'None'}. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
142
153
|
}
|
|
143
154
|
} else {
|
|
144
|
-
billingMessage +=
|
|
145
|
-
' No billing accounts found. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}';
|
|
155
|
+
billingMessage += ` However, no billing accounts were found. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
146
156
|
}
|
|
147
157
|
} catch (billingError) {
|
|
148
158
|
console.error(
|
|
149
159
|
`Error during billing operations for project ${projectId}:`,
|
|
150
160
|
billingError
|
|
151
161
|
);
|
|
152
|
-
billingMessage += `
|
|
162
|
+
billingMessage += ` However, an error occurred during billing operations: ${billingError.message}. Please check manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
return { projectId, billingMessage };
|
|
156
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deletes a Google Cloud Platform project.
|
|
170
|
+
* @async
|
|
171
|
+
* @function deleteProject
|
|
172
|
+
* @param {string} projectId - The ID of the project to delete.
|
|
173
|
+
* @returns {Promise<void>} A promise that resolves when the delete operation is initiated.
|
|
174
|
+
*/
|
|
175
|
+
export async function deleteProject(projectId) {
|
|
176
|
+
const { ProjectsClient } = await import('@google-cloud/resource-manager');
|
|
177
|
+
const client = new ProjectsClient();
|
|
178
|
+
try {
|
|
179
|
+
console.log(`Attempting to delete project with ID: ${projectId}`);
|
|
180
|
+
await client.deleteProject({ name: `projects/${projectId}` });
|
|
181
|
+
console.log(`Project ${projectId} deletion initiated successfully.`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(`Error deleting GCP project ${projectId}:`, error.message);
|
|
184
|
+
throw error; // Re-throw to be caught by the caller
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { callWithRetry } from './helpers.js';
|
|
18
|
+
import { logAndProgress } from '../util/helpers.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensures that an Artifact Registry repository exists.
|
|
22
|
+
* If the repository does not exist, it attempts to create it with the specified format.
|
|
23
|
+
*
|
|
24
|
+
* @async
|
|
25
|
+
* @param {object} context - The context object containing clients and other parameters.
|
|
26
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
27
|
+
* @param {string} location - The Google Cloud region for the repository.
|
|
28
|
+
* @param {string} repositoryId - The ID for the Artifact Registry repository.
|
|
29
|
+
* @param {string} [format='DOCKER'] - The format of the repository (e.g., 'DOCKER', 'NPM'). Defaults to 'DOCKER'.
|
|
30
|
+
* @param {function(object): void} [progressCallback] - Optional callback for progress updates.
|
|
31
|
+
* @returns {Promise<object>} A promise that resolves with the Artifact Registry repository object.
|
|
32
|
+
* @throws {Error} If there's an error checking or creating the repository.
|
|
33
|
+
*/
|
|
34
|
+
export async function ensureArtifactRegistryRepoExists(
|
|
35
|
+
context,
|
|
36
|
+
projectId,
|
|
37
|
+
location,
|
|
38
|
+
repositoryId,
|
|
39
|
+
format = 'DOCKER',
|
|
40
|
+
progressCallback
|
|
41
|
+
) {
|
|
42
|
+
const parent = `projects/${projectId}/locations/${location}`;
|
|
43
|
+
const repoPath = context.artifactRegistryClient.repositoryPath(
|
|
44
|
+
projectId,
|
|
45
|
+
location,
|
|
46
|
+
repositoryId
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const [repository] = await callWithRetry(
|
|
51
|
+
() => context.artifactRegistryClient.getRepository({ name: repoPath }),
|
|
52
|
+
`artifactRegistry.getRepository ${repositoryId}`
|
|
53
|
+
);
|
|
54
|
+
await logAndProgress(
|
|
55
|
+
`Repository ${repositoryId} already exists in ${location}.`,
|
|
56
|
+
progressCallback
|
|
57
|
+
);
|
|
58
|
+
return repository;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === 5) {
|
|
61
|
+
await logAndProgress(
|
|
62
|
+
`Repository ${repositoryId} does not exist in ${location}. Creating...`,
|
|
63
|
+
progressCallback
|
|
64
|
+
);
|
|
65
|
+
const repositoryToCreate = {
|
|
66
|
+
format: format,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const [operation] = await callWithRetry(
|
|
70
|
+
() =>
|
|
71
|
+
context.artifactRegistryClient.createRepository({
|
|
72
|
+
parent: parent,
|
|
73
|
+
repository: repositoryToCreate,
|
|
74
|
+
repositoryId: repositoryId,
|
|
75
|
+
}),
|
|
76
|
+
`artifactRegistry.createRepository ${repositoryId}`
|
|
77
|
+
);
|
|
78
|
+
await logAndProgress(
|
|
79
|
+
`Creating Artifact Registry repository ${repositoryId}...`,
|
|
80
|
+
progressCallback
|
|
81
|
+
);
|
|
82
|
+
const [result] = await operation.promise();
|
|
83
|
+
await logAndProgress(
|
|
84
|
+
`Artifact Registry repository ${result.name} created successfully.`,
|
|
85
|
+
progressCallback
|
|
86
|
+
);
|
|
87
|
+
return result;
|
|
88
|
+
} else {
|
|
89
|
+
const errorMessage = `Error checking/creating repository ${repositoryId}: ${error.message}`;
|
|
90
|
+
console.error(
|
|
91
|
+
`Error checking/creating repository ${repositoryId}:`,
|
|
92
|
+
error
|
|
93
|
+
);
|
|
94
|
+
await logAndProgress(errorMessage, progressCallback, 'error');
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|