@aws/ml-container-creator 0.9.1 → 0.10.3
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/LICENSE-THIRD-PARTY +9304 -0
- package/bin/cli.js +2 -0
- package/config/bootstrap-e2e-stack.json +341 -0
- package/config/bootstrap-stack.json +40 -3
- package/config/parameter-schema-v2.json +2049 -0
- package/config/tune-catalog.json +1781 -0
- package/infra/ci-harness/buildspec.yml +1 -0
- package/infra/ci-harness/lambda/path-prover/brain.ts +306 -0
- package/infra/ci-harness/lambda/path-prover/write-results.ts +152 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +837 -7
- package/infra/ci-harness/state-machines/path-prover.asl.json +496 -0
- package/package.json +53 -68
- package/servers/base-image-picker/index.js +121 -121
- package/servers/e2e-status/index.js +297 -0
- package/servers/e2e-status/manifest.json +14 -0
- package/servers/e2e-status/package.json +15 -0
- package/servers/endpoint-picker/LICENSE +202 -0
- package/servers/endpoint-picker/index.js +536 -0
- package/servers/endpoint-picker/manifest.json +14 -0
- package/servers/endpoint-picker/package.json +18 -0
- package/servers/hyperpod-cluster-picker/index.js +125 -125
- package/servers/instance-sizer/index.js +138 -138
- package/servers/instance-sizer/lib/instance-ranker.js +76 -76
- package/servers/instance-sizer/lib/model-resolver.js +61 -61
- package/servers/instance-sizer/lib/quota-resolver.js +113 -113
- package/servers/instance-sizer/lib/vram-estimator.js +31 -31
- package/servers/lib/bedrock-client.js +38 -38
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/model-servers.json +201 -3
- package/servers/lib/catalogs/models.json +182 -26
- package/servers/lib/custom-validators.js +13 -13
- package/servers/lib/dynamic-resolver.js +4 -4
- package/servers/marketplace-picker/index.js +342 -0
- package/servers/marketplace-picker/manifest.json +14 -0
- package/servers/marketplace-picker/package.json +18 -0
- package/servers/model-picker/index.js +382 -382
- package/servers/region-picker/index.js +56 -56
- package/servers/workload-picker/LICENSE +202 -0
- package/servers/workload-picker/catalogs/workload-profiles.json +67 -0
- package/servers/workload-picker/index.js +171 -0
- package/servers/workload-picker/manifest.json +16 -0
- package/servers/workload-picker/package.json +16 -0
- package/src/app.js +4 -390
- package/src/lib/bootstrap-command-handler.js +710 -1148
- package/src/lib/bootstrap-config.js +36 -0
- package/src/lib/bootstrap-profile-manager.js +641 -0
- package/src/lib/bootstrap-provisioners.js +421 -0
- package/src/lib/ci-register-helpers.js +74 -0
- package/src/lib/config-loader.js +408 -0
- package/src/lib/config-manager.js +66 -1685
- package/src/lib/config-mcp-client.js +118 -0
- package/src/lib/config-validator.js +634 -0
- package/src/lib/cuda-resolver.js +149 -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 +315 -311
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +71 -71
- 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/path-prover-brain.js +607 -0
- 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 +82 -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 +422 -0
- package/src/lib/tune-catalog-validator.js +37 -4
- package/templates/Dockerfile +9 -0
- package/templates/code/adapter_sidecar.py +444 -0
- package/templates/code/serve +6 -0
- package/templates/code/serve.d/vllm.ejs +1 -1
- package/templates/do/.benchmark_writer.py +1476 -0
- package/templates/do/.tune_helper.py +982 -57
- package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
- package/templates/do/adapter +149 -0
- package/templates/do/benchmark +639 -85
- package/templates/do/config +108 -5
- package/templates/do/deploy.d/managed-inference.ejs +192 -11
- package/templates/do/optimize +106 -37
- package/templates/do/register +89 -0
- package/templates/do/test +13 -0
- package/templates/do/tune +378 -59
- package/templates/do/validate +44 -4
- package/config/parameter-schema.json +0 -88
|
@@ -0,0 +1,641 @@
|
|
|
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 (metadata-only).
|
|
317
|
+
*
|
|
318
|
+
* Only removes the profile entry from config.json and the local manifest file.
|
|
319
|
+
* AWS resources (CloudFormation stack, S3 buckets, ECR repo, IAM roles) are
|
|
320
|
+
* intentionally retained — they may be shared across profiles or still in use.
|
|
321
|
+
*
|
|
322
|
+
* @param {string} profileName - Profile name to remove
|
|
323
|
+
* @param {object} options - Parsed CLI options (e.g., --force)
|
|
324
|
+
*/
|
|
325
|
+
async _handleRemove(profileName, options) {
|
|
326
|
+
if (!profileName) {
|
|
327
|
+
console.log('Usage: ml-container-creator bootstrap remove <profile> [--force]');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const profile = this.handler.config.getProfile(profileName);
|
|
332
|
+
if (!profile) {
|
|
333
|
+
console.log(`Profile "${profileName}" not found.`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check for manifest file with active resources
|
|
338
|
+
const assetManager = new AssetManager(profileName);
|
|
339
|
+
const hasManifest = existsSync(assetManager.manifestPath);
|
|
340
|
+
|
|
341
|
+
if (hasManifest) {
|
|
342
|
+
const counts = assetManager.getStatusCounts();
|
|
343
|
+
if (counts.active > 0 && !options.force) {
|
|
344
|
+
console.log(`⚠️ Profile "${profileName}" has ${counts.active} active resource${counts.active === 1 ? '' : 's'} in the deployment manifest.`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!options.force) {
|
|
349
|
+
const { confirm } = await this.handler._promptFn([{
|
|
350
|
+
type: 'confirm',
|
|
351
|
+
name: 'confirm',
|
|
352
|
+
message: `Remove bootstrap profile "${profileName}"?`,
|
|
353
|
+
default: false
|
|
354
|
+
}]);
|
|
355
|
+
|
|
356
|
+
if (!confirm) {
|
|
357
|
+
console.log('Removal cancelled.');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Delete manifest file if it exists
|
|
363
|
+
if (hasManifest) {
|
|
364
|
+
try {
|
|
365
|
+
unlinkSync(assetManager.manifestPath);
|
|
366
|
+
console.log(`Manifest file for "${profileName}" deleted.`);
|
|
367
|
+
} catch {
|
|
368
|
+
console.log(`⚠️ Could not delete manifest file for "${profileName}".`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.handler.config.removeProfile(profileName);
|
|
373
|
+
console.log(`Profile "${profileName}" removed.`);
|
|
374
|
+
|
|
375
|
+
// Advisory: AWS resources are retained for safety
|
|
376
|
+
const stackName = profile.stackName || `${STACK_NAME_PREFIX}-${profileName}`;
|
|
377
|
+
console.log('');
|
|
378
|
+
console.log('ℹ️ Profile removed from config. AWS resources (CloudFormation stack, S3 buckets, ECR repo, IAM roles) have been retained.');
|
|
379
|
+
console.log(` To delete AWS resources, manually delete the CloudFormation stack "${stackName}" in the AWS console.`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Scan AWS for pre-existing MLCC-managed resources and add them to the manifest.
|
|
384
|
+
*/
|
|
385
|
+
async _handleScan() {
|
|
386
|
+
const profile = this.handler.config.getActiveProfile();
|
|
387
|
+
if (!profile) {
|
|
388
|
+
console.log('No active bootstrap profile found.');
|
|
389
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log(`\n🔍 Scanning for pre-existing resources in ${profile.config.awsRegion}...`);
|
|
394
|
+
|
|
395
|
+
const assetManager = new AssetManager(profile.name);
|
|
396
|
+
const now = new Date().toISOString();
|
|
397
|
+
let discovered = 0;
|
|
398
|
+
let added = 0;
|
|
399
|
+
let skipped = 0;
|
|
400
|
+
|
|
401
|
+
// 1. Query Resource Groups Tagging API for mlcc:managed-by tagged resources
|
|
402
|
+
try {
|
|
403
|
+
console.log('\n Checking tagged resources...');
|
|
404
|
+
const tagResult = this.handler._execAws(
|
|
405
|
+
`resourcegroupstaggingapi get-resources --tag-filters Key=mlcc:managed-by,Values=ml-container-creator --region ${profile.config.awsRegion}`,
|
|
406
|
+
profile.config.awsProfile
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const taggedResources = tagResult.ResourceTagMappingList || [];
|
|
410
|
+
for (const tagged of taggedResources) {
|
|
411
|
+
discovered++;
|
|
412
|
+
const arn = tagged.ResourceARN;
|
|
413
|
+
const existing = assetManager.getResource(arn);
|
|
414
|
+
if (existing) {
|
|
415
|
+
skipped++;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const resourceType = this.handler._inferResourceTypeFromArn(arn);
|
|
420
|
+
if (!resourceType) {
|
|
421
|
+
skipped++;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const project = this.handler._inferProjectFromTags(tagged.Tags) || 'unknown';
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
assetManager.addResource({
|
|
429
|
+
resourceId: arn,
|
|
430
|
+
resourceType,
|
|
431
|
+
createdAt: now,
|
|
432
|
+
lastUpdatedAt: now,
|
|
433
|
+
project,
|
|
434
|
+
status: 'active',
|
|
435
|
+
metadata: { discoveredBy: 'scan' }
|
|
436
|
+
});
|
|
437
|
+
added++;
|
|
438
|
+
} catch {
|
|
439
|
+
skipped++;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
console.log(' ⚠️ Could not query tagged resources (credentials or API unavailable)');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 2. Query ECR for images in ml-container-creator repository
|
|
447
|
+
try {
|
|
448
|
+
console.log(' Checking ECR images...');
|
|
449
|
+
const ecrResult = this.handler._execAws(
|
|
450
|
+
`ecr describe-images --repository-name ml-container-creator --region ${profile.config.awsRegion}`,
|
|
451
|
+
profile.config.awsProfile
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const images = ecrResult.imageDetails || [];
|
|
455
|
+
for (const image of images) {
|
|
456
|
+
const tags = image.imageTags || [];
|
|
457
|
+
for (const tag of tags) {
|
|
458
|
+
discovered++;
|
|
459
|
+
const imageUri = `${profile.config.accountId}.dkr.ecr.${profile.config.awsRegion}.amazonaws.com/ml-container-creator:${tag}`;
|
|
460
|
+
const existing = assetManager.getResource(imageUri);
|
|
461
|
+
if (existing) {
|
|
462
|
+
skipped++;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
assetManager.addResource({
|
|
468
|
+
resourceId: imageUri,
|
|
469
|
+
resourceType: 'ecr-image',
|
|
470
|
+
createdAt: now,
|
|
471
|
+
lastUpdatedAt: now,
|
|
472
|
+
project: this.handler._inferProjectFromImageTag(tag),
|
|
473
|
+
status: 'active',
|
|
474
|
+
metadata: {
|
|
475
|
+
repositoryName: 'ml-container-creator',
|
|
476
|
+
imageTag: tag,
|
|
477
|
+
region: profile.config.awsRegion,
|
|
478
|
+
discoveredBy: 'scan'
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
added++;
|
|
482
|
+
} catch {
|
|
483
|
+
skipped++;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
console.log(' ⚠️ Could not query ECR images (credentials or API unavailable)');
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 3. Query CodeBuild for *-build-* projects
|
|
492
|
+
try {
|
|
493
|
+
console.log(' Checking CodeBuild projects...');
|
|
494
|
+
const cbResult = this.handler._execAws(
|
|
495
|
+
`codebuild list-projects --region ${profile.config.awsRegion}`,
|
|
496
|
+
profile.config.awsProfile
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const projects = (cbResult.projects || []).filter(name => name.includes('-build-'));
|
|
500
|
+
for (const projectName of projects) {
|
|
501
|
+
discovered++;
|
|
502
|
+
const arn = `arn:aws:codebuild:${profile.config.awsRegion}:${profile.config.accountId}:project/${projectName}`;
|
|
503
|
+
const existing = assetManager.getResource(arn);
|
|
504
|
+
if (existing) {
|
|
505
|
+
skipped++;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
assetManager.addResource({
|
|
511
|
+
resourceId: arn,
|
|
512
|
+
resourceType: 'codebuild-project',
|
|
513
|
+
createdAt: now,
|
|
514
|
+
lastUpdatedAt: now,
|
|
515
|
+
project: this.handler._inferProjectFromCodeBuildName(projectName),
|
|
516
|
+
status: 'active',
|
|
517
|
+
metadata: {
|
|
518
|
+
projectName,
|
|
519
|
+
region: profile.config.awsRegion,
|
|
520
|
+
discoveredBy: 'scan'
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
added++;
|
|
524
|
+
} catch {
|
|
525
|
+
skipped++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
console.log(' ⚠️ Could not query CodeBuild projects (credentials or API unavailable)');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Display summary
|
|
533
|
+
console.log(`\n Scan complete: ${discovered} discovered, ${added} added, ${skipped} skipped (duplicates or unsupported)`);
|
|
534
|
+
|
|
535
|
+
if (discovered === 0) {
|
|
536
|
+
console.log(' No MLCC-managed resources were discovered.');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Prune stale records from the manifest — removes entries with status
|
|
542
|
+
* 'deleted' or 'unknown' that are no longer useful.
|
|
543
|
+
*/
|
|
544
|
+
async _handlePrune() {
|
|
545
|
+
const profile = this.handler.config.getActiveProfile();
|
|
546
|
+
if (!profile) {
|
|
547
|
+
console.log('No active bootstrap profile found.');
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const assetManager = new AssetManager(profile.name);
|
|
552
|
+
|
|
553
|
+
if (!existsSync(assetManager.manifestPath)) {
|
|
554
|
+
console.log('No deployment tracking data to prune.');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const before = assetManager.listResources();
|
|
559
|
+
const toRemove = before.filter(r => r.status === 'deleted' || r.status === 'unknown');
|
|
560
|
+
|
|
561
|
+
if (toRemove.length === 0) {
|
|
562
|
+
console.log('Nothing to prune — no deleted or unknown records found.');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log(`\n🧹 Pruning ${toRemove.length} stale record${toRemove.length === 1 ? '' : 's'}:\n`);
|
|
567
|
+
|
|
568
|
+
for (const resource of toRemove) {
|
|
569
|
+
assetManager.removeResource(resource.resourceId);
|
|
570
|
+
console.log(` 🗑️ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const after = assetManager.listResources();
|
|
574
|
+
console.log(`\n Done. ${toRemove.length} removed, ${after.length} remaining.`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Handle sync-schemas subcommand: download service models and verify AWS CLI.
|
|
579
|
+
*/
|
|
580
|
+
async _handleSyncSchemas() {
|
|
581
|
+
console.log('\n📦 Schema Sync — Downloading AWS service models...\n');
|
|
582
|
+
|
|
583
|
+
// Verify AWS CLI is installed
|
|
584
|
+
try {
|
|
585
|
+
const version = execSync('aws --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
586
|
+
console.log(` AWS CLI: ${version}`);
|
|
587
|
+
} catch {
|
|
588
|
+
console.log(' ⚠️ AWS CLI not found.');
|
|
589
|
+
console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
|
|
590
|
+
console.log(' Continuing without AWS CLI verification...\n');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Dynamic import to avoid circular dependencies
|
|
594
|
+
const { syncSchemas } = await import('./schema-sync.js');
|
|
595
|
+
const result = await syncSchemas();
|
|
596
|
+
|
|
597
|
+
if (result.success) {
|
|
598
|
+
console.log('\n ✅ Schema sync complete.');
|
|
599
|
+
} else {
|
|
600
|
+
console.log('\n ⚠️ Schema sync completed with errors (some services may be unavailable).');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
console.log(` Manifest written: lastSynced = ${result.manifest.lastSynced}\n`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Handle sync-model-families subcommand: discover tune-eligible models from
|
|
608
|
+
* the SageMaker JumpStart Hub and update the tune catalog.
|
|
609
|
+
*
|
|
610
|
+
* Requires AWS credentials with sagemaker:ListHubContents and
|
|
611
|
+
* sagemaker:DescribeHubContent permissions.
|
|
612
|
+
*/
|
|
613
|
+
async _handleSyncModelFamilies() {
|
|
614
|
+
console.log('\n📦 Sync Model Families — Discovering supported models...\n');
|
|
615
|
+
|
|
616
|
+
// Determine region from active profile or environment
|
|
617
|
+
const profile = this.handler.config.getActiveProfile();
|
|
618
|
+
const region = profile?.config?.awsRegion || process.env.AWS_REGION || 'us-west-2';
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const { syncModelFamilies } = await import('../../scripts/sync-model-families.js');
|
|
622
|
+
const result = await syncModelFamilies({ region });
|
|
623
|
+
console.log(`\n✅ Sync complete: ${result.added} new, ${result.total} total models`);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
if (err.name === 'CredentialsProviderError' || err.message?.includes('credentials') || err.message?.includes('Could not load credentials')) {
|
|
626
|
+
console.log('❌ AWS credentials not available or insufficient permissions.');
|
|
627
|
+
console.log('');
|
|
628
|
+
console.log(' Required permissions:');
|
|
629
|
+
console.log(' • sagemaker:ListHubContents');
|
|
630
|
+
console.log(' • sagemaker:DescribeHubContent');
|
|
631
|
+
console.log('');
|
|
632
|
+
console.log(' Ensure your AWS credentials are configured:');
|
|
633
|
+
console.log(' aws configure');
|
|
634
|
+
console.log(' # or set AWS_PROFILE to a profile with SageMaker AI access');
|
|
635
|
+
} else {
|
|
636
|
+
console.log(`❌ Sync failed: ${err.message}`);
|
|
637
|
+
}
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|