@aws/ml-container-creator 0.2.6 → 0.4.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 (50) hide show
  1. package/bin/cli.js +38 -2
  2. package/config/bootstrap-stack.json +94 -1
  3. package/config/defaults.json +1 -1
  4. package/infra/ci-harness/package-lock.json +22 -9
  5. package/package.json +3 -1
  6. package/servers/instance-sizer/index.js +45 -8
  7. package/servers/instance-sizer/lib/instance-ranker.js +140 -11
  8. package/servers/instance-sizer/lib/model-resolver.js +10 -6
  9. package/servers/instance-sizer/lib/quota-resolver.js +368 -0
  10. package/servers/instance-sizer/package.json +2 -0
  11. package/servers/lib/catalogs/instances.json +527 -12
  12. package/servers/lib/catalogs/model-servers.json +298 -20
  13. package/servers/lib/catalogs/model-sizes.json +27 -0
  14. package/servers/lib/catalogs/models.json +101 -0
  15. package/servers/lib/schemas/image-catalog.schema.json +15 -1
  16. package/servers/model-picker/index.js +2 -1
  17. package/src/app.js +96 -2
  18. package/src/lib/architecture-sync.js +171 -0
  19. package/src/lib/arn-detection.js +22 -0
  20. package/src/lib/bootstrap-command-handler.js +178 -3
  21. package/src/lib/cli-handler.js +2 -2
  22. package/src/lib/config-manager.js +121 -1
  23. package/src/lib/cross-cutting-checker.js +119 -0
  24. package/src/lib/deployment-entry-schema.js +1 -2
  25. package/src/lib/prompt-runner.js +514 -20
  26. package/src/lib/prompts.js +67 -5
  27. package/src/lib/registry-command-handler.js +236 -0
  28. package/src/lib/schema-sync.js +31 -0
  29. package/src/lib/secret-classification.js +56 -0
  30. package/src/lib/secrets-command-handler.js +550 -0
  31. package/src/lib/template-manager.js +49 -1
  32. package/src/lib/validate-runner.js +174 -2
  33. package/src/lib/validation-report.js +8 -1
  34. package/src/prompt-adapter.js +3 -2
  35. package/templates/Dockerfile +10 -2
  36. package/templates/code/cuda_compat.sh +22 -0
  37. package/templates/code/serve +3 -0
  38. package/templates/code/start_server.sh +3 -0
  39. package/templates/diffusors/Dockerfile +2 -1
  40. package/templates/diffusors/serve +3 -0
  41. package/templates/do/README.md +33 -0
  42. package/templates/do/benchmark +646 -0
  43. package/templates/do/build +22 -0
  44. package/templates/do/clean +86 -0
  45. package/templates/do/config +41 -6
  46. package/templates/do/deploy +66 -6
  47. package/templates/do/logs +18 -3
  48. package/templates/do/register +8 -1
  49. package/templates/do/run +10 -0
  50. package/templates/triton/Dockerfile +5 -0
