@aws/ml-container-creator 0.9.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-THIRD-PARTY +9304 -0
- package/bin/cli.js +2 -0
- package/config/bootstrap-e2e-stack.json +341 -0
- package/config/bootstrap-stack.json +40 -3
- package/config/parameter-schema-v2.json +2049 -0
- package/config/tune-catalog.json +1781 -0
- package/infra/ci-harness/buildspec.yml +1 -0
- package/infra/ci-harness/lambda/path-prover/brain.ts +306 -0
- package/infra/ci-harness/lambda/path-prover/write-results.ts +152 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +837 -7
- package/infra/ci-harness/state-machines/path-prover.asl.json +496 -0
- package/package.json +53 -68
- package/servers/base-image-picker/index.js +121 -121
- package/servers/e2e-status/index.js +297 -0
- package/servers/e2e-status/manifest.json +14 -0
- package/servers/e2e-status/package.json +15 -0
- package/servers/endpoint-picker/LICENSE +202 -0
- package/servers/endpoint-picker/index.js +536 -0
- package/servers/endpoint-picker/manifest.json +14 -0
- package/servers/endpoint-picker/package.json +18 -0
- package/servers/hyperpod-cluster-picker/index.js +125 -125
- package/servers/instance-sizer/index.js +138 -138
- package/servers/instance-sizer/lib/instance-ranker.js +76 -76
- package/servers/instance-sizer/lib/model-resolver.js +61 -61
- package/servers/instance-sizer/lib/quota-resolver.js +113 -113
- package/servers/instance-sizer/lib/vram-estimator.js +31 -31
- package/servers/lib/bedrock-client.js +38 -38
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/model-servers.json +201 -3
- package/servers/lib/catalogs/models.json +182 -26
- package/servers/lib/custom-validators.js +13 -13
- package/servers/lib/dynamic-resolver.js +4 -4
- package/servers/marketplace-picker/index.js +342 -0
- package/servers/marketplace-picker/manifest.json +14 -0
- package/servers/marketplace-picker/package.json +18 -0
- package/servers/model-picker/index.js +382 -382
- package/servers/region-picker/index.js +56 -56
- package/servers/workload-picker/LICENSE +202 -0
- package/servers/workload-picker/catalogs/workload-profiles.json +67 -0
- package/servers/workload-picker/index.js +171 -0
- package/servers/workload-picker/manifest.json +16 -0
- package/servers/workload-picker/package.json +16 -0
- package/src/app.js +4 -390
- package/src/lib/bootstrap-command-handler.js +710 -1148
- package/src/lib/bootstrap-config.js +36 -0
- package/src/lib/bootstrap-profile-manager.js +641 -0
- package/src/lib/bootstrap-provisioners.js +421 -0
- package/src/lib/ci-register-helpers.js +74 -0
- package/src/lib/config-loader.js +408 -0
- package/src/lib/config-manager.js +66 -1685
- package/src/lib/config-mcp-client.js +118 -0
- package/src/lib/config-validator.js +634 -0
- package/src/lib/cuda-resolver.js +149 -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 +315 -311
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +71 -71
- 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/path-prover-brain.js +607 -0
- 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 +82 -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 +422 -0
- package/src/lib/tune-catalog-validator.js +37 -4
- package/templates/Dockerfile +9 -0
- package/templates/code/adapter_sidecar.py +444 -0
- package/templates/code/serve +6 -0
- package/templates/code/serve.d/vllm.ejs +1 -1
- package/templates/do/.benchmark_writer.py +1476 -0
- package/templates/do/.tune_helper.py +982 -57
- package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
- package/templates/do/adapter +149 -0
- package/templates/do/benchmark +639 -85
- package/templates/do/config +108 -5
- package/templates/do/deploy.d/managed-inference.ejs +192 -11
- package/templates/do/optimize +106 -37
- package/templates/do/register +89 -0
- package/templates/do/test +13 -0
- package/templates/do/tune +378 -59
- package/templates/do/validate +44 -4
- package/config/parameter-schema.json +0 -88
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles AWS resource provisioning for bootstrap (IAM role, ECR, S3 buckets).
|
|
14
|
+
* Delegates back to the BootstrapCommandHandler instance for shared helpers.
|
|
15
|
+
*/
|
|
16
|
+
export default class BootstrapProvisioners {
|
|
17
|
+
constructor(handler) {
|
|
18
|
+
this.handler = handler;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create or reuse the SageMaker execution IAM role.
|
|
23
|
+
* @param {object} options - Parsed CLI options
|
|
24
|
+
* @returns {Promise<string>} Role ARN
|
|
25
|
+
*/
|
|
26
|
+
async _setupIamRole(_options) {
|
|
27
|
+
const roleName = 'mlcc-sagemaker-execution-role';
|
|
28
|
+
|
|
29
|
+
// Define trust policy for SageMaker
|
|
30
|
+
const trustPolicy = {
|
|
31
|
+
Version: '2012-10-17',
|
|
32
|
+
Statement: [
|
|
33
|
+
{
|
|
34
|
+
Effect: 'Allow',
|
|
35
|
+
Principal: {
|
|
36
|
+
Service: 'sagemaker.amazonaws.com'
|
|
37
|
+
},
|
|
38
|
+
Action: 'sts:AssumeRole'
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Define execution policy with least-privilege permissions
|
|
44
|
+
const executionPolicy = {
|
|
45
|
+
Version: '2012-10-17',
|
|
46
|
+
Statement: [
|
|
47
|
+
{
|
|
48
|
+
Sid: 'SageMakerEndpoints',
|
|
49
|
+
Effect: 'Allow',
|
|
50
|
+
Action: [
|
|
51
|
+
'sagemaker:CreateEndpoint',
|
|
52
|
+
'sagemaker:CreateEndpointConfig',
|
|
53
|
+
'sagemaker:CreateModel',
|
|
54
|
+
'sagemaker:CreateInferenceComponent',
|
|
55
|
+
'sagemaker:UpdateEndpoint',
|
|
56
|
+
'sagemaker:UpdateEndpointWeightsAndCapacities',
|
|
57
|
+
'sagemaker:UpdateInferenceComponent',
|
|
58
|
+
'sagemaker:DeleteEndpoint',
|
|
59
|
+
'sagemaker:DeleteEndpointConfig',
|
|
60
|
+
'sagemaker:DeleteModel',
|
|
61
|
+
'sagemaker:DeleteInferenceComponent',
|
|
62
|
+
'sagemaker:DescribeEndpoint',
|
|
63
|
+
'sagemaker:DescribeEndpointConfig',
|
|
64
|
+
'sagemaker:DescribeModel',
|
|
65
|
+
'sagemaker:DescribeInferenceComponent',
|
|
66
|
+
'sagemaker:ListInferenceComponents',
|
|
67
|
+
'sagemaker:InvokeEndpoint',
|
|
68
|
+
'sagemaker:InvokeEndpointAsync'
|
|
69
|
+
],
|
|
70
|
+
Resource: '*'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
Sid: 'SageMakerBenchmarking',
|
|
74
|
+
Effect: 'Allow',
|
|
75
|
+
Action: [
|
|
76
|
+
'sagemaker:CreateAIBenchmarkJob',
|
|
77
|
+
'sagemaker:DescribeAIBenchmarkJob',
|
|
78
|
+
'sagemaker:ListAIBenchmarkJobs',
|
|
79
|
+
'sagemaker:StopAIBenchmarkJob',
|
|
80
|
+
'sagemaker:DeleteAIBenchmarkJob',
|
|
81
|
+
'sagemaker:CreateAIWorkloadConfig',
|
|
82
|
+
'sagemaker:DescribeAIWorkloadConfig',
|
|
83
|
+
'sagemaker:ListAIWorkloadConfigs',
|
|
84
|
+
'sagemaker:DeleteAIWorkloadConfig'
|
|
85
|
+
],
|
|
86
|
+
Resource: '*'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
Sid: 'ECRPull',
|
|
90
|
+
Effect: 'Allow',
|
|
91
|
+
Action: [
|
|
92
|
+
'ecr:GetAuthorizationToken',
|
|
93
|
+
'ecr:BatchCheckLayerAvailability',
|
|
94
|
+
'ecr:GetDownloadUrlForLayer',
|
|
95
|
+
'ecr:BatchGetImage'
|
|
96
|
+
],
|
|
97
|
+
Resource: 'arn:aws:ecr:*:*:repository/ml-container-creator'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
Sid: 'ECRAuth',
|
|
101
|
+
Effect: 'Allow',
|
|
102
|
+
Action: 'ecr:GetAuthorizationToken',
|
|
103
|
+
Resource: '*'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
Sid: 'CloudWatchLogs',
|
|
107
|
+
Effect: 'Allow',
|
|
108
|
+
Action: [
|
|
109
|
+
'logs:CreateLogGroup',
|
|
110
|
+
'logs:CreateLogStream',
|
|
111
|
+
'logs:PutLogEvents'
|
|
112
|
+
],
|
|
113
|
+
Resource: 'arn:aws:logs:*:*:*'
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
Sid: 'S3ModelRead',
|
|
117
|
+
Effect: 'Allow',
|
|
118
|
+
Action: [
|
|
119
|
+
's3:GetObject',
|
|
120
|
+
's3:PutObject',
|
|
121
|
+
's3:AbortMultipartUpload',
|
|
122
|
+
's3:ListBucket'
|
|
123
|
+
],
|
|
124
|
+
Resource: [
|
|
125
|
+
'arn:aws:s3:::ml-container-creator-*',
|
|
126
|
+
'arn:aws:s3:::ml-container-creator-*/*'
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
Sid: 'SNSPublish',
|
|
131
|
+
Effect: 'Allow',
|
|
132
|
+
Action: 'sns:Publish',
|
|
133
|
+
Resource: 'arn:aws:sns:*:*:ml-container-creator-*'
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
Sid: 'SecretsManagerBenchmark',
|
|
137
|
+
Effect: 'Allow',
|
|
138
|
+
Action: [
|
|
139
|
+
'secretsmanager:CreateSecret',
|
|
140
|
+
'secretsmanager:PutSecretValue',
|
|
141
|
+
'secretsmanager:GetSecretValue',
|
|
142
|
+
'secretsmanager:DescribeSecret'
|
|
143
|
+
],
|
|
144
|
+
Resource: 'arn:aws:secretsmanager:*:*:secret:ml-container-creator/*'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
Sid: 'QuotaAndAvailability',
|
|
148
|
+
Effect: 'Allow',
|
|
149
|
+
Action: [
|
|
150
|
+
'service-quotas:GetServiceQuota',
|
|
151
|
+
'service-quotas:ListServiceQuotas',
|
|
152
|
+
'sagemaker:ListTrainingPlans',
|
|
153
|
+
'sagemaker:DescribeTrainingPlan',
|
|
154
|
+
'sagemaker:ListEndpoints'
|
|
155
|
+
],
|
|
156
|
+
Resource: '*'
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Check if role already exists
|
|
162
|
+
const roleExists = this.handler._resourceExists(
|
|
163
|
+
`iam get-role --role-name ${roleName}`,
|
|
164
|
+
this.handler._currentProfile
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (roleExists) {
|
|
168
|
+
const existingRole = this.handler._execAws(
|
|
169
|
+
`iam get-role --role-name ${roleName}`,
|
|
170
|
+
this.handler._currentProfile
|
|
171
|
+
);
|
|
172
|
+
const roleArn = existingRole.Role.Arn;
|
|
173
|
+
console.log(` ✅ IAM role "${roleName}" already exists — reused`);
|
|
174
|
+
|
|
175
|
+
// Always update the inline policy and tags to ensure they're current
|
|
176
|
+
try {
|
|
177
|
+
const execPolicyFile = this.handler._writeJsonTempFile(executionPolicy, 'exec-policy');
|
|
178
|
+
this.handler._execAws(
|
|
179
|
+
`iam put-role-policy --role-name ${roleName} --policy-name mlcc-execution-policy --policy-document ${execPolicyFile}`,
|
|
180
|
+
this.handler._currentProfile
|
|
181
|
+
);
|
|
182
|
+
console.log(' ✅ IAM policy "mlcc-execution-policy" — updated');
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(` ⚠️ Could not update inline policy: ${err.message}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const tags = this._buildResourceTags();
|
|
189
|
+
this.handler._execAws(
|
|
190
|
+
`iam tag-role --role-name ${roleName} --tags ${this.handler._formatTagsForCli(tags)}`,
|
|
191
|
+
this.handler._currentProfile
|
|
192
|
+
);
|
|
193
|
+
console.log(' ✅ IAM role tags — updated');
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.log(` ⚠️ Could not update role tags: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return roleArn;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Display policies to user before creation
|
|
202
|
+
console.log('\n Trust Policy:');
|
|
203
|
+
console.log(JSON.stringify(trustPolicy, null, 2));
|
|
204
|
+
console.log('\n Execution Policy:');
|
|
205
|
+
console.log(JSON.stringify(executionPolicy, null, 2));
|
|
206
|
+
console.log('');
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Create the IAM role — write policy to temp file to avoid shell escaping issues
|
|
210
|
+
const trustPolicyFile = this.handler._writeJsonTempFile(trustPolicy, 'trust-policy');
|
|
211
|
+
const createRoleResult = this.handler._execAws(
|
|
212
|
+
`iam create-role --role-name ${roleName} --assume-role-policy-document ${trustPolicyFile}`,
|
|
213
|
+
this.handler._currentProfile
|
|
214
|
+
);
|
|
215
|
+
const roleArn = createRoleResult.Role.Arn;
|
|
216
|
+
|
|
217
|
+
// Attach inline execution policy
|
|
218
|
+
const execPolicyFile = this.handler._writeJsonTempFile(executionPolicy, 'exec-policy');
|
|
219
|
+
this.handler._execAws(
|
|
220
|
+
`iam put-role-policy --role-name ${roleName} --policy-name mlcc-execution-policy --policy-document ${execPolicyFile}`,
|
|
221
|
+
this.handler._currentProfile
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Apply resource tags
|
|
225
|
+
const tags = this._buildResourceTags();
|
|
226
|
+
this.handler._execAws(
|
|
227
|
+
`iam tag-role --role-name ${roleName} --tags ${this.handler._formatTagsForCli(tags)}`,
|
|
228
|
+
this.handler._currentProfile
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
console.log(` ✅ IAM role "${roleName}" — created`);
|
|
232
|
+
return roleArn;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const errorMessage = error.message || '';
|
|
235
|
+
if (errorMessage.includes('AccessDenied') || errorMessage.includes('UnauthorizedAccess')) {
|
|
236
|
+
console.log(' ⚠️ Permission denied for iam:CreateRole. Please provide an existing role ARN.');
|
|
237
|
+
const { roleArn } = await this.handler._promptFn([{
|
|
238
|
+
type: 'input',
|
|
239
|
+
name: 'roleArn',
|
|
240
|
+
message: 'Enter an existing IAM role ARN for SageMaker execution:'
|
|
241
|
+
}]);
|
|
242
|
+
return roleArn;
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create or reuse the ECR repository.
|
|
250
|
+
* @returns {Promise<string>} ECR repository name
|
|
251
|
+
*/
|
|
252
|
+
async _setupEcrRepository() {
|
|
253
|
+
const repoName = 'ml-container-creator';
|
|
254
|
+
|
|
255
|
+
// Check if repository already exists
|
|
256
|
+
const repoExists = this.handler._resourceExists(
|
|
257
|
+
`ecr describe-repositories --repository-names ${repoName} --region ${this.handler._currentRegion}`,
|
|
258
|
+
this.handler._currentProfile
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (repoExists) {
|
|
262
|
+
console.log(` ✅ ECR repository "${repoName}" already exists — reused`);
|
|
263
|
+
return repoName;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Build resource tags
|
|
267
|
+
const tags = this._buildResourceTags();
|
|
268
|
+
|
|
269
|
+
// Create the ECR repository with image scanning and AES256 encryption
|
|
270
|
+
this.handler._execAws(
|
|
271
|
+
`ecr create-repository --repository-name ${repoName} --image-scanning-configuration scanOnPush=true --encryption-configuration encryptionType=AES256 --region ${this.handler._currentRegion} --tags ${this.handler._formatTagsForCli(tags)}`,
|
|
272
|
+
this.handler._currentProfile
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Apply lifecycle policy to expire untagged images after 30 days
|
|
276
|
+
const lifecyclePolicy = {
|
|
277
|
+
rules: [
|
|
278
|
+
{
|
|
279
|
+
rulePriority: 1,
|
|
280
|
+
description: 'Expire untagged images after 30 days',
|
|
281
|
+
selection: {
|
|
282
|
+
tagStatus: 'untagged',
|
|
283
|
+
countType: 'sinceImagePushed',
|
|
284
|
+
countUnit: 'days',
|
|
285
|
+
countNumber: 30
|
|
286
|
+
},
|
|
287
|
+
action: {
|
|
288
|
+
type: 'expire'
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const lifecyclePolicyFile = this.handler._writeJsonTempFile(lifecyclePolicy, 'ecr-lifecycle');
|
|
295
|
+
this.handler._execAws(
|
|
296
|
+
`ecr put-lifecycle-policy --repository-name ${repoName} --lifecycle-policy-text ${lifecyclePolicyFile} --region ${this.handler._currentRegion}`,
|
|
297
|
+
this.handler._currentProfile
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
console.log(` ✅ ECR repository "${repoName}" — created`);
|
|
301
|
+
return repoName;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Optionally create S3 buckets for async/batch deployments.
|
|
306
|
+
* Always creates the benchmark S3 bucket (unconditional).
|
|
307
|
+
* @returns {Promise<object|null>} Bucket names or null if skipped
|
|
308
|
+
*/
|
|
309
|
+
async _setupS3Buckets() {
|
|
310
|
+
// Always create benchmark bucket (unconditional — avoids re-bootstrap when benchmarking is enabled later)
|
|
311
|
+
const benchmarkBucketName = `ml-container-creator-benchmark-${this.handler._currentRegion}-${this.handler._currentAccountId}`;
|
|
312
|
+
const tags = this._buildResourceTags();
|
|
313
|
+
const benchmarkS3Bucket = await this._createS3Bucket(benchmarkBucketName, tags);
|
|
314
|
+
|
|
315
|
+
const { useS3 } = await this.handler._promptFn([{
|
|
316
|
+
type: 'confirm',
|
|
317
|
+
name: 'useS3',
|
|
318
|
+
message: 'Will you use async inference or batch transform?',
|
|
319
|
+
default: false
|
|
320
|
+
}]);
|
|
321
|
+
|
|
322
|
+
if (!useS3) {
|
|
323
|
+
return { benchmarkS3Bucket };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const asyncBucketName = `ml-container-creator-async-${this.handler._currentRegion}-${this.handler._currentAccountId}`;
|
|
327
|
+
const batchBucketName = `ml-container-creator-batch-${this.handler._currentRegion}-${this.handler._currentAccountId}`;
|
|
328
|
+
|
|
329
|
+
const asyncS3Bucket = await this._createS3Bucket(asyncBucketName, tags);
|
|
330
|
+
const batchS3Bucket = await this._createS3Bucket(batchBucketName, tags);
|
|
331
|
+
|
|
332
|
+
return { asyncS3Bucket, batchS3Bucket, benchmarkS3Bucket };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Create or reuse a single S3 bucket with versioning, encryption, and tags.
|
|
337
|
+
* @param {string} bucketName - S3 bucket name
|
|
338
|
+
* @param {Array<{Key: string, Value: string}>} tags - Resource tags
|
|
339
|
+
* @returns {Promise<string>} Bucket name
|
|
340
|
+
*/
|
|
341
|
+
async _createS3Bucket(bucketName, tags) {
|
|
342
|
+
// Check if bucket already exists
|
|
343
|
+
const bucketExists = this.handler._resourceExists(
|
|
344
|
+
`s3api head-bucket --bucket ${bucketName}`,
|
|
345
|
+
this.handler._currentProfile
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (bucketExists) {
|
|
349
|
+
console.log(` ✅ S3 bucket "${bucketName}" already exists — reused`);
|
|
350
|
+
return bucketName;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Build create-bucket command with region-appropriate configuration
|
|
354
|
+
let createCommand = `s3api create-bucket --bucket ${bucketName} --region ${this.handler._currentRegion}`;
|
|
355
|
+
if (this.handler._currentRegion !== 'us-east-1') {
|
|
356
|
+
createCommand += ` --create-bucket-configuration LocationConstraint=${this.handler._currentRegion}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.handler._execAws(createCommand, this.handler._currentProfile);
|
|
360
|
+
|
|
361
|
+
// Enable versioning
|
|
362
|
+
this.handler._execAws(
|
|
363
|
+
`s3api put-bucket-versioning --bucket ${bucketName} --versioning-configuration Status=Enabled`,
|
|
364
|
+
this.handler._currentProfile
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Enable AES256 server-side encryption
|
|
368
|
+
const encryptionConfig = { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' } }] };
|
|
369
|
+
const encryptionFile = this.handler._writeJsonTempFile(encryptionConfig, 's3-encryption');
|
|
370
|
+
this.handler._execAws(
|
|
371
|
+
`s3api put-bucket-encryption --bucket ${bucketName} --server-side-encryption-configuration ${encryptionFile}`,
|
|
372
|
+
this.handler._currentProfile
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Apply resource tags
|
|
376
|
+
const tagging = { TagSet: tags };
|
|
377
|
+
const taggingFile = this.handler._writeJsonTempFile(tagging, 's3-tagging');
|
|
378
|
+
this.handler._execAws(
|
|
379
|
+
`s3api put-bucket-tagging --bucket ${bucketName} --tagging ${taggingFile}`,
|
|
380
|
+
this.handler._currentProfile
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
console.log(` ✅ S3 bucket "${bucketName}" — created`);
|
|
384
|
+
return bucketName;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Verify AWS CLI v2 is installed. Returns true if v2 is detected, false otherwise.
|
|
389
|
+
* @returns {boolean}
|
|
390
|
+
*/
|
|
391
|
+
_verifyCliV2() {
|
|
392
|
+
try {
|
|
393
|
+
const versionOutput = execSync('aws --version', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
394
|
+
if (!versionOutput.includes('aws-cli/2')) {
|
|
395
|
+
console.log(` ❌ AWS CLI v2 is required. Detected: ${versionOutput.split(' ')[0]}`);
|
|
396
|
+
console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html');
|
|
397
|
+
console.log(' Some features (benchmarking, newer SageMaker APIs) require CLI v2.\n');
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
} catch {
|
|
402
|
+
console.log(' ❌ AWS CLI not found.');
|
|
403
|
+
console.log(' Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html\n');
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Build the standard resource tag set.
|
|
410
|
+
* @returns {Array<{Key: string, Value: string}>} Tag array
|
|
411
|
+
*/
|
|
412
|
+
_buildResourceTags() {
|
|
413
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
414
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
415
|
+
return [
|
|
416
|
+
{ Key: 'mlcc:managed-by', Value: 'ml-container-creator' },
|
|
417
|
+
{ Key: 'mlcc:created-by', Value: 'bootstrap' },
|
|
418
|
+
{ Key: 'mlcc:version', Value: packageJson.version }
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -104,6 +104,15 @@ export function applyRecordDefaults(record) {
|
|
|
104
104
|
if (!record.projectName) {
|
|
105
105
|
record.projectName = '';
|
|
106
106
|
}
|
|
107
|
+
// Benchmark fields — optional, backward-compatible defaults (Requirement 7.1, 7.4)
|
|
108
|
+
if (record.benchmarkEnabled === undefined || record.benchmarkEnabled === null) {
|
|
109
|
+
record.benchmarkEnabled = false;
|
|
110
|
+
}
|
|
111
|
+
if (!record.benchmarkConcurrencyLevels) {
|
|
112
|
+
record.benchmarkConcurrencyLevels = [1, 4, 8];
|
|
113
|
+
}
|
|
114
|
+
// lastBenchmarkRunId, lastBenchmarkTimestamp, lastBenchmarkStatus are intentionally
|
|
115
|
+
// NOT defaulted — their absence indicates "never benchmarked" (Requirement 7.4)
|
|
107
116
|
return record;
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -122,3 +131,68 @@ export function extractBaseImageVersion(baseImage) {
|
|
|
122
131
|
}
|
|
123
132
|
return baseImage.split(':').pop();
|
|
124
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the benchmark fields to update on a DynamoDB CI record after
|
|
137
|
+
* a benchmark stage completes (or fails).
|
|
138
|
+
*
|
|
139
|
+
* Only returns the benchmark-specific fields — caller merges into the
|
|
140
|
+
* existing record. Existing fields (testStatus, configJson, etc.) are
|
|
141
|
+
* intentionally NOT included to satisfy Requirement 7.3.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} runId - Benchmark run identifier (e.g., "bmk-20260609T143022Z")
|
|
144
|
+
* @param {string} status - One of: "completed", "failed", "in-progress"
|
|
145
|
+
* @param {string} [timestamp] - ISO 8601 timestamp; defaults to current time
|
|
146
|
+
* @returns {object} Object with lastBenchmarkRunId, lastBenchmarkTimestamp, lastBenchmarkStatus
|
|
147
|
+
*/
|
|
148
|
+
export function buildBenchmarkFields(runId, status, timestamp) {
|
|
149
|
+
const validStatuses = ['completed', 'failed', 'in-progress'];
|
|
150
|
+
if (!validStatuses.includes(status)) {
|
|
151
|
+
throw new Error(`Invalid benchmark status: '${status}'. Must be one of: ${validStatuses.join(', ')}`);
|
|
152
|
+
}
|
|
153
|
+
if (!runId || typeof runId !== 'string') {
|
|
154
|
+
throw new Error('Benchmark runId is required and must be a non-empty string');
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
lastBenchmarkRunId: runId,
|
|
158
|
+
lastBenchmarkTimestamp: timestamp || new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
159
|
+
lastBenchmarkStatus: status
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check whether a CI record has ever been benchmarked.
|
|
165
|
+
*
|
|
166
|
+
* Per Requirement 7.4, absence of `lastBenchmarkRunId` indicates
|
|
167
|
+
* "never benchmarked" — this is the canonical check.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} record - A CI DynamoDB record
|
|
170
|
+
* @returns {boolean} True if the record has benchmark data
|
|
171
|
+
*/
|
|
172
|
+
export function hasBeenBenchmarked(record) {
|
|
173
|
+
return !!(record && record.lastBenchmarkRunId);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check whether benchmarking is enabled for a CI record/config.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} record - A CI DynamoDB record (with defaults applied)
|
|
180
|
+
* @returns {boolean} True if benchmarkEnabled is true
|
|
181
|
+
*/
|
|
182
|
+
export function isBenchmarkEnabled(record) {
|
|
183
|
+
if (!record) return false;
|
|
184
|
+
return record.benchmarkEnabled === true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get the benchmark concurrency levels for a CI record/config.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} record - A CI DynamoDB record (with defaults applied)
|
|
191
|
+
* @returns {number[]} Array of concurrency level integers
|
|
192
|
+
*/
|
|
193
|
+
export function getBenchmarkConcurrencyLevels(record) {
|
|
194
|
+
if (!record || !Array.isArray(record.benchmarkConcurrencyLevels)) {
|
|
195
|
+
return [1, 4, 8];
|
|
196
|
+
}
|
|
197
|
+
return record.benchmarkConcurrencyLevels;
|
|
198
|
+
}
|