@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.
- package/config/parameter-schema-v2.json +2065 -0
- package/package.json +4 -4
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/models.json +182 -26
- package/src/app.js +1 -389
- package/src/lib/bootstrap-command-handler.js +75 -1078
- package/src/lib/bootstrap-profile-manager.js +634 -0
- package/src/lib/bootstrap-provisioners.js +421 -0
- package/src/lib/config-loader.js +405 -0
- package/src/lib/config-manager.js +59 -1685
- package/src/lib/config-mcp-client.js +118 -0
- package/src/lib/config-validator.js +634 -0
- package/src/lib/cuda-resolver.js +140 -0
- package/src/lib/e2e-catalog-validator.js +251 -3
- package/src/lib/e2e-ci-recorder.js +103 -0
- package/src/lib/generated/cli-options.js +8 -4
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +2 -2
- package/src/lib/marketplace-flow.js +276 -0
- package/src/lib/mcp-query-runner.js +768 -0
- package/src/lib/parameter-schema-validator.js +62 -18
- package/src/lib/prompt-runner.js +41 -1504
- package/src/lib/prompts/feature-prompts.js +172 -0
- package/src/lib/prompts/index.js +48 -0
- package/src/lib/prompts/infrastructure-prompts.js +690 -0
- package/src/lib/prompts/model-prompts.js +552 -0
- package/src/lib/prompts/project-prompts.js +70 -0
- package/src/lib/prompts.js +2 -1446
- package/src/lib/registry-command-handler.js +135 -3
- package/src/lib/secrets-prompt-runner.js +251 -0
- package/src/lib/template-variable-resolver.js +398 -0
- package/config/parameter-schema.json +0 -88
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Config Validator - Handles configuration validation.
|
|
6
|
+
* Uses delegation pattern: receives parent ConfigManager reference to access shared state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { resolve, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { ValidationError } from './config-manager.js';
|
|
13
|
+
import { validationRules } from './generated/validation-rules.js';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const tritonBackendsCatalogPath = resolve(__dirname, '../../servers/lib/catalogs/triton-backends.json');
|
|
18
|
+
|
|
19
|
+
function loadTritonBackendsFromCatalog() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(tritonBackendsCatalogPath, 'utf8');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`Failed to load triton backends catalog: ${error.message}`);
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tritonBackends = loadTritonBackendsFromCatalog();
|
|
30
|
+
|
|
31
|
+
export default class ConfigValidator {
|
|
32
|
+
constructor(manager) {
|
|
33
|
+
this.manager = manager;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates the current configuration against the parameter matrix
|
|
38
|
+
* @returns {Array} Array of validation errors
|
|
39
|
+
*/
|
|
40
|
+
validateConfiguration() {
|
|
41
|
+
const errors = [];
|
|
42
|
+
const m = this.manager;
|
|
43
|
+
|
|
44
|
+
const oldFormatMigration = {
|
|
45
|
+
'sklearn-flask': 'Use --deployment-config=http-flask --engine=sklearn instead',
|
|
46
|
+
'sklearn-fastapi': 'Use --deployment-config=http-fastapi --engine=sklearn instead',
|
|
47
|
+
'xgboost-flask': 'Use --deployment-config=http-flask --engine=xgboost instead',
|
|
48
|
+
'xgboost-fastapi': 'Use --deployment-config=http-fastapi --engine=xgboost instead',
|
|
49
|
+
'tensorflow-flask': 'Use --deployment-config=http-flask --engine=tensorflow instead',
|
|
50
|
+
'tensorflow-fastapi': 'Use --deployment-config=http-fastapi --engine=tensorflow instead'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (m.config.deploymentConfig) {
|
|
54
|
+
const migrationMsg = oldFormatMigration[m.config.deploymentConfig];
|
|
55
|
+
if (migrationMsg) {
|
|
56
|
+
errors.push(`Unsupported deployment-config: ${m.config.deploymentConfig}. This value has been replaced. ${migrationMsg}`);
|
|
57
|
+
} else if (!m.deploymentConfigResolver.isValid(m.config.deploymentConfig)) {
|
|
58
|
+
const valid = m.deploymentConfigResolver.getAllConfigs().join(', ');
|
|
59
|
+
errors.push(`Unsupported deployment-config: ${m.config.deploymentConfig}. Valid configs: ${valid}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (m.config.engine) {
|
|
64
|
+
const validEngines = ['sklearn', 'xgboost', 'tensorflow'];
|
|
65
|
+
if (!validEngines.includes(m.config.engine)) {
|
|
66
|
+
errors.push(`Unsupported engine: ${m.config.engine}. Supported: ${validEngines.join(', ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (m.config.modelFormat && m.config.deploymentConfig) {
|
|
71
|
+
try {
|
|
72
|
+
const parts = m.deploymentConfigResolver.decompose(m.config.deploymentConfig);
|
|
73
|
+
if (parts.architecture === 'http') {
|
|
74
|
+
const engine = m.config.engine || parts.engine;
|
|
75
|
+
if (engine) {
|
|
76
|
+
const supportedOptions = this._getSupportedOptions();
|
|
77
|
+
const validFormats = supportedOptions.modelFormats[engine] || [];
|
|
78
|
+
if (validFormats.length > 0 && !validFormats.includes(m.config.modelFormat)) {
|
|
79
|
+
errors.push(`Unsupported model format '${m.config.modelFormat}' for engine '${engine}'. Supported: ${validFormats.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// deploymentConfig already flagged as invalid above
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (m.config.hfToken && m.config.hfTokenArn) {
|
|
89
|
+
errors.push('Cannot specify both --hf-token and --hf-token-arn. Use one or the other.');
|
|
90
|
+
}
|
|
91
|
+
if (m.config.ngcTokenArn) {
|
|
92
|
+
const ngcTokenFromCli = m.options['ngc-token'];
|
|
93
|
+
if (ngcTokenFromCli) {
|
|
94
|
+
errors.push('Cannot specify both --ngc-token and --ngc-token-arn. Use one or the other.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (m.config.awsRoleArn) {
|
|
99
|
+
try {
|
|
100
|
+
this._isValidArn(m.config.awsRoleArn);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof ValidationError) {
|
|
103
|
+
errors.push(error.message);
|
|
104
|
+
} else {
|
|
105
|
+
errors.push(`Invalid AWS Role ARN format: ${m.config.awsRoleArn}. Expected format: arn:aws:iam::123456789012:role/RoleName`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const buildTarget = m.config.buildTarget || m.config.deployTarget;
|
|
111
|
+
if (buildTarget && !this._getSupportedOptions().buildTargets.includes(buildTarget)) {
|
|
112
|
+
errors.push(`Unsupported build target: ${buildTarget}. Supported targets: ${this._getSupportedOptions().buildTargets.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (m.config.codebuildComputeType && !this._getSupportedOptions().codebuildComputeTypes.includes(m.config.codebuildComputeType)) {
|
|
116
|
+
errors.push(`Unsupported CodeBuild compute type: ${m.config.codebuildComputeType}. Supported types: ${this._getSupportedOptions().codebuildComputeTypes.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (m.config.codebuildProjectName) {
|
|
120
|
+
const projectNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_]{1,254}$/;
|
|
121
|
+
if (!projectNamePattern.test(m.config.codebuildProjectName)) {
|
|
122
|
+
errors.push(`Invalid CodeBuild project name: ${m.config.codebuildProjectName}. Project names must be 2-255 characters, start with a letter or number, and contain only letters, numbers, hyphens, and underscores.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (m.config.modelPackageArn) {
|
|
127
|
+
const modelPackageArnPattern = /^arn:aws:sagemaker:[a-z0-9-]+:\d{12}:model-package\/[a-zA-Z0-9]([a-zA-Z0-9-])*\/\d+$/;
|
|
128
|
+
if (!modelPackageArnPattern.test(m.config.modelPackageArn)) {
|
|
129
|
+
errors.push('❌ Invalid model package ARN format. Expected: arn:aws:sagemaker:<region>:<account>:model-package/<name>/<version>');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (m.skipPrompts) {
|
|
134
|
+
Object.entries(m.parameterMatrix).forEach(([param, config]) => {
|
|
135
|
+
if (config.required &&
|
|
136
|
+
(m.config[param] === null || m.config[param] === undefined)) {
|
|
137
|
+
|
|
138
|
+
if (param === 'modelFormat') {
|
|
139
|
+
try {
|
|
140
|
+
const parts = m.deploymentConfigResolver.decompose(m.config.deploymentConfig);
|
|
141
|
+
if (parts.architecture === 'transformers' || parts.architecture === 'triton' || parts.architecture === 'diffusors') {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (config.promptable && config.default === null && !this._canAutoGenerate(param)) {
|
|
150
|
+
errors.push(`Required parameter '${param}' is missing and prompts are disabled`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (m.config.deploymentConfig) {
|
|
156
|
+
try {
|
|
157
|
+
const parts = m.deploymentConfigResolver.decompose(m.config.deploymentConfig);
|
|
158
|
+
if (parts.architecture === 'diffusors') {
|
|
159
|
+
const explicitModelName = m.explicitConfig && m.explicitConfig.modelName;
|
|
160
|
+
if (!explicitModelName) {
|
|
161
|
+
errors.push('Model name is required for diffusors architecture. Use --model-name to specify a HuggingFace diffusion model.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// deploymentConfig already flagged as invalid above
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Validate schema-validated parameters
|
|
171
|
+
Object.entries(m.parameterMatrix).forEach(([param, config]) => {
|
|
172
|
+
if (config.schemaValidated && m.config[param] !== null && m.config[param] !== undefined) {
|
|
173
|
+
const result = m.schemaValidator.validate(param, m.config[param], m.config.deploymentTarget);
|
|
174
|
+
if (!result.valid) {
|
|
175
|
+
errors.push(result.error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return errors;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validates required parameters before file generation
|
|
185
|
+
* @param {Object} finalConfig - The complete configuration object
|
|
186
|
+
* @returns {Array} Array of validation errors
|
|
187
|
+
*/
|
|
188
|
+
validateRequiredParameters(finalConfig) {
|
|
189
|
+
const errors = [];
|
|
190
|
+
const m = this.manager;
|
|
191
|
+
|
|
192
|
+
// Validate individual parameter values
|
|
193
|
+
Object.entries(finalConfig).forEach(([param, value]) => {
|
|
194
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
195
|
+
try {
|
|
196
|
+
this._validateParameterValue(param, value, finalConfig);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof ValidationError) {
|
|
199
|
+
errors.push(error.message);
|
|
200
|
+
} else {
|
|
201
|
+
errors.push(`Invalid value for parameter '${param}': ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Validate required parameters are present
|
|
208
|
+
Object.entries(m.parameterMatrix).forEach(([param, config]) => {
|
|
209
|
+
if (config.required) {
|
|
210
|
+
const value = finalConfig[param];
|
|
211
|
+
const isEmpty = value === null || value === undefined || value === '';
|
|
212
|
+
|
|
213
|
+
if (param === 'modelFormat' && (finalConfig.architecture === 'transformers' || finalConfig.architecture === 'triton' || finalConfig.architecture === 'diffusors' || finalConfig.architecture === 'marketplace')) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (finalConfig.architecture === 'marketplace' && (param === 'includeSampleModel' || param === 'buildTarget')) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (param === 'instanceType' && finalConfig.deploymentTarget === 'hyperpod-eks' && !finalConfig.instanceType) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (param === 'instanceType' && finalConfig.existingEndpointName) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (isEmpty) {
|
|
230
|
+
if (config.promptable) {
|
|
231
|
+
errors.push(`Required parameter '${param}' is missing. This parameter is required for ${finalConfig.architecture || 'the selected'} architecture.`);
|
|
232
|
+
} else {
|
|
233
|
+
errors.push(`Required non-promptable parameter '${param}' is missing. This parameter must be provided through CLI options, environment variables, or configuration files.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Validate parameter combinations
|
|
240
|
+
const combinationErrors = this._validateParameterCombinations(finalConfig);
|
|
241
|
+
errors.push(...combinationErrors);
|
|
242
|
+
|
|
243
|
+
// Architecture-specific required parameters
|
|
244
|
+
if (finalConfig.architecture === 'http') {
|
|
245
|
+
if (!finalConfig.engine) {
|
|
246
|
+
errors.push('Required parameter \'engine\' is missing. This parameter is required for http architecture.');
|
|
247
|
+
}
|
|
248
|
+
if (!finalConfig.modelFormat) {
|
|
249
|
+
errors.push('Required parameter \'modelFormat\' is missing. This parameter is required for http architecture.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return errors;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Validates parameter combinations and dependencies
|
|
258
|
+
* @param {Object} config - The configuration object to validate
|
|
259
|
+
* @returns {Array} Array of validation errors
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
_validateParameterCombinations(config) {
|
|
263
|
+
const errors = [];
|
|
264
|
+
|
|
265
|
+
if (config.architecture === 'transformers' && config.includeSampleModel === true) {
|
|
266
|
+
errors.push(`Architecture '${config.architecture}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
|
|
267
|
+
}
|
|
268
|
+
if (config.architecture === 'diffusors' && config.includeSampleModel === true) {
|
|
269
|
+
errors.push(`Architecture '${config.architecture}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
|
|
270
|
+
}
|
|
271
|
+
if (config.architecture === 'triton' && config.includeSampleModel === true) {
|
|
272
|
+
const backendMeta = tritonBackends[config.backend];
|
|
273
|
+
if (!backendMeta || !backendMeta.supportsSampleModel) {
|
|
274
|
+
errors.push(`Triton backend '${config.backend}' does not support sample models. The 'includeSampleModel' parameter will be automatically set to false.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return errors;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Validates a single parameter value
|
|
283
|
+
* @param {string} parameter - Parameter name
|
|
284
|
+
* @param {*} value - Parameter value
|
|
285
|
+
* @param {Object} context - Additional context
|
|
286
|
+
* @throws {ValidationError} If parameter value is invalid
|
|
287
|
+
* @private
|
|
288
|
+
*/
|
|
289
|
+
_validateParameterValue(parameter, value, context = {}) {
|
|
290
|
+
const m = this.manager;
|
|
291
|
+
|
|
292
|
+
// Schema-derived validation rules
|
|
293
|
+
const schemaRule = validationRules[parameter];
|
|
294
|
+
if (schemaRule && value !== null && value !== undefined) {
|
|
295
|
+
const skipSchemaValidation = ['framework', 'modelServer', 'deploymentConfig', 'deploymentTarget', 'codebuildComputeType'].includes(parameter);
|
|
296
|
+
if (!skipSchemaValidation) {
|
|
297
|
+
const error = schemaRule(value);
|
|
298
|
+
if (error) {
|
|
299
|
+
throw new ValidationError(error, parameter, value);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const supportedOptions = this._getSupportedOptions();
|
|
305
|
+
|
|
306
|
+
switch (parameter) {
|
|
307
|
+
case 'deploymentConfig':
|
|
308
|
+
if (value) {
|
|
309
|
+
const oldFormatMigration = {
|
|
310
|
+
'sklearn-flask': 'Use --deployment-config=http-flask --engine=sklearn instead',
|
|
311
|
+
'sklearn-fastapi': 'Use --deployment-config=http-fastapi --engine=sklearn instead',
|
|
312
|
+
'xgboost-flask': 'Use --deployment-config=http-flask --engine=xgboost instead',
|
|
313
|
+
'xgboost-fastapi': 'Use --deployment-config=http-fastapi --engine=xgboost instead',
|
|
314
|
+
'tensorflow-flask': 'Use --deployment-config=http-flask --engine=tensorflow instead',
|
|
315
|
+
'tensorflow-fastapi': 'Use --deployment-config=http-fastapi --engine=tensorflow instead'
|
|
316
|
+
};
|
|
317
|
+
const migrationMsg = oldFormatMigration[value];
|
|
318
|
+
if (migrationMsg) {
|
|
319
|
+
throw new ValidationError(
|
|
320
|
+
`Unsupported deployment-config: ${value}. This value has been replaced. ${migrationMsg}`,
|
|
321
|
+
parameter,
|
|
322
|
+
value
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (!m.deploymentConfigResolver.isValid(value)) {
|
|
326
|
+
const valid = m.deploymentConfigResolver.getAllConfigs().join(', ');
|
|
327
|
+
throw new ValidationError(
|
|
328
|
+
`Unsupported deployment-config: ${value}. Valid configs: ${valid}`,
|
|
329
|
+
parameter,
|
|
330
|
+
value
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
|
|
336
|
+
case 'engine':
|
|
337
|
+
if (value) {
|
|
338
|
+
const validEngines = ['sklearn', 'xgboost', 'tensorflow'];
|
|
339
|
+
if (!validEngines.includes(value)) {
|
|
340
|
+
throw new ValidationError(
|
|
341
|
+
`Unsupported engine: ${value}. Supported: ${validEngines.join(', ')}`,
|
|
342
|
+
parameter,
|
|
343
|
+
value
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
|
|
349
|
+
case 'modelFormat':
|
|
350
|
+
if (value && context.architecture === 'http' && context.engine) {
|
|
351
|
+
const validFormats = supportedOptions.modelFormats[context.engine] || [];
|
|
352
|
+
if (validFormats.length > 0 && !validFormats.includes(value)) {
|
|
353
|
+
throw new ValidationError(
|
|
354
|
+
`Model format '${value}' is not compatible with engine '${context.engine}'. Compatible formats: ${validFormats.join(', ')}`,
|
|
355
|
+
parameter,
|
|
356
|
+
value
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
|
|
362
|
+
case 'instanceType':
|
|
363
|
+
if (value) {
|
|
364
|
+
const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)$/;
|
|
365
|
+
if (!instancePattern.test(value)) {
|
|
366
|
+
throw new ValidationError(
|
|
367
|
+
`Invalid instance type format: ${value}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g4dn.xlarge)`,
|
|
368
|
+
parameter,
|
|
369
|
+
value
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (context.architecture === 'transformers' || context.architecture === 'triton') {
|
|
373
|
+
const cpuFamilies = ['t2', 't3', 't3a', 't4g', 'm4', 'm5', 'm5a', 'm5ad', 'm5d', 'm5dn', 'm5n', 'm5zn', 'm6a', 'm6g', 'm6gd', 'm6i', 'm6id', 'm6idn', 'm6in', 'c4', 'c5', 'c5a', 'c5ad', 'c5d', 'c5n', 'c6a', 'c6g', 'c6gd', 'c6gn', 'c6i', 'c6id', 'c6in', 'r4', 'r5', 'r5a', 'r5ad', 'r5b', 'r5d', 'r5dn', 'r5n', 'r6a', 'r6g', 'r6gd', 'r6i', 'r6id', 'r6idn', 'r6in'];
|
|
374
|
+
const instanceFamily = value.split('.')[1];
|
|
375
|
+
if (cpuFamilies.includes(instanceFamily)) {
|
|
376
|
+
console.warn(`⚠️ Warning: Using CPU instance ${value} with ${context.architecture} architecture. GPU instances are recommended for better performance.`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'awsRegion':
|
|
383
|
+
if (value && !supportedOptions.awsRegions.includes(value)) {
|
|
384
|
+
throw new ValidationError(
|
|
385
|
+
`Unsupported AWS region: ${value}. Supported regions: ${supportedOptions.awsRegions.join(', ')}`,
|
|
386
|
+
parameter,
|
|
387
|
+
value
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
|
|
392
|
+
case 'awsRoleArn':
|
|
393
|
+
if (value) {
|
|
394
|
+
this._isValidArn(value);
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'buildTarget':
|
|
399
|
+
case 'deployTarget':
|
|
400
|
+
if (value && !supportedOptions.buildTargets.includes(value)) {
|
|
401
|
+
throw new ValidationError(
|
|
402
|
+
`Unsupported build target: ${value}. Supported targets: ${supportedOptions.buildTargets.join(', ')}`,
|
|
403
|
+
parameter,
|
|
404
|
+
value
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
case 'codebuildComputeType':
|
|
410
|
+
if (value && !supportedOptions.codebuildComputeTypes.includes(value)) {
|
|
411
|
+
throw new ValidationError(
|
|
412
|
+
`Unsupported CodeBuild compute type: ${value}. Supported types: ${supportedOptions.codebuildComputeTypes.join(', ')}`,
|
|
413
|
+
parameter,
|
|
414
|
+
value
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case 'codebuildProjectName':
|
|
420
|
+
if (value) {
|
|
421
|
+
const projectNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9\-_]{1,254}$/;
|
|
422
|
+
if (!projectNamePattern.test(value)) {
|
|
423
|
+
throw new ValidationError(
|
|
424
|
+
`Invalid CodeBuild project name: ${value}. Project names must be 2-255 characters, start with a letter or number, and contain only letters, numbers, hyphens, and underscores.`,
|
|
425
|
+
parameter,
|
|
426
|
+
value
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case 'modelPackageArn':
|
|
433
|
+
if (value) {
|
|
434
|
+
const modelPackageArnPattern = /^arn:aws:sagemaker:[a-z0-9-]+:\d{12}:model-package\/[a-zA-Z0-9]([a-zA-Z0-9-])*\/\d+$/;
|
|
435
|
+
if (!modelPackageArnPattern.test(value)) {
|
|
436
|
+
throw new ValidationError(
|
|
437
|
+
'❌ Invalid model package ARN format. Expected: arn:aws:sagemaker:<region>:<account>:model-package/<name>/<version>',
|
|
438
|
+
parameter,
|
|
439
|
+
value
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Resolves HF_TOKEN references to actual token values
|
|
449
|
+
* @param {string} tokenValue - The token value or "$HF_TOKEN" reference
|
|
450
|
+
* @returns {string|null} Resolved token value
|
|
451
|
+
*/
|
|
452
|
+
_resolveHfToken(tokenValue) {
|
|
453
|
+
if (!tokenValue || tokenValue.trim() === '') {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (tokenValue.trim() === '$HF_TOKEN') {
|
|
458
|
+
const envToken = process.env.HF_TOKEN;
|
|
459
|
+
if (!envToken) {
|
|
460
|
+
console.warn('⚠️ Warning: $HF_TOKEN specified but HF_TOKEN environment variable is not set');
|
|
461
|
+
console.warn(' The container will be built without authentication.');
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return envToken;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return tokenValue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Validates AWS Role ARN format
|
|
472
|
+
* @param {string} arn - The ARN to validate
|
|
473
|
+
* @throws {ValidationError} If ARN format is invalid
|
|
474
|
+
* @private
|
|
475
|
+
*/
|
|
476
|
+
_isValidArn(arn) {
|
|
477
|
+
const arnPattern = /^arn:aws:iam::\d{12}:role\/[\w+=,.@-]+$/;
|
|
478
|
+
if (!arnPattern.test(arn)) {
|
|
479
|
+
throw new ValidationError(
|
|
480
|
+
`Invalid AWS Role ARN format: ${arn}. Expected format: arn:aws:iam::123456789012:role/RoleName`,
|
|
481
|
+
'awsRoleArn',
|
|
482
|
+
arn
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Gets supported options for validation
|
|
490
|
+
* @private
|
|
491
|
+
*/
|
|
492
|
+
_getSupportedOptions() {
|
|
493
|
+
return {
|
|
494
|
+
deploymentConfigs: this.manager.deploymentConfigResolver.getAllConfigs(),
|
|
495
|
+
engines: ['sklearn', 'xgboost', 'tensorflow'],
|
|
496
|
+
modelFormats: {
|
|
497
|
+
'sklearn': ['pkl', 'joblib'],
|
|
498
|
+
'xgboost': ['json', 'model', 'ubj'],
|
|
499
|
+
'tensorflow': ['keras', 'h5', 'SavedModel']
|
|
500
|
+
},
|
|
501
|
+
buildTargets: ['codebuild'],
|
|
502
|
+
codebuildComputeTypes: ['BUILD_GENERAL1_SMALL', 'BUILD_GENERAL1_MEDIUM', 'BUILD_GENERAL1_LARGE'],
|
|
503
|
+
awsRegions: [
|
|
504
|
+
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
|
505
|
+
'eu-west-1', 'eu-west-2', 'eu-central-1', 'eu-north-1',
|
|
506
|
+
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
|
|
507
|
+
'ca-central-1', 'sa-east-1'
|
|
508
|
+
]
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Fills auto-prompt defaults for parameters that have sensible defaults.
|
|
514
|
+
* @private
|
|
515
|
+
*/
|
|
516
|
+
_fillAutoPromptDefaults() {
|
|
517
|
+
const m = this.manager;
|
|
518
|
+
if (!m.explicitConfig) {
|
|
519
|
+
m.explicitConfig = {};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let architecture = m.config.architecture;
|
|
523
|
+
if (!architecture && m.config.deploymentConfig) {
|
|
524
|
+
try {
|
|
525
|
+
const parts = m.deploymentConfigResolver.decompose(m.config.deploymentConfig);
|
|
526
|
+
architecture = parts.architecture;
|
|
527
|
+
m.config.architecture = parts.architecture;
|
|
528
|
+
m.config.backend = parts.backend;
|
|
529
|
+
m.config.engine = parts.engine;
|
|
530
|
+
} catch {
|
|
531
|
+
// Invalid deploymentConfig — will be caught by validation
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
Object.entries(m.parameterMatrix).forEach(([param, config]) => {
|
|
536
|
+
if (m.explicitConfig[param] !== undefined && m.explicitConfig[param] !== null) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!config.required) {
|
|
541
|
+
if (m.config[param] !== undefined && m.config[param] !== null) {
|
|
542
|
+
m.explicitConfig[param] = m.config[param];
|
|
543
|
+
} else if (config.default !== null && config.default !== undefined) {
|
|
544
|
+
m.config[param] = config.default;
|
|
545
|
+
m.explicitConfig[param] = config.default;
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (m.config[param] === undefined || m.config[param] === null) {
|
|
551
|
+
if (param === 'instanceType') {
|
|
552
|
+
const arch = architecture || 'http';
|
|
553
|
+
m.config[param] = arch === 'http' ? 'ml.m5.large' : 'ml.g5.xlarge';
|
|
554
|
+
} else if (param === 'modelFormat') {
|
|
555
|
+
if (architecture === 'transformers' || architecture === 'triton' || architecture === 'diffusors') {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const engine = m.config.engine || 'sklearn';
|
|
559
|
+
const formatMap = { sklearn: 'pkl', xgboost: 'json', tensorflow: 'keras' };
|
|
560
|
+
m.config[param] = formatMap[engine] || 'pkl';
|
|
561
|
+
} else if (param === 'projectName') {
|
|
562
|
+
m.config[param] = m._generateProjectName(architecture);
|
|
563
|
+
} else {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (m.config[param] !== undefined && m.config[param] !== null) {
|
|
569
|
+
if (config.default !== null || this._canAutoGenerate(param)) {
|
|
570
|
+
m.explicitConfig[param] = m.config[param];
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Returns whether auto-prompt mode is active
|
|
578
|
+
* @returns {boolean}
|
|
579
|
+
*/
|
|
580
|
+
isAutoPrompt() {
|
|
581
|
+
return this.manager.autoPrompt;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Gets the list of required parameters that are truly missing.
|
|
586
|
+
* @returns {string[]} Array of parameter names that need prompting
|
|
587
|
+
*/
|
|
588
|
+
getMissingRequiredParameters() {
|
|
589
|
+
const m = this.manager;
|
|
590
|
+
const missing = [];
|
|
591
|
+
|
|
592
|
+
Object.entries(m.parameterMatrix).forEach(([param, config]) => {
|
|
593
|
+
if (!config.required || !config.promptable) return;
|
|
594
|
+
|
|
595
|
+
const value = m.config[param];
|
|
596
|
+
const hasValue = value !== undefined && value !== null;
|
|
597
|
+
|
|
598
|
+
if (hasValue) return;
|
|
599
|
+
|
|
600
|
+
if (param === 'modelFormat') {
|
|
601
|
+
const architecture = m.config.architecture;
|
|
602
|
+
if (architecture === 'transformers' || architecture === 'triton' || architecture === 'diffusors') {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (m.config.engine || m.config.deploymentConfig) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (this._canAutoGenerate(param)) return;
|
|
611
|
+
if (config.default !== null && config.default !== undefined) return;
|
|
612
|
+
|
|
613
|
+
missing.push(param);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return missing;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Checks if a parameter can be auto-generated when missing
|
|
621
|
+
* @param {string} param - Parameter name
|
|
622
|
+
* @returns {boolean}
|
|
623
|
+
* @private
|
|
624
|
+
*/
|
|
625
|
+
_canAutoGenerate(param) {
|
|
626
|
+
const autoGeneratable = [
|
|
627
|
+
'modelFormat',
|
|
628
|
+
'includeSampleModel',
|
|
629
|
+
'includeTesting',
|
|
630
|
+
'instanceType'
|
|
631
|
+
];
|
|
632
|
+
return autoGeneratable.includes(param);
|
|
633
|
+
}
|
|
634
|
+
}
|