@aws/ml-container-creator 0.6.1 → 0.8.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 +1 -1
- package/infra/ci-harness/buildspec.yml +4 -0
- package/package.json +1 -1
- package/servers/lib/catalogs/model-servers.json +80 -0
- package/servers/model-picker/index.js +27 -16
- package/src/app.js +89 -21
- package/src/lib/cli-handler.js +1 -1
- package/src/lib/config-manager.js +39 -2
- package/src/lib/cross-cutting-checker.js +146 -33
- package/src/lib/deployment-config-resolver.js +10 -4
- package/src/lib/e2e-bootstrap.js +227 -0
- package/src/lib/e2e-catalog-validator.js +103 -0
- package/src/lib/e2e-quota-validator.js +135 -0
- package/src/lib/prompt-runner.js +290 -22
- package/src/lib/prompts.js +9 -3
- package/src/lib/template-manager.js +10 -4
- package/src/lib/tune-catalog-validator.js +5 -5
- package/templates/Dockerfile +2 -0
- package/templates/code/cw_log_forwarder.py +64 -0
- package/templates/code/serve +14 -3
- package/templates/code/serving.properties +2 -2
- package/templates/deploy_notebook_generator.py +897 -0
- package/templates/diffusors/serve +3 -3
- package/templates/do/.tune_helper.py +2 -2
- package/templates/do/export +19 -2
- package/templates/do/lib/endpoint-config.sh +3 -1
- package/templates/do/lib/inference-component.sh +5 -1
- package/templates/do/register +8 -2
- package/templates/do/test +5 -5
- package/templates/do/tune +2 -2
- package/templates/marketplace/config +118 -0
- package/templates/marketplace/deploy +890 -0
- package/templates/marketplace/test +453 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Canonical mapping from deployment-config strings to structured parts.
|
|
15
|
-
* 2 http + 5 transformers + 7 triton + 1 diffusors =
|
|
15
|
+
* 2 http + 5 transformers + 7 triton + 1 diffusors + 1 marketplace = 16 total configs.
|
|
16
16
|
*/
|
|
17
17
|
const CANONICAL_CONFIGS = new Map([
|
|
18
18
|
// HTTP architecture (2)
|
|
@@ -36,7 +36,10 @@ const CANONICAL_CONFIGS = new Map([
|
|
|
36
36
|
['triton-python', { architecture: 'triton', backend: 'python', engine: null }],
|
|
37
37
|
|
|
38
38
|
// Diffusors architecture (1)
|
|
39
|
-
['diffusors-vllm-omni', { architecture: 'diffusors', backend: 'vllm-omni', engine: null }]
|
|
39
|
+
['diffusors-vllm-omni', { architecture: 'diffusors', backend: 'vllm-omni', engine: null }],
|
|
40
|
+
|
|
41
|
+
// Marketplace architecture (1) — no backend, vendor controls the container
|
|
42
|
+
['marketplace', { architecture: 'marketplace', backend: null, engine: null }]
|
|
40
43
|
]);
|
|
41
44
|
|
|
42
45
|
export default class DeploymentConfigResolver {
|
|
@@ -62,15 +65,18 @@ export default class DeploymentConfigResolver {
|
|
|
62
65
|
* Compose a deployment-config string from structured parts.
|
|
63
66
|
* Inverse of decompose().
|
|
64
67
|
*
|
|
65
|
-
* @param {{ architecture: string, backend: string, engine?: string }} parts
|
|
68
|
+
* @param {{ architecture: string, backend: string|null, engine?: string }} parts
|
|
66
69
|
* @returns {string}
|
|
67
70
|
*/
|
|
68
71
|
compose(parts) {
|
|
72
|
+
if (!parts.backend) {
|
|
73
|
+
return parts.architecture;
|
|
74
|
+
}
|
|
69
75
|
return `${parts.architecture}-${parts.backend}`;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
/**
|
|
73
|
-
* Get all
|
|
79
|
+
* Get all 16 valid deployment-config strings.
|
|
74
80
|
*
|
|
75
81
|
* @returns {string[]}
|
|
76
82
|
*/
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* E2E Bootstrap Integration
|
|
6
|
+
*
|
|
7
|
+
* Handles the `bootstrap --ci --e2e` flow:
|
|
8
|
+
* 1. Loads the e2e catalog
|
|
9
|
+
* 2. Runs quota validation for the CI tier and emits warnings
|
|
10
|
+
* 3. Deploys the config/bootstrap-e2e-stack.json CloudFormation stack
|
|
11
|
+
* 4. Stores e2e config (bucket, SNS ARN, CodeBuild project name) in bootstrap config
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 3.3, 3.4
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { validateQuotas } from './e2e-quota-validator.js';
|
|
21
|
+
import { validateCatalog } from './e2e-catalog-validator.js';
|
|
22
|
+
import BootstrapConfig from './bootstrap-config.js';
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = path.dirname(__filename);
|
|
26
|
+
|
|
27
|
+
const E2E_STACK_NAME = 'mlcc-bootstrap-e2e';
|
|
28
|
+
const E2E_STACK_TEMPLATE_PATH = path.resolve(__dirname, '../../config/bootstrap-e2e-stack.json');
|
|
29
|
+
const DEFAULT_CATALOG_PATH = path.resolve(__dirname, '../../scripts/e2e-catalog.json');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Bootstrap E2E infrastructure.
|
|
33
|
+
*
|
|
34
|
+
* Loads the catalog, validates quotas for the CI tier, deploys the
|
|
35
|
+
* CloudFormation stack, and stores e2e config in the bootstrap profile.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} options
|
|
38
|
+
* @param {string} options.region - AWS region
|
|
39
|
+
* @param {string} options.profile - AWS CLI profile name
|
|
40
|
+
* @param {string} [options.catalogPath] - Path to the e2e catalog JSON file
|
|
41
|
+
* @param {string} [options.profileName] - Bootstrap profile name (default: 'default')
|
|
42
|
+
* @param {Object} [options.bootstrapConfig] - Pre-configured BootstrapConfig instance (for testing)
|
|
43
|
+
* @returns {Promise<Object>} The e2e config object with bucket, SNS ARN, and CodeBuild project name
|
|
44
|
+
*/
|
|
45
|
+
export async function bootstrapE2E(options) {
|
|
46
|
+
const {
|
|
47
|
+
region,
|
|
48
|
+
profile,
|
|
49
|
+
catalogPath = DEFAULT_CATALOG_PATH,
|
|
50
|
+
profileName = 'default',
|
|
51
|
+
bootstrapConfig
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
console.log('\n🧪 E2E Validation Infrastructure Setup\n');
|
|
55
|
+
|
|
56
|
+
// Step 1: Load and validate the catalog
|
|
57
|
+
console.log(' 📋 Loading e2e catalog...');
|
|
58
|
+
const catalog = loadCatalog(catalogPath);
|
|
59
|
+
console.log(` ✅ Catalog loaded (${catalog.configs.length} configs)`);
|
|
60
|
+
|
|
61
|
+
// Step 2: Run quota validation for CI tier
|
|
62
|
+
console.log('\n 🔍 Checking service quotas for CI tier...');
|
|
63
|
+
const quotaResults = await runQuotaValidation('ci', catalog, region);
|
|
64
|
+
|
|
65
|
+
if (quotaResults.length === 0) {
|
|
66
|
+
console.log(' ℹ️ No instance types to validate for CI tier');
|
|
67
|
+
} else {
|
|
68
|
+
const insufficient = quotaResults.filter(r => !r.sufficient);
|
|
69
|
+
if (insufficient.length === 0) {
|
|
70
|
+
console.log(' ✅ All quotas sufficient for CI tier');
|
|
71
|
+
} else {
|
|
72
|
+
for (const result of insufficient) {
|
|
73
|
+
console.log(` ⚠️ ${result.instanceType} quota is ${result.available}, need ${result.required} for CI tier`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 3: Deploy the E2E CloudFormation stack
|
|
79
|
+
console.log('\n ☁️ Deploying E2E infrastructure stack...');
|
|
80
|
+
const stackOutputs = deployE2EStack(profile, region);
|
|
81
|
+
console.log(' ✅ E2E stack deployed successfully');
|
|
82
|
+
|
|
83
|
+
// Step 4: Store e2e config in bootstrap profile
|
|
84
|
+
const e2eConfig = {
|
|
85
|
+
e2eInfraProvisioned: true,
|
|
86
|
+
e2eCodeBuildProject: stackOutputs.CodeBuildProjectName || 'ml-container-creator-e2e',
|
|
87
|
+
e2eResultsBucket: stackOutputs.ResultsBucketName || `mlcc-e2e-results-unknown-${region}`,
|
|
88
|
+
e2eSnsTopicArn: stackOutputs.NotificationsTopicArn || ''
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
console.log('\n 💾 Saving e2e config to bootstrap profile...');
|
|
92
|
+
const config = bootstrapConfig || new BootstrapConfig();
|
|
93
|
+
storeE2EConfig(config, profileName, e2eConfig);
|
|
94
|
+
console.log(' ✅ E2E config saved');
|
|
95
|
+
|
|
96
|
+
// Display summary
|
|
97
|
+
console.log('\n 📋 E2E Infrastructure Summary:');
|
|
98
|
+
console.log(` CodeBuild project: ${e2eConfig.e2eCodeBuildProject}`);
|
|
99
|
+
console.log(` Results bucket: ${e2eConfig.e2eResultsBucket}`);
|
|
100
|
+
console.log(` SNS topic: ${e2eConfig.e2eSnsTopicArn}`);
|
|
101
|
+
|
|
102
|
+
return e2eConfig;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Load and validate the e2e catalog from a JSON file.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} catalogPath - Path to the catalog JSON file
|
|
109
|
+
* @returns {Object} The validated catalog object
|
|
110
|
+
* @throws {Error} If the catalog file cannot be read or is invalid
|
|
111
|
+
*/
|
|
112
|
+
export function loadCatalog(catalogPath) {
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = readFileSync(catalogPath, 'utf8');
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new Error(`Failed to read e2e catalog at ${catalogPath}: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let catalog;
|
|
121
|
+
try {
|
|
122
|
+
catalog = JSON.parse(raw);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new Error(`Failed to parse e2e catalog JSON: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const validation = validateCatalog(catalog);
|
|
128
|
+
if (!validation.valid) {
|
|
129
|
+
const errorMessages = validation.errors.map(e => ` ${e.path}: ${e.message}`).join('\n');
|
|
130
|
+
throw new Error(`E2E catalog validation failed:\n${errorMessages}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return catalog;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run quota validation for a given tier and emit warnings.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} tier - The tier to validate (e.g., 'ci')
|
|
140
|
+
* @param {Object} catalog - The validated catalog object
|
|
141
|
+
* @param {string} region - AWS region
|
|
142
|
+
* @returns {Promise<Array<{instanceType: string, required: number, available: number, sufficient: boolean}>>}
|
|
143
|
+
*/
|
|
144
|
+
export async function runQuotaValidation(tier, catalog, region) {
|
|
145
|
+
try {
|
|
146
|
+
return await validateQuotas(tier, catalog, region);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.warn(` ⚠️ Quota validation failed: ${err.message}`);
|
|
149
|
+
console.warn(' Continuing without quota validation...');
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Deploy the E2E CloudFormation stack.
|
|
156
|
+
*
|
|
157
|
+
* Uses `aws cloudformation deploy` which handles both CREATE and UPDATE scenarios.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} awsProfile - AWS CLI profile name
|
|
160
|
+
* @param {string} region - AWS region
|
|
161
|
+
* @returns {Object} Map of stack output key → output value
|
|
162
|
+
* @throws {Error} If stack deployment fails
|
|
163
|
+
*/
|
|
164
|
+
export function deployE2EStack(awsProfile, region) {
|
|
165
|
+
const deployCommand = [
|
|
166
|
+
'aws cloudformation deploy',
|
|
167
|
+
`--template-file ${E2E_STACK_TEMPLATE_PATH}`,
|
|
168
|
+
`--stack-name ${E2E_STACK_NAME}`,
|
|
169
|
+
'--capabilities CAPABILITY_NAMED_IAM',
|
|
170
|
+
`--profile ${awsProfile}`,
|
|
171
|
+
`--region ${region}`
|
|
172
|
+
].join(' ');
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
execSync(deployCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// "No changes to deploy" is a success case — CloudFormation deploy
|
|
178
|
+
// exits with code 255 when there's nothing to update
|
|
179
|
+
const stderr = error.stderr || error.message || '';
|
|
180
|
+
if (stderr.includes('No changes to deploy')) {
|
|
181
|
+
console.log(' ℹ️ E2E stack is up to date — no changes needed');
|
|
182
|
+
} else {
|
|
183
|
+
throw new Error(`E2E stack deployment failed: ${stderr}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Read stack outputs
|
|
188
|
+
const describeCommand = `aws cloudformation describe-stacks --stack-name ${E2E_STACK_NAME} --region ${region} --profile ${awsProfile} --output json`;
|
|
189
|
+
let describeOutput;
|
|
190
|
+
try {
|
|
191
|
+
describeOutput = execSync(describeCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
throw new Error(`Failed to read E2E stack outputs: ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const describeResult = JSON.parse(describeOutput.trim());
|
|
197
|
+
const stack = describeResult.Stacks && describeResult.Stacks[0];
|
|
198
|
+
if (!stack) {
|
|
199
|
+
throw new Error(`Stack "${E2E_STACK_NAME}" not found after deployment`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const outputs = {};
|
|
203
|
+
for (const output of (stack.Outputs || [])) {
|
|
204
|
+
outputs[output.OutputKey] = output.OutputValue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return outputs;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Store e2e config fields in the bootstrap profile.
|
|
212
|
+
*
|
|
213
|
+
* @param {BootstrapConfig} config - BootstrapConfig instance
|
|
214
|
+
* @param {string} profileName - The profile name to update
|
|
215
|
+
* @param {Object} e2eConfig - The e2e config fields to store
|
|
216
|
+
*/
|
|
217
|
+
export function storeE2EConfig(config, profileName, e2eConfig) {
|
|
218
|
+
const fullConfig = config.read();
|
|
219
|
+
if (!fullConfig || !fullConfig.profiles || !fullConfig.profiles[profileName]) {
|
|
220
|
+
throw new Error(`Bootstrap profile "${profileName}" not found. Run bootstrap first.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const profileData = fullConfig.profiles[profileName];
|
|
224
|
+
Object.assign(profileData, e2eConfig);
|
|
225
|
+
fullConfig.profiles[profileName] = profileData;
|
|
226
|
+
config.write(fullConfig);
|
|
227
|
+
}
|
|
@@ -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 Catalog Validator
|
|
6
|
+
*
|
|
7
|
+
* Validates the e2e test catalog against a JSON Schema and enforces
|
|
8
|
+
* additional constraints (unique IDs) that JSON Schema alone cannot express.
|
|
9
|
+
* Also provides tier-based filtering of catalog entries.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: 1.1, 1.2, 1.3, 1.4
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import Ajv from 'ajv';
|
|
15
|
+
|
|
16
|
+
const catalogSchema = {
|
|
17
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: ['configs'],
|
|
20
|
+
properties: {
|
|
21
|
+
configs: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
required: ['id', 'tier', 'track', 'args', 'lifecycle', 'timeout'],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
id: { type: 'string', pattern: '^[a-z0-9-]+$' },
|
|
29
|
+
tier: { type: 'string', enum: ['ci', 'nightly', 'weekly'] },
|
|
30
|
+
track: { type: 'string', enum: ['realtime', 'hyperpod', 'async', 'batch'] },
|
|
31
|
+
args: { type: 'string' },
|
|
32
|
+
lifecycle: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' },
|
|
35
|
+
minItems: 1
|
|
36
|
+
},
|
|
37
|
+
timeout: { type: 'integer', minimum: 60 }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate an e2e catalog object against the schema and uniqueness constraints.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} catalog - The catalog object to validate
|
|
48
|
+
* @returns {{ valid: boolean, errors?: Array<{ path: string, message: string }> }}
|
|
49
|
+
*/
|
|
50
|
+
export function validateCatalog(catalog) {
|
|
51
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
52
|
+
const validate = ajv.compile(catalogSchema);
|
|
53
|
+
|
|
54
|
+
const valid = validate(catalog);
|
|
55
|
+
const errors = [];
|
|
56
|
+
|
|
57
|
+
if (!valid) {
|
|
58
|
+
for (const err of validate.errors) {
|
|
59
|
+
errors.push({
|
|
60
|
+
path: err.instancePath || '/',
|
|
61
|
+
message: err.message
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Custom check: unique IDs across all entries
|
|
67
|
+
if (catalog && catalog.configs && Array.isArray(catalog.configs)) {
|
|
68
|
+
const seen = new Map();
|
|
69
|
+
for (let i = 0; i < catalog.configs.length; i++) {
|
|
70
|
+
const entry = catalog.configs[i];
|
|
71
|
+
if (entry && typeof entry.id === 'string') {
|
|
72
|
+
if (seen.has(entry.id)) {
|
|
73
|
+
errors.push({
|
|
74
|
+
path: `/configs/${i}/id`,
|
|
75
|
+
message: `duplicate id "${entry.id}" (first seen at index ${seen.get(entry.id)})`
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
seen.set(entry.id, i);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
return { valid: false, errors };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { valid: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Filter catalog configs by tier.
|
|
93
|
+
*
|
|
94
|
+
* @param {Object} catalog - The catalog object with a `configs` array
|
|
95
|
+
* @param {string} tier - The tier to filter by (e.g., 'ci', 'nightly', 'weekly')
|
|
96
|
+
* @returns {Array<Object>} Configs matching the given tier
|
|
97
|
+
*/
|
|
98
|
+
export function filterByTier(catalog, tier) {
|
|
99
|
+
if (!catalog || !Array.isArray(catalog.configs)) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return catalog.configs.filter((config) => config.tier === tier);
|
|
103
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* E2E Quota Validator
|
|
6
|
+
*
|
|
7
|
+
* Validates that the AWS account has sufficient service quotas for the
|
|
8
|
+
* instance types required by a given tier in the e2e catalog.
|
|
9
|
+
*
|
|
10
|
+
* Requirements: 3.3, 3.4
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ServiceQuotasClient, GetServiceQuotaCommand } from '@aws-sdk/client-service-quotas';
|
|
14
|
+
import { filterByTier } from './e2e-catalog-validator.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Instance type to Service Quotas quota code mapping.
|
|
18
|
+
* SageMaker real-time endpoint instance quotas follow a naming pattern.
|
|
19
|
+
* This map covers the instance types used in the e2e catalog.
|
|
20
|
+
*/
|
|
21
|
+
const INSTANCE_QUOTA_CODES = {
|
|
22
|
+
'ml.g6e.xlarge': 'L-2D6591FA',
|
|
23
|
+
'ml.g6e.2xlarge': 'L-2D6591FA',
|
|
24
|
+
'ml.g6e.4xlarge': 'L-2D6591FA',
|
|
25
|
+
'ml.g6e.12xlarge': 'L-2D6591FA',
|
|
26
|
+
'ml.g5.xlarge': 'L-0100B498',
|
|
27
|
+
'ml.g5.2xlarge': 'L-0100B498',
|
|
28
|
+
'ml.m5.xlarge': 'L-ABB2FAC3',
|
|
29
|
+
'ml.p5.48xlarge': 'L-E89A212B'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SAGEMAKER_SERVICE_CODE = 'sagemaker';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse the instance type from a CLI args string.
|
|
36
|
+
*
|
|
37
|
+
* Looks for `--instance-type=<value>` or `--instance-type <value>` patterns.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} args - The CLI args string
|
|
40
|
+
* @returns {string|null} The instance type value, or null if not found
|
|
41
|
+
*/
|
|
42
|
+
export function parseInstanceType(args) {
|
|
43
|
+
if (!args || typeof args !== 'string') {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Match --instance-type=value or --instance-type value
|
|
48
|
+
const equalMatch = args.match(/--instance-type=(\S+)/);
|
|
49
|
+
if (equalMatch) {
|
|
50
|
+
return equalMatch[1];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const spaceMatch = args.match(/--instance-type\s+(\S+)/);
|
|
54
|
+
if (spaceMatch) {
|
|
55
|
+
return spaceMatch[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Sum instance counts per type for a given tier in the catalog.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} tier - The tier to filter by
|
|
65
|
+
* @param {Object} catalog - The catalog object
|
|
66
|
+
* @returns {Map<string, number>} Map of instance type to required count
|
|
67
|
+
*/
|
|
68
|
+
export function sumInstanceRequirements(tier, catalog) {
|
|
69
|
+
const configs = filterByTier(catalog, tier);
|
|
70
|
+
const counts = new Map();
|
|
71
|
+
|
|
72
|
+
for (const config of configs) {
|
|
73
|
+
const instanceType = parseInstanceType(config.args);
|
|
74
|
+
if (instanceType) {
|
|
75
|
+
counts.set(instanceType, (counts.get(instanceType) || 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return counts;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate that the AWS account has sufficient quotas for the instance types
|
|
84
|
+
* required by a given tier.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} tier - The tier to validate quotas for
|
|
87
|
+
* @param {Object} catalog - The catalog object
|
|
88
|
+
* @param {string} region - The AWS region to check quotas in
|
|
89
|
+
* @param {Object} [options] - Optional configuration
|
|
90
|
+
* @param {Object} [options.client] - Pre-configured ServiceQuotasClient (for testing)
|
|
91
|
+
* @returns {Promise<Array<{instanceType: string, required: number, available: number, sufficient: boolean}>>}
|
|
92
|
+
*/
|
|
93
|
+
export async function validateQuotas(tier, catalog, region, options = {}) {
|
|
94
|
+
const instanceRequirements = sumInstanceRequirements(tier, catalog);
|
|
95
|
+
const results = [];
|
|
96
|
+
|
|
97
|
+
if (instanceRequirements.size === 0) {
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const client = options.client || new ServiceQuotasClient({ region });
|
|
102
|
+
|
|
103
|
+
for (const [instanceType, required] of instanceRequirements) {
|
|
104
|
+
const quotaCode = INSTANCE_QUOTA_CODES[instanceType];
|
|
105
|
+
let available = 0;
|
|
106
|
+
|
|
107
|
+
if (quotaCode) {
|
|
108
|
+
try {
|
|
109
|
+
const command = new GetServiceQuotaCommand({
|
|
110
|
+
ServiceCode: SAGEMAKER_SERVICE_CODE,
|
|
111
|
+
QuotaCode: quotaCode
|
|
112
|
+
});
|
|
113
|
+
const response = await client.send(command);
|
|
114
|
+
available = response.Quota?.Value ?? 0;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// If we can't fetch the quota, assume 0 and warn
|
|
117
|
+
console.warn(`⚠️ Could not fetch quota for ${instanceType}: ${err.message}`);
|
|
118
|
+
available = 0;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.warn(`⚠️ No quota code mapping for ${instanceType}, skipping quota check`);
|
|
122
|
+
available = 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sufficient = available >= required;
|
|
126
|
+
|
|
127
|
+
if (!sufficient) {
|
|
128
|
+
console.warn(`⚠️ ${instanceType} quota is ${available}, need ${required} for ${tier} tier`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
results.push({ instanceType, required, available, sufficient });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return results;
|
|
135
|
+
}
|