@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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HuggingFace Hub API Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches model metadata from HuggingFace Hub API with graceful error handling.
|
|
5
|
+
* Uses Node.js built-in fetch API (available in Node 18+).
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 11.1, 11.2, 11.3, 11.4, 11.8, 11.10, 11.12
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export default class HuggingFaceClient {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.baseUrl = options.baseUrl || 'https://huggingface.co';
|
|
13
|
+
this.timeout = options.timeout || 5000;
|
|
14
|
+
this.offline = options.offline || false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetch model metadata from HuggingFace Hub API
|
|
19
|
+
* @param {string} modelId - Model ID (e.g., "meta-llama/Llama-2-7b-chat-hf")
|
|
20
|
+
* @returns {Promise<Object|null>} Model metadata or null on failure
|
|
21
|
+
*/
|
|
22
|
+
async fetchModelMetadata(modelId) {
|
|
23
|
+
if (this.offline) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
30
|
+
|
|
31
|
+
const response = await fetch(
|
|
32
|
+
`${this.baseUrl}/api/models/${modelId}`,
|
|
33
|
+
{ signal: controller.signal }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
// Handle rate limits
|
|
40
|
+
if (response.status === 429) {
|
|
41
|
+
console.warn('HuggingFace API rate limit reached');
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Handle not found
|
|
45
|
+
if (response.status === 404) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Other errors
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return await response.json();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Handle timeout
|
|
55
|
+
if (error.name === 'AbortError') {
|
|
56
|
+
console.warn(`HuggingFace API timeout after ${this.timeout}ms`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
// Handle network errors
|
|
60
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
61
|
+
console.warn('HuggingFace API network error');
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Other errors - graceful fallback
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Fetch tokenizer config for chat template detection
|
|
71
|
+
* @param {string} modelId - Model ID
|
|
72
|
+
* @returns {Promise<Object|null>} Tokenizer config or null on failure
|
|
73
|
+
*/
|
|
74
|
+
async fetchTokenizerConfig(modelId) {
|
|
75
|
+
if (this.offline) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
82
|
+
|
|
83
|
+
const response = await fetch(
|
|
84
|
+
`${this.baseUrl}/${modelId}/resolve/main/tokenizer_config.json`,
|
|
85
|
+
{ signal: controller.signal }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
// Handle rate limits
|
|
92
|
+
if (response.status === 429) {
|
|
93
|
+
console.warn('HuggingFace API rate limit reached');
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// Handle not found
|
|
97
|
+
if (response.status === 404) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// Other errors
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return await response.json();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Handle timeout
|
|
107
|
+
if (error.name === 'AbortError') {
|
|
108
|
+
console.warn(`HuggingFace API timeout after ${this.timeout}ms`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
// Handle network errors
|
|
112
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
113
|
+
console.warn('HuggingFace API network error');
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Other errors - graceful fallback
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fetch model config for model architecture
|
|
123
|
+
* @param {string} modelId - Model ID
|
|
124
|
+
* @returns {Promise<Object|null>} Model config or null on failure
|
|
125
|
+
*/
|
|
126
|
+
async fetchModelConfig(modelId) {
|
|
127
|
+
if (this.offline) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
134
|
+
|
|
135
|
+
const response = await fetch(
|
|
136
|
+
`${this.baseUrl}/${modelId}/resolve/main/config.json`,
|
|
137
|
+
{ signal: controller.signal }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
// Handle rate limits
|
|
144
|
+
if (response.status === 429) {
|
|
145
|
+
console.warn('HuggingFace API rate limit reached');
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// Handle not found
|
|
149
|
+
if (response.status === 404) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// Other errors
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return await response.json();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
// Handle timeout
|
|
159
|
+
if (error.name === 'AbortError') {
|
|
160
|
+
console.warn(`HuggingFace API timeout after ${this.timeout}ms`);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
// Handle network errors
|
|
164
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
165
|
+
console.warn('HuggingFace API network error');
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
// Other errors - graceful fallback
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* KEY=VALUE Parser Utility
|
|
6
|
+
*
|
|
7
|
+
* Parses KEY=VALUE strings for --model-env and --server-env CLI flags.
|
|
8
|
+
* Splits only on the first '=' character, allowing values to contain
|
|
9
|
+
* additional '=' characters.
|
|
10
|
+
*
|
|
11
|
+
* Requirements: 3.4, 3.5, 4.4, 4.5
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ValidationError } from './config-manager.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a KEY=VALUE string, splitting only on the first '=' character.
|
|
18
|
+
* @param {string} input - Raw CLI value (e.g., "TENSOR_PARALLEL_SIZE=4")
|
|
19
|
+
* @returns {{ key: string, value: string }}
|
|
20
|
+
* @throws {ValidationError} if no '=' is present
|
|
21
|
+
*/
|
|
22
|
+
export function parseKeyValue(input) {
|
|
23
|
+
const idx = input.indexOf('=')
|
|
24
|
+
|
|
25
|
+
if (idx === -1) {
|
|
26
|
+
throw new ValidationError(
|
|
27
|
+
`Invalid format for env var: expected KEY=VALUE, got '${input}'`,
|
|
28
|
+
'env',
|
|
29
|
+
input
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const key = input.substring(0, idx)
|
|
34
|
+
const value = input.substring(idx + 1)
|
|
35
|
+
|
|
36
|
+
return { key, value }
|
|
37
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known Flags Validator Strategy
|
|
3
|
+
*
|
|
4
|
+
* Validates environment variables against a registry of known flags for each framework.
|
|
5
|
+
* Checks variable names, types, and range constraints.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 13.9, 13.13, 13.14, 13.15
|
|
8
|
+
*/
|
|
9
|
+
export default class KnownFlagsValidator {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new KnownFlagsValidator.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} frameworkFlags - Framework flags registry
|
|
14
|
+
*/
|
|
15
|
+
constructor(frameworkFlags = {}) {
|
|
16
|
+
this.frameworkFlags = frameworkFlags;
|
|
17
|
+
this.name = 'known-flags-registry';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate environment variables against known flags registry.
|
|
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
|
+
// Get known flags for this framework version
|
|
35
|
+
const knownFlags = this.getKnownFlags(framework, version);
|
|
36
|
+
|
|
37
|
+
if (!knownFlags || Object.keys(knownFlags).length === 0) {
|
|
38
|
+
// No known flags data available
|
|
39
|
+
return { warnings, errors };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate each environment variable
|
|
43
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
44
|
+
const flagSpec = knownFlags[key];
|
|
45
|
+
|
|
46
|
+
if (!flagSpec) {
|
|
47
|
+
// Unknown flag - might be valid but not in our registry
|
|
48
|
+
warnings.push({
|
|
49
|
+
key,
|
|
50
|
+
message: `Unknown environment variable '${key}' for ${framework} ${version}`
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if flag is deprecated
|
|
56
|
+
if (flagSpec.deprecated) {
|
|
57
|
+
warnings.push({
|
|
58
|
+
key,
|
|
59
|
+
message: `Environment variable '${key}' is deprecated. ${flagSpec.deprecationMessage || ''}`
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (flagSpec.replacement) {
|
|
63
|
+
warnings.push({
|
|
64
|
+
key,
|
|
65
|
+
message: `Consider using '${flagSpec.replacement}' instead of '${key}'`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate type
|
|
71
|
+
const typeError = this.validateType(key, value, flagSpec.type);
|
|
72
|
+
if (typeError) {
|
|
73
|
+
errors.push(typeError);
|
|
74
|
+
continue; // Skip range validation if type is wrong
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate range constraints
|
|
78
|
+
const rangeError = this.validateRange(key, value, flagSpec);
|
|
79
|
+
if (rangeError) {
|
|
80
|
+
errors.push(rangeError);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { warnings, errors };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get known flags for a framework version.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} framework - Framework name
|
|
91
|
+
* @param {string} version - Framework version
|
|
92
|
+
* @returns {Object|null} Known flags specification
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
getKnownFlags(framework, version) {
|
|
96
|
+
if (!this.frameworkFlags[framework]) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Try exact version match first
|
|
101
|
+
if (this.frameworkFlags[framework][version]) {
|
|
102
|
+
return this.frameworkFlags[framework][version];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Try to find closest version (simplified - just use 'default' if available)
|
|
106
|
+
if (this.frameworkFlags[framework].default) {
|
|
107
|
+
return this.frameworkFlags[framework].default;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate environment variable type.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} key - Variable name
|
|
117
|
+
* @param {string} value - Variable value
|
|
118
|
+
* @param {string} expectedType - Expected type (integer, float, string, boolean)
|
|
119
|
+
* @returns {Object|null} Error object or null if valid
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
validateType(key, value, expectedType) {
|
|
123
|
+
if (!expectedType) {
|
|
124
|
+
return null; // No type constraint
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
switch (expectedType) {
|
|
128
|
+
case 'integer':
|
|
129
|
+
if (!/^-?\d+$/.test(value)) {
|
|
130
|
+
return {
|
|
131
|
+
key,
|
|
132
|
+
message: `Environment variable '${key}' must be an integer, got '${value}'`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'float':
|
|
138
|
+
if (!/^-?\d+(\.\d+)?$/.test(value)) {
|
|
139
|
+
return {
|
|
140
|
+
key,
|
|
141
|
+
message: `Environment variable '${key}' must be a float, got '${value}'`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'boolean':
|
|
147
|
+
if (!['true', 'false', '0', '1', 'yes', 'no'].includes(value.toLowerCase())) {
|
|
148
|
+
return {
|
|
149
|
+
key,
|
|
150
|
+
message: `Environment variable '${key}' must be a boolean (true/false, 0/1, yes/no), got '${value}'`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'string':
|
|
156
|
+
// String is always valid
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
// Unknown type - skip validation
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validate environment variable range constraints.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} key - Variable name
|
|
171
|
+
* @param {string} value - Variable value
|
|
172
|
+
* @param {Object} flagSpec - Flag specification with min/max constraints
|
|
173
|
+
* @returns {Object|null} Error object or null if valid
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
validateRange(key, value, flagSpec) {
|
|
177
|
+
// Only validate range for numeric types
|
|
178
|
+
if (flagSpec.type !== 'integer' && flagSpec.type !== 'float') {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const numValue = parseFloat(value);
|
|
183
|
+
|
|
184
|
+
if (flagSpec.min !== undefined && numValue < flagSpec.min) {
|
|
185
|
+
return {
|
|
186
|
+
key,
|
|
187
|
+
message: `Environment variable '${key}' must be >= ${flagSpec.min}, got ${value}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (flagSpec.max !== undefined && numValue > flagSpec.max) {
|
|
192
|
+
return {
|
|
193
|
+
key,
|
|
194
|
+
message: `Environment variable '${key}' must be <= ${flagSpec.max}, got ${value}`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manifest CLI Wrapper
|
|
8
|
+
*
|
|
9
|
+
* A standalone Node.js script invoked by the `do/manifest` shell helper.
|
|
10
|
+
* Reads the bootstrap config to resolve the active profile, instantiates
|
|
11
|
+
* AssetManager, and dispatches commands.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node do/lib/manifest-cli.js add --type <type> --id <id> --project <project> [--meta <json>]
|
|
15
|
+
* node do/lib/manifest-cli.js delete --id <id>
|
|
16
|
+
* node do/lib/manifest-cli.js list [--project <project>] [--status <status>] [--type <type>]
|
|
17
|
+
*
|
|
18
|
+
* Validates: Requirements 9.1–9.7
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import AssetManager, { VALID_RESOURCE_TYPES, VALID_STATUSES } from './asset-manager.js'
|
|
22
|
+
import BootstrapConfig from './bootstrap-config.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse command-line arguments into a map of flag → value pairs.
|
|
26
|
+
* Supports --flag value syntax. Positional args are ignored.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]} argv - The raw process.argv array
|
|
29
|
+
* @returns {{ subcommand: string|null, flags: Object }}
|
|
30
|
+
*/
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
// argv[0] = node, argv[1] = script path, argv[2] = subcommand, argv[3..] = flags
|
|
33
|
+
const args = argv.slice(2)
|
|
34
|
+
const subcommand = args.length > 0 && !args[0].startsWith('--') ? args[0] : null
|
|
35
|
+
const flags = {}
|
|
36
|
+
|
|
37
|
+
for (let i = subcommand ? 1 : 0; i < args.length; i++) {
|
|
38
|
+
if (args[i].startsWith('--') && i + 1 < args.length) {
|
|
39
|
+
const key = args[i].slice(2)
|
|
40
|
+
flags[key] = args[i + 1]
|
|
41
|
+
i++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { subcommand, flags }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Print usage information and exit.
|
|
50
|
+
*/
|
|
51
|
+
function printUsage() {
|
|
52
|
+
console.log('Usage:')
|
|
53
|
+
console.log(' manifest add --type <resourceType> --id <resourceId> --project <projectName> [--meta <json>]')
|
|
54
|
+
console.log(' manifest delete --id <resourceId>')
|
|
55
|
+
console.log(' manifest list [--project <project>] [--status <status>] [--type <type>]')
|
|
56
|
+
console.log(' manifest prune')
|
|
57
|
+
console.log('')
|
|
58
|
+
console.log('Valid resource types:')
|
|
59
|
+
console.log(` ${VALID_RESOURCE_TYPES.join(', ')}`)
|
|
60
|
+
console.log('')
|
|
61
|
+
console.log('Valid statuses:')
|
|
62
|
+
console.log(` ${VALID_STATUSES.join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format resources as a table for console output.
|
|
67
|
+
*
|
|
68
|
+
* @param {Array<Object>} resources - Array of Asset_Records
|
|
69
|
+
*/
|
|
70
|
+
function printResourceTable(resources) {
|
|
71
|
+
if (resources.length === 0) {
|
|
72
|
+
console.log('No resources found.')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Header
|
|
77
|
+
const header = ['Type', 'Resource ID', 'Project', 'Status', 'Created At']
|
|
78
|
+
const widths = header.map(h => h.length)
|
|
79
|
+
|
|
80
|
+
// Calculate column widths
|
|
81
|
+
for (const r of resources) {
|
|
82
|
+
widths[0] = Math.max(widths[0], (r.resourceType || '').length)
|
|
83
|
+
widths[1] = Math.max(widths[1], Math.min((r.resourceId || '').length, 60))
|
|
84
|
+
widths[2] = Math.max(widths[2], (r.project || '').length)
|
|
85
|
+
widths[3] = Math.max(widths[3], (r.status || '').length)
|
|
86
|
+
widths[4] = Math.max(widths[4], (r.createdAt || '').length)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pad = (str, width) => String(str).padEnd(width)
|
|
90
|
+
const separator = widths.map(w => '-'.repeat(w)).join(' ')
|
|
91
|
+
|
|
92
|
+
console.log(header.map((h, i) => pad(h, widths[i])).join(' '))
|
|
93
|
+
console.log(separator)
|
|
94
|
+
|
|
95
|
+
for (const r of resources) {
|
|
96
|
+
const id = (r.resourceId || '').length > 60
|
|
97
|
+
? r.resourceId.slice(0, 57) + '...'
|
|
98
|
+
: r.resourceId || ''
|
|
99
|
+
console.log([
|
|
100
|
+
pad(r.resourceType || '', widths[0]),
|
|
101
|
+
pad(id, widths[1]),
|
|
102
|
+
pad(r.project || '', widths[2]),
|
|
103
|
+
pad(r.status || '', widths[3]),
|
|
104
|
+
pad(r.createdAt || '', widths[4])
|
|
105
|
+
].join(' '))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Handle the `add` subcommand.
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} flags - Parsed flags
|
|
113
|
+
* @param {AssetManager} assetManager - The AssetManager instance
|
|
114
|
+
*/
|
|
115
|
+
function handleAdd(flags, assetManager) {
|
|
116
|
+
const { type, id, project, meta } = flags
|
|
117
|
+
|
|
118
|
+
if (!type || !id || !project) {
|
|
119
|
+
console.error('Error: --type, --id, and --project are required for the add command.')
|
|
120
|
+
console.log('')
|
|
121
|
+
printUsage()
|
|
122
|
+
process.exitCode = 1
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!VALID_RESOURCE_TYPES.includes(type)) {
|
|
127
|
+
console.error(`Error: Invalid resource type "${type}".`)
|
|
128
|
+
console.error(`Valid types: ${VALID_RESOURCE_TYPES.join(', ')}`)
|
|
129
|
+
process.exitCode = 1
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let metadata = {}
|
|
134
|
+
if (meta) {
|
|
135
|
+
try {
|
|
136
|
+
metadata = JSON.parse(meta)
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(`Error: Invalid JSON for --meta: ${err.message}`)
|
|
139
|
+
process.exitCode = 1
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const now = new Date().toISOString()
|
|
145
|
+
const record = {
|
|
146
|
+
resourceId: id,
|
|
147
|
+
resourceType: type,
|
|
148
|
+
createdAt: now,
|
|
149
|
+
lastUpdatedAt: now,
|
|
150
|
+
project,
|
|
151
|
+
status: 'active',
|
|
152
|
+
metadata
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
assetManager.addResource(record)
|
|
156
|
+
console.log(`Added ${type}: ${id}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle the `delete` subcommand.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} flags - Parsed flags
|
|
163
|
+
* @param {AssetManager} assetManager - The AssetManager instance
|
|
164
|
+
*/
|
|
165
|
+
function handleDelete(flags, assetManager) {
|
|
166
|
+
const { id } = flags
|
|
167
|
+
|
|
168
|
+
if (!id) {
|
|
169
|
+
console.error('Error: --id is required for the delete command.')
|
|
170
|
+
console.log('')
|
|
171
|
+
printUsage()
|
|
172
|
+
process.exitCode = 1
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const updated = assetManager.updateStatus(id, 'deleted')
|
|
177
|
+
if (updated) {
|
|
178
|
+
console.log(`Marked as deleted: ${id}`)
|
|
179
|
+
} else {
|
|
180
|
+
console.log(`Resource not found in manifest: ${id}`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle the `list` subcommand.
|
|
186
|
+
*
|
|
187
|
+
* @param {Object} flags - Parsed flags
|
|
188
|
+
* @param {AssetManager} assetManager - The AssetManager instance
|
|
189
|
+
*/
|
|
190
|
+
function handleList(flags, assetManager) {
|
|
191
|
+
const filters = {}
|
|
192
|
+
if (flags.project) filters.project = flags.project
|
|
193
|
+
if (flags.status) filters.status = flags.status
|
|
194
|
+
if (flags.type) filters.resourceType = flags.type
|
|
195
|
+
|
|
196
|
+
const resources = assetManager.listResources(filters)
|
|
197
|
+
printResourceTable(resources)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle the `prune` subcommand — remove deleted and unknown records.
|
|
202
|
+
*
|
|
203
|
+
* @param {AssetManager} assetManager - The AssetManager instance
|
|
204
|
+
*/
|
|
205
|
+
function handlePrune(assetManager) {
|
|
206
|
+
const all = assetManager.listResources()
|
|
207
|
+
const stale = all.filter(r => r.status === 'deleted' || r.status === 'unknown')
|
|
208
|
+
|
|
209
|
+
if (stale.length === 0) {
|
|
210
|
+
console.log('Nothing to prune — no deleted or unknown records found.')
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const resource of stale) {
|
|
215
|
+
assetManager.removeResource(resource.resourceId)
|
|
216
|
+
console.log(` 🗑️ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const remaining = assetManager.listResources()
|
|
220
|
+
console.log(`\nPruned ${stale.length} record${stale.length === 1 ? '' : 's'}. ${remaining.length} remaining.`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Main entry point. Resolves the active bootstrap profile,
|
|
225
|
+
* instantiates AssetManager, and dispatches the subcommand.
|
|
226
|
+
*
|
|
227
|
+
* @param {string[]} argv - The raw process.argv array
|
|
228
|
+
*/
|
|
229
|
+
export function main(argv) {
|
|
230
|
+
const { subcommand, flags } = parseArgs(argv)
|
|
231
|
+
|
|
232
|
+
if (!subcommand) {
|
|
233
|
+
printUsage()
|
|
234
|
+
process.exitCode = 1
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Resolve active bootstrap profile
|
|
239
|
+
const bootstrapConfig = new BootstrapConfig()
|
|
240
|
+
const activeProfile = bootstrapConfig.getActiveProfile()
|
|
241
|
+
|
|
242
|
+
if (!activeProfile) {
|
|
243
|
+
console.warn('Warning: No active bootstrap profile configured. Skipping manifest operation.')
|
|
244
|
+
console.warn('Run "ml-container-creator bootstrap" to configure a profile.')
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const assetManager = new AssetManager(activeProfile.name)
|
|
249
|
+
|
|
250
|
+
switch (subcommand) {
|
|
251
|
+
case 'add':
|
|
252
|
+
handleAdd(flags, assetManager)
|
|
253
|
+
break
|
|
254
|
+
case 'delete':
|
|
255
|
+
handleDelete(flags, assetManager)
|
|
256
|
+
break
|
|
257
|
+
case 'list':
|
|
258
|
+
handleList(flags, assetManager)
|
|
259
|
+
break
|
|
260
|
+
case 'prune':
|
|
261
|
+
handlePrune(assetManager)
|
|
262
|
+
break
|
|
263
|
+
default:
|
|
264
|
+
console.error(`Unknown subcommand: ${subcommand}`)
|
|
265
|
+
console.log('')
|
|
266
|
+
printUsage()
|
|
267
|
+
process.exitCode = 1
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Run when executed directly
|
|
273
|
+
const isDirectExecution = process.argv[1] && (
|
|
274
|
+
process.argv[1].endsWith('manifest-cli.js') ||
|
|
275
|
+
process.argv[1].endsWith('do/lib/manifest-cli.js')
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if (isDirectExecution) {
|
|
279
|
+
main(process.argv)
|
|
280
|
+
}
|