@aws/ml-container-creator 0.2.6 → 0.3.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 +14 -0
- package/infra/ci-harness/package-lock.json +22 -9
- package/package.json +1 -1
- package/servers/instance-sizer/index.js +9 -6
- package/servers/instance-sizer/lib/instance-ranker.js +35 -10
- package/servers/instance-sizer/lib/model-resolver.js +10 -6
- package/servers/lib/catalogs/model-servers.json +283 -5
- package/servers/lib/catalogs/models.json +30 -0
- package/servers/lib/schemas/image-catalog.schema.json +6 -0
- package/servers/model-picker/index.js +2 -1
- package/src/app.js +19 -0
- package/src/lib/architecture-sync.js +171 -0
- package/src/lib/arn-detection.js +22 -0
- package/src/lib/bootstrap-command-handler.js +82 -0
- package/src/lib/config-manager.js +43 -0
- package/src/lib/cross-cutting-checker.js +119 -0
- package/src/lib/deployment-entry-schema.js +1 -2
- package/src/lib/prompt-runner.js +427 -20
- package/src/lib/prompts.js +1 -1
- package/src/lib/registry-command-handler.js +236 -0
- package/src/lib/secret-classification.js +56 -0
- package/src/lib/secrets-command-handler.js +550 -0
- package/src/lib/validate-runner.js +49 -0
- package/src/lib/validation-report.js +8 -1
- package/src/prompt-adapter.js +3 -2
- package/templates/do/build +22 -0
- package/templates/do/config +15 -3
- package/templates/do/deploy +60 -5
- package/templates/do/logs +18 -3
- package/templates/do/run +10 -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
|
+
}
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
18
19
|
import PayloadBuilder from './payload-builder.js';
|
|
19
20
|
import SchemaValidationEngine from './schema-validation-engine.js';
|
|
20
21
|
import ServiceModelParser from './service-model-parser.js';
|
|
22
|
+
import CrossCuttingChecker from './cross-cutting-checker.js';
|
|
23
|
+
import HuggingFaceClient from './huggingface-client.js';
|
|
21
24
|
import { getRegistryPath, loadManifest } from './schema-sync.js';
|
|
22
25
|
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = path.dirname(__filename);
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* Parse a do/config shell file into a key-value object.
|
|
25
31
|
* Extracts lines matching: export KEY="value" or export KEY=value
|
|
@@ -122,6 +128,49 @@ export async function run(options = {}) {
|
|
|
122
128
|
});
|
|
123
129
|
|
|
124
130
|
const report = await engine.validate(context);
|
|
131
|
+
|
|
132
|
+
// Run model architecture compatibility check (Requirement 5.1-5.2)
|
|
133
|
+
if (config.MODEL_NAME) {
|
|
134
|
+
try {
|
|
135
|
+
const catalogPath = path.resolve(__dirname, '../../servers/lib/catalogs/model-servers.json');
|
|
136
|
+
if (existsSync(catalogPath)) {
|
|
137
|
+
const modelServersCatalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|
138
|
+
|
|
139
|
+
// Fetch model's config.json from HuggingFace to get model_type
|
|
140
|
+
const hfClient = new HuggingFaceClient({ timeout: 10000 });
|
|
141
|
+
const modelConfig = await hfClient.fetchModelConfig(config.MODEL_NAME);
|
|
142
|
+
const modelType = modelConfig?.model_type || null;
|
|
143
|
+
|
|
144
|
+
if (modelType) {
|
|
145
|
+
// Extract baseImageVersion from BASE_IMAGE (e.g., "vllm/vllm-openai:v0.10.1" → "v0.10.1")
|
|
146
|
+
const baseImage = config.BASE_IMAGE || '';
|
|
147
|
+
const baseImageVersion = baseImage.includes(':') ? baseImage.split(':').pop() : '';
|
|
148
|
+
// Strip leading 'v' to match catalog's framework_version format (e.g., "v0.10.1" → "0.10.1")
|
|
149
|
+
const frameworkVersion = baseImageVersion.replace(/^v/, '');
|
|
150
|
+
|
|
151
|
+
const modelServer = config.MODEL_SERVER || '';
|
|
152
|
+
|
|
153
|
+
// Build context fields for the architecture checker
|
|
154
|
+
const archContext = {
|
|
155
|
+
config: {
|
|
156
|
+
modelType,
|
|
157
|
+
modelServer,
|
|
158
|
+
baseImageVersion: frameworkVersion
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const checker = new CrossCuttingChecker();
|
|
163
|
+
const archFindings = checker.checkModelArchitectureCompatibility(archContext, modelServersCatalog);
|
|
164
|
+
for (const finding of archFindings) {
|
|
165
|
+
report.addFinding(finding);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Graceful degradation: if architecture check fails, continue without it
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
125
174
|
const summary = report.getSummary();
|
|
126
175
|
|
|
127
176
|
// Load manifest for version info
|
|
@@ -22,7 +22,14 @@ export default class ValidationReport {
|
|
|
22
22
|
const source = finding.source || '';
|
|
23
23
|
|
|
24
24
|
if (source === 'cross-cutting') {
|
|
25
|
-
|
|
25
|
+
// Cross-cutting findings with medium/low confidence are advisory, not errors
|
|
26
|
+
if (finding.confidence === 'medium' || finding.confidence === 'low') {
|
|
27
|
+
this.advisoryFindings.push(finding);
|
|
28
|
+
} else if (finding.severity === 'warning') {
|
|
29
|
+
this.warnings.push(finding);
|
|
30
|
+
} else {
|
|
31
|
+
this.crossCuttingErrors.push(finding);
|
|
32
|
+
}
|
|
26
33
|
} else if (source === 'smart-mode' || source.startsWith('smart:')) {
|
|
27
34
|
// Smart-mode findings are advisory UNLESS confidence is definitive AND severity is error
|
|
28
35
|
if (finding.confidence === 'definitive' && finding.severity === 'error') {
|
package/src/prompt-adapter.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { select, input, confirm, checkbox, number, Separator } from '@inquirer/prompts';
|
|
4
|
+
import { select, input, confirm, checkbox, number, password, Separator } from '@inquirer/prompts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Maps Yeoman prompt type names to @inquirer/prompts runner functions.
|
|
8
8
|
*/
|
|
9
|
-
const runners = { list: select, select, input, confirm, checkbox, number };
|
|
9
|
+
const runners = { list: select, select, input, confirm, checkbox, number, password };
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Runs a sequence of Yeoman-style prompt definitions using @inquirer/prompts.
|
|
@@ -55,6 +55,7 @@ export async function runPrompts(prompts, previousAnswers = {}, options = {}) {
|
|
|
55
55
|
if (mappedChoices !== undefined) config.choices = mappedChoices;
|
|
56
56
|
if (defaultVal !== undefined) config.default = defaultVal;
|
|
57
57
|
if (prompt.validate) config.validate = prompt.validate;
|
|
58
|
+
if (prompt.mask !== undefined) config.mask = prompt.mask;
|
|
58
59
|
|
|
59
60
|
answers[prompt.name] = await runner(config);
|
|
60
61
|
}
|
package/templates/do/build
CHANGED
|
@@ -27,6 +27,28 @@ fi
|
|
|
27
27
|
# fail on SageMaker with CannotStartContainerError.
|
|
28
28
|
PLATFORM_FLAG="--platform linux/amd64"
|
|
29
29
|
|
|
30
|
+
# --- Secrets Manager resolution (build-time) ---
|
|
31
|
+
if [ -n "${HF_TOKEN_ARN:-}" ]; then
|
|
32
|
+
echo "🔐 Resolving HuggingFace token from Secrets Manager..."
|
|
33
|
+
HF_TOKEN=$(aws secretsmanager get-secret-value --secret-id "${HF_TOKEN_ARN}" --query SecretString --output text) || {
|
|
34
|
+
echo "❌ Failed to resolve HuggingFace token from Secrets Manager"
|
|
35
|
+
exit 3
|
|
36
|
+
}
|
|
37
|
+
export HF_TOKEN
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ -n "${NGC_API_KEY_ARN:-}" ]; then
|
|
41
|
+
echo "🔐 Resolving NGC API key from Secrets Manager..."
|
|
42
|
+
NGC_API_KEY=$(aws secretsmanager get-secret-value --secret-id "${NGC_API_KEY_ARN}" --query SecretString --output text) || {
|
|
43
|
+
echo "❌ Failed to resolve NGC API key from Secrets Manager"
|
|
44
|
+
exit 3
|
|
45
|
+
}
|
|
46
|
+
export NGC_API_KEY
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# NOTE: Build-time secrets are passed as --build-arg. The secret value may persist
|
|
50
|
+
# in the image layer. A future improvement will use BuildKit --secret mounts.
|
|
51
|
+
|
|
30
52
|
# Framework-specific build logic
|
|
31
53
|
case "${DEPLOYMENT_CONFIG}" in
|
|
32
54
|
transformers-tensorrt-llm)
|