@aws/ml-container-creator 0.9.0 ā 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/bin/cli.js +31 -137
- package/config/parameter-schema-v2.json +2065 -0
- package/package.json +6 -3
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/models.json +182 -26
- package/src/app.js +6 -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 -1668
- 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 +471 -0
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +202 -0
- 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/templates/code/serve +5 -134
- package/templates/code/serve.d/lmi.ejs +19 -0
- package/templates/code/serve.d/sglang.ejs +47 -0
- package/templates/code/serve.d/tensorrt-llm.ejs +53 -0
- package/templates/code/serve.d/vllm.ejs +48 -0
- package/templates/do/clean +1 -1387
- package/templates/do/clean.d/async-inference.ejs +508 -0
- package/templates/do/clean.d/batch-transform.ejs +512 -0
- package/templates/do/clean.d/hyperpod-eks.ejs +481 -0
- package/templates/do/clean.d/managed-inference.ejs +1043 -0
- package/templates/do/deploy +1 -1766
- package/templates/do/deploy.d/async-inference.ejs +501 -0
- package/templates/do/deploy.d/batch-transform.ejs +529 -0
- package/templates/do/deploy.d/hyperpod-eks.ejs +339 -0
- package/templates/do/deploy.d/managed-inference.ejs +726 -0
- package/config/parameter-schema.json +0 -88
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CUDA Resolver - Handles CUDA version selection and AMI resolution.
|
|
6
|
+
* Uses delegation pattern: receives parent PromptRunner reference to access shared state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const CUDA_AMI_MAP = {
|
|
10
|
+
'11.0': 'al2-ami-sagemaker-inference-gpu-2',
|
|
11
|
+
'11.4': 'al2-ami-sagemaker-inference-gpu-2-1',
|
|
12
|
+
'11.8': 'al2-ami-sagemaker-inference-gpu-2-1',
|
|
13
|
+
'12.1': 'al2-ami-sagemaker-inference-gpu-3-1',
|
|
14
|
+
'12.2': 'al2-ami-sagemaker-inference-gpu-3-1',
|
|
15
|
+
'12.4': 'al2-ami-sagemaker-inference-gpu-3-1',
|
|
16
|
+
'12.6': 'al2-ami-sagemaker-inference-gpu-3-1',
|
|
17
|
+
'13.0': 'al2023-ami-sagemaker-inference-gpu-4-1'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default class CudaResolver {
|
|
21
|
+
constructor(runner) {
|
|
22
|
+
this.runner = runner;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prompt the user to select a CUDA version when the selected GPU instance
|
|
27
|
+
* supports multiple versions.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} instanceType - Selected instance type (e.g. "ml.g5.2xlarge")
|
|
30
|
+
* @param {string} framework - Selected framework name
|
|
31
|
+
* @param {string} frameworkVersion - Selected framework version
|
|
32
|
+
* @param {string} [baseImageCuda] - CUDA version from selected base image (for auto-resolution)
|
|
33
|
+
* @returns {Promise<{cudaVersion: string, inferenceAmiVersion: string}|null>}
|
|
34
|
+
*/
|
|
35
|
+
async _promptCudaVersion(instanceType, framework, frameworkVersion, baseImageCuda) {
|
|
36
|
+
if (!instanceType) return null;
|
|
37
|
+
|
|
38
|
+
const instanceInfo = this.runner._instanceAcceleratorMapping[instanceType];
|
|
39
|
+
if (!instanceInfo || instanceInfo.accelerator.type !== 'cuda') return null;
|
|
40
|
+
|
|
41
|
+
const instanceCudaVersions = instanceInfo.accelerator.versions;
|
|
42
|
+
if (!instanceCudaVersions || instanceCudaVersions.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
// Auto-resolution: when base image specifies a CUDA version, intersect with instance support
|
|
45
|
+
if (baseImageCuda) {
|
|
46
|
+
const majorRequired = baseImageCuda.split('.')[0];
|
|
47
|
+
const intersection = instanceCudaVersions.filter(v => {
|
|
48
|
+
if (v === baseImageCuda) return true;
|
|
49
|
+
if (v.startsWith(`${majorRequired}.`)) return true;
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (intersection.length > 0) {
|
|
54
|
+
const exactMatch = intersection.find(v => v === baseImageCuda);
|
|
55
|
+
const selectedVersion = exactMatch || intersection.sort().pop();
|
|
56
|
+
const inferenceAmiVersion = CUDA_AMI_MAP[selectedVersion];
|
|
57
|
+
if (inferenceAmiVersion) {
|
|
58
|
+
console.log(`\nš§ CUDA ${selectedVersion} auto-resolved from base image (requires ${baseImageCuda})`);
|
|
59
|
+
console.log(` AMI: ${inferenceAmiVersion}`);
|
|
60
|
+
return { cudaVersion: selectedVersion, inferenceAmiVersion };
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`\n ā ļø Base image requires CUDA ${baseImageCuda} but instance ${instanceType} supports: ${instanceCudaVersions.join(', ')}`);
|
|
64
|
+
console.log(' No compatible CUDA version found. Falling back to manual selection.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get framework CUDA requirements (if available)
|
|
69
|
+
const registryConfigManager = this.runner.registryConfigManager;
|
|
70
|
+
const frameworkConfig = registryConfigManager?.frameworkRegistry?.[framework]?.[frameworkVersion];
|
|
71
|
+
const frameworkAccel = frameworkConfig?.accelerator;
|
|
72
|
+
|
|
73
|
+
// Compute compatible CUDA versions
|
|
74
|
+
let compatibleVersions;
|
|
75
|
+
if (frameworkAccel?.versionRange) {
|
|
76
|
+
const { min, max } = frameworkAccel.versionRange;
|
|
77
|
+
compatibleVersions = instanceCudaVersions.filter(v => {
|
|
78
|
+
return v >= min && v <= max;
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
compatibleVersions = [...instanceCudaVersions];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (compatibleVersions.length === 0) {
|
|
85
|
+
compatibleVersions = [...instanceCudaVersions];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If only one option, auto-select it silently
|
|
89
|
+
if (compatibleVersions.length === 1) {
|
|
90
|
+
const cudaVersion = compatibleVersions[0];
|
|
91
|
+
const inferenceAmiVersion = CUDA_AMI_MAP[cudaVersion];
|
|
92
|
+
if (inferenceAmiVersion) {
|
|
93
|
+
console.log(`\nš§ CUDA ${cudaVersion} auto-selected (only compatible version for ${instanceType})`);
|
|
94
|
+
console.log(` AMI: ${inferenceAmiVersion}`);
|
|
95
|
+
}
|
|
96
|
+
return inferenceAmiVersion ? { cudaVersion, inferenceAmiVersion } : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Multiple options ā let the user choose (or auto-select in auto-prompt mode)
|
|
100
|
+
const defaultVersion = frameworkAccel?.version
|
|
101
|
+
&& compatibleVersions.includes(frameworkAccel.version)
|
|
102
|
+
? frameworkAccel.version
|
|
103
|
+
: instanceInfo.accelerator.default || compatibleVersions[compatibleVersions.length - 1];
|
|
104
|
+
|
|
105
|
+
// In auto-prompt mode, auto-select the default without prompting
|
|
106
|
+
if (this.runner.configManager?.isAutoPrompt()) {
|
|
107
|
+
const inferenceAmiVersion = CUDA_AMI_MAP[defaultVersion];
|
|
108
|
+
if (inferenceAmiVersion) {
|
|
109
|
+
console.log(`\nš§ CUDA ${defaultVersion} auto-selected (auto-prompt mode)`);
|
|
110
|
+
console.log(` AMI: ${inferenceAmiVersion}`);
|
|
111
|
+
}
|
|
112
|
+
return inferenceAmiVersion ? { cudaVersion: defaultVersion, inferenceAmiVersion } : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const choices = compatibleVersions.map(v => {
|
|
116
|
+
const ami = CUDA_AMI_MAP[v] || 'unknown';
|
|
117
|
+
const isDefault = v === defaultVersion ? ' (recommended)' : '';
|
|
118
|
+
return {
|
|
119
|
+
name: `CUDA ${v}${isDefault} ā AMI: ${ami}`,
|
|
120
|
+
value: v,
|
|
121
|
+
short: `CUDA ${v}`
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const { cudaVersion } = await this.runner._runPrompts([{
|
|
126
|
+
type: 'list',
|
|
127
|
+
name: 'cudaVersion',
|
|
128
|
+
message: `Select CUDA version for ${instanceType} (${instanceInfo.accelerator.hardware}):`,
|
|
129
|
+
choices,
|
|
130
|
+
default: defaultVersion
|
|
131
|
+
}]);
|
|
132
|
+
|
|
133
|
+
const inferenceAmiVersion = CUDA_AMI_MAP[cudaVersion];
|
|
134
|
+
if (inferenceAmiVersion) {
|
|
135
|
+
console.log(` ā
CUDA ${cudaVersion} ā AMI: ${inferenceAmiVersion}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return inferenceAmiVersion ? { cudaVersion, inferenceAmiVersion } : null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import Ajv from 'ajv';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
15
17
|
|
|
16
18
|
const catalogSchema = {
|
|
17
19
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
@@ -34,20 +36,214 @@ const catalogSchema = {
|
|
|
34
36
|
items: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
|
|
35
37
|
minItems: 1
|
|
36
38
|
},
|
|
37
|
-
timeout: { type: 'integer', minimum: 60 }
|
|
39
|
+
timeout: { type: 'integer', minimum: 60 },
|
|
40
|
+
tuneTimeout: { type: 'integer', minimum: 60 },
|
|
41
|
+
tuneConfig: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
required: ['tuneId', 'technique', 'trainingType', 'dataset'],
|
|
44
|
+
properties: {
|
|
45
|
+
tuneId: { type: 'string' },
|
|
46
|
+
technique: { type: 'string', enum: ['sft', 'dpo', 'rlaif', 'rlvr'] },
|
|
47
|
+
trainingType: { type: 'string', enum: ['lora', 'full-rank'] },
|
|
48
|
+
dataset: { type: 'string' }
|
|
49
|
+
},
|
|
50
|
+
additionalProperties: false
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
};
|
|
43
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Custom validation rules for tune lifecycle entries.
|
|
60
|
+
*
|
|
61
|
+
* Enforces:
|
|
62
|
+
* - Entries with tune lifecycle steps (steps starting with "tune-") must have a `tuneConfig` object
|
|
63
|
+
* - Entries with tune lifecycle steps must include `--enable-lora` in their `args` field
|
|
64
|
+
* - If `tuneTimeout` is present, it must be a positive integer >= 60
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} catalog - The catalog object with a `configs` array
|
|
67
|
+
* @param {Array<{ path: string, message: string }>} errors - Errors array to append to
|
|
68
|
+
*/
|
|
69
|
+
export function validateTuneConstraints(catalog, errors) {
|
|
70
|
+
if (!catalog || !Array.isArray(catalog.configs)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < catalog.configs.length; i++) {
|
|
75
|
+
const entry = catalog.configs[i];
|
|
76
|
+
if (!entry || !Array.isArray(entry.lifecycle)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hasTuneSteps = entry.lifecycle.some((s) => typeof s === 'string' && s.startsWith('tune-'));
|
|
81
|
+
|
|
82
|
+
if (hasTuneSteps) {
|
|
83
|
+
// Must have tuneConfig
|
|
84
|
+
if (!entry.tuneConfig) {
|
|
85
|
+
errors.push({
|
|
86
|
+
path: `/configs/${i}`,
|
|
87
|
+
message: `entry "${entry.id}" has tune lifecycle steps but no tuneConfig`
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Must have --enable-lora in args
|
|
93
|
+
if (!entry.args || !entry.args.includes('--enable-lora')) {
|
|
94
|
+
errors.push({
|
|
95
|
+
path: `/configs/${i}/args`,
|
|
96
|
+
message: `entry "${entry.id}" has tune steps but args missing --enable-lora`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// tuneTimeout validation (if present)
|
|
102
|
+
if (entry.tuneTimeout !== undefined) {
|
|
103
|
+
if (typeof entry.tuneTimeout !== 'number' || !Number.isInteger(entry.tuneTimeout) || entry.tuneTimeout < 60) {
|
|
104
|
+
errors.push({
|
|
105
|
+
path: `/configs/${i}/tuneTimeout`,
|
|
106
|
+
message: `entry "${entry.id}": tuneTimeout must be a positive integer >= 60`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate lifecycle ordering for tune-group steps.
|
|
115
|
+
*
|
|
116
|
+
* Tune-group steps are: any step starting with "tune-", "adapter-add", and "test-adapter".
|
|
117
|
+
* These must appear AFTER the "test" step and BEFORE the "clean" step in the lifecycle array.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} catalog - The catalog object with a `configs` array
|
|
120
|
+
* @param {Array<{ path: string, message: string }>} errors - Errors array to append to
|
|
121
|
+
*/
|
|
122
|
+
export function validateLifecycleOrdering(catalog, errors) {
|
|
123
|
+
if (!catalog || !Array.isArray(catalog.configs)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < catalog.configs.length; i++) {
|
|
128
|
+
const entry = catalog.configs[i];
|
|
129
|
+
if (!entry || !Array.isArray(entry.lifecycle)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Identify tune-group steps in this entry's lifecycle
|
|
134
|
+
const tuneGroupSteps = entry.lifecycle.filter(
|
|
135
|
+
(s) => typeof s === 'string' && (s.startsWith('tune-') || s === 'adapter-add' || s === 'test-adapter')
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (tuneGroupSteps.length === 0) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const testIdx = entry.lifecycle.indexOf('test');
|
|
143
|
+
const cleanIdx = entry.lifecycle.indexOf('clean');
|
|
144
|
+
|
|
145
|
+
for (const step of tuneGroupSteps) {
|
|
146
|
+
const stepIdx = entry.lifecycle.indexOf(step);
|
|
147
|
+
|
|
148
|
+
if (testIdx >= 0 && stepIdx <= testIdx) {
|
|
149
|
+
errors.push({
|
|
150
|
+
path: `/configs/${i}/lifecycle`,
|
|
151
|
+
message: `entry "${entry.id}": "${step}" must come after "test"`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (cleanIdx >= 0 && stepIdx >= cleanIdx) {
|
|
156
|
+
errors.push({
|
|
157
|
+
path: `/configs/${i}/lifecycle`,
|
|
158
|
+
message: `entry "${entry.id}": "${step}" must come before "clean"`
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validate tuneConfig entries against tune-catalog.json.
|
|
167
|
+
* This is a "soft" validation ā returns an empty errors array if tune-catalog is unavailable.
|
|
168
|
+
*
|
|
169
|
+
* For each catalog entry with a tuneConfig:
|
|
170
|
+
* - Checks that tuneConfig.tuneId exists as a key in the tune-catalog's models object
|
|
171
|
+
* - Checks that the specified technique is supported for that model
|
|
172
|
+
* - Checks that the specified trainingType is in the technique's trainingTypes array
|
|
173
|
+
*
|
|
174
|
+
* @param {Object} catalog - The catalog object with a configs array
|
|
175
|
+
* @param {string} tuneCatalogPath - Path to the tune-catalog.json file
|
|
176
|
+
* @returns {Array<{ path: string, message: string }>} Array of validation errors
|
|
177
|
+
*/
|
|
178
|
+
export function validateTuneCatalogReferences(catalog, tuneCatalogPath) {
|
|
179
|
+
const errors = [];
|
|
180
|
+
|
|
181
|
+
if (!catalog || !Array.isArray(catalog.configs)) {
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let tuneCatalog;
|
|
186
|
+
try {
|
|
187
|
+
const raw = readFileSync(tuneCatalogPath, 'utf8');
|
|
188
|
+
tuneCatalog = JSON.parse(raw);
|
|
189
|
+
} catch {
|
|
190
|
+
// tune-catalog not available or unparseable ā skip cross-reference validation
|
|
191
|
+
return errors;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!tuneCatalog || !tuneCatalog.models) {
|
|
195
|
+
return errors;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < catalog.configs.length; i++) {
|
|
199
|
+
const entry = catalog.configs[i];
|
|
200
|
+
if (!entry || !entry.tuneConfig) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const { tuneId, technique, trainingType } = entry.tuneConfig;
|
|
205
|
+
|
|
206
|
+
// Check tuneId exists in tune-catalog models
|
|
207
|
+
const tuneModel = tuneCatalog.models[tuneId];
|
|
208
|
+
if (!tuneModel) {
|
|
209
|
+
errors.push({
|
|
210
|
+
path: `/configs/${i}/tuneConfig/tuneId`,
|
|
211
|
+
message: `entry "${entry.id}": tuneId "${tuneId}" not found in tune-catalog`
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check technique is supported for this model
|
|
217
|
+
if (!tuneModel.techniques || !tuneModel.techniques[technique]) {
|
|
218
|
+
errors.push({
|
|
219
|
+
path: `/configs/${i}/tuneConfig/technique`,
|
|
220
|
+
message: `entry "${entry.id}": technique "${technique}" not supported for model "${tuneId}"`
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check trainingType is in the technique's trainingTypes array
|
|
226
|
+
const supportedTrainingTypes = tuneModel.techniques[technique].trainingTypes;
|
|
227
|
+
if (!Array.isArray(supportedTrainingTypes) || !supportedTrainingTypes.includes(trainingType)) {
|
|
228
|
+
errors.push({
|
|
229
|
+
path: `/configs/${i}/tuneConfig/trainingType`,
|
|
230
|
+
message: `entry "${entry.id}": trainingType "${trainingType}" not supported for ${tuneId}/${technique}`
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return errors;
|
|
236
|
+
}
|
|
237
|
+
|
|
44
238
|
/**
|
|
45
239
|
* Validate an e2e catalog object against the schema and uniqueness constraints.
|
|
46
240
|
*
|
|
47
241
|
* @param {Object} catalog - The catalog object to validate
|
|
242
|
+
* @param {Object} [options] - Optional configuration
|
|
243
|
+
* @param {string} [options.tuneCatalogPath] - Custom path to tune-catalog.json for cross-reference validation
|
|
48
244
|
* @returns {{ valid: boolean, errors?: Array<{ path: string, message: string }> }}
|
|
49
245
|
*/
|
|
50
|
-
export function validateCatalog(catalog) {
|
|
246
|
+
export function validateCatalog(catalog, options = {}) {
|
|
51
247
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
52
248
|
const validate = ajv.compile(catalogSchema);
|
|
53
249
|
|
|
@@ -56,9 +252,19 @@ export function validateCatalog(catalog) {
|
|
|
56
252
|
|
|
57
253
|
if (!valid) {
|
|
58
254
|
for (const err of validate.errors) {
|
|
255
|
+
// Try to include the entry id in the error message for better diagnostics
|
|
256
|
+
let message = err.message;
|
|
257
|
+
const pathMatch = (err.instancePath || '').match(/^\/configs\/(\d+)/);
|
|
258
|
+
if (pathMatch && catalog && Array.isArray(catalog.configs)) {
|
|
259
|
+
const idx = parseInt(pathMatch[1], 10);
|
|
260
|
+
const entry = catalog.configs[idx];
|
|
261
|
+
if (entry && typeof entry.id === 'string') {
|
|
262
|
+
message = `entry "${entry.id}": ${err.message}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
59
265
|
errors.push({
|
|
60
266
|
path: err.instancePath || '/',
|
|
61
|
-
message
|
|
267
|
+
message
|
|
62
268
|
});
|
|
63
269
|
}
|
|
64
270
|
}
|
|
@@ -81,6 +287,17 @@ export function validateCatalog(catalog) {
|
|
|
81
287
|
}
|
|
82
288
|
}
|
|
83
289
|
|
|
290
|
+
// Custom check: tune constraints (runs after Ajv schema validation)
|
|
291
|
+
validateTuneConstraints(catalog, errors);
|
|
292
|
+
|
|
293
|
+
// Custom check: lifecycle ordering for tune-group steps
|
|
294
|
+
validateLifecycleOrdering(catalog, errors);
|
|
295
|
+
|
|
296
|
+
// Cross-reference validation against tune-catalog.json
|
|
297
|
+
const tuneCatalogPath = options.tuneCatalogPath || path.resolve(process.cwd(), 'config/tune-catalog.json');
|
|
298
|
+
const crossRefErrors = validateTuneCatalogReferences(catalog, tuneCatalogPath);
|
|
299
|
+
errors.push(...crossRefErrors);
|
|
300
|
+
|
|
84
301
|
if (errors.length > 0) {
|
|
85
302
|
return { valid: false, errors };
|
|
86
303
|
}
|
|
@@ -101,3 +318,34 @@ export function filterByTier(catalog, tier) {
|
|
|
101
318
|
}
|
|
102
319
|
return catalog.configs.filter((config) => config.tier === tier);
|
|
103
320
|
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Filter configs by a specific config id.
|
|
324
|
+
*
|
|
325
|
+
* First attempts to find the config within the provided (tier-filtered) configs array.
|
|
326
|
+
* If not found there, falls back to searching the full catalog ā this is a convenience
|
|
327
|
+
* for re-runs where the user specifies --config without matching the tier.
|
|
328
|
+
*
|
|
329
|
+
* @param {Array<Object>} configs - Pre-filtered configs (e.g., from filterByTier)
|
|
330
|
+
* @param {Object} catalog - The full catalog object with a `configs` array
|
|
331
|
+
* @param {string} configId - The config id to filter by
|
|
332
|
+
* @returns {Array<Object>} Matching configs (0 or 1 element)
|
|
333
|
+
*/
|
|
334
|
+
export function filterByConfig(configs, catalog, configId) {
|
|
335
|
+
if (!configId) {
|
|
336
|
+
return configs;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// First try within the already-filtered set (tier-filtered)
|
|
340
|
+
const inTier = configs.filter(c => c.id === configId);
|
|
341
|
+
if (inTier.length > 0) {
|
|
342
|
+
return inTier;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Fallback: search the full catalog (convenience for --config without matching tier)
|
|
346
|
+
if (catalog && Array.isArray(catalog.configs)) {
|
|
347
|
+
return catalog.configs.filter(c => c.id === configId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* E2E CI Recorder
|
|
6
|
+
*
|
|
7
|
+
* Records E2E validation results to the DynamoDB CI table.
|
|
8
|
+
* Gracefully degrades if the CI table is not provisioned ā
|
|
9
|
+
* all recording calls become no-ops.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { computeConfigId } from './ci-register-helpers.js';
|
|
13
|
+
import BootstrapConfig from './bootstrap-config.js';
|
|
14
|
+
|
|
15
|
+
export class E2ECIRecorder {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.config = new BootstrapConfig();
|
|
18
|
+
this.client = null;
|
|
19
|
+
this.tableName = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the recorder by checking if the CI table is provisioned.
|
|
24
|
+
* If not provisioned, logs a warning and returns false ā all subsequent
|
|
25
|
+
* recordConfigResult calls become no-ops.
|
|
26
|
+
*
|
|
27
|
+
* @returns {Promise<boolean>} true if ready to record, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
async init() {
|
|
30
|
+
const profile = this.config.getActiveProfileWithDefaults();
|
|
31
|
+
if (!profile || !profile.config.ciInfraProvisioned) {
|
|
32
|
+
console.warn('ā ļø CI table not provisioned ā skipping result recording');
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
this.tableName = profile.config.ciTableName;
|
|
36
|
+
const { DynamoDBClient } = await import('@aws-sdk/client-dynamodb');
|
|
37
|
+
this.client = new DynamoDBClient({ region: profile.config.awsRegion });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Record a config's E2E result to the DynamoDB CI table.
|
|
43
|
+
* No-op if init() returned false or was not called.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} catalogEntry - The catalog entry that was tested
|
|
46
|
+
* @param {object} configResult - The result of running the config
|
|
47
|
+
*/
|
|
48
|
+
async recordConfigResult(catalogEntry, configResult) {
|
|
49
|
+
if (!this.client) return;
|
|
50
|
+
|
|
51
|
+
const configId = this.deriveConfigId(catalogEntry);
|
|
52
|
+
const item = {
|
|
53
|
+
configId,
|
|
54
|
+
schemaVersion: 2,
|
|
55
|
+
testStatus: configResult.status === 'pass'
|
|
56
|
+
? 'pass'
|
|
57
|
+
: `fail-${configResult.steps.find(s => s.status === 'fail')?.name || 'unknown'}`,
|
|
58
|
+
lastTestTimestamp: new Date().toISOString(),
|
|
59
|
+
stageResults: Object.fromEntries(
|
|
60
|
+
configResult.steps.map(s => [s.name, { status: s.status, duration: s.duration, error: s.error || '' }])
|
|
61
|
+
),
|
|
62
|
+
e2eCatalogId: catalogEntry.id,
|
|
63
|
+
tier: catalogEntry.tier,
|
|
64
|
+
duration: configResult.duration
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const { PutItemCommand } = await import('@aws-sdk/client-dynamodb');
|
|
69
|
+
const { marshall } = await import('@aws-sdk/util-dynamodb');
|
|
70
|
+
await this.client.send(new PutItemCommand({
|
|
71
|
+
TableName: this.tableName,
|
|
72
|
+
Item: marshall(item, { removeUndefinedValues: true })
|
|
73
|
+
}));
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.warn(`ā ļø Failed to record ${catalogEntry.id} to CI table: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Derive a deterministic configId from a catalog entry's args field,
|
|
81
|
+
* using the same SHA256 algorithm as `do/register --ci`.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} catalogEntry - A catalog entry with an `args` string
|
|
84
|
+
* @returns {string} 16-character hex configId
|
|
85
|
+
*/
|
|
86
|
+
deriveConfigId(catalogEntry) {
|
|
87
|
+
const args = Object.fromEntries(
|
|
88
|
+
catalogEntry.args.split(/\s+/)
|
|
89
|
+
.filter(a => a.startsWith('--'))
|
|
90
|
+
.map(a => a.replace(/^--/, '').split('='))
|
|
91
|
+
);
|
|
92
|
+
const deploymentTarget = catalogEntry.track === 'realtime'
|
|
93
|
+
? 'realtime-inference'
|
|
94
|
+
: catalogEntry.track;
|
|
95
|
+
return computeConfigId(
|
|
96
|
+
args['deployment-config'] || '',
|
|
97
|
+
args['model-name'] || 'none',
|
|
98
|
+
args['instance-type'] || '',
|
|
99
|
+
args['region'] || 'us-west-2',
|
|
100
|
+
deploymentTarget
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|