@aws/ml-container-creator 0.9.1 → 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.
Files changed (32) hide show
  1. package/config/parameter-schema-v2.json +2065 -0
  2. package/package.json +4 -4
  3. package/servers/lib/catalogs/jumpstart-public.json +101 -16
  4. package/servers/lib/catalogs/models.json +182 -26
  5. package/src/app.js +1 -389
  6. package/src/lib/bootstrap-command-handler.js +75 -1078
  7. package/src/lib/bootstrap-profile-manager.js +634 -0
  8. package/src/lib/bootstrap-provisioners.js +421 -0
  9. package/src/lib/config-loader.js +405 -0
  10. package/src/lib/config-manager.js +59 -1685
  11. package/src/lib/config-mcp-client.js +118 -0
  12. package/src/lib/config-validator.js +634 -0
  13. package/src/lib/cuda-resolver.js +140 -0
  14. package/src/lib/e2e-catalog-validator.js +251 -3
  15. package/src/lib/e2e-ci-recorder.js +103 -0
  16. package/src/lib/generated/cli-options.js +8 -4
  17. package/src/lib/generated/parameter-matrix.js +671 -0
  18. package/src/lib/generated/validation-rules.js +2 -2
  19. package/src/lib/marketplace-flow.js +276 -0
  20. package/src/lib/mcp-query-runner.js +768 -0
  21. package/src/lib/parameter-schema-validator.js +62 -18
  22. package/src/lib/prompt-runner.js +41 -1504
  23. package/src/lib/prompts/feature-prompts.js +172 -0
  24. package/src/lib/prompts/index.js +48 -0
  25. package/src/lib/prompts/infrastructure-prompts.js +690 -0
  26. package/src/lib/prompts/model-prompts.js +552 -0
  27. package/src/lib/prompts/project-prompts.js +70 -0
  28. package/src/lib/prompts.js +2 -1446
  29. package/src/lib/registry-command-handler.js +135 -3
  30. package/src/lib/secrets-prompt-runner.js +251 -0
  31. package/src/lib/template-variable-resolver.js +398 -0
  32. package/config/parameter-schema.json +0 -88
