@aws/ml-container-creator 0.9.0 โ 0.10.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/bin/cli.js +31 -137
- package/config/parameter-schema-v2.json +2065 -0
- package/package.json +6 -3
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/models.json +182 -26
- package/src/app.js +6 -389
- package/src/lib/bootstrap-command-handler.js +75 -1078
- package/src/lib/bootstrap-profile-manager.js +634 -0
- package/src/lib/bootstrap-provisioners.js +421 -0
- package/src/lib/config-loader.js +405 -0
- package/src/lib/config-manager.js +59 -1668
- package/src/lib/config-mcp-client.js +118 -0
- package/src/lib/config-validator.js +634 -0
- package/src/lib/cuda-resolver.js +140 -0
- package/src/lib/e2e-catalog-validator.js +251 -3
- package/src/lib/e2e-ci-recorder.js +103 -0
- package/src/lib/generated/cli-options.js +471 -0
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +202 -0
- package/src/lib/marketplace-flow.js +276 -0
- package/src/lib/mcp-query-runner.js +768 -0
- package/src/lib/parameter-schema-validator.js +62 -18
- package/src/lib/prompt-runner.js +41 -1504
- package/src/lib/prompts/feature-prompts.js +172 -0
- package/src/lib/prompts/index.js +48 -0
- package/src/lib/prompts/infrastructure-prompts.js +690 -0
- package/src/lib/prompts/model-prompts.js +552 -0
- package/src/lib/prompts/project-prompts.js +70 -0
- package/src/lib/prompts.js +2 -1446
- package/src/lib/registry-command-handler.js +135 -3
- package/src/lib/secrets-prompt-runner.js +251 -0
- package/src/lib/template-variable-resolver.js +398 -0
- package/templates/code/serve +5 -134
- package/templates/code/serve.d/lmi.ejs +19 -0
- package/templates/code/serve.d/sglang.ejs +47 -0
- package/templates/code/serve.d/tensorrt-llm.ejs +53 -0
- package/templates/code/serve.d/vllm.ejs +48 -0
- package/templates/do/clean +1 -1387
- package/templates/do/clean.d/async-inference.ejs +508 -0
- package/templates/do/clean.d/batch-transform.ejs +512 -0
- package/templates/do/clean.d/hyperpod-eks.ejs +481 -0
- package/templates/do/clean.d/managed-inference.ejs +1043 -0
- package/templates/do/deploy +1 -1766
- package/templates/do/deploy.d/async-inference.ejs +501 -0
- package/templates/do/deploy.d/batch-transform.ejs +529 -0
- package/templates/do/deploy.d/hyperpod-eks.ejs +339 -0
- package/templates/do/deploy.d/managed-inference.ejs +726 -0
- package/config/parameter-schema.json +0 -88
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import AssetManager from './asset-manager.js';
|
|
7
|
+
|
|
8
|
+
const STACK_NAME_PREFIX = 'mlcc-bootstrap';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles bootstrap profile management subcommands (status, use, list, remove, scan, prune, sync-schemas).
|
|
12
|
+
* Delegates back to the BootstrapCommandHandler instance for shared helpers.
|
|
13
|
+
*/
|
|
14
|
+
export default class BootstrapProfileManager {
|
|
15
|
+
constructor(handler) {
|
|
16
|
+
this.handler = handler;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Display active bootstrap profile and resource state.
|
|
21
|
+
* @param {object} [options] - Parsed CLI options (e.g., --verify)
|
|
22
|
+
*/
|
|
23
|
+
async _handleStatus(options = {}) {
|
|
24
|
+
const config = this.handler.config.read();
|
|
25
|
+
if (!config) {
|
|
26
|
+
console.log('No bootstrap configuration found.');
|
|
27
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const profile = this.handler.config.getActiveProfile();
|
|
32
|
+
if (!profile) {
|
|
33
|
+
console.log('No active bootstrap profile found.');
|
|
34
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const allProfiles = this.handler.config.listProfiles();
|
|
39
|
+
console.log(`\n๐ Active Profile: ${profile.name} (${allProfiles.length} profile${allProfiles.length === 1 ? '' : 's'} total)`);
|
|
40
|
+
console.log('โ'.repeat(40));
|
|
41
|
+
|
|
42
|
+
for (const [key, value] of Object.entries(profile.config)) {
|
|
43
|
+
console.log(` ${key}: ${value}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('โ'.repeat(40));
|
|
47
|
+
|
|
48
|
+
// Validate bootstrap stack
|
|
49
|
+
console.log('\n๐ Resource Validation:');
|
|
50
|
+
|
|
51
|
+
const stackName = profile.config.stackName || `${STACK_NAME_PREFIX}-${profile.name}`;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const stackInfo = this.handler._execAws(
|
|
55
|
+
`cloudformation describe-stacks --stack-name ${stackName} --region ${profile.config.awsRegion}`,
|
|
56
|
+
profile.config.awsProfile
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const stack = stackInfo.Stacks && stackInfo.Stacks[0];
|
|
60
|
+
if (stack) {
|
|
61
|
+
const status = stack.StackStatus;
|
|
62
|
+
const statusIcon = status === 'CREATE_COMPLETE' || status === 'UPDATE_COMPLETE' ? 'โ
' : 'โ ๏ธ';
|
|
63
|
+
console.log(` ${statusIcon} Bootstrap stack: ${stackName} (${status})`);
|
|
64
|
+
|
|
65
|
+
// Show stack outputs
|
|
66
|
+
const outputs = {};
|
|
67
|
+
for (const output of (stack.Outputs || [])) {
|
|
68
|
+
outputs[output.OutputKey] = output.OutputValue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (outputs.RoleArn) {
|
|
72
|
+
console.log(` โ
IAM role: ${outputs.RoleArn.split('/').pop()}`);
|
|
73
|
+
}
|
|
74
|
+
if (outputs.EcrRepositoryName) {
|
|
75
|
+
console.log(` โ
ECR repository: ${outputs.EcrRepositoryName}`);
|
|
76
|
+
}
|
|
77
|
+
if (outputs.AsyncS3BucketName) {
|
|
78
|
+
console.log(` โ
S3 bucket (async): ${outputs.AsyncS3BucketName}`);
|
|
79
|
+
}
|
|
80
|
+
if (outputs.BatchS3BucketName) {
|
|
81
|
+
console.log(` โ
S3 bucket (batch): ${outputs.BatchS3BucketName}`);
|
|
82
|
+
}
|
|
83
|
+
if (outputs.AdapterS3BucketName) {
|
|
84
|
+
console.log(` โ
S3 bucket (adapters): ${outputs.AdapterS3BucketName}`);
|
|
85
|
+
}
|
|
86
|
+
if (outputs.BenchmarkS3BucketName) {
|
|
87
|
+
console.log(` โ
S3 bucket (benchmark): ${outputs.BenchmarkS3BucketName}`);
|
|
88
|
+
}
|
|
89
|
+
if (outputs.StackVersion) {
|
|
90
|
+
console.log(` ๐ Stack version: ${outputs.StackVersion}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Fall back to individual resource checks for profiles created before CloudFormation migration
|
|
95
|
+
console.log(` โ ๏ธ Bootstrap stack "${stackName}" not found โ checking resources individually`);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const defaultRoleName = 'mlcc-sagemaker-execution-role';
|
|
99
|
+
let roleName = defaultRoleName;
|
|
100
|
+
if (profile.config.roleArn) {
|
|
101
|
+
const arnParts = profile.config.roleArn.split('/');
|
|
102
|
+
roleName = arnParts[arnParts.length - 1];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const roleExists = this.handler._resourceExists(
|
|
106
|
+
`iam get-role --role-name ${roleName}`,
|
|
107
|
+
profile.config.awsProfile
|
|
108
|
+
);
|
|
109
|
+
if (roleExists) {
|
|
110
|
+
console.log(` โ
IAM role: ${roleName}`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log(` โ ๏ธ IAM role: ${roleName} โ missing`);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
console.log(' โ ๏ธ IAM role: could not validate');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const ecrExists = this.handler._resourceExists(
|
|
120
|
+
`ecr describe-repositories --repository-names ml-container-creator --region ${profile.config.awsRegion}`,
|
|
121
|
+
profile.config.awsProfile
|
|
122
|
+
);
|
|
123
|
+
if (ecrExists) {
|
|
124
|
+
console.log(' โ
ECR repository: ml-container-creator');
|
|
125
|
+
} else {
|
|
126
|
+
console.log(' โ ๏ธ ECR repository: ml-container-creator โ missing');
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
console.log(' โ ๏ธ ECR repository: could not validate');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (profile.config.asyncS3Bucket) {
|
|
133
|
+
try {
|
|
134
|
+
const asyncExists = this.handler._resourceExists(
|
|
135
|
+
`s3api head-bucket --bucket ${profile.config.asyncS3Bucket}`,
|
|
136
|
+
profile.config.awsProfile
|
|
137
|
+
);
|
|
138
|
+
console.log(asyncExists
|
|
139
|
+
? ` โ
S3 bucket: ${profile.config.asyncS3Bucket}`
|
|
140
|
+
: ` โ ๏ธ S3 bucket: ${profile.config.asyncS3Bucket} โ missing`);
|
|
141
|
+
} catch {
|
|
142
|
+
console.log(` โ ๏ธ S3 bucket: ${profile.config.asyncS3Bucket} โ could not validate`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (profile.config.batchS3Bucket) {
|
|
147
|
+
try {
|
|
148
|
+
const batchExists = this.handler._resourceExists(
|
|
149
|
+
`s3api head-bucket --bucket ${profile.config.batchS3Bucket}`,
|
|
150
|
+
profile.config.awsProfile
|
|
151
|
+
);
|
|
152
|
+
console.log(batchExists
|
|
153
|
+
? ` โ
S3 bucket: ${profile.config.batchS3Bucket}`
|
|
154
|
+
: ` โ ๏ธ S3 bucket: ${profile.config.batchS3Bucket} โ missing`);
|
|
155
|
+
} catch {
|
|
156
|
+
console.log(` โ ๏ธ S3 bucket: ${profile.config.batchS3Bucket} โ could not validate`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (profile.config.benchmarkS3Bucket) {
|
|
161
|
+
try {
|
|
162
|
+
const benchmarkExists = this.handler._resourceExists(
|
|
163
|
+
`s3api head-bucket --bucket ${profile.config.benchmarkS3Bucket}`,
|
|
164
|
+
profile.config.awsProfile
|
|
165
|
+
);
|
|
166
|
+
console.log(benchmarkExists
|
|
167
|
+
? ` โ
S3 bucket (benchmark): ${profile.config.benchmarkS3Bucket}`
|
|
168
|
+
: ` โ ๏ธ S3 bucket (benchmark): ${profile.config.benchmarkS3Bucket} โ missing`);
|
|
169
|
+
} catch {
|
|
170
|
+
console.log(` โ ๏ธ S3 bucket (benchmark): ${profile.config.benchmarkS3Bucket} โ could not validate`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Display deployed resources from manifest
|
|
176
|
+
console.log('\n๐ฆ Deployed Resources:');
|
|
177
|
+
|
|
178
|
+
const assetManager = new AssetManager(profile.name);
|
|
179
|
+
|
|
180
|
+
if (!existsSync(assetManager.manifestPath)) {
|
|
181
|
+
console.log(' No deployment tracking data available.');
|
|
182
|
+
console.log(' Resources will be tracked after running deploy, push, or submit scripts.');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const resourcesByProject = assetManager.getResourcesByProject();
|
|
187
|
+
|
|
188
|
+
if (resourcesByProject.size === 0) {
|
|
189
|
+
console.log(' No deployed resources tracked.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [project, resources] of resourcesByProject) {
|
|
194
|
+
console.log(`\n Project: ${project}`);
|
|
195
|
+
for (const resource of resources) {
|
|
196
|
+
const timestamp = resource.createdAt || resource.lastUpdatedAt;
|
|
197
|
+
console.log(` ${resource.resourceType} ${resource.resourceId} [${resource.status}] ${timestamp}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const counts = assetManager.getStatusCounts();
|
|
202
|
+
console.log(`\n Summary: ${counts.active} active, ${counts.deleted} deleted, ${counts.unknown} unknown`);
|
|
203
|
+
|
|
204
|
+
// Drift detection if --verify flag is set
|
|
205
|
+
if (options.verify) {
|
|
206
|
+
await this._handleStatusVerify(profile, assetManager);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Perform drift detection for active resources.
|
|
212
|
+
* @param {object} profile - Active profile object with name and config
|
|
213
|
+
* @param {AssetManager} assetManager - AssetManager instance for the profile
|
|
214
|
+
*/
|
|
215
|
+
async _handleStatusVerify(profile, assetManager) {
|
|
216
|
+
console.log('\n๐ Drift Detection:');
|
|
217
|
+
|
|
218
|
+
const activeResources = assetManager.listResources({ status: 'active' });
|
|
219
|
+
|
|
220
|
+
if (activeResources.length === 0) {
|
|
221
|
+
console.log(' No active resources to verify.');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let verified = 0;
|
|
226
|
+
let drifted = 0;
|
|
227
|
+
let unchecked = 0;
|
|
228
|
+
|
|
229
|
+
for (const resource of activeResources) {
|
|
230
|
+
const checkCommand = this.handler._buildDriftCheckCommand(resource);
|
|
231
|
+
|
|
232
|
+
if (!checkCommand) {
|
|
233
|
+
unchecked++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const exists = this.handler._resourceExists(checkCommand, profile.config.awsProfile);
|
|
239
|
+
|
|
240
|
+
if (exists) {
|
|
241
|
+
verified++;
|
|
242
|
+
console.log(` โ
${resource.resourceType}: ${resource.resourceId}`);
|
|
243
|
+
} else {
|
|
244
|
+
drifted++;
|
|
245
|
+
assetManager.updateStatus(resource.resourceId, 'unknown');
|
|
246
|
+
console.log(` โ ๏ธ ${resource.resourceType}: ${resource.resourceId} โ not found (status updated to unknown)`);
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
unchecked++;
|
|
250
|
+
console.log(` โ ๏ธ ${resource.resourceType}: ${resource.resourceId} โ could not verify (credentials or API unavailable)`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(`\n Drift Summary: ${verified} verified, ${drifted} drifted, ${unchecked} unchecked`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Switch the active bootstrap profile.
|
|
259
|
+
* @param {string} profileName - Profile name to activate
|
|
260
|
+
*/
|
|
261
|
+
async _handleUse(profileName) {
|
|
262
|
+
if (!profileName) {
|
|
263
|
+
console.log('Usage: ml-container-creator bootstrap use <profile>');
|
|
264
|
+
console.log(' ml-container-creator bootstrap use none (deactivate)');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (profileName === 'none') {
|
|
269
|
+
this.handler.config.setActiveProfile(null);
|
|
270
|
+
console.log('Active profile cleared. No bootstrap profile is active.');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const profile = this.handler.config.getProfile(profileName);
|
|
275
|
+
if (!profile) {
|
|
276
|
+
const available = this.handler.config.listProfiles();
|
|
277
|
+
console.log(`Profile "${profileName}" not found.`);
|
|
278
|
+
if (available.length > 0) {
|
|
279
|
+
console.log(`Available profiles: ${available.join(', ')}`);
|
|
280
|
+
} else {
|
|
281
|
+
console.log('No profiles configured. Run `ml-container-creator bootstrap` to create one.');
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this.handler.config.setActiveProfile(profileName);
|
|
287
|
+
console.log(`Switched active profile to "${profileName}".`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* List all bootstrap profiles.
|
|
292
|
+
*/
|
|
293
|
+
async _handleList() {
|
|
294
|
+
const profiles = this.handler.config.listProfiles();
|
|
295
|
+
|
|
296
|
+
if (profiles.length === 0) {
|
|
297
|
+
console.log('No bootstrap profiles configured.');
|
|
298
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const config = this.handler.config.read();
|
|
303
|
+
const activeProfileName = config ? config.activeProfile : null;
|
|
304
|
+
|
|
305
|
+
console.log('\nBootstrap Profiles:');
|
|
306
|
+
for (const name of profiles) {
|
|
307
|
+
if (name === activeProfileName) {
|
|
308
|
+
console.log(` * ${name} (active)`);
|
|
309
|
+
} else {
|
|
310
|
+
console.log(` ${name}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Remove a bootstrap profile.
|
|
317
|
+
* @param {string} profileName - Profile name to remove
|
|
318
|
+
* @param {object} options - Parsed CLI options (e.g., --force)
|
|
319
|
+
*/
|
|
320
|
+
async _handleRemove(profileName, options) {
|
|
321
|
+
if (!profileName) {
|
|
322
|
+
console.log('Usage: ml-container-creator bootstrap remove <profile> [--force]');
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const profile = this.handler.config.getProfile(profileName);
|
|
327
|
+
if (!profile) {
|
|
328
|
+
console.log(`Profile "${profileName}" not found.`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check for manifest file with active resources
|
|
333
|
+
const assetManager = new AssetManager(profileName);
|
|
334
|
+
const hasManifest = existsSync(assetManager.manifestPath);
|
|
335
|
+
|
|
336
|
+
if (hasManifest) {
|
|
337
|
+
const counts = assetManager.getStatusCounts();
|
|
338
|
+
if (counts.active > 0 && !options.force) {
|
|
339
|
+
console.log(`โ ๏ธ Profile "${profileName}" has ${counts.active} active resource${counts.active === 1 ? '' : 's'} in the deployment manifest.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for CloudFormation stack
|
|
344
|
+
const stackName = profile.stackName || `${STACK_NAME_PREFIX}-${profileName}`;
|
|
345
|
+
let hasStack = false;
|
|
346
|
+
try {
|
|
347
|
+
hasStack = this.handler._resourceExists(
|
|
348
|
+
`cloudformation describe-stacks --stack-name ${stackName} --region ${profile.awsRegion}`,
|
|
349
|
+
profile.awsProfile
|
|
350
|
+
);
|
|
351
|
+
} catch {
|
|
352
|
+
// ignore
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (hasStack && !options.force) {
|
|
356
|
+
console.log(`โ ๏ธ Profile "${profileName}" has a CloudFormation stack: ${stackName}`);
|
|
357
|
+
console.log(' Use --delete-stack to also delete the AWS resources, or --force to remove the profile only.');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!options.force) {
|
|
361
|
+
const { confirm } = await this.handler._promptFn([{
|
|
362
|
+
type: 'confirm',
|
|
363
|
+
name: 'confirm',
|
|
364
|
+
message: `Remove bootstrap profile "${profileName}"?`,
|
|
365
|
+
default: false
|
|
366
|
+
}]);
|
|
367
|
+
|
|
368
|
+
if (!confirm) {
|
|
369
|
+
console.log('Removal cancelled.');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Delete CloudFormation stack if requested
|
|
375
|
+
if (hasStack && options['delete-stack']) {
|
|
376
|
+
try {
|
|
377
|
+
console.log(`๐๏ธ Deleting CloudFormation stack: ${stackName}`);
|
|
378
|
+
execSync(
|
|
379
|
+
`aws cloudformation delete-stack --stack-name ${stackName} --region ${profile.awsRegion} --profile ${profile.awsProfile}`,
|
|
380
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
381
|
+
);
|
|
382
|
+
console.log('โณ Waiting for stack deletion...');
|
|
383
|
+
execSync(
|
|
384
|
+
`aws cloudformation wait stack-delete-complete --stack-name ${stackName} --region ${profile.awsRegion} --profile ${profile.awsProfile}`,
|
|
385
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
386
|
+
);
|
|
387
|
+
console.log(`โ
Stack "${stackName}" deleted.`);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.log(`โ ๏ธ Could not delete stack "${stackName}": ${err.message}`);
|
|
390
|
+
console.log(' You may need to delete it manually from the CloudFormation console.');
|
|
391
|
+
}
|
|
392
|
+
} else if (hasStack) {
|
|
393
|
+
console.log(`Note: CloudFormation stack "${stackName}" was left in place.`);
|
|
394
|
+
console.log(' To delete AWS resources, re-run with --delete-stack');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Delete manifest file if it exists
|
|
398
|
+
if (hasManifest) {
|
|
399
|
+
try {
|
|
400
|
+
unlinkSync(assetManager.manifestPath);
|
|
401
|
+
console.log(`Manifest file for "${profileName}" deleted.`);
|
|
402
|
+
} catch {
|
|
403
|
+
console.log(`โ ๏ธ Could not delete manifest file for "${profileName}".`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.handler.config.removeProfile(profileName);
|
|
408
|
+
console.log(`Profile "${profileName}" removed.`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Scan AWS for pre-existing MLCC-managed resources and add them to the manifest.
|
|
413
|
+
*/
|
|
414
|
+
async _handleScan() {
|
|
415
|
+
const profile = this.handler.config.getActiveProfile();
|
|
416
|
+
if (!profile) {
|
|
417
|
+
console.log('No active bootstrap profile found.');
|
|
418
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log(`\n๐ Scanning for pre-existing resources in ${profile.config.awsRegion}...`);
|
|
423
|
+
|
|
424
|
+
const assetManager = new AssetManager(profile.name);
|
|
425
|
+
const now = new Date().toISOString();
|
|
426
|
+
let discovered = 0;
|
|
427
|
+
let added = 0;
|
|
428
|
+
let skipped = 0;
|
|
429
|
+
|
|
430
|
+
// 1. Query Resource Groups Tagging API for mlcc:managed-by tagged resources
|
|
431
|
+
try {
|
|
432
|
+
console.log('\n Checking tagged resources...');
|
|
433
|
+
const tagResult = this.handler._execAws(
|
|
434
|
+
`resourcegroupstaggingapi get-resources --tag-filters Key=mlcc:managed-by,Values=ml-container-creator --region ${profile.config.awsRegion}`,
|
|
435
|
+
profile.config.awsProfile
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const taggedResources = tagResult.ResourceTagMappingList || [];
|
|
439
|
+
for (const tagged of taggedResources) {
|
|
440
|
+
discovered++;
|
|
441
|
+
const arn = tagged.ResourceARN;
|
|
442
|
+
const existing = assetManager.getResource(arn);
|
|
443
|
+
if (existing) {
|
|
444
|
+
skipped++;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const resourceType = this.handler._inferResourceTypeFromArn(arn);
|
|
449
|
+
if (!resourceType) {
|
|
450
|
+
skipped++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const project = this.handler._inferProjectFromTags(tagged.Tags) || 'unknown';
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
assetManager.addResource({
|
|
458
|
+
resourceId: arn,
|
|
459
|
+
resourceType,
|
|
460
|
+
createdAt: now,
|
|
461
|
+
lastUpdatedAt: now,
|
|
462
|
+
project,
|
|
463
|
+
status: 'active',
|
|
464
|
+
metadata: { discoveredBy: 'scan' }
|
|
465
|
+
});
|
|
466
|
+
added++;
|
|
467
|
+
} catch {
|
|
468
|
+
skipped++;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
console.log(' โ ๏ธ Could not query tagged resources (credentials or API unavailable)');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 2. Query ECR for images in ml-container-creator repository
|
|
476
|
+
try {
|
|
477
|
+
console.log(' Checking ECR images...');
|
|
478
|
+
const ecrResult = this.handler._execAws(
|
|
479
|
+
`ecr describe-images --repository-name ml-container-creator --region ${profile.config.awsRegion}`,
|
|
480
|
+
profile.config.awsProfile
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const images = ecrResult.imageDetails || [];
|
|
484
|
+
for (const image of images) {
|
|
485
|
+
const tags = image.imageTags || [];
|
|
486
|
+
for (const tag of tags) {
|
|
487
|
+
discovered++;
|
|
488
|
+
const imageUri = `${profile.config.accountId}.dkr.ecr.${profile.config.awsRegion}.amazonaws.com/ml-container-creator:${tag}`;
|
|
489
|
+
const existing = assetManager.getResource(imageUri);
|
|
490
|
+
if (existing) {
|
|
491
|
+
skipped++;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
assetManager.addResource({
|
|
497
|
+
resourceId: imageUri,
|
|
498
|
+
resourceType: 'ecr-image',
|
|
499
|
+
createdAt: now,
|
|
500
|
+
lastUpdatedAt: now,
|
|
501
|
+
project: this.handler._inferProjectFromImageTag(tag),
|
|
502
|
+
status: 'active',
|
|
503
|
+
metadata: {
|
|
504
|
+
repositoryName: 'ml-container-creator',
|
|
505
|
+
imageTag: tag,
|
|
506
|
+
region: profile.config.awsRegion,
|
|
507
|
+
discoveredBy: 'scan'
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
added++;
|
|
511
|
+
} catch {
|
|
512
|
+
skipped++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
console.log(' โ ๏ธ Could not query ECR images (credentials or API unavailable)');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 3. Query CodeBuild for *-build-* projects
|
|
521
|
+
try {
|
|
522
|
+
console.log(' Checking CodeBuild projects...');
|
|
523
|
+
const cbResult = this.handler._execAws(
|
|
524
|
+
`codebuild list-projects --region ${profile.config.awsRegion}`,
|
|
525
|
+
profile.config.awsProfile
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const projects = (cbResult.projects || []).filter(name => name.includes('-build-'));
|
|
529
|
+
for (const projectName of projects) {
|
|
530
|
+
discovered++;
|
|
531
|
+
const arn = `arn:aws:codebuild:${profile.config.awsRegion}:${profile.config.accountId}:project/${projectName}`;
|
|
532
|
+
const existing = assetManager.getResource(arn);
|
|
533
|
+
if (existing) {
|
|
534
|
+
skipped++;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
assetManager.addResource({
|
|
540
|
+
resourceId: arn,
|
|
541
|
+
resourceType: 'codebuild-project',
|
|
542
|
+
createdAt: now,
|
|
543
|
+
lastUpdatedAt: now,
|
|
544
|
+
project: this.handler._inferProjectFromCodeBuildName(projectName),
|
|
545
|
+
status: 'active',
|
|
546
|
+
metadata: {
|
|
547
|
+
projectName,
|
|
548
|
+
region: profile.config.awsRegion,
|
|
549
|
+
discoveredBy: 'scan'
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
added++;
|
|
553
|
+
} catch {
|
|
554
|
+
skipped++;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} catch {
|
|
558
|
+
console.log(' โ ๏ธ Could not query CodeBuild projects (credentials or API unavailable)');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Display summary
|
|
562
|
+
console.log(`\n Scan complete: ${discovered} discovered, ${added} added, ${skipped} skipped (duplicates or unsupported)`);
|
|
563
|
+
|
|
564
|
+
if (discovered === 0) {
|
|
565
|
+
console.log(' No MLCC-managed resources were discovered.');
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Prune stale records from the manifest โ removes entries with status
|
|
571
|
+
* 'deleted' or 'unknown' that are no longer useful.
|
|
572
|
+
*/
|
|
573
|
+
async _handlePrune() {
|
|
574
|
+
const profile = this.handler.config.getActiveProfile();
|
|
575
|
+
if (!profile) {
|
|
576
|
+
console.log('No active bootstrap profile found.');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const assetManager = new AssetManager(profile.name);
|
|
581
|
+
|
|
582
|
+
if (!existsSync(assetManager.manifestPath)) {
|
|
583
|
+
console.log('No deployment tracking data to prune.');
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const before = assetManager.listResources();
|
|
588
|
+
const toRemove = before.filter(r => r.status === 'deleted' || r.status === 'unknown');
|
|
589
|
+
|
|
590
|
+
if (toRemove.length === 0) {
|
|
591
|
+
console.log('Nothing to prune โ no deleted or unknown records found.');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
console.log(`\n๐งน Pruning ${toRemove.length} stale record${toRemove.length === 1 ? '' : 's'}:\n`);
|
|
596
|
+
|
|
597
|
+
for (const resource of toRemove) {
|
|
598
|
+
assetManager.removeResource(resource.resourceId);
|
|
599
|
+
console.log(` ๐๏ธ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const after = assetManager.listResources();
|
|
603
|
+
console.log(`\n Done. ${toRemove.length} removed, ${after.length} remaining.`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Handle sync-schemas subcommand: download service models and verify AWS CLI.
|
|
608
|
+
*/
|
|
609
|
+
async _handleSyncSchemas() {
|
|
610
|
+
console.log('\n๐ฆ Schema Sync โ Downloading AWS service models...\n');
|
|
611
|
+
|
|
612
|
+
// Verify AWS CLI is installed
|
|
613
|
+
try {
|
|
614
|
+
const version = execSync('aws --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
615
|
+
console.log(` AWS CLI: ${version}`);
|
|
616
|
+
} catch {
|
|
617
|
+
console.log(' โ ๏ธ AWS CLI not found.');
|
|
618
|
+
console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
|
|
619
|
+
console.log(' Continuing without AWS CLI verification...\n');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Dynamic import to avoid circular dependencies
|
|
623
|
+
const { syncSchemas } = await import('./schema-sync.js');
|
|
624
|
+
const result = await syncSchemas();
|
|
625
|
+
|
|
626
|
+
if (result.success) {
|
|
627
|
+
console.log('\n โ
Schema sync complete.');
|
|
628
|
+
} else {
|
|
629
|
+
console.log('\n โ ๏ธ Schema sync completed with errors (some services may be unavailable).');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
console.log(` Manifest written: lastSynced = ${result.manifest.lastSynced}\n`);
|
|
633
|
+
}
|
|
634
|
+
}
|