@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.
- package/bin/cli.js +38 -2
- package/config/bootstrap-stack.json +94 -1
- package/config/defaults.json +1 -1
- package/infra/ci-harness/package-lock.json +22 -9
- package/package.json +3 -1
- package/servers/instance-sizer/index.js +45 -8
- package/servers/instance-sizer/lib/instance-ranker.js +140 -11
- package/servers/instance-sizer/lib/model-resolver.js +10 -6
- package/servers/instance-sizer/lib/quota-resolver.js +368 -0
- package/servers/instance-sizer/package.json +2 -0
- package/servers/lib/catalogs/instances.json +527 -12
- package/servers/lib/catalogs/model-servers.json +298 -20
- package/servers/lib/catalogs/model-sizes.json +27 -0
- package/servers/lib/catalogs/models.json +101 -0
- package/servers/lib/schemas/image-catalog.schema.json +15 -1
- package/servers/model-picker/index.js +2 -1
- package/src/app.js +96 -2
- package/src/lib/architecture-sync.js +171 -0
- package/src/lib/arn-detection.js +22 -0
- package/src/lib/bootstrap-command-handler.js +178 -3
- package/src/lib/cli-handler.js +2 -2
- package/src/lib/config-manager.js +121 -1
- package/src/lib/cross-cutting-checker.js +119 -0
- package/src/lib/deployment-entry-schema.js +1 -2
- package/src/lib/prompt-runner.js +514 -20
- package/src/lib/prompts.js +67 -5
- package/src/lib/registry-command-handler.js +236 -0
- package/src/lib/schema-sync.js +31 -0
- package/src/lib/secret-classification.js +56 -0
- package/src/lib/secrets-command-handler.js +550 -0
- package/src/lib/template-manager.js +49 -1
- package/src/lib/validate-runner.js +174 -2
- package/src/lib/validation-report.js +8 -1
- package/src/prompt-adapter.js +3 -2
- package/templates/Dockerfile +10 -2
- package/templates/code/cuda_compat.sh +22 -0
- package/templates/code/serve +3 -0
- package/templates/code/start_server.sh +3 -0
- package/templates/diffusors/Dockerfile +2 -1
- package/templates/diffusors/serve +3 -0
- package/templates/do/README.md +33 -0
- package/templates/do/benchmark +646 -0
- package/templates/do/build +22 -0
- package/templates/do/clean +86 -0
- package/templates/do/config +41 -6
- package/templates/do/deploy +66 -6
- package/templates/do/logs +18 -3
- package/templates/do/register +8 -1
- package/templates/do/run +10 -0
- 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.
|