@aws/ml-container-creator 0.2.1 → 0.2.3
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 +88 -86
- package/config/bootstrap-stack.json +211 -0
- package/config/parameter-schema.json +88 -0
- package/infra/ci-harness/bin/ci-harness.ts +26 -0
- package/infra/ci-harness/buildspec.yml +352 -0
- package/infra/ci-harness/cdk.json +27 -0
- package/infra/ci-harness/lambda/scanner/index.ts +199 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
- package/infra/ci-harness/package-lock.json +3979 -0
- package/infra/ci-harness/package.json +32 -0
- package/infra/ci-harness/tsconfig.json +38 -0
- package/package.json +13 -3
- package/src/app.js +318 -318
- package/src/copy-tpl.js +19 -19
- package/src/lib/asset-manager.js +74 -74
- package/src/lib/aws-profile-parser.js +45 -45
- package/src/lib/bootstrap-command-handler.js +560 -547
- package/src/lib/bootstrap-config.js +45 -45
- package/src/lib/ci-register-helpers.js +19 -19
- package/src/lib/ci-report-helpers.js +37 -37
- package/src/lib/ci-stage-helpers.js +49 -49
- package/src/lib/comment-generator.js +4 -4
- package/src/lib/config-manager.js +105 -105
- package/src/lib/deployment-config-resolver.js +10 -10
- package/src/lib/deployment-registry.js +153 -153
- package/src/lib/engine-prefix-resolver.js +8 -8
- package/src/lib/key-value-parser.js +6 -6
- package/src/lib/manifest-cli.js +108 -108
- package/src/lib/prompt-runner.js +224 -224
- package/src/lib/prompts.js +121 -121
- package/src/lib/registry-command-handler.js +174 -174
- package/src/lib/registry-loader.js +52 -52
- package/src/lib/sensitive-redactor.js +9 -9
- package/src/lib/template-engine.js +1 -1
- package/src/lib/template-manager.js +62 -62
- package/src/prompt-adapter.js +18 -18
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* and the generator's internal data shapes. No MCP runtime dependency.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readFileSync } from 'node:fs'
|
|
13
|
-
import { resolve, dirname } from 'node:path'
|
|
14
|
-
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { resolve, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
15
|
|
|
16
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
17
|
-
const __dirname = dirname(__filename)
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
18
|
|
|
19
19
|
// Catalog file paths relative to this module
|
|
20
20
|
const CATALOG_PATHS = {
|
|
@@ -22,12 +22,12 @@ const CATALOG_PATHS = {
|
|
|
22
22
|
tritonBackends: resolve(__dirname, '../../servers/base-image-picker/catalogs/triton-backends.json'),
|
|
23
23
|
instances: resolve(__dirname, '../../servers/instance-recommender/catalogs/instances.json'),
|
|
24
24
|
popularTransformers: resolve(__dirname, '../../servers/model-picker/catalogs/popular-transformers.json'),
|
|
25
|
-
popularDiffusors: resolve(__dirname, '../../servers/model-picker/catalogs/popular-diffusors.json')
|
|
26
|
-
}
|
|
25
|
+
popularDiffusors: resolve(__dirname, '../../servers/model-picker/catalogs/popular-diffusors.json')
|
|
26
|
+
};
|
|
27
27
|
|
|
28
28
|
class RegistryLoader {
|
|
29
29
|
constructor() {
|
|
30
|
-
this._catalogCache = {}
|
|
30
|
+
this._catalogCache = {};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -36,17 +36,17 @@ class RegistryLoader {
|
|
|
36
36
|
*/
|
|
37
37
|
_loadCatalog(catalogPath) {
|
|
38
38
|
if (this._catalogCache[catalogPath] !== undefined) {
|
|
39
|
-
return this._catalogCache[catalogPath]
|
|
39
|
+
return this._catalogCache[catalogPath];
|
|
40
40
|
}
|
|
41
41
|
try {
|
|
42
|
-
const raw = readFileSync(catalogPath, 'utf8')
|
|
43
|
-
const data = JSON.parse(raw)
|
|
44
|
-
this._catalogCache[catalogPath] = data
|
|
45
|
-
return data
|
|
42
|
+
const raw = readFileSync(catalogPath, 'utf8');
|
|
43
|
+
const data = JSON.parse(raw);
|
|
44
|
+
this._catalogCache[catalogPath] = data;
|
|
45
|
+
return data;
|
|
46
46
|
} catch (error) {
|
|
47
|
-
console.warn(`Failed to load catalog ${catalogPath}: ${error.message}`)
|
|
48
|
-
this._catalogCache[catalogPath] = null
|
|
49
|
-
return null
|
|
47
|
+
console.warn(`Failed to load catalog ${catalogPath}: ${error.message}`);
|
|
48
|
+
this._catalogCache[catalogPath] = null;
|
|
49
|
+
return null;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -65,17 +65,17 @@ class RegistryLoader {
|
|
|
65
65
|
*/
|
|
66
66
|
async loadFrameworkRegistry() {
|
|
67
67
|
try {
|
|
68
|
-
const catalog = this._loadCatalog(CATALOG_PATHS.modelServers)
|
|
69
|
-
if (!catalog) return {}
|
|
68
|
+
const catalog = this._loadCatalog(CATALOG_PATHS.modelServers);
|
|
69
|
+
if (!catalog) return {};
|
|
70
70
|
|
|
71
|
-
const registry = {}
|
|
71
|
+
const registry = {};
|
|
72
72
|
for (const [frameworkName, entries] of Object.entries(catalog)) {
|
|
73
|
-
if (!Array.isArray(entries)) continue
|
|
74
|
-
registry[frameworkName] = {}
|
|
73
|
+
if (!Array.isArray(entries)) continue;
|
|
74
|
+
registry[frameworkName] = {};
|
|
75
75
|
|
|
76
76
|
for (const entry of entries) {
|
|
77
|
-
const version = entry.labels?.framework_version
|
|
78
|
-
if (!version) continue
|
|
77
|
+
const version = entry.labels?.framework_version;
|
|
78
|
+
if (!version) continue;
|
|
79
79
|
|
|
80
80
|
registry[frameworkName][version] = {
|
|
81
81
|
baseImage: entry.image,
|
|
@@ -85,14 +85,14 @@ class RegistryLoader {
|
|
|
85
85
|
recommendedInstanceTypes: entry.defaults?.recommendedInstanceTypes || [],
|
|
86
86
|
validationLevel: entry.validationLevel || 'untested',
|
|
87
87
|
profiles: entry.profiles || {},
|
|
88
|
-
notes: entry.notes || ''
|
|
89
|
-
}
|
|
88
|
+
notes: entry.notes || ''
|
|
89
|
+
};
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
return registry
|
|
92
|
+
return registry;
|
|
93
93
|
} catch (error) {
|
|
94
|
-
console.warn(`Failed to load framework registry: ${error.message}`)
|
|
95
|
-
return {}
|
|
94
|
+
console.warn(`Failed to load framework registry: ${error.message}`);
|
|
95
|
+
return {};
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -116,27 +116,27 @@ class RegistryLoader {
|
|
|
116
116
|
*/
|
|
117
117
|
async loadModelRegistry() {
|
|
118
118
|
try {
|
|
119
|
-
const transformers = this._loadCatalog(CATALOG_PATHS.popularTransformers) || {}
|
|
120
|
-
const diffusors = this._loadCatalog(CATALOG_PATHS.popularDiffusors) || {}
|
|
119
|
+
const transformers = this._loadCatalog(CATALOG_PATHS.popularTransformers) || {};
|
|
120
|
+
const diffusors = this._loadCatalog(CATALOG_PATHS.popularDiffusors) || {};
|
|
121
121
|
|
|
122
|
-
const registry = {}
|
|
123
|
-
const allModels = { ...transformers, ...diffusors }
|
|
122
|
+
const registry = {};
|
|
123
|
+
const allModels = { ...transformers, ...diffusors };
|
|
124
124
|
|
|
125
125
|
for (const [modelId, entry] of Object.entries(allModels)) {
|
|
126
126
|
registry[modelId] = {
|
|
127
127
|
family: entry.family || '',
|
|
128
128
|
chatTemplate: entry.chat_template ?? null,
|
|
129
|
-
requiresTemplate: entry.chat_template
|
|
129
|
+
requiresTemplate: entry.chat_template !== null && entry.chat_template !== undefined && entry.chat_template !== '',
|
|
130
130
|
validationLevel: entry.validation_level || 'experimental',
|
|
131
131
|
frameworkCompatibility: entry.framework_compatibility || {},
|
|
132
132
|
profiles: entry.profiles || {},
|
|
133
|
-
notes: entry.notes || ''
|
|
134
|
-
}
|
|
133
|
+
notes: entry.notes || ''
|
|
134
|
+
};
|
|
135
135
|
}
|
|
136
|
-
return registry
|
|
136
|
+
return registry;
|
|
137
137
|
} catch (error) {
|
|
138
|
-
console.warn(`Failed to load model registry: ${error.message}`)
|
|
139
|
-
return {}
|
|
138
|
+
console.warn(`Failed to load model registry: ${error.message}`);
|
|
139
|
+
return {};
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -150,10 +150,10 @@ class RegistryLoader {
|
|
|
150
150
|
*/
|
|
151
151
|
async loadInstanceAcceleratorMapping() {
|
|
152
152
|
try {
|
|
153
|
-
const catalog = this._loadCatalog(CATALOG_PATHS.instances)
|
|
154
|
-
if (!catalog || !catalog.catalog) return {}
|
|
153
|
+
const catalog = this._loadCatalog(CATALOG_PATHS.instances);
|
|
154
|
+
if (!catalog || !catalog.catalog) return {};
|
|
155
155
|
|
|
156
|
-
const mapping = {}
|
|
156
|
+
const mapping = {};
|
|
157
157
|
for (const [instanceType, entry] of Object.entries(catalog.catalog)) {
|
|
158
158
|
mapping[instanceType] = {
|
|
159
159
|
family: entry.family || '',
|
|
@@ -162,17 +162,17 @@ class RegistryLoader {
|
|
|
162
162
|
hardware: entry.hardware || 'None',
|
|
163
163
|
architecture: entry.gpuArchitecture || 'None',
|
|
164
164
|
versions: entry.cudaVersions || null,
|
|
165
|
-
default: entry.defaultCudaVersion || null
|
|
165
|
+
default: entry.defaultCudaVersion || null
|
|
166
166
|
},
|
|
167
167
|
memory: entry.memGb ? `${entry.memGb} GB` : '0 GB',
|
|
168
168
|
vcpus: entry.vcpus || 0,
|
|
169
|
-
notes: entry.notes || ''
|
|
170
|
-
}
|
|
169
|
+
notes: entry.notes || ''
|
|
170
|
+
};
|
|
171
171
|
}
|
|
172
|
-
return mapping
|
|
172
|
+
return mapping;
|
|
173
173
|
} catch (error) {
|
|
174
|
-
console.warn(`Failed to load instance accelerator mapping: ${error.message}`)
|
|
175
|
-
return {}
|
|
174
|
+
console.warn(`Failed to load instance accelerator mapping: ${error.message}`);
|
|
175
|
+
return {};
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -186,13 +186,13 @@ class RegistryLoader {
|
|
|
186
186
|
*/
|
|
187
187
|
async loadTritonBackends() {
|
|
188
188
|
try {
|
|
189
|
-
const catalog = this._loadCatalog(CATALOG_PATHS.tritonBackends)
|
|
190
|
-
return catalog || {}
|
|
189
|
+
const catalog = this._loadCatalog(CATALOG_PATHS.tritonBackends);
|
|
190
|
+
return catalog || {};
|
|
191
191
|
} catch (error) {
|
|
192
|
-
console.warn(`Failed to load triton backends: ${error.message}`)
|
|
193
|
-
return {}
|
|
192
|
+
console.warn(`Failed to load triton backends: ${error.message}`);
|
|
193
|
+
return {};
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
export default RegistryLoader
|
|
198
|
+
export default RegistryLoader;
|
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Redaction marker used to replace sensitive values.
|
|
16
16
|
*/
|
|
17
|
-
export const REDACTION_MARKER = '***REDACTED***'
|
|
17
|
+
export const REDACTION_MARKER = '***REDACTED***';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Exact key names that are always considered sensitive.
|
|
21
21
|
*/
|
|
22
|
-
export const SENSITIVE_EXACT_KEYS = ['HF_TOKEN', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN']
|
|
22
|
+
export const SENSITIVE_EXACT_KEYS = ['HF_TOKEN', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'];
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Substrings that, when found in a key (case-insensitive), mark it as sensitive.
|
|
26
26
|
*/
|
|
27
|
-
export const SENSITIVE_SUBSTRINGS = ['SECRET', 'TOKEN']
|
|
27
|
+
export const SENSITIVE_SUBSTRINGS = ['SECRET', 'TOKEN'];
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Determine whether a given key matches sensitive patterns.
|
|
@@ -37,9 +37,9 @@ export const SENSITIVE_SUBSTRINGS = ['SECRET', 'TOKEN']
|
|
|
37
37
|
* @returns {boolean} True if the key is sensitive
|
|
38
38
|
*/
|
|
39
39
|
export function isSensitiveKey(key) {
|
|
40
|
-
if (SENSITIVE_EXACT_KEYS.includes(key)) return true
|
|
41
|
-
const upper = key.toUpperCase()
|
|
42
|
-
return SENSITIVE_SUBSTRINGS.some(sub => upper.includes(sub))
|
|
40
|
+
if (SENSITIVE_EXACT_KEYS.includes(key)) return true;
|
|
41
|
+
const upper = key.toUpperCase();
|
|
42
|
+
return SENSITIVE_SUBSTRINGS.some(sub => upper.includes(sub));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
@@ -51,9 +51,9 @@ export function isSensitiveKey(key) {
|
|
|
51
51
|
* @returns {Object<string, string>} New object with sensitive values redacted
|
|
52
52
|
*/
|
|
53
53
|
export function redactSensitiveValues(params) {
|
|
54
|
-
const result = {}
|
|
54
|
+
const result = {};
|
|
55
55
|
for (const [key, value] of Object.entries(params)) {
|
|
56
|
-
result[key] = isSensitiveKey(key) ? REDACTION_MARKER : value
|
|
56
|
+
result[key] = isSensitiveKey(key) ? REDACTION_MARKER : value;
|
|
57
57
|
}
|
|
58
|
-
return result
|
|
58
|
+
return result;
|
|
59
59
|
}
|
|
@@ -51,7 +51,7 @@ export default class TemplateEngine {
|
|
|
51
51
|
* @param {Object} config - Configuration profile
|
|
52
52
|
* @returns {void}
|
|
53
53
|
*/
|
|
54
|
-
generateDeploymentScript(
|
|
54
|
+
generateDeploymentScript(_config) {
|
|
55
55
|
// No-op: legacy deploy/ scripts have been removed.
|
|
56
56
|
// Deployment is handled by do/deploy in the do-framework.
|
|
57
57
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
/**
|
|
16
16
|
* GPU-requiring Triton backends that must use GPU instance types
|
|
17
17
|
*/
|
|
18
|
-
const GPU_REQUIRING_BACKENDS = ['triton-vllm', 'triton-tensorrtllm', 'diffusors-vllm-omni']
|
|
18
|
+
const GPU_REQUIRING_BACKENDS = ['triton-vllm', 'triton-tensorrtllm', 'diffusors-vllm-omni'];
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* CPU-only instance type families (patterns that indicate non-GPU instances)
|
|
@@ -24,8 +24,8 @@ const CPU_ONLY_INSTANCE_PATTERNS = [
|
|
|
24
24
|
/^ml\.m[0-9]+\./, // ml.m4.*, ml.m5.*, ml.m6i.*, etc.
|
|
25
25
|
/^ml\.c[0-9]+\./, // ml.c4.*, ml.c5.*, ml.c6i.*, etc.
|
|
26
26
|
/^ml\.t[0-9]+\./, // ml.t2.*, ml.t3.*, etc.
|
|
27
|
-
/^ml\.r[0-9]
|
|
28
|
-
]
|
|
27
|
+
/^ml\.r[0-9]+\./ // ml.r5.*, ml.r6i.*, etc.
|
|
28
|
+
];
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Check if an instance type is CPU-only (no GPU)
|
|
@@ -34,14 +34,14 @@ const CPU_ONLY_INSTANCE_PATTERNS = [
|
|
|
34
34
|
*/
|
|
35
35
|
function isCpuOnlyInstance(instanceType) {
|
|
36
36
|
if (!instanceType || instanceType === 'custom') {
|
|
37
|
-
return false
|
|
37
|
+
return false;
|
|
38
38
|
}
|
|
39
|
-
return CPU_ONLY_INSTANCE_PATTERNS.some(pattern => pattern.test(instanceType))
|
|
39
|
+
return CPU_ONLY_INSTANCE_PATTERNS.some(pattern => pattern.test(instanceType));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export default class TemplateManager {
|
|
43
43
|
constructor(answers) {
|
|
44
|
-
this.answers = answers
|
|
44
|
+
this.answers = answers;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -72,17 +72,17 @@ export default class TemplateManager {
|
|
|
72
72
|
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1',
|
|
73
73
|
'ca-central-1', 'sa-east-1'
|
|
74
74
|
]
|
|
75
|
-
}
|
|
75
|
+
};
|
|
76
76
|
|
|
77
77
|
// Validate deployment configuration if present
|
|
78
78
|
if (this.answers.deploymentConfig) {
|
|
79
|
-
this._validateChoice('deploymentConfig', supportedOptions.deploymentConfigs)
|
|
79
|
+
this._validateChoice('deploymentConfig', supportedOptions.deploymentConfigs);
|
|
80
80
|
|
|
81
81
|
// GPU instance type enforcement for GPU-requiring backends
|
|
82
|
-
this._validateGpuRequirement()
|
|
82
|
+
this._validateGpuRequirement();
|
|
83
83
|
} else {
|
|
84
84
|
// Fallback: validate architecture and backend separately (new canonical format)
|
|
85
|
-
const architectures = ['http', 'transformers', 'triton', 'diffusors']
|
|
85
|
+
const architectures = ['http', 'transformers', 'triton', 'diffusors'];
|
|
86
86
|
const backends = [
|
|
87
87
|
// http backends
|
|
88
88
|
'flask', 'fastapi',
|
|
@@ -92,63 +92,63 @@ export default class TemplateManager {
|
|
|
92
92
|
'fil', 'onnxruntime', 'tensorflow', 'pytorch', 'tensorrtllm', 'python',
|
|
93
93
|
// diffusors backends
|
|
94
94
|
'vllm-omni'
|
|
95
|
-
]
|
|
95
|
+
];
|
|
96
96
|
|
|
97
|
-
this._validateChoice('architecture', architectures)
|
|
98
|
-
this._validateChoice('backend', backends)
|
|
97
|
+
this._validateChoice('architecture', architectures);
|
|
98
|
+
this._validateChoice('backend', backends);
|
|
99
99
|
|
|
100
100
|
// Validate tensorrt-llm is only used with transformers architecture
|
|
101
101
|
if (this.answers.backend === 'tensorrt-llm' && this.answers.architecture !== 'transformers') {
|
|
102
|
-
throw new Error('⚠️ TensorRT-LLM is only supported with the transformers architecture. Please select "transformers" as your architecture or choose a different backend.')
|
|
102
|
+
throw new Error('⚠️ TensorRT-LLM is only supported with the transformers architecture. Please select "transformers" as your architecture or choose a different backend.');
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
// GPU instance type enforcement for GPU-requiring backends (fallback path)
|
|
106
106
|
const deploymentConfig = this.answers.architecture && this.answers.backend
|
|
107
107
|
? `${this.answers.architecture}-${this.answers.backend}`
|
|
108
|
-
: null
|
|
108
|
+
: null;
|
|
109
109
|
if (deploymentConfig && GPU_REQUIRING_BACKENDS.includes(deploymentConfig)) {
|
|
110
|
-
this._validateGpuRequirementForConfig(deploymentConfig)
|
|
110
|
+
this._validateGpuRequirementForConfig(deploymentConfig);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// Validate buildTarget (replaces deployTarget)
|
|
115
115
|
if (this.answers.buildTarget) {
|
|
116
|
-
this._validateChoice('buildTarget', supportedOptions.buildTargets)
|
|
116
|
+
this._validateChoice('buildTarget', supportedOptions.buildTargets);
|
|
117
117
|
} else if (this.answers.deployTarget) {
|
|
118
118
|
// Backward compatibility: validate deployTarget against buildTargets
|
|
119
|
-
this._validateChoice('deployTarget', supportedOptions.buildTargets)
|
|
119
|
+
this._validateChoice('deployTarget', supportedOptions.buildTargets);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Validate deploymentTarget
|
|
123
123
|
if (this.answers.deploymentTarget) {
|
|
124
|
-
this._validateChoice('deploymentTarget', supportedOptions.deploymentTargets)
|
|
124
|
+
this._validateChoice('deploymentTarget', supportedOptions.deploymentTargets);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// Validate HyperPod EKS specific fields
|
|
128
128
|
if (this.answers.deploymentTarget === 'hyperpod-eks') {
|
|
129
|
-
this._validateHyperPodConfig()
|
|
129
|
+
this._validateHyperPodConfig();
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Validate async inference specific fields
|
|
133
|
-
this._validateAsyncConfig()
|
|
133
|
+
this._validateAsyncConfig();
|
|
134
134
|
|
|
135
135
|
// Validate batch transform specific fields
|
|
136
|
-
this._validateBatchTransformConfig()
|
|
136
|
+
this._validateBatchTransformConfig();
|
|
137
137
|
|
|
138
138
|
// Validate instance type format (ml.*.*) - only for managed-inference
|
|
139
139
|
if (this.answers.instanceType && this.answers.instanceType !== 'custom') {
|
|
140
|
-
const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)
|
|
140
|
+
const instancePattern = /^ml\.[a-z0-9]+\.(nano|micro|small|medium|large|xlarge|[0-9]+xlarge)$/;
|
|
141
141
|
if (!instancePattern.test(this.answers.instanceType)) {
|
|
142
|
-
throw new Error(`⚠️ Invalid instance type format: ${this.answers.instanceType}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g5.xlarge)`)
|
|
142
|
+
throw new Error(`⚠️ Invalid instance type format: ${this.answers.instanceType}. Expected format: ml.{family}.{size} (e.g., ml.m5.large, ml.g5.xlarge)`);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
this._validateChoice('awsRegion', supportedOptions.awsRegions)
|
|
146
|
+
this._validateChoice('awsRegion', supportedOptions.awsRegions);
|
|
147
147
|
|
|
148
148
|
// Validate test types if testing is enabled
|
|
149
149
|
if (this.answers.includeTesting && this.answers.testTypes) {
|
|
150
150
|
for (const testType of this.answers.testTypes) {
|
|
151
|
-
this._validateChoice('testType', supportedOptions.testTypes, testType)
|
|
151
|
+
this._validateChoice('testType', supportedOptions.testTypes, testType);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -161,21 +161,21 @@ export default class TemplateManager {
|
|
|
161
161
|
_validateHyperPodConfig() {
|
|
162
162
|
// Validate hyperPodCluster is non-empty
|
|
163
163
|
if (!this.answers.hyperPodCluster || this.answers.hyperPodCluster.trim() === '') {
|
|
164
|
-
throw new Error('⚠️ hyperPodCluster is required when deploymentTarget is "hyperpod-eks". Please provide a valid HyperPod cluster name.')
|
|
164
|
+
throw new Error('⚠️ hyperPodCluster is required when deploymentTarget is "hyperpod-eks". Please provide a valid HyperPod cluster name.');
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// Validate hyperPodNamespace conforms to RFC 1123 DNS label format
|
|
168
168
|
if (this.answers.hyperPodNamespace) {
|
|
169
169
|
if (!this._isValidRfc1123DnsLabel(this.answers.hyperPodNamespace)) {
|
|
170
|
-
throw new Error(`⚠️ Invalid hyperPodNamespace: "${this.answers.hyperPodNamespace}". Namespace must conform to RFC 1123 DNS label format: lowercase alphanumeric characters or hyphens, must start and end with an alphanumeric character, and be at most 63 characters.`)
|
|
170
|
+
throw new Error(`⚠️ Invalid hyperPodNamespace: "${this.answers.hyperPodNamespace}". Namespace must conform to RFC 1123 DNS label format: lowercase alphanumeric characters or hyphens, must start and end with an alphanumeric character, and be at most 63 characters.`);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
// Validate hyperPodReplicas is an integer >= 1
|
|
175
175
|
if (this.answers.hyperPodReplicas !== undefined) {
|
|
176
|
-
const replicas = this.answers.hyperPodReplicas
|
|
176
|
+
const replicas = this.answers.hyperPodReplicas;
|
|
177
177
|
if (!Number.isInteger(replicas) || replicas < 1) {
|
|
178
|
-
throw new Error(`⚠️ Invalid hyperPodReplicas: "${replicas}". Replicas must be an integer greater than or equal to 1.`)
|
|
178
|
+
throw new Error(`⚠️ Invalid hyperPodReplicas: "${replicas}". Replicas must be an integer greater than or equal to 1.`);
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -188,11 +188,11 @@ export default class TemplateManager {
|
|
|
188
188
|
*/
|
|
189
189
|
_isValidRfc1123DnsLabel(value) {
|
|
190
190
|
if (!value || typeof value !== 'string') {
|
|
191
|
-
return false
|
|
191
|
+
return false;
|
|
192
192
|
}
|
|
193
193
|
// RFC 1123 DNS label: lowercase alphanumeric, hyphens allowed (not at start/end), max 63 chars
|
|
194
|
-
const rfc1123Pattern = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])
|
|
195
|
-
return value.length <= 63 && rfc1123Pattern.test(value)
|
|
194
|
+
const rfc1123Pattern = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
195
|
+
return value.length <= 63 && rfc1123Pattern.test(value);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/**
|
|
@@ -201,33 +201,33 @@ export default class TemplateManager {
|
|
|
201
201
|
* @throws {Error} If async configuration is invalid
|
|
202
202
|
*/
|
|
203
203
|
_validateAsyncConfig() {
|
|
204
|
-
if (this.answers.deploymentTarget !== 'async-inference') return
|
|
204
|
+
if (this.answers.deploymentTarget !== 'async-inference') return;
|
|
205
205
|
|
|
206
206
|
// Validate S3 output path format if explicitly provided
|
|
207
207
|
if (this.answers.asyncS3OutputPath && this.answers.asyncS3OutputPath.trim() !== '') {
|
|
208
208
|
if (!this.answers.asyncS3OutputPath.startsWith('s3://')) {
|
|
209
|
-
throw new Error('⚠️ asyncS3OutputPath must start with "s3://". Example: s3://my-bucket/output/')
|
|
209
|
+
throw new Error('⚠️ asyncS3OutputPath must start with "s3://". Example: s3://my-bucket/output/');
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// Validate SNS topic ARN format if explicitly provided
|
|
214
|
-
const snsArnPattern = /^arn:aws:sns:[a-z0-9-]+:\d{12}
|
|
214
|
+
const snsArnPattern = /^arn:aws:sns:[a-z0-9-]+:\d{12}:.+$/;
|
|
215
215
|
if (this.answers.asyncSnsSuccessTopic && this.answers.asyncSnsSuccessTopic.trim() !== '') {
|
|
216
216
|
if (!snsArnPattern.test(this.answers.asyncSnsSuccessTopic)) {
|
|
217
|
-
throw new Error('⚠️ asyncSnsSuccessTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>')
|
|
217
|
+
throw new Error('⚠️ asyncSnsSuccessTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>');
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
if (this.answers.asyncSnsErrorTopic && this.answers.asyncSnsErrorTopic.trim() !== '') {
|
|
221
221
|
if (!snsArnPattern.test(this.answers.asyncSnsErrorTopic)) {
|
|
222
|
-
throw new Error('⚠️ asyncSnsErrorTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>')
|
|
222
|
+
throw new Error('⚠️ asyncSnsErrorTopic must be a valid SNS ARN. Format: arn:aws:sns:<region>:<account-id>:<topic-name>');
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Validate max concurrent invocations
|
|
227
227
|
if (this.answers.asyncMaxConcurrentInvocations !== undefined) {
|
|
228
|
-
const val = this.answers.asyncMaxConcurrentInvocations
|
|
228
|
+
const val = this.answers.asyncMaxConcurrentInvocations;
|
|
229
229
|
if (!Number.isInteger(val) || val < 1) {
|
|
230
|
-
throw new Error('⚠️ asyncMaxConcurrentInvocations must be an integer >= 1')
|
|
230
|
+
throw new Error('⚠️ asyncMaxConcurrentInvocations must be an integer >= 1');
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
@@ -238,61 +238,61 @@ export default class TemplateManager {
|
|
|
238
238
|
* @throws {Error} If batch transform configuration is invalid
|
|
239
239
|
*/
|
|
240
240
|
_validateBatchTransformConfig() {
|
|
241
|
-
if (this.answers.deploymentTarget !== 'batch-transform') return
|
|
241
|
+
if (this.answers.deploymentTarget !== 'batch-transform') return;
|
|
242
242
|
|
|
243
243
|
// Validate S3 input path format if provided
|
|
244
244
|
if (this.answers.batchInputPath && this.answers.batchInputPath.trim() !== '') {
|
|
245
245
|
if (!this.answers.batchInputPath.startsWith('s3://')) {
|
|
246
|
-
throw new Error('⚠️ batchInputPath must start with "s3://". Example: s3://my-bucket/input/')
|
|
246
|
+
throw new Error('⚠️ batchInputPath must start with "s3://". Example: s3://my-bucket/input/');
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Validate S3 output path format if provided
|
|
251
251
|
if (this.answers.batchOutputPath && this.answers.batchOutputPath.trim() !== '') {
|
|
252
252
|
if (!this.answers.batchOutputPath.startsWith('s3://')) {
|
|
253
|
-
throw new Error('⚠️ batchOutputPath must start with "s3://". Example: s3://my-bucket/output/')
|
|
253
|
+
throw new Error('⚠️ batchOutputPath must start with "s3://". Example: s3://my-bucket/output/');
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// Validate instance count
|
|
258
258
|
if (this.answers.batchInstanceCount !== undefined) {
|
|
259
|
-
const val = this.answers.batchInstanceCount
|
|
259
|
+
const val = this.answers.batchInstanceCount;
|
|
260
260
|
if (!Number.isInteger(val) || val < 1) {
|
|
261
|
-
throw new Error('⚠️ batchInstanceCount must be an integer >= 1')
|
|
261
|
+
throw new Error('⚠️ batchInstanceCount must be an integer >= 1');
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
// Validate split type
|
|
266
|
-
const validSplitTypes = ['Line', 'RecordIO', 'None']
|
|
266
|
+
const validSplitTypes = ['Line', 'RecordIO', 'None'];
|
|
267
267
|
if (this.answers.batchSplitType && !validSplitTypes.includes(this.answers.batchSplitType)) {
|
|
268
|
-
throw new Error(`⚠️ batchSplitType must be one of: ${validSplitTypes.join(', ')}`)
|
|
268
|
+
throw new Error(`⚠️ batchSplitType must be one of: ${validSplitTypes.join(', ')}`);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
// Validate batch strategy
|
|
272
|
-
const validStrategies = ['MultiRecord', 'SingleRecord']
|
|
272
|
+
const validStrategies = ['MultiRecord', 'SingleRecord'];
|
|
273
273
|
if (this.answers.batchStrategy && !validStrategies.includes(this.answers.batchStrategy)) {
|
|
274
|
-
throw new Error(`⚠️ batchStrategy must be one of: ${validStrategies.join(', ')}`)
|
|
274
|
+
throw new Error(`⚠️ batchStrategy must be one of: ${validStrategies.join(', ')}`);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Validate join source
|
|
278
|
-
const validJoinSources = ['Input', 'None']
|
|
278
|
+
const validJoinSources = ['Input', 'None'];
|
|
279
279
|
if (this.answers.batchJoinSource && !validJoinSources.includes(this.answers.batchJoinSource)) {
|
|
280
|
-
throw new Error(`⚠️ batchJoinSource must be one of: ${validJoinSources.join(', ')}`)
|
|
280
|
+
throw new Error(`⚠️ batchJoinSource must be one of: ${validJoinSources.join(', ')}`);
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// Validate max concurrent transforms
|
|
284
284
|
if (this.answers.batchMaxConcurrentTransforms !== undefined) {
|
|
285
|
-
const val = this.answers.batchMaxConcurrentTransforms
|
|
285
|
+
const val = this.answers.batchMaxConcurrentTransforms;
|
|
286
286
|
if (!Number.isInteger(val) || val < 0) {
|
|
287
|
-
throw new Error('⚠️ batchMaxConcurrentTransforms must be an integer >= 0')
|
|
287
|
+
throw new Error('⚠️ batchMaxConcurrentTransforms must be an integer >= 0');
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// Validate max payload in MB
|
|
292
292
|
if (this.answers.batchMaxPayloadInMB !== undefined) {
|
|
293
|
-
const val = this.answers.batchMaxPayloadInMB
|
|
293
|
+
const val = this.answers.batchMaxPayloadInMB;
|
|
294
294
|
if (!Number.isInteger(val) || val < 0 || val > 100) {
|
|
295
|
-
throw new Error('⚠️ batchMaxPayloadInMB must be an integer between 0 and 100')
|
|
295
|
+
throw new Error('⚠️ batchMaxPayloadInMB must be an integer between 0 and 100');
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
}
|
|
@@ -304,9 +304,9 @@ export default class TemplateManager {
|
|
|
304
304
|
* @throws {Error} If a GPU-requiring backend is paired with a CPU-only instance
|
|
305
305
|
*/
|
|
306
306
|
_validateGpuRequirement() {
|
|
307
|
-
const dc = this.answers.deploymentConfig
|
|
307
|
+
const dc = this.answers.deploymentConfig;
|
|
308
308
|
if (GPU_REQUIRING_BACKENDS.includes(dc)) {
|
|
309
|
-
this._validateGpuRequirementForConfig(dc)
|
|
309
|
+
this._validateGpuRequirementForConfig(dc);
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -317,13 +317,13 @@ export default class TemplateManager {
|
|
|
317
317
|
* @throws {Error} If instance type is CPU-only
|
|
318
318
|
*/
|
|
319
319
|
_validateGpuRequirementForConfig(deploymentConfig) {
|
|
320
|
-
const instanceType = this.answers.instanceType
|
|
320
|
+
const instanceType = this.answers.instanceType;
|
|
321
321
|
if (isCpuOnlyInstance(instanceType)) {
|
|
322
322
|
throw new Error(
|
|
323
323
|
`⚠️ ${deploymentConfig} requires a GPU instance type. ` +
|
|
324
324
|
`Selected: ${instanceType}. ` +
|
|
325
|
-
|
|
326
|
-
)
|
|
325
|
+
'Recommended: ml.g5.xlarge, ml.g5.2xlarge'
|
|
326
|
+
);
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
|
|
@@ -332,9 +332,9 @@ export default class TemplateManager {
|
|
|
332
332
|
* @private
|
|
333
333
|
*/
|
|
334
334
|
_validateChoice(field, supportedValues, value = null) {
|
|
335
|
-
const actualValue = value || this.answers[field]
|
|
335
|
+
const actualValue = value || this.answers[field];
|
|
336
336
|
if (actualValue && !supportedValues.includes(actualValue)) {
|
|
337
|
-
throw new Error(`⚠️ ${actualValue} not implemented yet for ${field}.`)
|
|
337
|
+
throw new Error(`⚠️ ${actualValue} not implemented yet for ${field}.`);
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
}
|