@@ -0,0 +1,550 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Secrets Command Handler
6
+ *
7
+ * Handles the `secrets` CLI subcommand tree for managing secrets in
8
+ * AWS Secrets Manager. Follows the same dispatch pattern as
9
+ * BootstrapCommandHandler.
10
+ *
11
+ * Subcommands:
12
+ * create Create a new secret in Secrets Manager
13
+ * list List all mlcc-managed secrets
14
+ * describe <name-or-arn> Show metadata for a specific secret
15
+ */
16
+
17
+ import { execSync } from 'node:child_process';
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { tmpdir } from 'node:os';
21
+ import { runPrompts } from '../prompt-adapter.js';
22
+ import { SECRET_CLASSIFICATIONS } from './secret-classification.js';
23
+ import BootstrapConfig from './bootstrap-config.js';
24
+
25
+ export default class SecretsCommandHandler {
26
+ constructor({ promptFn, execAwsFn } = {}) {
27
+ this._promptFn = promptFn || runPrompts;
28
+ this._execAwsFn = execAwsFn || null;
29
+ this._bootstrapConfig = new BootstrapConfig();
30
+ }
31
+
32
+ /**
33
+ * Dispatch secrets subcommands.
34
+ * @param {string[]} args - Positional args after 'secrets'
35
+ * @param {object} options - Parsed CLI options
36
+ */
37
+ async handle(args, options) {
38
+ if (args.length === 0) {
39
+ this._showHelp();
40
+ return;
41
+ }
42
+
43
+ const subcommand = args[0].toLowerCase();
44
+
45
+ switch (subcommand) {
46
+ case 'create':
47
+ await this._handleCreate(options);
48
+ break;
49
+ case 'list':
50
+ await this._handleList();
51
+ break;
52
+ case 'describe':
53
+ await this._handleDescribe(args[1]);
54
+ break;
55
+ default:
56
+ console.log(`Unknown secrets subcommand: ${subcommand}`);
57
+ this._showHelp();
58
+ break;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Create a new secret in AWS Secrets Manager.
64
+ * Supports three input modes:
65
+ * 1. Interactive — prompts for type, name, and value
66
+ * 2. --json flag — inline JSON or file:// path
67
+ * 3. Individual flags — --type, --name, --secret-value, etc.
68
+ * @param {object} options - Parsed CLI options
69
+ */
70
+ async _handleCreate(options) {
71
+ let secretType, label, secretValue, description, kmsKeyId, userTags;
72
+
73
+ if (options.json) {
74
+ // JSON mode: parse inline JSON or read from file
75
+ const jsonInput = await this._resolveJsonInput(options.json);
76
+ if (!jsonInput) return;
77
+
78
+ secretType = jsonInput.type || jsonInput.secretType;
79
+ label = jsonInput.name || jsonInput.label;
80
+ secretValue = jsonInput.secretValue || jsonInput['secret-value'];
81
+ description = jsonInput.description;
82
+ kmsKeyId = jsonInput.kmsKeyId || jsonInput['kms-key-id'];
83
+ userTags = jsonInput.tags || [];
84
+ } else if (options.type || options.name || options.secretValue) {
85
+ // Flag mode: use individual CLI flags
86
+ secretType = options.type;
87
+ label = options.name;
88
+ secretValue = options.secretValue;
89
+ description = options.description;
90
+ kmsKeyId = options.kmsKeyId;
91
+ userTags = [];
92
+ } else {
93
+ // Interactive mode: prompt for all required fields
94
+ const result = await this._runInteractiveCreate();
95
+ if (!result) return;
96
+ secretType = result.type;
97
+ label = result.name;
98
+ secretValue = result.secretValue;
99
+ description = result.description;
100
+ kmsKeyId = result.kmsKeyId;
101
+ userTags = [];
102
+ }
103
+
104
+ // Validate required fields
105
+ const missing = [];
106
+ if (!secretType) missing.push('--type');
107
+ if (!label) missing.push('--name');
108
+ if (!secretValue) missing.push('--secret-value');
109
+
110
+ if (missing.length > 0) {
111
+ console.log(`❌ Missing required fields: ${missing.join(', ')}`);
112
+ console.log(' Provide all required flags or run without flags for interactive mode.');
113
+ process.exitCode = 1;
114
+ return;
115
+ }
116
+
117
+ // Validate secret type against registry
118
+ const classification = SECRET_CLASSIFICATIONS.find(c => c.identifier === secretType);
119
+ if (!classification) {
120
+ const validTypes = SECRET_CLASSIFICATIONS.map(c => c.identifier).join(', ');
121
+ console.log(`❌ Unknown secret type: ${secretType}`);
122
+ console.log(` Valid types: ${validTypes}`);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+
127
+ // Construct the secret name
128
+ const secretName = this._constructSecretName(secretType, label);
129
+
130
+ // Merge tags
131
+ const tags = this._mergeTags(userTags || [], secretType);
132
+
133
+ // Build the create-secret command
134
+ const { profile, region } = this._getActiveBootstrapContext();
135
+ if (!profile) {
136
+ console.log('❌ No active bootstrap profile found.');
137
+ console.log(' Run `ml-container-creator bootstrap` to set up shared infrastructure.');
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+
142
+ // Write secret value to temp file to avoid shell exposure
143
+ const secretValueFile = this._writeJsonTempFile(secretValue, 'secret-value');
144
+
145
+ let command = `secretsmanager create-secret --name ${secretName} --secret-string ${secretValueFile} --tags ${this._formatTagsForCli(tags)} --region ${region}`;
146
+
147
+ if (description) {
148
+ command += ` --description "${description}"`;
149
+ }
150
+ if (kmsKeyId) {
151
+ command += ` --kms-key-id ${kmsKeyId}`;
152
+ }
153
+
154
+ try {
155
+ const result = this._execAws(command, profile);
156
+ const arn = result.ARN || result.Name;
157
+ console.log('✅ Secret created successfully');
158
+ console.log(` Name: ${secretName}`);
159
+ console.log(` ARN: ${arn}`);
160
+ } catch (error) {
161
+ console.log(`❌ Failed to create secret: ${error.message}`);
162
+ process.exitCode = 1;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Run the interactive secret creation flow.
168
+ * Prompts for type, name, and value (password-masked).
169
+ * @returns {object|null} Object with type, name, secretValue, description, kmsKeyId
170
+ */
171
+ async _runInteractiveCreate() {
172
+ console.log('\n🔐 Create a new secret in AWS Secrets Manager\n');
173
+
174
+ // Prompt for secret type
175
+ const typeChoices = SECRET_CLASSIFICATIONS.map(c => ({
176
+ name: `${c.displayName} — ${c.purpose}`,
177
+ value: c.identifier
178
+ }));
179
+
180
+ const { secretType } = await this._promptFn([{
181
+ type: 'list',
182
+ name: 'secretType',
183
+ message: 'Secret type:',
184
+ choices: typeChoices
185
+ }]);
186
+
187
+ // Prompt for label
188
+ const { label } = await this._promptFn([{
189
+ type: 'input',
190
+ name: 'label',
191
+ message: 'Secret name (label):',
192
+ validate: (val) => val && val.trim().length > 0 ? true : 'Name is required'
193
+ }]);
194
+
195
+ // Prompt for secret value (password-masked)
196
+ const { value } = await this._promptFn([{
197
+ type: 'password',
198
+ name: 'value',
199
+ message: 'Secret value:',
200
+ mask: '*',
201
+ validate: (val) => val && val.trim().length > 0 ? true : 'Value is required'
202
+ }]);
203
+
204
+ // Optional: description
205
+ const { description } = await this._promptFn([{
206
+ type: 'input',
207
+ name: 'description',
208
+ message: 'Description (optional):',
209
+ default: ''
210
+ }]);
211
+
212
+ return {
213
+ type: secretType,
214
+ name: label.trim(),
215
+ secretValue: value,
216
+ description: description || undefined,
217
+ kmsKeyId: undefined
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Resolve JSON input from inline string or file:// path.
223
+ * @param {string} jsonOrPath - Inline JSON string or file://path
224
+ * @returns {object|null} Parsed JSON object, or null on error
225
+ */
226
+ async _resolveJsonInput(jsonOrPath) {
227
+ let rawJson;
228
+
229
+ if (jsonOrPath.startsWith('file://')) {
230
+ const filePath = jsonOrPath.slice(7);
231
+ if (!existsSync(filePath)) {
232
+ console.log(`❌ File not found: ${filePath}`);
233
+ process.exitCode = 1;
234
+ return null;
235
+ }
236
+ rawJson = readFileSync(filePath, 'utf8');
237
+ } else {
238
+ rawJson = jsonOrPath;
239
+ }
240
+
241
+ try {
242
+ return JSON.parse(rawJson);
243
+ } catch (error) {
244
+ console.log(`❌ Invalid JSON: ${error.message}`);
245
+ process.exitCode = 1;
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Construct the secret name following the mlcc naming convention.
252
+ * @param {string} type - Secret type identifier (e.g., 'hf-token')
253
+ * @param {string} label - User-provided label
254
+ * @returns {string} Constructed name in format mlcc/<type>/<label>
255
+ */
256
+ _constructSecretName(type, label) {
257
+ return `mlcc/${type}/${label}`;
258
+ }
259
+
260
+ /**
261
+ * Merge user-provided tags with required system tags.
262
+ * System tags always win over user-provided tags with the mlcc: prefix.
263
+ * User tags without the mlcc: prefix are preserved.
264
+ * @param {Array<{Key: string, Value: string}>} userTags - User-provided tags
265
+ * @param {string} secretType - Secret type identifier
266
+ * @returns {Array<{Key: string, Value: string}>} Merged tag array
267
+ */
268
+ _mergeTags(userTags, secretType) {
269
+ const systemTags = [
270
+ { Key: 'mlcc:managed-by', Value: 'ml-container-creator' },
271
+ { Key: 'mlcc:created-by', Value: 'secrets' },
272
+ { Key: 'mlcc:secret-type', Value: secretType }
273
+ ];
274
+
275
+ const systemTagKeys = new Set(systemTags.map(t => t.Key));
276
+
277
+ // Filter user tags: preserve non-mlcc: tags, warn about mlcc: conflicts
278
+ const preservedUserTags = [];
279
+ for (const tag of (userTags || [])) {
280
+ if (!tag || !tag.Key) continue;
281
+ if (tag.Key.startsWith('mlcc:')) {
282
+ if (systemTagKeys.has(tag.Key)) {
283
+ console.log(`⚠️ Tag "${tag.Key}" is reserved and will be overwritten with system value`);
284
+ } else {
285
+ console.log(`⚠️ Tag "${tag.Key}" uses reserved mlcc: prefix and will be removed`);
286
+ }
287
+ } else {
288
+ preservedUserTags.push(tag);
289
+ }
290
+ }
291
+
292
+ return [...systemTags, ...preservedUserTags];
293
+ }
294
+
295
+ /**
296
+ * Get the active bootstrap profile's AWS profile and region.
297
+ * @returns {{ profile: string|null, region: string|null }}
298
+ */
299
+ _getActiveBootstrapContext() {
300
+ const active = this._bootstrapConfig.getActiveProfile();
301
+ if (!active) {
302
+ return { profile: null, region: null };
303
+ }
304
+ return {
305
+ profile: active.config.awsProfile,
306
+ region: active.config.awsRegion
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Execute an AWS CLI command and return parsed JSON output.
312
+ * @param {string} command - AWS CLI command (without 'aws' prefix)
313
+ * @param {string} profile - AWS CLI profile name
314
+ * @returns {object} Parsed JSON output
315
+ */
316
+ _execAws(command, profile) {
317
+ if (this._execAwsFn) {
318
+ return this._execAwsFn(command, profile);
319
+ }
320
+ const fullCommand = `aws ${command} --profile ${profile} --output json`;
321
+ const output = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
322
+ const trimmed = output.trim();
323
+ if (!trimmed) {
324
+ return {};
325
+ }
326
+ return JSON.parse(trimmed);
327
+ }
328
+
329
+ /**
330
+ * Write a JSON value to a temp file and return the file:// path.
331
+ * Used to avoid shell escaping issues with complex values.
332
+ * @param {*} value - Value to serialize (string or object)
333
+ * @param {string} prefix - Filename prefix
334
+ * @returns {string} file:// path to the temp file
335
+ */
336
+ _writeJsonTempFile(value, prefix = 'mlcc-secret') {
337
+ const dir = path.join(tmpdir(), 'mlcc-secrets');
338
+ if (!existsSync(dir)) {
339
+ mkdirSync(dir, { recursive: true });
340
+ }
341
+ const filePath = path.join(dir, `${prefix}-${Date.now()}.json`);
342
+ const content = typeof value === 'string' ? value : JSON.stringify(value);
343
+ writeFileSync(filePath, content);
344
+ return `file://${filePath}`;
345
+ }
346
+
347
+ /**
348
+ * Format tags for the AWS CLI --tags parameter.
349
+ * Writes tags to a temp file and returns the file:// reference.
350
+ * @param {Array<{Key: string, Value: string}>} tags - Tag array
351
+ * @returns {string} file:// path to the tags JSON file
352
+ */
353
+ _formatTagsForCli(tags) {
354
+ return this._writeJsonTempFile(tags, 'tags');
355
+ }
356
+
357
+ /**
358
+ * List all mlcc-managed secrets.
359
+ * Calls list-secrets filtered by the mlcc:managed-by tag and displays
360
+ * name, ARN, secret type, creation date, and last accessed date.
361
+ * Never displays secret values.
362
+ *
363
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5
364
+ */
365
+ async _handleList() {
366
+ const { profile, region } = this._getActiveBootstrapContext();
367
+ if (!profile) {
368
+ console.log('❌ No active bootstrap profile found.');
369
+ console.log(' Run `ml-container-creator bootstrap` to set up shared infrastructure.');
370
+ process.exitCode = 1;
371
+ return;
372
+ }
373
+
374
+ const command = `secretsmanager list-secrets --filters Key=tag-key,Values=mlcc:managed-by Key=tag-value,Values=ml-container-creator --region ${region}`;
375
+
376
+ let result;
377
+ try {
378
+ result = this._execAws(command, profile);
379
+ } catch (error) {
380
+ console.log(`❌ Failed to list secrets: ${error.message}`);
381
+ process.exitCode = 1;
382
+ return;
383
+ }
384
+
385
+ const secrets = result.SecretList || [];
386
+
387
+ if (secrets.length === 0) {
388
+ console.log('\nNo mlcc-managed secrets found.');
389
+ console.log(' Run `ml-container-creator secrets create` to create your first secret.\n');
390
+ return;
391
+ }
392
+
393
+ console.log(`\n🔐 Managed Secrets (${secrets.length})\n`);
394
+ console.log('─'.repeat(80));
395
+
396
+ for (const secret of secrets) {
397
+ const secretType = this._extractTagValue(secret.Tags, 'mlcc:secret-type') || 'unknown';
398
+ const createdDate = secret.CreatedDate ? new Date(secret.CreatedDate).toLocaleDateString() : 'N/A';
399
+ const lastAccessed = secret.LastAccessedDate ? new Date(secret.LastAccessedDate).toLocaleDateString() : 'Never';
400
+
401
+ console.log(` Name: ${secret.Name}`);
402
+ console.log(` ARN: ${secret.ARN}`);
403
+ console.log(` Type: ${secretType}`);
404
+ console.log(` Created: ${createdDate}`);
405
+ console.log(` Last Accessed: ${lastAccessed}`);
406
+ console.log('─'.repeat(80));
407
+ }
408
+
409
+ console.log('');
410
+ }
411
+
412
+ /**
413
+ * Extract a tag value from a Tags array by key.
414
+ * @param {Array<{Key: string, Value: string}>} tags - Tags array
415
+ * @param {string} key - Tag key to find
416
+ * @returns {string|undefined} Tag value or undefined if not found
417
+ */
418
+ _extractTagValue(tags, key) {
419
+ if (!Array.isArray(tags)) return undefined;
420
+ const tag = tags.find(t => t.Key === key);
421
+ return tag ? tag.Value : undefined;
422
+ }
423
+
424
+ /**
425
+ * Describe a specific secret's metadata (never reveals the value).
426
+ * Calls `aws secretsmanager describe-secret` and displays name, ARN,
427
+ * description, tags, creation date, last changed date, last accessed date,
428
+ * and rotation configuration.
429
+ *
430
+ * Never calls GetSecretValue. Displays error if secret not found or
431
+ * not a managed secret.
432
+ *
433
+ * Requirements: 4.1, 4.2, 4.3, 4.4
434
+ *
435
+ * @param {string} nameOrArn - Secret name or ARN to describe
436
+ */
437
+ async _handleDescribe(nameOrArn) {
438
+ if (!nameOrArn) {
439
+ console.log('❌ Missing secret name or ARN.');
440
+ console.log(' Usage: ml-container-creator secrets describe <name-or-arn>');
441
+ process.exitCode = 1;
442
+ return;
443
+ }
444
+
445
+ const { profile, region } = this._getActiveBootstrapContext();
446
+ if (!profile) {
447
+ console.log('❌ No active bootstrap profile found.');
448
+ console.log(' Run `ml-container-creator bootstrap` to set up shared infrastructure.');
449
+ process.exitCode = 1;
450
+ return;
451
+ }
452
+
453
+ const command = `secretsmanager describe-secret --secret-id ${nameOrArn} --region ${region}`;
454
+
455
+ let result;
456
+ try {
457
+ result = this._execAws(command, profile);
458
+ } catch (error) {
459
+ console.log(`❌ Secret not found: ${nameOrArn}`);
460
+ console.log(` ${error.message}`);
461
+ process.exitCode = 1;
462
+ return;
463
+ }
464
+
465
+ // Verify this is a managed secret by checking the mlcc:managed-by tag
466
+ const managedByValue = this._extractTagValue(result.Tags, 'mlcc:managed-by');
467
+ if (managedByValue !== 'ml-container-creator') {
468
+ console.log(`❌ Secret "${nameOrArn}" is not managed by ml-container-creator.`);
469
+ console.log(' Only secrets created with `ml-container-creator secrets create` can be described.');
470
+ process.exitCode = 1;
471
+ return;
472
+ }
473
+
474
+ // Display secret metadata
475
+ const createdDate = result.CreatedDate ? new Date(result.CreatedDate).toLocaleString() : 'N/A';
476
+ const lastChanged = result.LastChangedDate ? new Date(result.LastChangedDate).toLocaleString() : 'N/A';
477
+ const lastAccessed = result.LastAccessedDate ? new Date(result.LastAccessedDate).toLocaleDateString() : 'Never';
478
+ const secretType = this._extractTagValue(result.Tags, 'mlcc:secret-type') || 'unknown';
479
+
480
+ console.log('\n🔐 Secret Details\n');
481
+ console.log('─'.repeat(80));
482
+ console.log(` Name: ${result.Name}`);
483
+ console.log(` ARN: ${result.ARN}`);
484
+ console.log(` Type: ${secretType}`);
485
+ console.log(` Description: ${result.Description || '(none)'}`);
486
+ console.log(` Created: ${createdDate}`);
487
+ console.log(` Last Changed: ${lastChanged}`);
488
+ console.log(` Last Accessed: ${lastAccessed}`);
489
+
490
+ // Rotation configuration
491
+ if (result.RotationEnabled) {
492
+ console.log(' Rotation: Enabled');
493
+ if (result.RotationRules) {
494
+ if (result.RotationRules.AutomaticallyAfterDays) {
495
+ console.log(` Rotation Rule: Every ${result.RotationRules.AutomaticallyAfterDays} days`);
496
+ }
497
+ if (result.RotationRules.Duration) {
498
+ console.log(` Rotation Window: ${result.RotationRules.Duration}`);
499
+ }
500
+ if (result.RotationRules.ScheduleExpression) {
501
+ console.log(` Rotation Schedule: ${result.RotationRules.ScheduleExpression}`);
502
+ }
503
+ }
504
+ } else {
505
+ console.log(' Rotation: Disabled');
506
+ }
507
+
508
+ // Tags
509
+ if (Array.isArray(result.Tags) && result.Tags.length > 0) {
510
+ console.log(' Tags:');
511
+ for (const tag of result.Tags) {
512
+ console.log(` ${tag.Key} = ${tag.Value}`);
513
+ }
514
+ }
515
+
516
+ console.log('─'.repeat(80));
517
+ console.log('');
518
+ }
519
+
520
+ /**
521
+ * Show secrets usage help.
522
+ */
523
+ _showHelp() {
524
+ console.log(`
525
+ Secrets — Manage secrets in AWS Secrets Manager
526
+
527
+ USAGE:
528
+ ml-container-creator secrets <action> [options]
529
+
530
+ ACTIONS:
531
+ create Create a new secret
532
+ list List all mlcc-managed secrets
533
+ describe <name-or-arn> Show metadata for a specific secret
534
+
535
+ CREATE OPTIONS:
536
+ --type <type> Secret type (e.g., hf-token, ngc-token)
537
+ --name <label> Secret label (used in naming convention)
538
+ --secret-value <value> Secret value
539
+ --description <text> Secret description
540
+ --kms-key-id <key> KMS key for encryption
541
+ --json <json-or-path> JSON input (inline or file://path)
542
+
543
+ EXAMPLES:
544
+ ml-container-creator secrets create --type hf-token --name production --secret-value hf_***
545
+ ml-container-creator secrets create --json file://secret.json
546
+ ml-container-creator secrets list
547
+ ml-container-creator secrets describe mlcc/hf-token/production
548
+ `.trim());
549
+ }
550
+ }
@@ -65,7 +65,7 @@ export default class TemplateManager {
65
65
  ],
66
66
  buildTargets: ['codebuild'],
67
67
  deploymentTargets: ['realtime-inference', 'async-inference', 'batch-transform', 'hyperpod-eks'],
68
- testTypes: ['local-model-cli', 'local-model-server', 'hosted-model-endpoint'],
68
+ testTypes: ['local-model-cli', 'local-model-server', 'hosted-model-endpoint', 'sagemaker-ai-automated-benchmarking'],
69
69
  awsRegions: [
70
70
  'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
71
71
  'eu-west-1', 'eu-west-2', 'eu-central-1', 'eu-north-1',
@@ -134,6 +134,9 @@ export default class TemplateManager {
134
134
 
135
135
  // Validate batch transform specific fields
136
136
  this._validateBatchTransformConfig();
137
+
138
+ // Validate benchmark specific fields
139
+ this._validateBenchmarkConfig();
137
140
 
138
141
  // Validate instance type format (ml.*.*) - only for realtime-inference
139
142
  if (this.answers.instanceType && this.answers.instanceType !== 'custom') {
@@ -297,6 +300,51 @@ export default class TemplateManager {
297
300
  }
298
301
  }
299
302
 
303
+ /**
304
+ * Validates benchmark configuration parameters
305
+ * @private
306
+ * @throws {Error} If benchmark configuration is invalid
307
+ */
308
+ _validateBenchmarkConfig() {
309
+ if (!this.answers.includeBenchmark) return;
310
+
311
+ // Gate to supported architectures
312
+ const dc = this.answers.deploymentConfig;
313
+ const arch = dc ? dc.split('-')[0] : this.answers.architecture;
314
+ if (arch !== 'transformers' && arch !== 'diffusors') {
315
+ throw new Error('⚠️ Benchmarking is only supported with transformers and diffusors architectures.');
316
+ }
317
+
318
+ // Gate to supported deployment targets
319
+ if (this.answers.deploymentTarget === 'hyperpod-eks') {
320
+ throw new Error('⚠️ Benchmarking is only supported with managed-inference, async-inference, and batch-transform deployment targets');
321
+ }
322
+
323
+ // Validate numeric parameters
324
+ if (this.answers.benchmarkConcurrency !== undefined) {
325
+ if (!Number.isInteger(this.answers.benchmarkConcurrency) || this.answers.benchmarkConcurrency < 1) {
326
+ throw new Error('⚠️ benchmarkConcurrency must be an integer >= 1');
327
+ }
328
+ }
329
+ if (this.answers.benchmarkInputTokensMean !== undefined) {
330
+ if (!Number.isInteger(this.answers.benchmarkInputTokensMean) || this.answers.benchmarkInputTokensMean < 1) {
331
+ throw new Error('⚠️ benchmarkInputTokensMean must be an integer >= 1');
332
+ }
333
+ }
334
+ if (this.answers.benchmarkOutputTokensMean !== undefined) {
335
+ if (!Number.isInteger(this.answers.benchmarkOutputTokensMean) || this.answers.benchmarkOutputTokensMean < 1) {
336
+ throw new Error('⚠️ benchmarkOutputTokensMean must be an integer >= 1');
337
+ }
338
+ }
339
+
340
+ // Validate S3 path format
341
+ if (this.answers.benchmarkS3OutputPath && this.answers.benchmarkS3OutputPath.trim() !== '') {
342
+ if (!this.answers.benchmarkS3OutputPath.startsWith('s3://')) {
343
+ throw new Error('⚠️ benchmarkS3OutputPath must start with "s3://". Example: s3://my-bucket/benchmark-results/');
344
+ }
345
+ }
346
+ }
347
+
300
348
  /**
301
349
  * Validates GPU instance type requirement for GPU-requiring backends.
302
350
  * Called when deploymentConfig is present.