@aws/ml-container-creator 0.2.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/LICENSE +202 -0
- package/LICENSE-THIRD-PARTY +68620 -0
- package/NOTICE +2 -0
- package/README.md +106 -0
- package/bin/cli.js +365 -0
- package/config/defaults.json +32 -0
- package/config/presets/transformers-djl.json +26 -0
- package/config/presets/transformers-gpu.json +24 -0
- package/config/presets/transformers-lmi.json +27 -0
- package/package.json +129 -0
- package/servers/README.md +419 -0
- package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
- package/servers/base-image-picker/catalogs/python-slim.json +38 -0
- package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
- package/servers/base-image-picker/catalogs/triton.json +38 -0
- package/servers/base-image-picker/index.js +495 -0
- package/servers/base-image-picker/manifest.json +17 -0
- package/servers/base-image-picker/package.json +15 -0
- package/servers/hyperpod-cluster-picker/LICENSE +202 -0
- package/servers/hyperpod-cluster-picker/index.js +424 -0
- package/servers/hyperpod-cluster-picker/manifest.json +14 -0
- package/servers/hyperpod-cluster-picker/package.json +17 -0
- package/servers/instance-recommender/LICENSE +202 -0
- package/servers/instance-recommender/catalogs/instances.json +852 -0
- package/servers/instance-recommender/index.js +284 -0
- package/servers/instance-recommender/manifest.json +16 -0
- package/servers/instance-recommender/package.json +15 -0
- package/servers/lib/LICENSE +202 -0
- package/servers/lib/bedrock-client.js +160 -0
- package/servers/lib/custom-validators.js +46 -0
- package/servers/lib/dynamic-resolver.js +36 -0
- package/servers/lib/package.json +11 -0
- package/servers/lib/schemas/image-catalog.schema.json +185 -0
- package/servers/lib/schemas/instances.schema.json +124 -0
- package/servers/lib/schemas/manifest.schema.json +64 -0
- package/servers/lib/schemas/model-catalog.schema.json +91 -0
- package/servers/lib/schemas/regions.schema.json +26 -0
- package/servers/lib/schemas/triton-backends.schema.json +51 -0
- package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
- package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
- package/servers/model-picker/catalogs/popular-transformers.json +226 -0
- package/servers/model-picker/index.js +1693 -0
- package/servers/model-picker/manifest.json +18 -0
- package/servers/model-picker/package.json +20 -0
- package/servers/region-picker/LICENSE +202 -0
- package/servers/region-picker/catalogs/regions.json +263 -0
- package/servers/region-picker/index.js +230 -0
- package/servers/region-picker/manifest.json +16 -0
- package/servers/region-picker/package.json +15 -0
- package/src/app.js +1007 -0
- package/src/copy-tpl.js +77 -0
- package/src/lib/accelerator-validator.js +39 -0
- package/src/lib/asset-manager.js +385 -0
- package/src/lib/aws-profile-parser.js +181 -0
- package/src/lib/bootstrap-command-handler.js +1647 -0
- package/src/lib/bootstrap-config.js +238 -0
- package/src/lib/ci-register-helpers.js +124 -0
- package/src/lib/ci-report-helpers.js +158 -0
- package/src/lib/ci-stage-helpers.js +268 -0
- package/src/lib/cli-handler.js +529 -0
- package/src/lib/comment-generator.js +544 -0
- package/src/lib/community-reports-validator.js +91 -0
- package/src/lib/config-manager.js +2106 -0
- package/src/lib/configuration-exporter.js +204 -0
- package/src/lib/configuration-manager.js +695 -0
- package/src/lib/configuration-matcher.js +221 -0
- package/src/lib/cpu-validator.js +36 -0
- package/src/lib/cuda-validator.js +57 -0
- package/src/lib/deployment-config-resolver.js +103 -0
- package/src/lib/deployment-entry-schema.js +125 -0
- package/src/lib/deployment-registry.js +598 -0
- package/src/lib/docker-introspection-validator.js +51 -0
- package/src/lib/engine-prefix-resolver.js +60 -0
- package/src/lib/huggingface-client.js +172 -0
- package/src/lib/key-value-parser.js +37 -0
- package/src/lib/known-flags-validator.js +200 -0
- package/src/lib/manifest-cli.js +280 -0
- package/src/lib/mcp-client.js +303 -0
- package/src/lib/mcp-command-handler.js +532 -0
- package/src/lib/neuron-validator.js +80 -0
- package/src/lib/parameter-schema-validator.js +284 -0
- package/src/lib/prompt-runner.js +1349 -0
- package/src/lib/prompts.js +1138 -0
- package/src/lib/registry-command-handler.js +519 -0
- package/src/lib/registry-loader.js +198 -0
- package/src/lib/rocm-validator.js +80 -0
- package/src/lib/schema-validator.js +157 -0
- package/src/lib/sensitive-redactor.js +59 -0
- package/src/lib/template-engine.js +156 -0
- package/src/lib/template-manager.js +341 -0
- package/src/lib/validation-engine.js +314 -0
- package/src/prompt-adapter.js +63 -0
- package/templates/Dockerfile +300 -0
- package/templates/IAM_PERMISSIONS.md +84 -0
- package/templates/MIGRATION.md +488 -0
- package/templates/PROJECT_README.md +439 -0
- package/templates/TEMPLATE_SYSTEM.md +243 -0
- package/templates/buildspec.yml +64 -0
- package/templates/code/chat_template.jinja +1 -0
- package/templates/code/flask/gunicorn_config.py +35 -0
- package/templates/code/flask/wsgi.py +10 -0
- package/templates/code/model_handler.py +387 -0
- package/templates/code/serve +300 -0
- package/templates/code/serve.py +175 -0
- package/templates/code/serving.properties +105 -0
- package/templates/code/start_server.py +39 -0
- package/templates/code/start_server.sh +39 -0
- package/templates/diffusors/Dockerfile +72 -0
- package/templates/diffusors/patch_image_api.py +35 -0
- package/templates/diffusors/serve +115 -0
- package/templates/diffusors/start_server.sh +114 -0
- package/templates/do/.gitkeep +1 -0
- package/templates/do/README.md +541 -0
- package/templates/do/build +83 -0
- package/templates/do/ci +681 -0
- package/templates/do/clean +811 -0
- package/templates/do/config +260 -0
- package/templates/do/deploy +1560 -0
- package/templates/do/export +306 -0
- package/templates/do/logs +319 -0
- package/templates/do/manifest +12 -0
- package/templates/do/push +119 -0
- package/templates/do/register +580 -0
- package/templates/do/run +113 -0
- package/templates/do/submit +417 -0
- package/templates/do/test +1147 -0
- package/templates/hyperpod/configmap.yaml +24 -0
- package/templates/hyperpod/deployment.yaml +71 -0
- package/templates/hyperpod/pvc.yaml +42 -0
- package/templates/hyperpod/service.yaml +17 -0
- package/templates/nginx-diffusors.conf +74 -0
- package/templates/nginx-predictors.conf +47 -0
- package/templates/nginx-tensorrt.conf +74 -0
- package/templates/requirements.txt +61 -0
- package/templates/sample_model/test_inference.py +123 -0
- package/templates/sample_model/train_abalone.py +252 -0
- package/templates/test/test_endpoint.sh +79 -0
- package/templates/test/test_local_image.sh +80 -0
- package/templates/test/test_model_handler.py +180 -0
- package/templates/triton/Dockerfile +128 -0
- package/templates/triton/config.pbtxt +163 -0
- package/templates/triton/model.py +130 -0
- package/templates/triton/requirements.txt +11 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deployment Registry
|
|
6
|
+
*
|
|
7
|
+
* Core data-access module for the deployment registry system.
|
|
8
|
+
* Handles CRUD, search, import/export, and schema validation
|
|
9
|
+
* for deployment entries stored as JSON files.
|
|
10
|
+
*
|
|
11
|
+
* Registry file format:
|
|
12
|
+
* {
|
|
13
|
+
* "schemaVersion": "2026-03-20",
|
|
14
|
+
* "entries": [ ...Deployment_Entry objects ]
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
19
|
+
import { dirname } from 'node:path'
|
|
20
|
+
import { createHash } from 'node:crypto'
|
|
21
|
+
import Ajv from 'ajv'
|
|
22
|
+
import { minimatch } from 'minimatch'
|
|
23
|
+
import deploymentEntrySchema from './deployment-entry-schema.js'
|
|
24
|
+
|
|
25
|
+
const CURRENT_SCHEMA_VERSION = '2026-03-20'
|
|
26
|
+
|
|
27
|
+
export default class DeploymentRegistry {
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} registryPath - Absolute path to the registry JSON file
|
|
30
|
+
*/
|
|
31
|
+
constructor(registryPath) {
|
|
32
|
+
this.registryPath = registryPath
|
|
33
|
+
this._ajv = new Ajv({ allErrors: true, strict: false })
|
|
34
|
+
this._validate = this._ajv.compile(deploymentEntrySchema)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the registry file and return the entries array.
|
|
39
|
+
*
|
|
40
|
+
* Handles:
|
|
41
|
+
* - Missing file → return []
|
|
42
|
+
* - Invalid JSON → throw with descriptive message
|
|
43
|
+
* - Missing/unrecognized schemaVersion → console.warn + best-effort
|
|
44
|
+
*
|
|
45
|
+
* @returns {Array<Object>} entries array
|
|
46
|
+
*/
|
|
47
|
+
_readRegistry() {
|
|
48
|
+
if (!existsSync(this.registryPath)) {
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const raw = readFileSync(this.registryPath, 'utf8')
|
|
53
|
+
|
|
54
|
+
let data
|
|
55
|
+
try {
|
|
56
|
+
data = JSON.parse(raw)
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`Invalid JSON in registry file ${this.registryPath}: ${err.message}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const migrated = this._migrateIfNeeded(data)
|
|
62
|
+
|
|
63
|
+
if (!migrated.schemaVersion) {
|
|
64
|
+
console.warn(`Warning: Registry file ${this.registryPath} has no schemaVersion. Attempting best-effort read.`)
|
|
65
|
+
} else if (migrated.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
66
|
+
console.warn(`Warning: Registry file ${this.registryPath} has unrecognized schemaVersion "${migrated.schemaVersion}". Attempting best-effort read.`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Array.isArray(migrated.entries) ? migrated.entries : []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Write entries to the registry file wrapped in a versioned envelope.
|
|
74
|
+
*
|
|
75
|
+
* Creates parent directories if they don't exist.
|
|
76
|
+
* Uses 2-space indentation and a trailing newline.
|
|
77
|
+
*
|
|
78
|
+
* @param {Array<Object>} entries - The entries array to write
|
|
79
|
+
*/
|
|
80
|
+
_writeRegistry(entries) {
|
|
81
|
+
const dir = dirname(this.registryPath)
|
|
82
|
+
if (!existsSync(dir)) {
|
|
83
|
+
mkdirSync(dir, { recursive: true })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const envelope = {
|
|
87
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
88
|
+
entries
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
writeFileSync(this.registryPath, JSON.stringify(envelope, null, 2) + '\n')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate an 8-character hex ID from a hash of the entry's timestamp
|
|
96
|
+
* and deploymentConfig. Retries with random entropy on collision.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} entry - The deployment entry
|
|
99
|
+
* @param {Array<Object>} [existingEntries] - Existing entries to check for collisions
|
|
100
|
+
* @returns {string} 8-character hex string
|
|
101
|
+
*/
|
|
102
|
+
_generateId(entry, existingEntries = []) {
|
|
103
|
+
const baseInput = `${entry.timestamp}:${entry.deployment.deploymentConfig}`
|
|
104
|
+
let id = createHash('sha256').update(baseInput).digest('hex').slice(0, 8)
|
|
105
|
+
|
|
106
|
+
const existingIds = new Set(existingEntries.map(e => e.id))
|
|
107
|
+
|
|
108
|
+
while (existingIds.has(id)) {
|
|
109
|
+
const entropy = Math.random().toString(36).slice(2)
|
|
110
|
+
id = createHash('sha256').update(baseInput + entropy).digest('hex').slice(0, 8)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return id
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate an entry against the deployment entry schema using ajv.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} entry - The entry to validate
|
|
120
|
+
* @returns {{ valid: boolean, errors: Array|null }} Validation result
|
|
121
|
+
*/
|
|
122
|
+
_validateEntry(entry) {
|
|
123
|
+
const valid = this._validate(entry)
|
|
124
|
+
return {
|
|
125
|
+
valid: !!valid,
|
|
126
|
+
errors: valid ? null : this._validate.errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Migrate old registry formats to the current versioned envelope.
|
|
132
|
+
*
|
|
133
|
+
* Handles:
|
|
134
|
+
* - Plain arrays (legacy format) → wrap in envelope
|
|
135
|
+
* - Already-enveloped data → return as-is
|
|
136
|
+
*
|
|
137
|
+
* @param {*} data - Parsed JSON data from the registry file
|
|
138
|
+
* @returns {{ schemaVersion: string|undefined, entries: Array }}
|
|
139
|
+
*/
|
|
140
|
+
_migrateIfNeeded(data) {
|
|
141
|
+
// Legacy format: plain array of entries
|
|
142
|
+
if (Array.isArray(data)) {
|
|
143
|
+
return {
|
|
144
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
145
|
+
entries: data
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Already an envelope with schemaVersion and entries
|
|
150
|
+
if (data && typeof data === 'object' && 'entries' in data) {
|
|
151
|
+
return data
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Unknown format — wrap in envelope with empty entries
|
|
155
|
+
return {
|
|
156
|
+
schemaVersion: undefined,
|
|
157
|
+
entries: []
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add a new deployment entry to the registry.
|
|
163
|
+
*
|
|
164
|
+
* Validates the entry against the schema, generates a unique ID,
|
|
165
|
+
* reads existing entries, appends the new entry, and writes back.
|
|
166
|
+
*
|
|
167
|
+
* @param {Object} entry - The deployment entry (without id)
|
|
168
|
+
* @returns {string} The generated 8-character hex ID
|
|
169
|
+
* @throws {Error} If the entry fails schema validation
|
|
170
|
+
*/
|
|
171
|
+
add(entry) {
|
|
172
|
+
const existingEntries = this._readRegistry()
|
|
173
|
+
const id = this._generateId(entry, existingEntries)
|
|
174
|
+
|
|
175
|
+
const fullEntry = { ...entry, id }
|
|
176
|
+
|
|
177
|
+
const { valid, errors } = this._validateEntry(fullEntry)
|
|
178
|
+
if (!valid) {
|
|
179
|
+
const details = errors.map(e => `${e.instancePath || '/'} ${e.message}`).join(', ')
|
|
180
|
+
throw new Error(`Validation failed: ${details}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
existingEntries.push(fullEntry)
|
|
184
|
+
this._writeRegistry(existingEntries)
|
|
185
|
+
|
|
186
|
+
return id
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get a deployment entry by its ID.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} id - The entry ID to look up
|
|
193
|
+
* @returns {Object|null} The matching entry, or null if not found
|
|
194
|
+
*/
|
|
195
|
+
get(id) {
|
|
196
|
+
const entries = this._readRegistry()
|
|
197
|
+
return entries.find(e => e.id === id) || null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Remove a deployment entry by its ID.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} id - The entry ID to remove
|
|
204
|
+
* @returns {boolean} true if an entry was removed, false if not found
|
|
205
|
+
*/
|
|
206
|
+
remove(id) {
|
|
207
|
+
const entries = this._readRegistry()
|
|
208
|
+
const filtered = entries.filter(e => e.id !== id)
|
|
209
|
+
|
|
210
|
+
if (filtered.length === entries.length) {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this._writeRegistry(filtered)
|
|
215
|
+
return true
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check whether an entry matches all provided filters (AND logic).
|
|
220
|
+
*
|
|
221
|
+
* Supported filter keys:
|
|
222
|
+
* - backend: exact match on deployment.backend
|
|
223
|
+
* - architecture: exact match on deployment.architecture
|
|
224
|
+
* - model: substring match on model.modelName (case-insensitive)
|
|
225
|
+
* - 'instance-type': exact match on infrastructure.instanceType
|
|
226
|
+
* - status: exact match on status
|
|
227
|
+
*
|
|
228
|
+
* @param {Object} entry - The deployment entry to test
|
|
229
|
+
* @param {Object} filters - Key-value pairs of filter criteria
|
|
230
|
+
* @returns {boolean} true if the entry matches all filters
|
|
231
|
+
*/
|
|
232
|
+
_matchesFilters(entry, filters) {
|
|
233
|
+
if (!filters || typeof filters !== 'object') {
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
238
|
+
if (value === undefined || value === null) {
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
switch (key) {
|
|
243
|
+
case 'backend':
|
|
244
|
+
if (entry.deployment?.backend !== value) return false
|
|
245
|
+
break
|
|
246
|
+
case 'architecture':
|
|
247
|
+
if (entry.deployment?.architecture !== value) return false
|
|
248
|
+
break
|
|
249
|
+
case 'model':
|
|
250
|
+
if (!entry.model?.modelName?.toLowerCase().includes(value.toLowerCase())) return false
|
|
251
|
+
break
|
|
252
|
+
case 'instance-type':
|
|
253
|
+
if (entry.infrastructure?.instanceType !== value) return false
|
|
254
|
+
break
|
|
255
|
+
case 'status':
|
|
256
|
+
if (entry.status !== value) return false
|
|
257
|
+
break
|
|
258
|
+
default:
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* List entries from the registry, optionally filtered.
|
|
268
|
+
*
|
|
269
|
+
* Reads all entries and returns those matching every provided filter
|
|
270
|
+
* using AND logic.
|
|
271
|
+
*
|
|
272
|
+
* @param {Object} [filters] - Optional filter criteria
|
|
273
|
+
* @returns {Array<Object>} Matching entries
|
|
274
|
+
*/
|
|
275
|
+
list(filters) {
|
|
276
|
+
const entries = this._readRegistry()
|
|
277
|
+
|
|
278
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
279
|
+
return entries
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return entries.filter(entry => this._matchesFilters(entry, filters))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Search entries using glob-based model matching and standard filters.
|
|
287
|
+
*
|
|
288
|
+
* Similar to list(), but the `model` filter uses glob pattern matching
|
|
289
|
+
* (via minimatch) instead of substring matching.
|
|
290
|
+
*
|
|
291
|
+
* @param {Object} [query] - Search criteria; `model` supports glob patterns
|
|
292
|
+
* @returns {Array<Object>} Matching entries
|
|
293
|
+
*/
|
|
294
|
+
search(query) {
|
|
295
|
+
const entries = this._readRegistry()
|
|
296
|
+
|
|
297
|
+
if (!query || Object.keys(query).length === 0) {
|
|
298
|
+
return entries
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return entries.filter(entry => {
|
|
302
|
+
for (const [key, value] of Object.entries(query)) {
|
|
303
|
+
if (value === undefined || value === null) {
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
switch (key) {
|
|
308
|
+
case 'model':
|
|
309
|
+
if (!entry.model?.modelName || !minimatch(entry.model.modelName, value)) return false
|
|
310
|
+
break
|
|
311
|
+
case 'backend':
|
|
312
|
+
if (entry.deployment?.backend !== value) return false
|
|
313
|
+
break
|
|
314
|
+
case 'architecture':
|
|
315
|
+
if (entry.deployment?.architecture !== value) return false
|
|
316
|
+
break
|
|
317
|
+
case 'instance-type':
|
|
318
|
+
if (entry.infrastructure?.instanceType !== value) return false
|
|
319
|
+
break
|
|
320
|
+
case 'status':
|
|
321
|
+
if (entry.status !== value) return false
|
|
322
|
+
break
|
|
323
|
+
default:
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return true
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Strip sensitive fields from a deployment entry.
|
|
334
|
+
*
|
|
335
|
+
* Returns a deep-cloned copy with the following fields removed:
|
|
336
|
+
* - infrastructure.roleArn
|
|
337
|
+
* - infrastructure.region
|
|
338
|
+
* - configuration.parameters.HF_TOKEN
|
|
339
|
+
* - configuration.parameters.NGC_API_KEY
|
|
340
|
+
*
|
|
341
|
+
* The original entry is not mutated.
|
|
342
|
+
*
|
|
343
|
+
* @param {Object} entry - The deployment entry to sanitize
|
|
344
|
+
* @returns {Object} A sanitized deep copy of the entry
|
|
345
|
+
*/
|
|
346
|
+
_stripSensitiveFields(entry) {
|
|
347
|
+
const copy = JSON.parse(JSON.stringify(entry))
|
|
348
|
+
|
|
349
|
+
if (copy.infrastructure) {
|
|
350
|
+
delete copy.infrastructure.roleArn
|
|
351
|
+
delete copy.infrastructure.region
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (copy.configuration?.parameters) {
|
|
355
|
+
delete copy.configuration.parameters.HF_TOKEN
|
|
356
|
+
delete copy.configuration.parameters.NGC_API_KEY
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return copy
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Export deployment entries in the standard Export_Format.
|
|
364
|
+
*
|
|
365
|
+
* If an id is provided, exports only that single entry.
|
|
366
|
+
* Otherwise exports all entries matching the status filter.
|
|
367
|
+
* By default, only entries with status "success" are exported.
|
|
368
|
+
* All exported entries have sensitive fields stripped.
|
|
369
|
+
*
|
|
370
|
+
* @param {string|null} [id] - Optional entry ID to export a single entry
|
|
371
|
+
* @param {Object} [options] - Export options
|
|
372
|
+
* @param {string} [options.status] - Status filter override (default: 'success')
|
|
373
|
+
* @returns {Object} Export_Format object with version, exportedAt, exportedBy, entries
|
|
374
|
+
*/
|
|
375
|
+
exportEntries(id, options = {}) {
|
|
376
|
+
const entries = this._readRegistry()
|
|
377
|
+
const statusFilter = options.status || 'success'
|
|
378
|
+
|
|
379
|
+
let filtered
|
|
380
|
+
if (id) {
|
|
381
|
+
const entry = entries.find(e => e.id === id)
|
|
382
|
+
filtered = entry ? [entry] : []
|
|
383
|
+
} else {
|
|
384
|
+
filtered = entries.filter(e => e.status === statusFilter)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const sanitized = filtered.map(e => this._stripSensitiveFields(e))
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
version: '1.0',
|
|
391
|
+
exportedAt: new Date().toISOString(),
|
|
392
|
+
exportedBy: 'anonymous',
|
|
393
|
+
entries: sanitized
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Reconstruct CLI flags from a stored deployment entry.
|
|
399
|
+
* Delegates to the standalone reconstructReplayFlags function.
|
|
400
|
+
*
|
|
401
|
+
* @param {Object} entry - The deployment entry
|
|
402
|
+
* @param {Object} [overrides] - User-provided CLI overrides
|
|
403
|
+
* @returns {Object} CLI flag key-value pairs
|
|
404
|
+
*/
|
|
405
|
+
reconstructReplayFlags(entry, overrides = {}) {
|
|
406
|
+
return reconstructReplayFlags(entry, overrides)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Import deployment entries from an Export_Format JSON object.
|
|
411
|
+
*
|
|
412
|
+
* Validates the import format, sets metadata on each entry,
|
|
413
|
+
* detects conflicts, and applies the specified resolution strategy.
|
|
414
|
+
*
|
|
415
|
+
* Conflict detection matches on: modelName, backend, instanceType,
|
|
416
|
+
* and parameters (deep equality via JSON.stringify).
|
|
417
|
+
*
|
|
418
|
+
* Strategies:
|
|
419
|
+
* - 'skip' (default): skip conflicting entries
|
|
420
|
+
* - 'merge': keep both existing and imported entries
|
|
421
|
+
* - 'replace': overwrite existing entries with imported ones
|
|
422
|
+
*
|
|
423
|
+
* @param {Object} json - Parsed Export_Format JSON object
|
|
424
|
+
* @param {string} [strategy='skip'] - Conflict resolution strategy
|
|
425
|
+
* @param {string} [filename='unknown'] - Source filename for metadata
|
|
426
|
+
* @returns {{ added: number, skipped: number, conflicts: number }}
|
|
427
|
+
* @throws {Error} If the import format is invalid
|
|
428
|
+
*/
|
|
429
|
+
importEntries(json, strategy = 'skip', filename = 'unknown') {
|
|
430
|
+
if (!json || typeof json !== 'object' || !json.version || !Array.isArray(json.entries)) {
|
|
431
|
+
throw new Error('Invalid export format: missing required "version" or "entries" fields')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const existingEntries = this._readRegistry()
|
|
435
|
+
let added = 0
|
|
436
|
+
let skipped = 0
|
|
437
|
+
let conflicts = 0
|
|
438
|
+
|
|
439
|
+
for (const importedEntry of json.entries) {
|
|
440
|
+
const entry = JSON.parse(JSON.stringify(importedEntry))
|
|
441
|
+
|
|
442
|
+
if (!entry.metadata) {
|
|
443
|
+
entry.metadata = {}
|
|
444
|
+
}
|
|
445
|
+
entry.metadata.source = 'imported'
|
|
446
|
+
entry.metadata.importedFrom = filename
|
|
447
|
+
|
|
448
|
+
const isConflict = existingEntries.some(existing =>
|
|
449
|
+
existing.model?.modelName === entry.model?.modelName &&
|
|
450
|
+
existing.deployment?.backend === entry.deployment?.backend &&
|
|
451
|
+
existing.infrastructure?.instanceType === entry.infrastructure?.instanceType &&
|
|
452
|
+
JSON.stringify(existing.configuration?.parameters) === JSON.stringify(entry.configuration?.parameters)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if (isConflict) {
|
|
456
|
+
conflicts++
|
|
457
|
+
if (strategy === 'merge') {
|
|
458
|
+
const id = this._generateId(entry, existingEntries)
|
|
459
|
+
entry.id = id
|
|
460
|
+
existingEntries.push(entry)
|
|
461
|
+
} else if (strategy === 'replace') {
|
|
462
|
+
const conflictIndex = existingEntries.findIndex(existing =>
|
|
463
|
+
existing.model?.modelName === entry.model?.modelName &&
|
|
464
|
+
existing.deployment?.backend === entry.deployment?.backend &&
|
|
465
|
+
existing.infrastructure?.instanceType === entry.infrastructure?.instanceType &&
|
|
466
|
+
JSON.stringify(existing.configuration?.parameters) === JSON.stringify(entry.configuration?.parameters)
|
|
467
|
+
)
|
|
468
|
+
if (conflictIndex !== -1) {
|
|
469
|
+
entry.id = existingEntries[conflictIndex].id
|
|
470
|
+
existingEntries[conflictIndex] = entry
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
skipped++
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const id = this._generateId(entry, existingEntries)
|
|
477
|
+
entry.id = id
|
|
478
|
+
existingEntries.push(entry)
|
|
479
|
+
added++
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this._writeRegistry(existingEntries)
|
|
484
|
+
|
|
485
|
+
return { added, skipped, conflicts }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Reconstruct CLI flags from a stored deployment entry.
|
|
492
|
+
*
|
|
493
|
+
* Maps entry fields to their corresponding CLI flags:
|
|
494
|
+
* - deployment.deploymentConfig → --deployment-config
|
|
495
|
+
* - model.modelName → --model-name
|
|
496
|
+
* - infrastructure.instanceType → --instance-type
|
|
497
|
+
* - infrastructure.region → --region
|
|
498
|
+
* - model.modelFormat → --model-format (omitted for transformers architecture)
|
|
499
|
+
*
|
|
500
|
+
* Null/undefined fields are omitted so the generator prompts for them.
|
|
501
|
+
* User overrides take precedence over stored entry values.
|
|
502
|
+
*
|
|
503
|
+
* @param {Object} entry - The deployment entry
|
|
504
|
+
* @param {Object} [overrides={}] - User-provided CLI overrides (keyed by flag name, e.g. '--model-name')
|
|
505
|
+
* @returns {Object} CLI flag key-value pairs
|
|
506
|
+
*/
|
|
507
|
+
export function reconstructReplayFlags(entry, overrides = {}) {
|
|
508
|
+
const flags = {}
|
|
509
|
+
|
|
510
|
+
const mappings = [
|
|
511
|
+
{ field: entry?.deployment?.deploymentConfig, flag: '--deployment-config' },
|
|
512
|
+
{ field: entry?.model?.modelName, flag: '--model-name' },
|
|
513
|
+
{ field: entry?.infrastructure?.instanceType, flag: '--instance-type' },
|
|
514
|
+
{ field: entry?.infrastructure?.region, flag: '--region' },
|
|
515
|
+
]
|
|
516
|
+
|
|
517
|
+
for (const { field, flag } of mappings) {
|
|
518
|
+
if (field != null) {
|
|
519
|
+
flags[flag] = field
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Omit --model-format for transformers architecture
|
|
524
|
+
const isTransformers = entry?.deployment?.architecture === 'transformers'
|
|
525
|
+
if (!isTransformers && entry?.model?.modelFormat != null) {
|
|
526
|
+
flags['--model-format'] = entry.model.modelFormat
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Apply user overrides with higher precedence
|
|
530
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
531
|
+
if (value != null) {
|
|
532
|
+
flags[key] = value
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return flags
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* System environment variables excluded from http architecture deployments.
|
|
541
|
+
* These are standard system/Python vars that are not relevant to the
|
|
542
|
+
* deployment configuration.
|
|
543
|
+
*/
|
|
544
|
+
const HTTP_SYSTEM_VARS = new Set([
|
|
545
|
+
'PATH',
|
|
546
|
+
'PYTHONPATH',
|
|
547
|
+
'SAGEMAKER_BIND_TO_PORT',
|
|
548
|
+
'LANG',
|
|
549
|
+
'GPG_KEY',
|
|
550
|
+
'PYTHON_VERSION',
|
|
551
|
+
'PYTHON_PIP_VERSION',
|
|
552
|
+
'PYTHON_SETUPTOOLS_VERSION',
|
|
553
|
+
'PYTHON_GET_PIP_URL',
|
|
554
|
+
'PYTHON_GET_PIP_SHA256',
|
|
555
|
+
])
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Filter environment variables for transformer architecture deployments.
|
|
559
|
+
*
|
|
560
|
+
* Keeps only vars whose key starts with the given engine prefix
|
|
561
|
+
* (e.g. 'VLLM_', 'SGLANG_'), plus HF_TOKEN and HF_MODEL_ID if present.
|
|
562
|
+
*
|
|
563
|
+
* @param {Object} envVars - Key-value pairs of environment variables
|
|
564
|
+
* @param {string} enginePrefix - Engine prefix string (e.g. 'VLLM_')
|
|
565
|
+
* @returns {Object} Filtered key-value pairs
|
|
566
|
+
*/
|
|
567
|
+
export function filterTransformerEnvVars(envVars, enginePrefix) {
|
|
568
|
+
const result = {}
|
|
569
|
+
|
|
570
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
571
|
+
if (key.startsWith(enginePrefix) || key === 'HF_TOKEN' || key === 'HF_MODEL_ID') {
|
|
572
|
+
result[key] = value
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return result
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Filter environment variables for http architecture deployments.
|
|
581
|
+
*
|
|
582
|
+
* Excludes known system variables (PATH, PYTHONPATH, etc.) and
|
|
583
|
+
* returns everything else.
|
|
584
|
+
*
|
|
585
|
+
* @param {Object} envVars - Key-value pairs of environment variables
|
|
586
|
+
* @returns {Object} Filtered key-value pairs with system vars removed
|
|
587
|
+
*/
|
|
588
|
+
export function filterHttpEnvVars(envVars) {
|
|
589
|
+
const result = {}
|
|
590
|
+
|
|
591
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
592
|
+
if (!HTTP_SYSTEM_VARS.has(key)) {
|
|
593
|
+
result[key] = value
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return result
|
|
598
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker Introspection Validator Strategy (Opt-in)
|
|
3
|
+
*
|
|
4
|
+
* Validates environment variables by introspecting Docker images.
|
|
5
|
+
* This is an experimental strategy that requires Docker to be available.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 13.11, 13.18
|
|
8
|
+
*/
|
|
9
|
+
export default class DockerIntrospectionValidator {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new DockerIntrospectionValidator.
|
|
12
|
+
*/
|
|
13
|
+
constructor() {
|
|
14
|
+
this.name = 'docker-introspection';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate environment variables using Docker introspection.
|
|
19
|
+
*
|
|
20
|
+
* Note: This is an experimental feature and not tested in CI/CD.
|
|
21
|
+
* It requires Docker to be available and the framework image to be pullable.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} framework - Framework name
|
|
24
|
+
* @param {string} version - Framework version
|
|
25
|
+
* @param {Object} envVars - Environment variables to validate
|
|
26
|
+
* @returns {Object} ValidationResult
|
|
27
|
+
* @returns {Array<Object>} ValidationResult.warnings - Warning messages
|
|
28
|
+
* @returns {Array<Object>} ValidationResult.errors - Error messages
|
|
29
|
+
*/
|
|
30
|
+
async validate(_framework, _version, _envVars) {
|
|
31
|
+
const warnings = [];
|
|
32
|
+
const errors = [];
|
|
33
|
+
|
|
34
|
+
// Add experimental warning
|
|
35
|
+
warnings.push({
|
|
36
|
+
key: null,
|
|
37
|
+
message: 'Docker introspection validation is experimental and not tested in CI/CD'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Docker introspection implementation would go here
|
|
41
|
+
// This is a placeholder for the opt-in experimental feature
|
|
42
|
+
// Actual implementation would:
|
|
43
|
+
// 1. Pull the framework Docker image
|
|
44
|
+
// 2. Run a container with the env vars
|
|
45
|
+
// 3. Check if the container starts successfully
|
|
46
|
+
// 4. Parse any error messages from the container logs
|
|
47
|
+
|
|
48
|
+
// For now, just return the experimental warning
|
|
49
|
+
return { warnings, errors };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Engine Prefix Resolver
|
|
6
|
+
*
|
|
7
|
+
* Maps model server names to their engine-specific environment variable
|
|
8
|
+
* prefixes. When propagating --server-env values to the Dockerfile and
|
|
9
|
+
* do/config templates, the resolver prepends the appropriate prefix so
|
|
10
|
+
* users don't need to know internal prefix conventions.
|
|
11
|
+
*
|
|
12
|
+
* Requirements: 4.6
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Engine-to-prefix mapping for server environment variables.
|
|
17
|
+
* Engines not listed here (flask, fastapi) pass keys through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export const ENGINE_PREFIX_MAP = {
|
|
20
|
+
'vllm': 'VLLM_',
|
|
21
|
+
'vllm-omni': 'VLLM_OMNI_',
|
|
22
|
+
'sglang': 'SGLANG_',
|
|
23
|
+
'tensorrt-llm': 'TRTLLM_',
|
|
24
|
+
'lmi': 'LMI_',
|
|
25
|
+
'djl': 'DJL_'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the prefixed key for a given engine and user-provided key.
|
|
30
|
+
* If the engine has a defined prefix, prepends it to the key.
|
|
31
|
+
* If the engine has no prefix (flask, fastapi, or unknown), returns the key unchanged.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} engine - The model server engine name (e.g., 'vllm', 'flask')
|
|
34
|
+
* @param {string} key - The user-provided environment variable key
|
|
35
|
+
* @returns {string} The resolved key with engine prefix applied (or unchanged)
|
|
36
|
+
*/
|
|
37
|
+
export function resolvePrefix(engine, key) {
|
|
38
|
+
const prefix = ENGINE_PREFIX_MAP[engine]
|
|
39
|
+
if (prefix) {
|
|
40
|
+
return `${prefix}${key}`
|
|
41
|
+
}
|
|
42
|
+
return key
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve prefixed keys for a batch of server environment variables.
|
|
47
|
+
* Returns a new object with all keys prefixed according to the engine mapping.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} engine - The model server engine name
|
|
50
|
+
* @param {Object<string, string>} serverEnvVars - Map of user-provided key-value pairs
|
|
51
|
+
* @returns {Object<string, string>} New object with prefixed keys and original values
|
|
52
|
+
*/
|
|
53
|
+
export function resolvePrefixedEnvVars(engine, serverEnvVars) {
|
|
54
|
+
const result = {}
|
|
55
|
+
for (const [key, value] of Object.entries(serverEnvVars)) {
|
|
56
|
+
const prefixedKey = resolvePrefix(engine, key)
|
|
57
|
+
result[prefixedKey] = value
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|