@@ -0,0 +1,421 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { execSync } from 'node:child_process';
5
+ import { readFileSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ /**
13
+ * Handles AWS resource provisioning for bootstrap (IAM role, ECR, S3 buckets).
14
+ * Delegates back to the BootstrapCommandHandler instance for shared helpers.
15
+ */
16
+ export default class BootstrapProvisioners {
17
+ constructor(handler) {
18
+ this.handler = handler;
19
+ }
20
+
21
+ /**
22
+ * Create or reuse the SageMaker execution IAM role.
23
+ * @param {object} options - Parsed CLI options
24
+ * @returns {Promise<string>} Role ARN
25
+ */
26
+ async _setupIamRole(_options) {
27
+ const roleName = 'mlcc-sagemaker-execution-role';
28
+
29
+ // Define trust policy for SageMaker
30
+ const trustPolicy = {
31
+ Version: '2012-10-17',
32
+ Statement: [
33
+ {
34
+ Effect: 'Allow',
35
+ Principal: {
36
+ Service: 'sagemaker.amazonaws.com'
37
+ },
38
+ Action: 'sts:AssumeRole'
39
+ }
40
+ ]
41
+ };
42
+
43
+ // Define execution policy with least-privilege permissions
44
+ const executionPolicy = {
45
+ Version: '2012-10-17',
46
+ Statement: [
47
+ {
48
+ Sid: 'SageMakerEndpoints',
49
+ Effect: 'Allow',
50
+ Action: [
51
+ 'sagemaker:CreateEndpoint',
52
+ 'sagemaker:CreateEndpointConfig',
53
+ 'sagemaker:CreateModel',
54
+ 'sagemaker:CreateInferenceComponent',
55
+ 'sagemaker:UpdateEndpoint',
56
+ 'sagemaker:UpdateEndpointWeightsAndCapacities',
57
+ 'sagemaker:UpdateInferenceComponent',
58
+ 'sagemaker:DeleteEndpoint',
59
+ 'sagemaker:DeleteEndpointConfig',
60
+ 'sagemaker:DeleteModel',
61
+ 'sagemaker:DeleteInferenceComponent',
62
+ 'sagemaker:DescribeEndpoint',
63
+ 'sagemaker:DescribeEndpointConfig',
64
+ 'sagemaker:DescribeModel',
65
+ 'sagemaker:DescribeInferenceComponent',
66
+ 'sagemaker:ListInferenceComponents',
67
+ 'sagemaker:InvokeEndpoint',
68
+ 'sagemaker:InvokeEndpointAsync'
69
+ ],
70
+ Resource: '*'
71
+ },
72
+ {
73
+ Sid: 'SageMakerBenchmarking',
74
+ Effect: 'Allow',
75
+ Action: [
76
+ 'sagemaker:CreateAIBenchmarkJob',
77
+ 'sagemaker:DescribeAIBenchmarkJob',
78
+ 'sagemaker:ListAIBenchmarkJobs',
79
+ 'sagemaker:StopAIBenchmarkJob',
80
+ 'sagemaker:DeleteAIBenchmarkJob',
81
+ 'sagemaker:CreateAIWorkloadConfig',
82
+ 'sagemaker:DescribeAIWorkloadConfig',
83
+ 'sagemaker:ListAIWorkloadConfigs',
84
+ 'sagemaker:DeleteAIWorkloadConfig'
85
+ ],
86
+ Resource: '*'
87
+ },
88
+ {
89
+ Sid: 'ECRPull',
90
+ Effect: 'Allow',
91
+ Action: [
92
+ 'ecr:GetAuthorizationToken',
93
+ 'ecr:BatchCheckLayerAvailability',
94
+ 'ecr:GetDownloadUrlForLayer',
95
+ 'ecr:BatchGetImage'
96
+ ],
97
+ Resource: 'arn:aws:ecr:*:*:repository/ml-container-creator'
98
+ },
99
+ {
100
+ Sid: 'ECRAuth',
101
+ Effect: 'Allow',
102
+ Action: 'ecr:GetAuthorizationToken',
103
+ Resource: '*'
104
+ },
105
+ {
106
+ Sid: 'CloudWatchLogs',
107
+ Effect: 'Allow',
108
+ Action: [
109
+ 'logs:CreateLogGroup',
110
+ 'logs:CreateLogStream',
111
+ 'logs:PutLogEvents'
112
+ ],
113
+ Resource: 'arn:aws:logs:*:*:*'
114
+ },
115
+ {
116
+ Sid: 'S3ModelRead',
117
+ Effect: 'Allow',
118
+ Action: [
119
+ 's3:GetObject',
120
+ 's3:PutObject',
121
+ 's3:AbortMultipartUpload',
122
+ 's3:ListBucket'
123
+ ],
124
+ Resource: [
125
+ 'arn:aws:s3:::ml-container-creator-*',
126
+ 'arn:aws:s3:::ml-container-creator-*/*'
127
+ ]
128
+ },
129
+ {
130
+ Sid: 'SNSPublish',
131
+ Effect: 'Allow',
132
+ Action: 'sns:Publish',
133
+ Resource: 'arn:aws:sns:*:*:ml-container-creator-*'
134
+ },
135
+ {
136
+ Sid: 'SecretsManagerBenchmark',
137
+ Effect: 'Allow',
138
+ Action: [
139
+ 'secretsmanager:CreateSecret',
140
+ 'secretsmanager:PutSecretValue',
141
+ 'secretsmanager:GetSecretValue',
142
+ 'secretsmanager:DescribeSecret'
143
+ ],
144
+ Resource: 'arn:aws:secretsmanager:*:*:secret:ml-container-creator/*'
145
+ },
146
+ {
147
+ Sid: 'QuotaAndAvailability',
148
+ Effect: 'Allow',
149
+ Action: [
150
+ 'service-quotas:GetServiceQuota',
151
+ 'service-quotas:ListServiceQuotas',
152
+ 'sagemaker:ListTrainingPlans',
153
+ 'sagemaker:DescribeTrainingPlan',
154
+ 'sagemaker:ListEndpoints'
155
+ ],
156
+ Resource: '*'
157
+ }
158
+ ]
159
+ };
160
+
161
+ // Check if role already exists
162
+ const roleExists = this.handler._resourceExists(
163
+ `iam get-role --role-name ${roleName}`,
164
+ this.handler._currentProfile
165
+ );
166
+
167
+ if (roleExists) {
168
+ const existingRole = this.handler._execAws(
169
+ `iam get-role --role-name ${roleName}`,
170
+ this.handler._currentProfile
171
+ );
172
+ const roleArn = existingRole.Role.Arn;
173
+ console.log(` ✅ IAM role "${roleName}" already exists — reused`);
174
+
175
+ // Always update the inline policy and tags to ensure they're current
176
+ try {
177
+ const execPolicyFile = this.handler._writeJsonTempFile(executionPolicy, 'exec-policy');
178
+ this.handler._execAws(
179
+ `iam put-role-policy --role-name ${roleName} --policy-name mlcc-execution-policy --policy-document ${execPolicyFile}`,
180
+ this.handler._currentProfile
181
+ );
182
+ console.log(' ✅ IAM policy "mlcc-execution-policy" — updated');
183
+ } catch (err) {
184
+ console.log(` ⚠️ Could not update inline policy: ${err.message}`);
185
+ }
186
+
187
+ try {
188
+ const tags = this._buildResourceTags();
189
+ this.handler._execAws(
190
+ `iam tag-role --role-name ${roleName} --tags ${this.handler._formatTagsForCli(tags)}`,
191
+ this.handler._currentProfile
192
+ );
193
+ console.log(' ✅ IAM role tags — updated');
194
+ } catch (err) {
195
+ console.log(` ⚠️ Could not update role tags: ${err.message}`);
196
+ }
197
+
198
+ return roleArn;
199
+ }
200
+
201
+ // Display policies to user before creation
202
+ console.log('\n Trust Policy:');
203
+ console.log(JSON.stringify(trustPolicy, null, 2));
204
+ console.log('\n Execution Policy:');
205
+ console.log(JSON.stringify(executionPolicy, null, 2));
206
+ console.log('');
207
+
208
+ try {
209
+ // Create the IAM role — write policy to temp file to avoid shell escaping issues
210
+ const trustPolicyFile = this.handler._writeJsonTempFile(trustPolicy, 'trust-policy');
211
+ const createRoleResult = this.handler._execAws(
212
+ `iam create-role --role-name ${roleName} --assume-role-policy-document ${trustPolicyFile}`,
213
+ this.handler._currentProfile
214
+ );
215
+ const roleArn = createRoleResult.Role.Arn;
216
+
217
+ // Attach inline execution policy
218
+ const execPolicyFile = this.handler._writeJsonTempFile(executionPolicy, 'exec-policy');
219
+ this.handler._execAws(
220
+ `iam put-role-policy --role-name ${roleName} --policy-name mlcc-execution-policy --policy-document ${execPolicyFile}`,
221
+ this.handler._currentProfile
222
+ );
223
+
224
+ // Apply resource tags
225
+ const tags = this._buildResourceTags();
226
+ this.handler._execAws(
227
+ `iam tag-role --role-name ${roleName} --tags ${this.handler._formatTagsForCli(tags)}`,
228
+ this.handler._currentProfile
229
+ );
230
+
231
+ console.log(` ✅ IAM role "${roleName}" — created`);
232
+ return roleArn;
233
+ } catch (error) {
234
+ const errorMessage = error.message || '';
235
+ if (errorMessage.includes('AccessDenied') || errorMessage.includes('UnauthorizedAccess')) {
236
+ console.log(' ⚠️ Permission denied for iam:CreateRole. Please provide an existing role ARN.');
237
+ const { roleArn } = await this.handler._promptFn([{
238
+ type: 'input',
239
+ name: 'roleArn',
240
+ message: 'Enter an existing IAM role ARN for SageMaker execution:'
241
+ }]);
242
+ return roleArn;
243
+ }
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Create or reuse the ECR repository.
250
+ * @returns {Promise<string>} ECR repository name
251
+ */
252
+ async _setupEcrRepository() {
253
+ const repoName = 'ml-container-creator';
254
+
255
+ // Check if repository already exists
256
+ const repoExists = this.handler._resourceExists(
257
+ `ecr describe-repositories --repository-names ${repoName} --region ${this.handler._currentRegion}`,
258
+ this.handler._currentProfile
259
+ );
260
+
261
+ if (repoExists) {
262
+ console.log(` ✅ ECR repository "${repoName}" already exists — reused`);
263
+ return repoName;
264
+ }
265
+
266
+ // Build resource tags
267
+ const tags = this._buildResourceTags();
268
+
269
+ // Create the ECR repository with image scanning and AES256 encryption
270
+ this.handler._execAws(
271
+ `ecr create-repository --repository-name ${repoName} --image-scanning-configuration scanOnPush=true --encryption-configuration encryptionType=AES256 --region ${this.handler._currentRegion} --tags ${this.handler._formatTagsForCli(tags)}`,
272
+ this.handler._currentProfile
273
+ );
274
+
275
+ // Apply lifecycle policy to expire untagged images after 30 days
276
+ const lifecyclePolicy = {
277
+ rules: [
278
+ {
279
+ rulePriority: 1,
280
+ description: 'Expire untagged images after 30 days',
281
+ selection: {
282
+ tagStatus: 'untagged',
283
+ countType: 'sinceImagePushed',
284
+ countUnit: 'days',
285
+ countNumber: 30
286
+ },
287
+ action: {
288
+ type: 'expire'
289
+ }
290
+ }
291
+ ]
292
+ };
293
+
294
+ const lifecyclePolicyFile = this.handler._writeJsonTempFile(lifecyclePolicy, 'ecr-lifecycle');
295
+ this.handler._execAws(
296
+ `ecr put-lifecycle-policy --repository-name ${repoName} --lifecycle-policy-text ${lifecyclePolicyFile} --region ${this.handler._currentRegion}`,
297
+ this.handler._currentProfile
298
+ );
299
+
300
+ console.log(` ✅ ECR repository "${repoName}" — created`);
301
+ return repoName;
302
+ }
303
+
304
+ /**
305
+ * Optionally create S3 buckets for async/batch deployments.
306
+ * Always creates the benchmark S3 bucket (unconditional).
307
+ * @returns {Promise<object|null>} Bucket names or null if skipped
308
+ */
309
+ async _setupS3Buckets() {
310
+ // Always create benchmark bucket (unconditional — avoids re-bootstrap when benchmarking is enabled later)
311
+ const benchmarkBucketName = `ml-container-creator-benchmark-${this.handler._currentRegion}-${this.handler._currentAccountId}`;
312
+ const tags = this._buildResourceTags();
313
+ const benchmarkS3Bucket = await this._createS3Bucket(benchmarkBucketName, tags);
314
+
315
+ const { useS3 } = await this.handler._promptFn([{
316
+ type: 'confirm',
317
+ name: 'useS3',
318
+ message: 'Will you use async inference or batch transform?',
319
+ default: false
320
+ }]);
321
+
322
+ if (!useS3) {
323
+ return { benchmarkS3Bucket };
324
+ }
325
+
326
+ const asyncBucketName = `ml-container-creator-async-${this.handler._currentRegion}-${this.handler._currentAccountId}`;
327
+ const batchBucketName = `ml-container-creator-batch-${this.handler._currentRegion}-${this.handler._currentAccountId}`;
328
+
329
+ const asyncS3Bucket = await this._createS3Bucket(asyncBucketName, tags);
330
+ const batchS3Bucket = await this._createS3Bucket(batchBucketName, tags);
331
+
332
+ return { asyncS3Bucket, batchS3Bucket, benchmarkS3Bucket };
333
+ }
334
+
335
+ /**
336
+ * Create or reuse a single S3 bucket with versioning, encryption, and tags.
337
+ * @param {string} bucketName - S3 bucket name
338
+ * @param {Array<{Key: string, Value: string}>} tags - Resource tags
339
+ * @returns {Promise<string>} Bucket name
340
+ */
341
+ async _createS3Bucket(bucketName, tags) {
342
+ // Check if bucket already exists
343
+ const bucketExists = this.handler._resourceExists(
344
+ `s3api head-bucket --bucket ${bucketName}`,
345
+ this.handler._currentProfile
346
+ );
347
+
348
+ if (bucketExists) {
349
+ console.log(` ✅ S3 bucket "${bucketName}" already exists — reused`);
350
+ return bucketName;
351
+ }
352
+
353
+ // Build create-bucket command with region-appropriate configuration
354
+ let createCommand = `s3api create-bucket --bucket ${bucketName} --region ${this.handler._currentRegion}`;
355
+ if (this.handler._currentRegion !== 'us-east-1') {
356
+ createCommand += ` --create-bucket-configuration LocationConstraint=${this.handler._currentRegion}`;
357
+ }
358
+
359
+ this.handler._execAws(createCommand, this.handler._currentProfile);
360
+
361
+ // Enable versioning
362
+ this.handler._execAws(
363
+ `s3api put-bucket-versioning --bucket ${bucketName} --versioning-configuration Status=Enabled`,
364
+ this.handler._currentProfile
365
+ );
366
+
367
+ // Enable AES256 server-side encryption
368
+ const encryptionConfig = { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' } }] };
369
+ const encryptionFile = this.handler._writeJsonTempFile(encryptionConfig, 's3-encryption');
370
+ this.handler._execAws(
371
+ `s3api put-bucket-encryption --bucket ${bucketName} --server-side-encryption-configuration ${encryptionFile}`,
372
+ this.handler._currentProfile
373
+ );
374
+
375
+ // Apply resource tags
376
+ const tagging = { TagSet: tags };
377
+ const taggingFile = this.handler._writeJsonTempFile(tagging, 's3-tagging');
378
+ this.handler._execAws(
379
+ `s3api put-bucket-tagging --bucket ${bucketName} --tagging ${taggingFile}`,
380
+ this.handler._currentProfile
381
+ );
382
+
383
+ console.log(` ✅ S3 bucket "${bucketName}" — created`);
384
+ return bucketName;
385
+ }
386
+
387
+ /**
388
+ * Verify AWS CLI v2 is installed. Returns true if v2 is detected, false otherwise.
389
+ * @returns {boolean}
390
+ */
391
+ _verifyCliV2() {
392
+ try {
393
+ const versionOutput = execSync('aws --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
394
+ if (!versionOutput.includes('aws-cli/2')) {
395
+ console.log(` ❌ AWS CLI v2 is required. Detected: ${versionOutput.split(' ')[0]}`);
396
+ console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
397
+ console.log(' Some features (benchmarking, newer SageMaker APIs) require CLI v2.\n');
398
+ return false;
399
+ }
400
+ return true;
401
+ } catch {
402
+ console.log(' ❌ AWS CLI not found.');
403
+ console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\n');
404
+ return false;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Build the standard resource tag set.
410
+ * @returns {Array<{Key: string, Value: string}>} Tag array
411
+ */
412
+ _buildResourceTags() {
413
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
414
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
415
+ return [
416
+ { Key: 'mlcc:managed-by', Value: 'ml-container-creator' },
417
+ { Key: 'mlcc:created-by', Value: 'bootstrap' },
418
+ { Key: 'mlcc:version', Value: packageJson.version }
419
+ ];
420
+ }
421
+ }