@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
|
@@ -0,0 +1,181 @@
|
|
|
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 protofiles from 'google-proto-files';
|
|
18
|
+
|
|
19
|
+
let runClient;
|
|
20
|
+
let loggingClient;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lists all Cloud Run services in a given project and location.
|
|
24
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
25
|
+
* @param {string} location - The Google Cloud location (e.g., 'europe-west1').
|
|
26
|
+
* @returns {Promise<Array<object>>} - A promise that resolves to an array of service objects.
|
|
27
|
+
*/
|
|
28
|
+
export async function listServices(projectId, location) {
|
|
29
|
+
// TODO make location optional, if not provided, list all services from all regions
|
|
30
|
+
if (!runClient) {
|
|
31
|
+
const { v2 } = await import('@google-cloud/run');
|
|
32
|
+
const { ServicesClient } = v2;
|
|
33
|
+
runClient = new ServicesClient({ projectId });
|
|
34
|
+
}
|
|
35
|
+
const parent = runClient.locationPath(projectId, location);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
console.log(
|
|
39
|
+
`Listing Cloud Run services in project ${projectId}, location ${location}...`
|
|
40
|
+
);
|
|
41
|
+
const [services] = await runClient.listServices({ parent });
|
|
42
|
+
return services;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(`Error listing Cloud Run services:`, error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Gets details for a specific Cloud Run service.
|
|
51
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
52
|
+
* @param {string} location - The Google Cloud location (e.g., 'europe-west1').
|
|
53
|
+
* @param {string} serviceId - The ID of the Cloud Run service.
|
|
54
|
+
* @returns {Promise<object>} - A promise that resolves to the service object.
|
|
55
|
+
*/
|
|
56
|
+
export async function getService(projectId, location, serviceId) {
|
|
57
|
+
if (!runClient) {
|
|
58
|
+
const { v2 } = await import('@google-cloud/run');
|
|
59
|
+
const { ServicesClient } = v2;
|
|
60
|
+
runClient = new ServicesClient({ projectId });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const servicePath = runClient.servicePath(projectId, location, serviceId);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
console.log(
|
|
67
|
+
`Getting details for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
|
|
68
|
+
);
|
|
69
|
+
const [service] = await runClient.getService({ name: servicePath });
|
|
70
|
+
return service;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(
|
|
73
|
+
`Error getting details for Cloud Run service ${serviceId}:`,
|
|
74
|
+
error
|
|
75
|
+
);
|
|
76
|
+
// Check if the error is a "not found" error (gRPC code 5)
|
|
77
|
+
if (error.code === 5) {
|
|
78
|
+
console.log(`Cloud Run service ${serviceId} not found.`);
|
|
79
|
+
return null; // Or throw a custom error, or handle as needed
|
|
80
|
+
}
|
|
81
|
+
throw error; // Re-throw other errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetches a paginated list of logs for a specific Cloud Run service.
|
|
87
|
+
* @param {string} projectId - The Google Cloud project ID.
|
|
88
|
+
* @param {string} location - The Google Cloud location (e.g., 'europe-west1').
|
|
89
|
+
* @param {string} serviceId - The ID of the Cloud Run service.
|
|
90
|
+
* @param {string} [requestOptions] - The token for the next page of results.
|
|
91
|
+
* @returns {Promise<{logs: string, requestOptions: object | undefined }>} - A promise that resolves to an object with log entries and a token for the next page.
|
|
92
|
+
*/
|
|
93
|
+
export async function getServiceLogs(
|
|
94
|
+
projectId,
|
|
95
|
+
location,
|
|
96
|
+
serviceId,
|
|
97
|
+
requestOptions
|
|
98
|
+
) {
|
|
99
|
+
if (!loggingClient) {
|
|
100
|
+
const { Logging } = await import('@google-cloud/logging');
|
|
101
|
+
loggingClient = new Logging({ projectId });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const LOG_SEVERITY = 'DEFAULT'; // e.g., 'DEFAULT', 'INFO', 'WARNING', 'ERROR'
|
|
106
|
+
const PAGE_SIZE = 100; // Number of log entries to retrieve per page
|
|
107
|
+
|
|
108
|
+
const filter = `resource.type="cloud_run_revision"
|
|
109
|
+
resource.labels.service_name="${serviceId}"
|
|
110
|
+
resource.labels.location="${location}"
|
|
111
|
+
severity>=${LOG_SEVERITY}`;
|
|
112
|
+
|
|
113
|
+
console.log(
|
|
114
|
+
`Fetching logs for Cloud Run service ${serviceId} in project ${projectId}, location ${location}...`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Options for the getEntries API call
|
|
118
|
+
const options = requestOptions || {
|
|
119
|
+
filter: filter,
|
|
120
|
+
orderBy: 'timestamp desc', // Get the latest logs first
|
|
121
|
+
pageSize: PAGE_SIZE,
|
|
122
|
+
};
|
|
123
|
+
console.log(`Request options: ${JSON.stringify(options)}`);
|
|
124
|
+
|
|
125
|
+
// getEntries returns the entries and the full API response
|
|
126
|
+
const [entries, nextRequestOptions, apiResponse] =
|
|
127
|
+
await loggingClient.getEntries(options);
|
|
128
|
+
|
|
129
|
+
const formattedLogLines = entries
|
|
130
|
+
.map((entry) => formatLogEntry(entry))
|
|
131
|
+
.join('\n');
|
|
132
|
+
|
|
133
|
+
// The nextPageToken is available in the apiResponse object
|
|
134
|
+
const nextOptions = apiResponse?.nextPageToken
|
|
135
|
+
? nextRequestOptions
|
|
136
|
+
: undefined;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
logs: formattedLogLines,
|
|
140
|
+
requestOptions: nextOptions,
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(
|
|
144
|
+
`Error fetching logs for Cloud Run service ${serviceId}:`,
|
|
145
|
+
error
|
|
146
|
+
);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Formats a single log entry for display.
|
|
153
|
+
* @param {object} entry - A log entry object from the Cloud Logging API.
|
|
154
|
+
* @returns {string} - A formatted string representation of the log entry.
|
|
155
|
+
*/
|
|
156
|
+
function formatLogEntry(entry) {
|
|
157
|
+
const timestampStr = entry.metadata.timestamp.toISOString() || 'N/A';
|
|
158
|
+
const severity = entry.metadata.severity || 'N/A';
|
|
159
|
+
let responseData = '';
|
|
160
|
+
if (entry.metadata.httpRequest) {
|
|
161
|
+
const responseMethod = entry.metadata.httpRequest.requestMethod;
|
|
162
|
+
const responseCode = entry.metadata.httpRequest.status;
|
|
163
|
+
const requestUrl = entry.metadata.httpRequest.requestUrl;
|
|
164
|
+
const responseSize = entry.metadata.httpRequest.responseSize;
|
|
165
|
+
responseData = `HTTP Request: ${responseMethod} StatusCode: ${responseCode} ResponseSize: ${responseSize} Byte - ${requestUrl}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let data = '';
|
|
169
|
+
if (entry.data && entry.data.value) {
|
|
170
|
+
const protopath = protofiles.getProtoPath(
|
|
171
|
+
'../google/cloud/audit/audit_log.proto'
|
|
172
|
+
);
|
|
173
|
+
const root = protofiles.loadSync(protopath);
|
|
174
|
+
const type = root.lookupType('google.cloud.audit.AuditLog');
|
|
175
|
+
const value = type.decode(entry.data.value);
|
|
176
|
+
data = `${value.methodName}: ${value.status?.message || ''}${value.authenticationInfo?.principalEmail || ''}`;
|
|
177
|
+
} else if (entry.data) {
|
|
178
|
+
data = entry.data;
|
|
179
|
+
}
|
|
180
|
+
return `[${timestampStr}] [${severity}] ${responseData} ${data}`;
|
|
181
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { GoogleAuth } from 'google-auth-library';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks for the presence of properly setup Google Cloud Platform (GCP) authentication
|
|
21
|
+
* using the official Google Auth Library for Node.js. If GCP auth is not found, it logs an
|
|
22
|
+
* error message and exits the process, instructing the user on how to set them up.
|
|
23
|
+
* This check is primarily for local/stdio environments where explicit setup is often needed.
|
|
24
|
+
* @async
|
|
25
|
+
* @returns {Promise<void>} A promise that resolves if GCP auth are found, or rejects and exits the process if not.
|
|
26
|
+
*/
|
|
27
|
+
export async function ensureGCPCredentials() {
|
|
28
|
+
console.error('Checking for Google Cloud Application Default Credentials...');
|
|
29
|
+
try {
|
|
30
|
+
const auth = new GoogleAuth();
|
|
31
|
+
// Attempt to get credentials. This will throw an error if ADC are not found.
|
|
32
|
+
const client = await auth.getClient();
|
|
33
|
+
// Attempt to get an access token to verify credentials are usable.
|
|
34
|
+
// This is done because getClient() might succeed but credentials might be invalid/expired.
|
|
35
|
+
await client.getAccessToken();
|
|
36
|
+
|
|
37
|
+
console.log('Application Default Credentials found.');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(
|
|
40
|
+
'ERROR: Google Cloud Application Default Credentials are not set up.'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (error.response && error.response.status) {
|
|
44
|
+
console.error(
|
|
45
|
+
`An HTTP error occurred (Status: ${error.response.status}). This often means misconfigured, expired credentials, or a network issue.`
|
|
46
|
+
);
|
|
47
|
+
} else if (error instanceof TypeError) {
|
|
48
|
+
// Catches TypeErrors specifically, which might indicate a malformed response or unexpected data type
|
|
49
|
+
console.error(
|
|
50
|
+
'An unexpected error occurred during credential verification (e.g., malformed response or invalid type).'
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
// General fallback for any other unexpected errors
|
|
54
|
+
console.error(
|
|
55
|
+
'An unexpected error occurred during credential verification.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.error('\nFor more details or alternative setup methods, consider:');
|
|
60
|
+
console.error(
|
|
61
|
+
'1. If running locally, run: gcloud auth application-default login.'
|
|
62
|
+
);
|
|
63
|
+
console.error(
|
|
64
|
+
'2. Ensuring the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a valid service account key file.'
|
|
65
|
+
);
|
|
66
|
+
console.error(
|
|
67
|
+
'3. If on a Google Cloud environment (e.g., GCE, Cloud Run), verify the associated service account has necessary permissions.'
|
|
68
|
+
);
|
|
69
|
+
console.error(
|
|
70
|
+
`\nOriginal error message from Google Auth Library: ${error.message}`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Print the stack for debugging
|
|
74
|
+
if (error.stack) {
|
|
75
|
+
console.error('Error stack:', error.stack);
|
|
76
|
+
}
|
|
77
|
+
// process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -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
|
+
/**
|
|
18
|
+
* Lists all accessible Google Cloud Billing Accounts.
|
|
19
|
+
* @async
|
|
20
|
+
* @function listBillingAccounts
|
|
21
|
+
* @returns {Promise<Array<{name: string, displayName: string, open: boolean}>>} A promise that resolves to an array of billing account objects,
|
|
22
|
+
* each with 'name', 'displayName', and 'open' status. Returns an empty array on error.
|
|
23
|
+
*/
|
|
24
|
+
export async function listBillingAccounts() {
|
|
25
|
+
const { CloudBillingClient } = await import('@google-cloud/billing');
|
|
26
|
+
const client = new CloudBillingClient();
|
|
27
|
+
try {
|
|
28
|
+
const [accounts] = await client.listBillingAccounts();
|
|
29
|
+
if (!accounts || accounts.length === 0) {
|
|
30
|
+
console.log('No billing accounts found.');
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return accounts.map((account) => ({
|
|
34
|
+
name: account.name, // e.g., billingAccounts/0X0X0X-0X0X0X-0X0X0X
|
|
35
|
+
displayName: account.displayName,
|
|
36
|
+
open: account.open,
|
|
37
|
+
}));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Error listing GCP billing accounts:', error);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attaches a Google Cloud Project to a specified Billing Account.
|
|
46
|
+
* @async
|
|
47
|
+
* @function attachProjectToBillingAccount
|
|
48
|
+
* @param {string} projectId - The ID of the project to attach.
|
|
49
|
+
* @param {string} billingAccountName - The resource name of the billing account (e.g., 'billingAccounts/0X0X0X-0X0X0X-0X0X0X').
|
|
50
|
+
* @returns {Promise<object|null>} A promise that resolves to the updated project billing information object if successful, or null on error.
|
|
51
|
+
*/
|
|
52
|
+
export async function attachProjectToBillingAccount(
|
|
53
|
+
projectId,
|
|
54
|
+
billingAccountName
|
|
55
|
+
) {
|
|
56
|
+
const { CloudBillingClient } = await import('@google-cloud/billing');
|
|
57
|
+
const client = new CloudBillingClient();
|
|
58
|
+
const projectName = `projects/${projectId}`;
|
|
59
|
+
|
|
60
|
+
if (!projectId) {
|
|
61
|
+
console.error('Error: projectId is required.');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
!billingAccountName ||
|
|
66
|
+
!billingAccountName.startsWith('billingAccounts/')
|
|
67
|
+
) {
|
|
68
|
+
console.error(
|
|
69
|
+
'Error: billingAccountName is required and must be in the format "billingAccounts/XXXXXX-XXXXXX-XXXXXX".'
|
|
70
|
+
);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
console.log(
|
|
76
|
+
`Attempting to attach project ${projectId} to billing account ${billingAccountName}...`
|
|
77
|
+
);
|
|
78
|
+
const [updatedBillingInfo] = await client.updateProjectBillingInfo({
|
|
79
|
+
name: projectName,
|
|
80
|
+
projectBillingInfo: {
|
|
81
|
+
billingAccountName: billingAccountName,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
console.log(
|
|
85
|
+
`Successfully attached project ${projectId} to billing account ${billingAccountName}.`
|
|
86
|
+
);
|
|
87
|
+
console.log(`Billing enabled: ${updatedBillingInfo.billingEnabled}`);
|
|
88
|
+
return updatedBillingInfo;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(
|
|
91
|
+
`Error attaching project ${projectId} to billing account ${billingAccountName}:`,
|
|
92
|
+
error.message || error
|
|
93
|
+
);
|
|
94
|
+
// Log more details if available, e.g. error.details
|
|
95
|
+
// if (error.details) console.error("Error details:", error.details);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
// Helper function to fetch data from GCP metadata server
|
|
18
|
+
/**
|
|
19
|
+
* Fetches metadata from the Google Cloud metadata server.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} path - The metadata path to fetch (e.g., `/computeMetadata/v1/...`).
|
|
22
|
+
* @returns {Promise<string>} A promise that resolves to the metadata value as a string.
|
|
23
|
+
* @throws {Error} If the metadata request fails with a non-OK status.
|
|
24
|
+
*/
|
|
25
|
+
async function fetchMetadata(path) {
|
|
26
|
+
const response = await fetch(`http://metadata.google.internal${path}`, {
|
|
27
|
+
headers: {
|
|
28
|
+
'Metadata-Flavor': 'Google',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`Metadata request failed with status ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
return await response.text();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if the GCP metadata server is available and retrieves project ID and region.
|
|
39
|
+
* @returns {Promise<Object|null>} A promise that resolves to an object { project: string, region: string } or null if not available or an error occurs.
|
|
40
|
+
*/
|
|
41
|
+
export async function checkGCP() {
|
|
42
|
+
try {
|
|
43
|
+
const projectId = await fetchMetadata(
|
|
44
|
+
'/computeMetadata/v1/project/project-id'
|
|
45
|
+
);
|
|
46
|
+
// Expected format: projects/PROJECT_NUMBER/regions/REGION_NAME
|
|
47
|
+
const regionPath = await fetchMetadata(
|
|
48
|
+
'/computeMetadata/v1/instance/region'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (projectId && regionPath) {
|
|
52
|
+
const regionParts = regionPath.split('/');
|
|
53
|
+
const region = regionParts[regionParts.length - 1];
|
|
54
|
+
return { project: projectId, region: region };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Intentionally suppress error logging for cleaner output if metadata server is not available.
|
|
59
|
+
// console.warn('Failed to fetch GCP metadata:', error.message); // Uncomment for debugging
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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 {
|
|
18
|
+
listBillingAccounts,
|
|
19
|
+
attachProjectToBillingAccount,
|
|
20
|
+
} from './gcp-billing-accounts.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lists all accessible Google Cloud Platform projects.
|
|
24
|
+
* @async
|
|
25
|
+
* @function listProjects
|
|
26
|
+
* @returns {Promise<Array<{id: string}>>} A promise that resolves to an array of project objects, each with an 'id' property. Returns an empty array on error.
|
|
27
|
+
*/
|
|
28
|
+
export async function listProjects() {
|
|
29
|
+
const { ProjectsClient } = await import('@google-cloud/resource-manager');
|
|
30
|
+
const client = new ProjectsClient();
|
|
31
|
+
try {
|
|
32
|
+
const [projects] = await client.searchProjects();
|
|
33
|
+
return projects.map((project) => ({
|
|
34
|
+
id: project.projectId,
|
|
35
|
+
}));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error listing GCP projects:', error);
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a new Google Cloud Platform project.
|
|
44
|
+
* @async
|
|
45
|
+
* @function createProject
|
|
46
|
+
* @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).
|
|
47
|
+
* @returns {Promise<{projectId: string}|null>} A promise that resolves to an object containing the new project's ID.
|
|
48
|
+
*/
|
|
49
|
+
export async function createProject(projectId) {
|
|
50
|
+
const { ProjectsClient } = await import('@google-cloud/resource-manager');
|
|
51
|
+
const client = new ProjectsClient();
|
|
52
|
+
let projectIdToUse = projectId;
|
|
53
|
+
|
|
54
|
+
if (!projectIdToUse) {
|
|
55
|
+
const consonants = 'bcdfghjklmnpqrstvwxyz';
|
|
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}`;
|
|
71
|
+
console.log(`Project ID not provided, generated ID: ${projectIdToUse}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const projectPayload = { projectId: projectIdToUse, name: projectIdToUse };
|
|
76
|
+
|
|
77
|
+
console.log(`Attempting to create project with ID: ${projectIdToUse}`);
|
|
78
|
+
|
|
79
|
+
const [operation] = await client.createProject({ project: projectPayload });
|
|
80
|
+
|
|
81
|
+
const [createdProjectResponse] = await operation.promise();
|
|
82
|
+
|
|
83
|
+
console.log(
|
|
84
|
+
`Project ${createdProjectResponse.projectId} created successfully.`
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
projectId: createdProjectResponse.projectId,
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(
|
|
91
|
+
`Error creating GCP project ${projectIdToUse}:`,
|
|
92
|
+
error.message
|
|
93
|
+
);
|
|
94
|
+
throw error; // Re-throw to be caught by the caller
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates a new Google Cloud Platform project and attempts to attach it to the first available billing account.
|
|
100
|
+
* @async
|
|
101
|
+
* @function createProjectAndAttachBilling
|
|
102
|
+
* @param {string} [projectIdParam] - Optional. The desired ID for the new project.
|
|
103
|
+
* @returns {Promise<{projectId: string, billingMessage: string}>} A promise that resolves to an object containing the project ID and a billing status message.
|
|
104
|
+
*/
|
|
105
|
+
export async function createProjectAndAttachBilling(projectIdParam) {
|
|
106
|
+
let newProject;
|
|
107
|
+
try {
|
|
108
|
+
newProject = await createProject(projectIdParam);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new Error(`Failed to create project: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!newProject || !newProject.projectId) {
|
|
114
|
+
throw new Error('Project creation did not return a valid project ID.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { projectId } = newProject;
|
|
118
|
+
let billingMessage = `Project ${projectId} created.`;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const billingAccounts = await listBillingAccounts();
|
|
122
|
+
if (billingAccounts && billingAccounts.length > 0) {
|
|
123
|
+
const firstBillingAccount = billingAccounts.find((acc) => acc.open); // Prefer an open account
|
|
124
|
+
if (firstBillingAccount) {
|
|
125
|
+
console.log(
|
|
126
|
+
`Found billing account: ${firstBillingAccount.displayName} (${firstBillingAccount.name}). Attempting to attach project ${projectId}.`
|
|
127
|
+
);
|
|
128
|
+
const billingInfo = await attachProjectToBillingAccount(
|
|
129
|
+
projectId,
|
|
130
|
+
firstBillingAccount.name
|
|
131
|
+
);
|
|
132
|
+
if (billingInfo && billingInfo.billingEnabled) {
|
|
133
|
+
billingMessage += ` Successfully attached to billing account ${firstBillingAccount.displayName}.`;
|
|
134
|
+
} else {
|
|
135
|
+
billingMessage += ` Could not attach to billing account ${firstBillingAccount.displayName} or billing not enabled. Please check manually. Console: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
const allBillingAccounts = billingAccounts
|
|
139
|
+
.map((b) => `${b.displayName} (Open: ${b.open})`)
|
|
140
|
+
.join(', ');
|
|
141
|
+
billingMessage += ` No open billing accounts found. Available (may not be usable): ${allBillingAccounts || 'None'}. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
billingMessage +=
|
|
145
|
+
' No billing accounts found. Please link billing manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}';
|
|
146
|
+
}
|
|
147
|
+
} catch (billingError) {
|
|
148
|
+
console.error(
|
|
149
|
+
`Error during billing operations for project ${projectId}:`,
|
|
150
|
+
billingError
|
|
151
|
+
);
|
|
152
|
+
billingMessage += ` Error during billing operations: ${billingError.message}. Please check manually: https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { projectId, billingMessage };
|
|
156
|
+
}
|