@aws/ml-container-creator 0.2.1 → 0.2.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/bin/cli.js +88 -86
- package/config/bootstrap-stack.json +211 -0
- package/config/parameter-schema.json +88 -0
- package/infra/ci-harness/bin/ci-harness.ts +26 -0
- package/infra/ci-harness/buildspec.yml +352 -0
- package/infra/ci-harness/cdk.json +27 -0
- package/infra/ci-harness/lambda/scanner/index.ts +199 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +609 -0
- package/infra/ci-harness/package-lock.json +3979 -0
- package/infra/ci-harness/package.json +32 -0
- package/infra/ci-harness/tsconfig.json +38 -0
- package/package.json +13 -3
- package/src/app.js +318 -318
- package/src/copy-tpl.js +19 -19
- package/src/lib/asset-manager.js +74 -74
- package/src/lib/aws-profile-parser.js +45 -45
- package/src/lib/bootstrap-command-handler.js +560 -547
- package/src/lib/bootstrap-config.js +45 -45
- package/src/lib/ci-register-helpers.js +19 -19
- package/src/lib/ci-report-helpers.js +37 -37
- package/src/lib/ci-stage-helpers.js +49 -49
- package/src/lib/comment-generator.js +4 -4
- package/src/lib/config-manager.js +105 -105
- package/src/lib/deployment-config-resolver.js +10 -10
- package/src/lib/deployment-registry.js +153 -153
- package/src/lib/engine-prefix-resolver.js +8 -8
- package/src/lib/key-value-parser.js +6 -6
- package/src/lib/manifest-cli.js +108 -108
- package/src/lib/prompt-runner.js +224 -224
- package/src/lib/prompts.js +121 -121
- package/src/lib/registry-command-handler.js +174 -174
- package/src/lib/registry-loader.js +52 -52
- package/src/lib/sensitive-redactor.js +9 -9
- package/src/lib/template-engine.js +1 -1
- package/src/lib/template-manager.js +62 -62
- package/src/prompt-adapter.js +18 -18
|
@@ -16,27 +16,27 @@
|
|
|
16
16
|
* remove <profile> [--force] Remove a bootstrap profile
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { execSync } from 'node:child_process'
|
|
20
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
21
|
-
import path from 'node:path'
|
|
22
|
-
import { tmpdir } from 'node:os'
|
|
23
|
-
import { fileURLToPath } from 'node:url'
|
|
24
|
-
import BootstrapConfig from './bootstrap-config.js'
|
|
25
|
-
import AwsProfileParser from './aws-profile-parser.js'
|
|
26
|
-
import AssetManager from './asset-manager.js'
|
|
27
|
-
import { runPrompts } from '../prompt-adapter.js'
|
|
28
|
-
|
|
29
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
30
|
-
const __dirname = path.dirname(__filename)
|
|
31
|
-
|
|
32
|
-
const STACK_NAME_PREFIX = 'mlcc-bootstrap'
|
|
33
|
-
const STACK_TEMPLATE_PATH = path.resolve(__dirname, '../../config/bootstrap-stack.json')
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import BootstrapConfig from './bootstrap-config.js';
|
|
25
|
+
import AwsProfileParser from './aws-profile-parser.js';
|
|
26
|
+
import AssetManager from './asset-manager.js';
|
|
27
|
+
import { runPrompts } from '../prompt-adapter.js';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
const STACK_NAME_PREFIX = 'mlcc-bootstrap';
|
|
33
|
+
const STACK_TEMPLATE_PATH = path.resolve(__dirname, '../../config/bootstrap-stack.json');
|
|
34
34
|
|
|
35
35
|
export default class BootstrapCommandHandler {
|
|
36
36
|
constructor({ promptFn } = {}) {
|
|
37
|
-
this.config = new BootstrapConfig()
|
|
38
|
-
this.profileParser = new AwsProfileParser()
|
|
39
|
-
this._promptFn = promptFn || runPrompts
|
|
37
|
+
this.config = new BootstrapConfig();
|
|
38
|
+
this.profileParser = new AwsProfileParser();
|
|
39
|
+
this._promptFn = promptFn || runPrompts;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -46,38 +46,38 @@ export default class BootstrapCommandHandler {
|
|
|
46
46
|
*/
|
|
47
47
|
async handle(args, options) {
|
|
48
48
|
if (args.length === 0) {
|
|
49
|
-
await this._handleInteractiveSetup(options)
|
|
50
|
-
return
|
|
49
|
+
await this._handleInteractiveSetup(options);
|
|
50
|
+
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const subcommand = args[0].toLowerCase()
|
|
53
|
+
const subcommand = args[0].toLowerCase();
|
|
54
54
|
|
|
55
55
|
switch (subcommand) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
56
|
+
case 'status':
|
|
57
|
+
await this._handleStatus(options);
|
|
58
|
+
break;
|
|
59
|
+
case 'use':
|
|
60
|
+
await this._handleUse(args[1]);
|
|
61
|
+
break;
|
|
62
|
+
case 'list':
|
|
63
|
+
await this._handleList();
|
|
64
|
+
break;
|
|
65
|
+
case 'remove':
|
|
66
|
+
await this._handleRemove(args[1], options);
|
|
67
|
+
break;
|
|
68
|
+
case 'scan':
|
|
69
|
+
await this._handleScan();
|
|
70
|
+
break;
|
|
71
|
+
case 'prune':
|
|
72
|
+
await this._handlePrune();
|
|
73
|
+
break;
|
|
74
|
+
case 'update':
|
|
75
|
+
await this._handleUpdate(options);
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
console.log(`Unknown bootstrap subcommand: ${subcommand}`);
|
|
79
|
+
this._showHelp();
|
|
80
|
+
break;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -86,125 +86,125 @@ export default class BootstrapCommandHandler {
|
|
|
86
86
|
* @param {object} options - Parsed CLI options
|
|
87
87
|
*/
|
|
88
88
|
async _handleInteractiveSetup(options) {
|
|
89
|
-
const nonInteractive = options['non-interactive']
|
|
89
|
+
const nonInteractive = options['non-interactive'];
|
|
90
90
|
|
|
91
91
|
// Non-interactive mode: validate required flags upfront
|
|
92
92
|
if (nonInteractive) {
|
|
93
|
-
const missingFlags = []
|
|
93
|
+
const missingFlags = [];
|
|
94
94
|
if (!options.profile) {
|
|
95
|
-
missingFlags.push('--profile')
|
|
95
|
+
missingFlags.push('--profile');
|
|
96
96
|
}
|
|
97
97
|
if (!options.region) {
|
|
98
|
-
missingFlags.push('--region')
|
|
98
|
+
missingFlags.push('--region');
|
|
99
99
|
}
|
|
100
100
|
if (missingFlags.length > 0) {
|
|
101
|
-
console.log(`❌ Missing required flags for non-interactive mode: ${missingFlags.join(', ')}`)
|
|
102
|
-
return
|
|
101
|
+
console.log(`❌ Missing required flags for non-interactive mode: ${missingFlags.join(', ')}`);
|
|
102
|
+
return;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
console.log('\n🚀 Bootstrap — Shared AWS Infrastructure Setup\n')
|
|
106
|
+
console.log('\n🚀 Bootstrap — Shared AWS Infrastructure Setup\n');
|
|
107
107
|
|
|
108
108
|
// Determine bootstrap profile name
|
|
109
|
-
let profileName
|
|
109
|
+
let profileName;
|
|
110
110
|
if (nonInteractive) {
|
|
111
|
-
profileName = options.name || 'default'
|
|
111
|
+
profileName = options.name || 'default';
|
|
112
112
|
} else {
|
|
113
113
|
const answer = await this._promptFn([{
|
|
114
114
|
type: 'input',
|
|
115
115
|
name: 'profileName',
|
|
116
116
|
message: 'Bootstrap profile name:',
|
|
117
117
|
default: 'default'
|
|
118
|
-
}])
|
|
119
|
-
profileName = answer.profileName
|
|
118
|
+
}]);
|
|
119
|
+
profileName = answer.profileName;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const profileData = {}
|
|
122
|
+
const profileData = {};
|
|
123
123
|
|
|
124
124
|
// Step 1: AWS profile selection
|
|
125
|
-
this._displayProgress('🔍', 'Selecting AWS profile...')
|
|
126
|
-
let awsProfile
|
|
125
|
+
this._displayProgress('🔍', 'Selecting AWS profile...');
|
|
126
|
+
let awsProfile;
|
|
127
127
|
if (nonInteractive) {
|
|
128
|
-
awsProfile = options.profile
|
|
128
|
+
awsProfile = options.profile;
|
|
129
129
|
} else {
|
|
130
|
-
awsProfile = await this._selectProfile(options)
|
|
130
|
+
awsProfile = await this._selectProfile(options);
|
|
131
131
|
}
|
|
132
|
-
profileData.awsProfile = awsProfile
|
|
133
|
-
this._currentProfile = awsProfile
|
|
132
|
+
profileData.awsProfile = awsProfile;
|
|
133
|
+
this._currentProfile = awsProfile;
|
|
134
134
|
|
|
135
135
|
// Step 2: Credential validation
|
|
136
|
-
this._displayProgress('🔑', 'Validating AWS credentials...')
|
|
137
|
-
const { accountId, region } = await this._validateCredentials(awsProfile, nonInteractive ? options.region : undefined)
|
|
138
|
-
profileData.accountId = accountId
|
|
139
|
-
profileData.awsRegion = region
|
|
140
|
-
this._currentRegion = region
|
|
141
|
-
this._currentAccountId = accountId
|
|
136
|
+
this._displayProgress('🔑', 'Validating AWS credentials...');
|
|
137
|
+
const { accountId, region } = await this._validateCredentials(awsProfile, nonInteractive ? options.region : undefined);
|
|
138
|
+
profileData.accountId = accountId;
|
|
139
|
+
profileData.awsRegion = region;
|
|
140
|
+
this._currentRegion = region;
|
|
141
|
+
this._currentAccountId = accountId;
|
|
142
142
|
|
|
143
143
|
// Step 3: Determine stack parameters
|
|
144
|
-
let useExistingRoleArn = ''
|
|
144
|
+
let useExistingRoleArn = '';
|
|
145
145
|
if (nonInteractive && options['role-arn']) {
|
|
146
|
-
useExistingRoleArn = options['role-arn']
|
|
147
|
-
console.log(` Using provided IAM role ARN: ${options['role-arn']}`)
|
|
146
|
+
useExistingRoleArn = options['role-arn'];
|
|
147
|
+
console.log(` Using provided IAM role ARN: ${options['role-arn']}`);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
let createS3Buckets = false
|
|
150
|
+
let createS3Buckets = false;
|
|
151
151
|
if (nonInteractive && options['skip-s3']) {
|
|
152
|
-
console.log(' ⏭️ Skipping S3 bucket creation (--skip-s3)')
|
|
152
|
+
console.log(' ⏭️ Skipping S3 bucket creation (--skip-s3)');
|
|
153
153
|
} else if (nonInteractive) {
|
|
154
|
-
createS3Buckets = true
|
|
154
|
+
createS3Buckets = true;
|
|
155
155
|
} else {
|
|
156
156
|
const { useS3 } = await this._promptFn([{
|
|
157
157
|
type: 'confirm',
|
|
158
158
|
name: 'useS3',
|
|
159
159
|
message: 'Will you use async inference or batch transform?',
|
|
160
160
|
default: false
|
|
161
|
-
}])
|
|
162
|
-
createS3Buckets = useS3
|
|
161
|
+
}]);
|
|
162
|
+
createS3Buckets = useS3;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// Step 4: Deploy CloudFormation stack
|
|
166
|
-
this._displayProgress('☁️', 'Deploying bootstrap infrastructure stack...')
|
|
167
|
-
const stackName = `${STACK_NAME_PREFIX}-${profileName}
|
|
166
|
+
this._displayProgress('☁️', 'Deploying bootstrap infrastructure stack...');
|
|
167
|
+
const stackName = `${STACK_NAME_PREFIX}-${profileName}`;
|
|
168
168
|
|
|
169
169
|
try {
|
|
170
170
|
const stackOutputs = this._deployStack(stackName, {
|
|
171
171
|
CreateS3Buckets: createS3Buckets ? 'true' : 'false',
|
|
172
172
|
UseExistingRoleArn: useExistingRoleArn
|
|
173
|
-
}, awsProfile, region)
|
|
173
|
+
}, awsProfile, region);
|
|
174
174
|
|
|
175
175
|
// Read outputs into profile data
|
|
176
|
-
profileData.roleArn = stackOutputs.RoleArn
|
|
177
|
-
profileData.ecrRepositoryName = stackOutputs.EcrRepositoryName
|
|
178
|
-
profileData.stackName = stackName
|
|
176
|
+
profileData.roleArn = stackOutputs.RoleArn;
|
|
177
|
+
profileData.ecrRepositoryName = stackOutputs.EcrRepositoryName;
|
|
178
|
+
profileData.stackName = stackName;
|
|
179
179
|
|
|
180
180
|
if (stackOutputs.AsyncS3BucketName) {
|
|
181
|
-
profileData.asyncS3Bucket = stackOutputs.AsyncS3BucketName
|
|
181
|
+
profileData.asyncS3Bucket = stackOutputs.AsyncS3BucketName;
|
|
182
182
|
}
|
|
183
183
|
if (stackOutputs.BatchS3BucketName) {
|
|
184
|
-
profileData.batchS3Bucket = stackOutputs.BatchS3BucketName
|
|
184
|
+
profileData.batchS3Bucket = stackOutputs.BatchS3BucketName;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
console.log(' ✅ Bootstrap stack deployed successfully')
|
|
187
|
+
console.log(' ✅ Bootstrap stack deployed successfully');
|
|
188
188
|
} catch (error) {
|
|
189
|
-
console.log(` ❌ Stack deployment failed: ${error.message}`)
|
|
190
|
-
console.log(' Check the CloudFormation console for details:')
|
|
191
|
-
console.log(` https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks`)
|
|
192
|
-
return
|
|
189
|
+
console.log(` ❌ Stack deployment failed: ${error.message}`);
|
|
190
|
+
console.log(' Check the CloudFormation console for details:');
|
|
191
|
+
console.log(` https://console.aws.amazon.com/cloudformation/home?region=${region}#/stacks`);
|
|
192
|
+
return;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Step 5: CI Infrastructure setup (separate CDK stack — unchanged)
|
|
196
|
-
this._displayProgress('🧪', 'CI Testing Infrastructure...')
|
|
196
|
+
this._displayProgress('🧪', 'CI Testing Infrastructure...');
|
|
197
197
|
try {
|
|
198
|
-
let provisionCi = false
|
|
198
|
+
let provisionCi = false;
|
|
199
199
|
|
|
200
200
|
if (nonInteractive) {
|
|
201
201
|
if (options.ci) {
|
|
202
|
-
provisionCi = true
|
|
202
|
+
provisionCi = true;
|
|
203
203
|
} else if (options['skip-ci']) {
|
|
204
|
-
console.log(' ⏭️ Skipping CI infrastructure (--skip-ci)')
|
|
205
|
-
provisionCi = false
|
|
204
|
+
console.log(' ⏭️ Skipping CI infrastructure (--skip-ci)');
|
|
205
|
+
provisionCi = false;
|
|
206
206
|
} else {
|
|
207
|
-
provisionCi = false
|
|
207
|
+
provisionCi = false;
|
|
208
208
|
}
|
|
209
209
|
} else {
|
|
210
210
|
const ciAnswer = await this._promptFn([{
|
|
@@ -212,8 +212,8 @@ export default class BootstrapCommandHandler {
|
|
|
212
212
|
name: 'useCi',
|
|
213
213
|
message: 'Do you want CI testing infrastructure?',
|
|
214
214
|
default: false
|
|
215
|
-
}])
|
|
216
|
-
provisionCi = ciAnswer.useCi
|
|
215
|
+
}]);
|
|
216
|
+
provisionCi = ciAnswer.useCi;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
if (provisionCi) {
|
|
@@ -221,10 +221,10 @@ export default class BootstrapCommandHandler {
|
|
|
221
221
|
const cdkBootstrapped = this._resourceExists(
|
|
222
222
|
`ssm get-parameter --name /cdk-bootstrap/hnb659fds/version --region ${profileData.awsRegion}`,
|
|
223
223
|
profileData.awsProfile
|
|
224
|
-
)
|
|
224
|
+
);
|
|
225
225
|
|
|
226
226
|
if (!cdkBootstrapped) {
|
|
227
|
-
console.log(' 📦 CDK has not been bootstrapped in this account/region — bootstrapping now...')
|
|
227
|
+
console.log(' 📦 CDK has not been bootstrapped in this account/region — bootstrapping now...');
|
|
228
228
|
try {
|
|
229
229
|
execSync(
|
|
230
230
|
`npx cdk bootstrap aws://${profileData.accountId}/${profileData.awsRegion}`,
|
|
@@ -236,12 +236,12 @@ export default class BootstrapCommandHandler {
|
|
|
236
236
|
AWS_PROFILE: profileData.awsProfile
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
)
|
|
240
|
-
console.log(' ✅ CDK bootstrap complete')
|
|
239
|
+
);
|
|
240
|
+
console.log(' ✅ CDK bootstrap complete');
|
|
241
241
|
} catch (cdkErr) {
|
|
242
|
-
console.log(` ❌ CDK bootstrap failed: ${cdkErr.message}`)
|
|
243
|
-
console.log(` Run manually: npx cdk bootstrap aws://${profileData.accountId}/${profileData.awsRegion} --profile ${profileData.awsProfile}`)
|
|
244
|
-
throw cdkErr
|
|
242
|
+
console.log(` ❌ CDK bootstrap failed: ${cdkErr.message}`);
|
|
243
|
+
console.log(` Run manually: npx cdk bootstrap aws://${profileData.accountId}/${profileData.awsRegion} --profile ${profileData.awsProfile}`);
|
|
244
|
+
throw cdkErr;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -249,52 +249,59 @@ export default class BootstrapCommandHandler {
|
|
|
249
249
|
const ciStackExists = this._resourceExists(
|
|
250
250
|
`cloudformation describe-stacks --stack-name MlccCiHarnessStack --region ${profileData.awsRegion}`,
|
|
251
251
|
profileData.awsProfile
|
|
252
|
-
)
|
|
252
|
+
);
|
|
253
253
|
|
|
254
254
|
if (ciStackExists) {
|
|
255
|
-
console.log(' ✅ CI stack already deployed — updating if needed...')
|
|
255
|
+
console.log(' ✅ CI stack already deployed — updating if needed...');
|
|
256
256
|
} else {
|
|
257
|
-
console.log(' 🚀 Deploying CI harness stack...')
|
|
257
|
+
console.log(' 🚀 Deploying CI harness stack...');
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
const ciHarnessDir = path.resolve(__dirname, '../../infra/ci-harness')
|
|
260
|
+
const ciHarnessDir = path.resolve(__dirname, '../../infra/ci-harness');
|
|
261
261
|
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
`npx cdk deploy MlccCiHarnessStack --require-approval never`,
|
|
271
|
-
{
|
|
262
|
+
// CI harness source is not bundled in the npm package — only available from git clone
|
|
263
|
+
if (!existsSync(ciHarnessDir)) {
|
|
264
|
+
console.log(' ⚠️ CI harness source not available (npm install does not include infra/)');
|
|
265
|
+
console.log(' To deploy the CI stack, clone the repo: git clone https://github.com/awslabs/ml-container-creator');
|
|
266
|
+
console.log(' Then run: cd ml-container-creator/infra/ci-harness && npx cdk deploy MlccCiHarnessStack');
|
|
267
|
+
} else {
|
|
268
|
+
// Ensure dependencies are installed (handles cold starts / fresh clones)
|
|
269
|
+
execSync('npm install --silent', {
|
|
272
270
|
cwd: ciHarnessDir,
|
|
273
271
|
encoding: 'utf8',
|
|
274
|
-
stdio: '
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
272
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
execSync(
|
|
276
|
+
'npx cdk deploy MlccCiHarnessStack --require-approval never',
|
|
277
|
+
{
|
|
278
|
+
cwd: ciHarnessDir,
|
|
279
|
+
encoding: 'utf8',
|
|
280
|
+
stdio: 'inherit',
|
|
281
|
+
env: {
|
|
282
|
+
...process.env,
|
|
283
|
+
CDK_DEFAULT_REGION: profileData.awsRegion,
|
|
284
|
+
CDK_DEFAULT_ACCOUNT: profileData.accountId,
|
|
285
|
+
AWS_PROFILE: profileData.awsProfile
|
|
286
|
+
}
|
|
280
287
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
console.log(' ✅ CI harness stack deployed')
|
|
288
|
+
);
|
|
289
|
+
console.log(' ✅ CI harness stack deployed');
|
|
284
290
|
|
|
285
|
-
|
|
286
|
-
|
|
291
|
+
profileData.ciInfraProvisioned = true;
|
|
292
|
+
profileData.ciTableName = 'mlcc-ci-table';
|
|
293
|
+
}
|
|
287
294
|
}
|
|
288
295
|
} catch (error) {
|
|
289
|
-
console.log(`⚠️ CI infrastructure setup failed: ${error.message}`)
|
|
296
|
+
console.log(`⚠️ CI infrastructure setup failed: ${error.message}`);
|
|
290
297
|
}
|
|
291
298
|
|
|
292
299
|
// Save profile to config
|
|
293
|
-
this.config.setProfile(profileName, profileData)
|
|
294
|
-
this._displayProgress('✅', `Profile "${profileName}" saved to config`)
|
|
300
|
+
this.config.setProfile(profileName, profileData);
|
|
301
|
+
this._displayProgress('✅', `Profile "${profileName}" saved to config`);
|
|
295
302
|
|
|
296
303
|
// Display summary
|
|
297
|
-
this._displaySummary(profileName, profileData)
|
|
304
|
+
this._displaySummary(profileName, profileData);
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
/**
|
|
@@ -302,106 +309,106 @@ export default class BootstrapCommandHandler {
|
|
|
302
309
|
* @param {object} [options] - Parsed CLI options (e.g., --verify)
|
|
303
310
|
*/
|
|
304
311
|
async _handleStatus(options = {}) {
|
|
305
|
-
const config = this.config.read()
|
|
312
|
+
const config = this.config.read();
|
|
306
313
|
if (!config) {
|
|
307
|
-
console.log('No bootstrap configuration found.')
|
|
308
|
-
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.')
|
|
309
|
-
return
|
|
314
|
+
console.log('No bootstrap configuration found.');
|
|
315
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
316
|
+
return;
|
|
310
317
|
}
|
|
311
318
|
|
|
312
|
-
const profile = this.config.getActiveProfile()
|
|
319
|
+
const profile = this.config.getActiveProfile();
|
|
313
320
|
if (!profile) {
|
|
314
|
-
console.log('No active bootstrap profile found.')
|
|
315
|
-
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.')
|
|
316
|
-
return
|
|
321
|
+
console.log('No active bootstrap profile found.');
|
|
322
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
323
|
+
return;
|
|
317
324
|
}
|
|
318
325
|
|
|
319
|
-
const allProfiles = this.config.listProfiles()
|
|
320
|
-
console.log(`\n📋 Active Profile: ${profile.name} (${allProfiles.length} profile${allProfiles.length === 1 ? '' : 's'} total)`)
|
|
321
|
-
console.log('─'.repeat(40))
|
|
326
|
+
const allProfiles = this.config.listProfiles();
|
|
327
|
+
console.log(`\n📋 Active Profile: ${profile.name} (${allProfiles.length} profile${allProfiles.length === 1 ? '' : 's'} total)`);
|
|
328
|
+
console.log('─'.repeat(40));
|
|
322
329
|
|
|
323
330
|
for (const [key, value] of Object.entries(profile.config)) {
|
|
324
|
-
console.log(` ${key}: ${value}`)
|
|
331
|
+
console.log(` ${key}: ${value}`);
|
|
325
332
|
}
|
|
326
333
|
|
|
327
|
-
console.log('─'.repeat(40))
|
|
334
|
+
console.log('─'.repeat(40));
|
|
328
335
|
|
|
329
336
|
// Validate bootstrap stack
|
|
330
|
-
console.log('\n🔍 Resource Validation:')
|
|
337
|
+
console.log('\n🔍 Resource Validation:');
|
|
331
338
|
|
|
332
|
-
const stackName = profile.config.stackName || `${STACK_NAME_PREFIX}-${profile.name}
|
|
339
|
+
const stackName = profile.config.stackName || `${STACK_NAME_PREFIX}-${profile.name}`;
|
|
333
340
|
|
|
334
341
|
try {
|
|
335
342
|
const stackInfo = this._execAws(
|
|
336
343
|
`cloudformation describe-stacks --stack-name ${stackName} --region ${profile.config.awsRegion}`,
|
|
337
344
|
profile.config.awsProfile
|
|
338
|
-
)
|
|
345
|
+
);
|
|
339
346
|
|
|
340
|
-
const stack = stackInfo.Stacks && stackInfo.Stacks[0]
|
|
347
|
+
const stack = stackInfo.Stacks && stackInfo.Stacks[0];
|
|
341
348
|
if (stack) {
|
|
342
|
-
const status = stack.StackStatus
|
|
343
|
-
const statusIcon = status === 'CREATE_COMPLETE' || status === 'UPDATE_COMPLETE' ? '✅' : '⚠️'
|
|
344
|
-
console.log(` ${statusIcon} Bootstrap stack: ${stackName} (${status})`)
|
|
349
|
+
const status = stack.StackStatus;
|
|
350
|
+
const statusIcon = status === 'CREATE_COMPLETE' || status === 'UPDATE_COMPLETE' ? '✅' : '⚠️';
|
|
351
|
+
console.log(` ${statusIcon} Bootstrap stack: ${stackName} (${status})`);
|
|
345
352
|
|
|
346
353
|
// Show stack outputs
|
|
347
|
-
const outputs = {}
|
|
354
|
+
const outputs = {};
|
|
348
355
|
for (const output of (stack.Outputs || [])) {
|
|
349
|
-
outputs[output.OutputKey] = output.OutputValue
|
|
356
|
+
outputs[output.OutputKey] = output.OutputValue;
|
|
350
357
|
}
|
|
351
358
|
|
|
352
359
|
if (outputs.RoleArn) {
|
|
353
|
-
console.log(` ✅ IAM role: ${outputs.RoleArn.split('/').pop()}`)
|
|
360
|
+
console.log(` ✅ IAM role: ${outputs.RoleArn.split('/').pop()}`);
|
|
354
361
|
}
|
|
355
362
|
if (outputs.EcrRepositoryName) {
|
|
356
|
-
console.log(` ✅ ECR repository: ${outputs.EcrRepositoryName}`)
|
|
363
|
+
console.log(` ✅ ECR repository: ${outputs.EcrRepositoryName}`);
|
|
357
364
|
}
|
|
358
365
|
if (outputs.AsyncS3BucketName) {
|
|
359
|
-
console.log(` ✅ S3 bucket (async): ${outputs.AsyncS3BucketName}`)
|
|
366
|
+
console.log(` ✅ S3 bucket (async): ${outputs.AsyncS3BucketName}`);
|
|
360
367
|
}
|
|
361
368
|
if (outputs.BatchS3BucketName) {
|
|
362
|
-
console.log(` ✅ S3 bucket (batch): ${outputs.BatchS3BucketName}`)
|
|
369
|
+
console.log(` ✅ S3 bucket (batch): ${outputs.BatchS3BucketName}`);
|
|
363
370
|
}
|
|
364
371
|
if (outputs.StackVersion) {
|
|
365
|
-
console.log(` 📋 Stack version: ${outputs.StackVersion}`)
|
|
372
|
+
console.log(` 📋 Stack version: ${outputs.StackVersion}`);
|
|
366
373
|
}
|
|
367
374
|
}
|
|
368
375
|
} catch {
|
|
369
376
|
// Fall back to individual resource checks for profiles created before CloudFormation migration
|
|
370
|
-
console.log(` ⚠️ Bootstrap stack "${stackName}" not found — checking resources individually`)
|
|
377
|
+
console.log(` ⚠️ Bootstrap stack "${stackName}" not found — checking resources individually`);
|
|
371
378
|
|
|
372
379
|
try {
|
|
373
|
-
const defaultRoleName = 'mlcc-sagemaker-execution-role'
|
|
374
|
-
let roleName = defaultRoleName
|
|
380
|
+
const defaultRoleName = 'mlcc-sagemaker-execution-role';
|
|
381
|
+
let roleName = defaultRoleName;
|
|
375
382
|
if (profile.config.roleArn) {
|
|
376
|
-
const arnParts = profile.config.roleArn.split('/')
|
|
377
|
-
roleName = arnParts[arnParts.length - 1]
|
|
383
|
+
const arnParts = profile.config.roleArn.split('/');
|
|
384
|
+
roleName = arnParts[arnParts.length - 1];
|
|
378
385
|
}
|
|
379
386
|
|
|
380
387
|
const roleExists = this._resourceExists(
|
|
381
388
|
`iam get-role --role-name ${roleName}`,
|
|
382
389
|
profile.config.awsProfile
|
|
383
|
-
)
|
|
390
|
+
);
|
|
384
391
|
if (roleExists) {
|
|
385
|
-
console.log(` ✅ IAM role: ${roleName}`)
|
|
392
|
+
console.log(` ✅ IAM role: ${roleName}`);
|
|
386
393
|
} else {
|
|
387
|
-
console.log(` ⚠️ IAM role: ${roleName} — missing`)
|
|
394
|
+
console.log(` ⚠️ IAM role: ${roleName} — missing`);
|
|
388
395
|
}
|
|
389
396
|
} catch {
|
|
390
|
-
console.log(' ⚠️ IAM role: could not validate')
|
|
397
|
+
console.log(' ⚠️ IAM role: could not validate');
|
|
391
398
|
}
|
|
392
399
|
|
|
393
400
|
try {
|
|
394
401
|
const ecrExists = this._resourceExists(
|
|
395
402
|
`ecr describe-repositories --repository-names ml-container-creator --region ${profile.config.awsRegion}`,
|
|
396
403
|
profile.config.awsProfile
|
|
397
|
-
)
|
|
404
|
+
);
|
|
398
405
|
if (ecrExists) {
|
|
399
|
-
console.log(' ✅ ECR repository: ml-container-creator')
|
|
406
|
+
console.log(' ✅ ECR repository: ml-container-creator');
|
|
400
407
|
} else {
|
|
401
|
-
console.log(' ⚠️ ECR repository: ml-container-creator — missing')
|
|
408
|
+
console.log(' ⚠️ ECR repository: ml-container-creator — missing');
|
|
402
409
|
}
|
|
403
410
|
} catch {
|
|
404
|
-
console.log(' ⚠️ ECR repository: could not validate')
|
|
411
|
+
console.log(' ⚠️ ECR repository: could not validate');
|
|
405
412
|
}
|
|
406
413
|
|
|
407
414
|
if (profile.config.asyncS3Bucket) {
|
|
@@ -409,12 +416,12 @@ export default class BootstrapCommandHandler {
|
|
|
409
416
|
const asyncExists = this._resourceExists(
|
|
410
417
|
`s3api head-bucket --bucket ${profile.config.asyncS3Bucket}`,
|
|
411
418
|
profile.config.awsProfile
|
|
412
|
-
)
|
|
419
|
+
);
|
|
413
420
|
console.log(asyncExists
|
|
414
421
|
? ` ✅ S3 bucket: ${profile.config.asyncS3Bucket}`
|
|
415
|
-
: ` ⚠️ S3 bucket: ${profile.config.asyncS3Bucket} — missing`)
|
|
422
|
+
: ` ⚠️ S3 bucket: ${profile.config.asyncS3Bucket} — missing`);
|
|
416
423
|
} catch {
|
|
417
|
-
console.log(` ⚠️ S3 bucket: ${profile.config.asyncS3Bucket} — could not validate`)
|
|
424
|
+
console.log(` ⚠️ S3 bucket: ${profile.config.asyncS3Bucket} — could not validate`);
|
|
418
425
|
}
|
|
419
426
|
}
|
|
420
427
|
|
|
@@ -423,48 +430,48 @@ export default class BootstrapCommandHandler {
|
|
|
423
430
|
const batchExists = this._resourceExists(
|
|
424
431
|
`s3api head-bucket --bucket ${profile.config.batchS3Bucket}`,
|
|
425
432
|
profile.config.awsProfile
|
|
426
|
-
)
|
|
433
|
+
);
|
|
427
434
|
console.log(batchExists
|
|
428
435
|
? ` ✅ S3 bucket: ${profile.config.batchS3Bucket}`
|
|
429
|
-
: ` ⚠️ S3 bucket: ${profile.config.batchS3Bucket} — missing`)
|
|
436
|
+
: ` ⚠️ S3 bucket: ${profile.config.batchS3Bucket} — missing`);
|
|
430
437
|
} catch {
|
|
431
|
-
console.log(` ⚠️ S3 bucket: ${profile.config.batchS3Bucket} — could not validate`)
|
|
438
|
+
console.log(` ⚠️ S3 bucket: ${profile.config.batchS3Bucket} — could not validate`);
|
|
432
439
|
}
|
|
433
440
|
}
|
|
434
441
|
}
|
|
435
442
|
|
|
436
443
|
// Display deployed resources from manifest
|
|
437
|
-
console.log('\n📦 Deployed Resources:')
|
|
444
|
+
console.log('\n📦 Deployed Resources:');
|
|
438
445
|
|
|
439
|
-
const assetManager = new AssetManager(profile.name)
|
|
446
|
+
const assetManager = new AssetManager(profile.name);
|
|
440
447
|
|
|
441
448
|
if (!existsSync(assetManager.manifestPath)) {
|
|
442
|
-
console.log(' No deployment tracking data available.')
|
|
443
|
-
console.log(' Resources will be tracked after running deploy, push, or submit scripts.')
|
|
444
|
-
return
|
|
449
|
+
console.log(' No deployment tracking data available.');
|
|
450
|
+
console.log(' Resources will be tracked after running deploy, push, or submit scripts.');
|
|
451
|
+
return;
|
|
445
452
|
}
|
|
446
453
|
|
|
447
|
-
const resourcesByProject = assetManager.getResourcesByProject()
|
|
454
|
+
const resourcesByProject = assetManager.getResourcesByProject();
|
|
448
455
|
|
|
449
456
|
if (resourcesByProject.size === 0) {
|
|
450
|
-
console.log(' No deployed resources tracked.')
|
|
451
|
-
return
|
|
457
|
+
console.log(' No deployed resources tracked.');
|
|
458
|
+
return;
|
|
452
459
|
}
|
|
453
460
|
|
|
454
461
|
for (const [project, resources] of resourcesByProject) {
|
|
455
|
-
console.log(`\n Project: ${project}`)
|
|
462
|
+
console.log(`\n Project: ${project}`);
|
|
456
463
|
for (const resource of resources) {
|
|
457
|
-
const timestamp = resource.createdAt || resource.lastUpdatedAt
|
|
458
|
-
console.log(` ${resource.resourceType} ${resource.resourceId} [${resource.status}] ${timestamp}`)
|
|
464
|
+
const timestamp = resource.createdAt || resource.lastUpdatedAt;
|
|
465
|
+
console.log(` ${resource.resourceType} ${resource.resourceId} [${resource.status}] ${timestamp}`);
|
|
459
466
|
}
|
|
460
467
|
}
|
|
461
468
|
|
|
462
|
-
const counts = assetManager.getStatusCounts()
|
|
463
|
-
console.log(`\n Summary: ${counts.active} active, ${counts.deleted} deleted, ${counts.unknown} unknown`)
|
|
469
|
+
const counts = assetManager.getStatusCounts();
|
|
470
|
+
console.log(`\n Summary: ${counts.active} active, ${counts.deleted} deleted, ${counts.unknown} unknown`);
|
|
464
471
|
|
|
465
472
|
// Drift detection if --verify flag is set
|
|
466
473
|
if (options.verify) {
|
|
467
|
-
await this._handleStatusVerify(profile, assetManager)
|
|
474
|
+
await this._handleStatusVerify(profile, assetManager);
|
|
468
475
|
}
|
|
469
476
|
}
|
|
470
477
|
|
|
@@ -474,45 +481,45 @@ export default class BootstrapCommandHandler {
|
|
|
474
481
|
* @param {AssetManager} assetManager - AssetManager instance for the profile
|
|
475
482
|
*/
|
|
476
483
|
async _handleStatusVerify(profile, assetManager) {
|
|
477
|
-
console.log('\n🔎 Drift Detection:')
|
|
484
|
+
console.log('\n🔎 Drift Detection:');
|
|
478
485
|
|
|
479
|
-
const activeResources = assetManager.listResources({ status: 'active' })
|
|
486
|
+
const activeResources = assetManager.listResources({ status: 'active' });
|
|
480
487
|
|
|
481
488
|
if (activeResources.length === 0) {
|
|
482
|
-
console.log(' No active resources to verify.')
|
|
483
|
-
return
|
|
489
|
+
console.log(' No active resources to verify.');
|
|
490
|
+
return;
|
|
484
491
|
}
|
|
485
492
|
|
|
486
|
-
let verified = 0
|
|
487
|
-
let drifted = 0
|
|
488
|
-
let unchecked = 0
|
|
493
|
+
let verified = 0;
|
|
494
|
+
let drifted = 0;
|
|
495
|
+
let unchecked = 0;
|
|
489
496
|
|
|
490
497
|
for (const resource of activeResources) {
|
|
491
|
-
const checkCommand = this._buildDriftCheckCommand(resource)
|
|
498
|
+
const checkCommand = this._buildDriftCheckCommand(resource);
|
|
492
499
|
|
|
493
500
|
if (!checkCommand) {
|
|
494
|
-
unchecked
|
|
495
|
-
continue
|
|
501
|
+
unchecked++;
|
|
502
|
+
continue;
|
|
496
503
|
}
|
|
497
504
|
|
|
498
505
|
try {
|
|
499
|
-
const exists = this._resourceExists(checkCommand, profile.config.awsProfile)
|
|
506
|
+
const exists = this._resourceExists(checkCommand, profile.config.awsProfile);
|
|
500
507
|
|
|
501
508
|
if (exists) {
|
|
502
|
-
verified
|
|
503
|
-
console.log(` ✅ ${resource.resourceType}: ${resource.resourceId}`)
|
|
509
|
+
verified++;
|
|
510
|
+
console.log(` ✅ ${resource.resourceType}: ${resource.resourceId}`);
|
|
504
511
|
} else {
|
|
505
|
-
drifted
|
|
506
|
-
assetManager.updateStatus(resource.resourceId, 'unknown')
|
|
507
|
-
console.log(` ⚠️ ${resource.resourceType}: ${resource.resourceId} — not found (status updated to unknown)`)
|
|
512
|
+
drifted++;
|
|
513
|
+
assetManager.updateStatus(resource.resourceId, 'unknown');
|
|
514
|
+
console.log(` ⚠️ ${resource.resourceType}: ${resource.resourceId} — not found (status updated to unknown)`);
|
|
508
515
|
}
|
|
509
516
|
} catch {
|
|
510
|
-
unchecked
|
|
511
|
-
console.log(` ⚠️ ${resource.resourceType}: ${resource.resourceId} — could not verify (credentials or API unavailable)`)
|
|
517
|
+
unchecked++;
|
|
518
|
+
console.log(` ⚠️ ${resource.resourceType}: ${resource.resourceId} — could not verify (credentials or API unavailable)`);
|
|
512
519
|
}
|
|
513
520
|
}
|
|
514
521
|
|
|
515
|
-
console.log(`\n Drift Summary: ${verified} verified, ${drifted} drifted, ${unchecked} unchecked`)
|
|
522
|
+
console.log(`\n Drift Summary: ${verified} verified, ${drifted} drifted, ${unchecked} unchecked`);
|
|
516
523
|
}
|
|
517
524
|
|
|
518
525
|
/**
|
|
@@ -521,38 +528,38 @@ export default class BootstrapCommandHandler {
|
|
|
521
528
|
* @returns {string|null} AWS CLI command string, or null if resource type is not supported
|
|
522
529
|
*/
|
|
523
530
|
_buildDriftCheckCommand(resource) {
|
|
524
|
-
const resourceId = resource.resourceId
|
|
531
|
+
const resourceId = resource.resourceId;
|
|
525
532
|
|
|
526
533
|
switch (resource.resourceType) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
534
|
+
case 'sagemaker-endpoint': {
|
|
535
|
+
const name = this._extractNameFromArn(resourceId);
|
|
536
|
+
return `sagemaker describe-endpoint --endpoint-name ${name}`;
|
|
537
|
+
}
|
|
538
|
+
case 'sagemaker-model': {
|
|
539
|
+
const name = this._extractNameFromArn(resourceId);
|
|
540
|
+
return `sagemaker describe-model --model-name ${name}`;
|
|
541
|
+
}
|
|
542
|
+
case 'sagemaker-inference-component': {
|
|
543
|
+
const name = this._extractNameFromArn(resourceId);
|
|
544
|
+
return `sagemaker describe-inference-component --inference-component-name ${name}`;
|
|
545
|
+
}
|
|
546
|
+
case 'ecr-image': {
|
|
547
|
+
// resourceId is a full image URI like 111111111111.dkr.ecr.us-east-1.amazonaws.com/repo:tag
|
|
548
|
+
const parts = resourceId.split('/');
|
|
549
|
+
const repoAndTag = parts[parts.length - 1];
|
|
550
|
+
const [repo, tag] = repoAndTag.split(':');
|
|
551
|
+
return `ecr describe-images --repository-name ${repo} --image-ids imageTag=${tag || 'latest'}`;
|
|
552
|
+
}
|
|
553
|
+
case 'codebuild-project': {
|
|
554
|
+
const name = this._extractNameFromArn(resourceId);
|
|
555
|
+
return `codebuild batch-get-projects --names ${name}`;
|
|
556
|
+
}
|
|
557
|
+
case 'iam-role': {
|
|
558
|
+
const name = this._extractNameFromArn(resourceId);
|
|
559
|
+
return `iam get-role --role-name ${name}`;
|
|
560
|
+
}
|
|
561
|
+
default:
|
|
562
|
+
return null;
|
|
556
563
|
}
|
|
557
564
|
}
|
|
558
565
|
|
|
@@ -567,8 +574,8 @@ export default class BootstrapCommandHandler {
|
|
|
567
574
|
// arn:aws:sagemaker:us-east-1:111111111111:endpoint/my-endpoint
|
|
568
575
|
// arn:aws:iam::111111111111:role/my-role
|
|
569
576
|
// arn:aws:codebuild:us-east-1:111111111111:project/my-project
|
|
570
|
-
const parts = arn.split('/')
|
|
571
|
-
return parts[parts.length - 1]
|
|
577
|
+
const parts = arn.split('/');
|
|
578
|
+
return parts[parts.length - 1];
|
|
572
579
|
}
|
|
573
580
|
|
|
574
581
|
/**
|
|
@@ -577,54 +584,54 @@ export default class BootstrapCommandHandler {
|
|
|
577
584
|
*/
|
|
578
585
|
async _handleUse(profileName) {
|
|
579
586
|
if (!profileName) {
|
|
580
|
-
console.log('Usage: ml-container-creator bootstrap use <profile>')
|
|
581
|
-
console.log(' ml-container-creator bootstrap use none (deactivate)')
|
|
582
|
-
return
|
|
587
|
+
console.log('Usage: ml-container-creator bootstrap use <profile>');
|
|
588
|
+
console.log(' ml-container-creator bootstrap use none (deactivate)');
|
|
589
|
+
return;
|
|
583
590
|
}
|
|
584
591
|
|
|
585
592
|
if (profileName === 'none') {
|
|
586
|
-
this.config.setActiveProfile(null)
|
|
587
|
-
console.log('Active profile cleared. No bootstrap profile is active.')
|
|
588
|
-
return
|
|
593
|
+
this.config.setActiveProfile(null);
|
|
594
|
+
console.log('Active profile cleared. No bootstrap profile is active.');
|
|
595
|
+
return;
|
|
589
596
|
}
|
|
590
597
|
|
|
591
|
-
const profile = this.config.getProfile(profileName)
|
|
598
|
+
const profile = this.config.getProfile(profileName);
|
|
592
599
|
if (!profile) {
|
|
593
|
-
const available = this.config.listProfiles()
|
|
594
|
-
console.log(`Profile "${profileName}" not found.`)
|
|
600
|
+
const available = this.config.listProfiles();
|
|
601
|
+
console.log(`Profile "${profileName}" not found.`);
|
|
595
602
|
if (available.length > 0) {
|
|
596
|
-
console.log(`Available profiles: ${available.join(', ')}`)
|
|
603
|
+
console.log(`Available profiles: ${available.join(', ')}`);
|
|
597
604
|
} else {
|
|
598
|
-
console.log('No profiles configured. Run `ml-container-creator bootstrap` to create one.')
|
|
605
|
+
console.log('No profiles configured. Run `ml-container-creator bootstrap` to create one.');
|
|
599
606
|
}
|
|
600
|
-
return
|
|
607
|
+
return;
|
|
601
608
|
}
|
|
602
609
|
|
|
603
|
-
this.config.setActiveProfile(profileName)
|
|
604
|
-
console.log(`Switched active profile to "${profileName}".`)
|
|
610
|
+
this.config.setActiveProfile(profileName);
|
|
611
|
+
console.log(`Switched active profile to "${profileName}".`);
|
|
605
612
|
}
|
|
606
613
|
|
|
607
614
|
/**
|
|
608
615
|
* List all bootstrap profiles.
|
|
609
616
|
*/
|
|
610
617
|
async _handleList() {
|
|
611
|
-
const profiles = this.config.listProfiles()
|
|
618
|
+
const profiles = this.config.listProfiles();
|
|
612
619
|
|
|
613
620
|
if (profiles.length === 0) {
|
|
614
|
-
console.log('No bootstrap profiles configured.')
|
|
615
|
-
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.')
|
|
616
|
-
return
|
|
621
|
+
console.log('No bootstrap profiles configured.');
|
|
622
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
623
|
+
return;
|
|
617
624
|
}
|
|
618
625
|
|
|
619
|
-
const config = this.config.read()
|
|
620
|
-
const activeProfileName = config ? config.activeProfile : null
|
|
626
|
+
const config = this.config.read();
|
|
627
|
+
const activeProfileName = config ? config.activeProfile : null;
|
|
621
628
|
|
|
622
|
-
console.log('\nBootstrap Profiles:')
|
|
629
|
+
console.log('\nBootstrap Profiles:');
|
|
623
630
|
for (const name of profiles) {
|
|
624
631
|
if (name === activeProfileName) {
|
|
625
|
-
console.log(` * ${name} (active)`)
|
|
632
|
+
console.log(` * ${name} (active)`);
|
|
626
633
|
} else {
|
|
627
|
-
console.log(` ${name}`)
|
|
634
|
+
console.log(` ${name}`);
|
|
628
635
|
}
|
|
629
636
|
}
|
|
630
637
|
}
|
|
@@ -636,42 +643,42 @@ export default class BootstrapCommandHandler {
|
|
|
636
643
|
*/
|
|
637
644
|
async _handleRemove(profileName, options) {
|
|
638
645
|
if (!profileName) {
|
|
639
|
-
console.log('Usage: ml-container-creator bootstrap remove <profile> [--force]')
|
|
640
|
-
return
|
|
646
|
+
console.log('Usage: ml-container-creator bootstrap remove <profile> [--force]');
|
|
647
|
+
return;
|
|
641
648
|
}
|
|
642
649
|
|
|
643
|
-
const profile = this.config.getProfile(profileName)
|
|
650
|
+
const profile = this.config.getProfile(profileName);
|
|
644
651
|
if (!profile) {
|
|
645
|
-
console.log(`Profile "${profileName}" not found.`)
|
|
646
|
-
return
|
|
652
|
+
console.log(`Profile "${profileName}" not found.`);
|
|
653
|
+
return;
|
|
647
654
|
}
|
|
648
655
|
|
|
649
656
|
// Check for manifest file with active resources
|
|
650
|
-
const assetManager = new AssetManager(profileName)
|
|
651
|
-
const hasManifest = existsSync(assetManager.manifestPath)
|
|
657
|
+
const assetManager = new AssetManager(profileName);
|
|
658
|
+
const hasManifest = existsSync(assetManager.manifestPath);
|
|
652
659
|
|
|
653
660
|
if (hasManifest) {
|
|
654
|
-
const counts = assetManager.getStatusCounts()
|
|
661
|
+
const counts = assetManager.getStatusCounts();
|
|
655
662
|
if (counts.active > 0 && !options.force) {
|
|
656
|
-
console.log(`⚠️ Profile "${profileName}" has ${counts.active} active resource${counts.active === 1 ? '' : 's'} in the deployment manifest.`)
|
|
663
|
+
console.log(`⚠️ Profile "${profileName}" has ${counts.active} active resource${counts.active === 1 ? '' : 's'} in the deployment manifest.`);
|
|
657
664
|
}
|
|
658
665
|
}
|
|
659
666
|
|
|
660
667
|
// Check for CloudFormation stack
|
|
661
|
-
const stackName = profile.stackName || `${STACK_NAME_PREFIX}-${profileName}
|
|
662
|
-
let hasStack = false
|
|
668
|
+
const stackName = profile.stackName || `${STACK_NAME_PREFIX}-${profileName}`;
|
|
669
|
+
let hasStack = false;
|
|
663
670
|
try {
|
|
664
671
|
hasStack = this._resourceExists(
|
|
665
672
|
`cloudformation describe-stacks --stack-name ${stackName} --region ${profile.awsRegion}`,
|
|
666
673
|
profile.awsProfile
|
|
667
|
-
)
|
|
674
|
+
);
|
|
668
675
|
} catch {
|
|
669
676
|
// ignore
|
|
670
677
|
}
|
|
671
678
|
|
|
672
679
|
if (hasStack && !options.force) {
|
|
673
|
-
console.log(`⚠️ Profile "${profileName}" has a CloudFormation stack: ${stackName}`)
|
|
674
|
-
console.log(' Use --delete-stack to also delete the AWS resources, or --force to remove the profile only.')
|
|
680
|
+
console.log(`⚠️ Profile "${profileName}" has a CloudFormation stack: ${stackName}`);
|
|
681
|
+
console.log(' Use --delete-stack to also delete the AWS resources, or --force to remove the profile only.');
|
|
675
682
|
}
|
|
676
683
|
|
|
677
684
|
if (!options.force) {
|
|
@@ -680,95 +687,95 @@ export default class BootstrapCommandHandler {
|
|
|
680
687
|
name: 'confirm',
|
|
681
688
|
message: `Remove bootstrap profile "${profileName}"?`,
|
|
682
689
|
default: false
|
|
683
|
-
}])
|
|
690
|
+
}]);
|
|
684
691
|
|
|
685
692
|
if (!confirm) {
|
|
686
|
-
console.log('Removal cancelled.')
|
|
687
|
-
return
|
|
693
|
+
console.log('Removal cancelled.');
|
|
694
|
+
return;
|
|
688
695
|
}
|
|
689
696
|
}
|
|
690
697
|
|
|
691
698
|
// Delete CloudFormation stack if requested
|
|
692
699
|
if (hasStack && options['delete-stack']) {
|
|
693
700
|
try {
|
|
694
|
-
console.log(`🗑️ Deleting CloudFormation stack: ${stackName}`)
|
|
701
|
+
console.log(`🗑️ Deleting CloudFormation stack: ${stackName}`);
|
|
695
702
|
execSync(
|
|
696
703
|
`aws cloudformation delete-stack --stack-name ${stackName} --region ${profile.awsRegion} --profile ${profile.awsProfile}`,
|
|
697
704
|
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
698
|
-
)
|
|
699
|
-
console.log(
|
|
705
|
+
);
|
|
706
|
+
console.log('⏳ Waiting for stack deletion...');
|
|
700
707
|
execSync(
|
|
701
708
|
`aws cloudformation wait stack-delete-complete --stack-name ${stackName} --region ${profile.awsRegion} --profile ${profile.awsProfile}`,
|
|
702
709
|
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
703
|
-
)
|
|
704
|
-
console.log(`✅ Stack "${stackName}" deleted.`)
|
|
710
|
+
);
|
|
711
|
+
console.log(`✅ Stack "${stackName}" deleted.`);
|
|
705
712
|
} catch (err) {
|
|
706
|
-
console.log(`⚠️ Could not delete stack "${stackName}": ${err.message}`)
|
|
707
|
-
console.log(' You may need to delete it manually from the CloudFormation console.')
|
|
713
|
+
console.log(`⚠️ Could not delete stack "${stackName}": ${err.message}`);
|
|
714
|
+
console.log(' You may need to delete it manually from the CloudFormation console.');
|
|
708
715
|
}
|
|
709
716
|
} else if (hasStack) {
|
|
710
|
-
console.log(`Note: CloudFormation stack "${stackName}" was left in place.`)
|
|
711
|
-
console.log(' To delete AWS resources, re-run with --delete-stack')
|
|
717
|
+
console.log(`Note: CloudFormation stack "${stackName}" was left in place.`);
|
|
718
|
+
console.log(' To delete AWS resources, re-run with --delete-stack');
|
|
712
719
|
}
|
|
713
720
|
|
|
714
721
|
// Delete manifest file if it exists
|
|
715
722
|
if (hasManifest) {
|
|
716
723
|
try {
|
|
717
|
-
unlinkSync(assetManager.manifestPath)
|
|
718
|
-
console.log(`Manifest file for "${profileName}" deleted.`)
|
|
724
|
+
unlinkSync(assetManager.manifestPath);
|
|
725
|
+
console.log(`Manifest file for "${profileName}" deleted.`);
|
|
719
726
|
} catch {
|
|
720
|
-
console.log(`⚠️ Could not delete manifest file for "${profileName}".`)
|
|
727
|
+
console.log(`⚠️ Could not delete manifest file for "${profileName}".`);
|
|
721
728
|
}
|
|
722
729
|
}
|
|
723
730
|
|
|
724
|
-
this.config.removeProfile(profileName)
|
|
725
|
-
console.log(`Profile "${profileName}" removed.`)
|
|
731
|
+
this.config.removeProfile(profileName);
|
|
732
|
+
console.log(`Profile "${profileName}" removed.`);
|
|
726
733
|
}
|
|
727
734
|
|
|
728
735
|
/**
|
|
729
736
|
* Scan AWS for pre-existing MLCC-managed resources and add them to the manifest.
|
|
730
737
|
*/
|
|
731
738
|
async _handleScan() {
|
|
732
|
-
const profile = this.config.getActiveProfile()
|
|
739
|
+
const profile = this.config.getActiveProfile();
|
|
733
740
|
if (!profile) {
|
|
734
|
-
console.log('No active bootstrap profile found.')
|
|
735
|
-
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.')
|
|
736
|
-
return
|
|
741
|
+
console.log('No active bootstrap profile found.');
|
|
742
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure.');
|
|
743
|
+
return;
|
|
737
744
|
}
|
|
738
745
|
|
|
739
|
-
console.log(`\n🔍 Scanning for pre-existing resources in ${profile.config.awsRegion}...`)
|
|
746
|
+
console.log(`\n🔍 Scanning for pre-existing resources in ${profile.config.awsRegion}...`);
|
|
740
747
|
|
|
741
|
-
const assetManager = new AssetManager(profile.name)
|
|
742
|
-
const now = new Date().toISOString()
|
|
743
|
-
let discovered = 0
|
|
744
|
-
let added = 0
|
|
745
|
-
let skipped = 0
|
|
748
|
+
const assetManager = new AssetManager(profile.name);
|
|
749
|
+
const now = new Date().toISOString();
|
|
750
|
+
let discovered = 0;
|
|
751
|
+
let added = 0;
|
|
752
|
+
let skipped = 0;
|
|
746
753
|
|
|
747
754
|
// 1. Query Resource Groups Tagging API for mlcc:managed-by tagged resources
|
|
748
755
|
try {
|
|
749
|
-
console.log('\n Checking tagged resources...')
|
|
756
|
+
console.log('\n Checking tagged resources...');
|
|
750
757
|
const tagResult = this._execAws(
|
|
751
758
|
`resourcegroupstaggingapi get-resources --tag-filters Key=mlcc:managed-by,Values=ml-container-creator --region ${profile.config.awsRegion}`,
|
|
752
759
|
profile.config.awsProfile
|
|
753
|
-
)
|
|
760
|
+
);
|
|
754
761
|
|
|
755
|
-
const taggedResources = tagResult.ResourceTagMappingList || []
|
|
762
|
+
const taggedResources = tagResult.ResourceTagMappingList || [];
|
|
756
763
|
for (const tagged of taggedResources) {
|
|
757
|
-
discovered
|
|
758
|
-
const arn = tagged.ResourceARN
|
|
759
|
-
const existing = assetManager.getResource(arn)
|
|
764
|
+
discovered++;
|
|
765
|
+
const arn = tagged.ResourceARN;
|
|
766
|
+
const existing = assetManager.getResource(arn);
|
|
760
767
|
if (existing) {
|
|
761
|
-
skipped
|
|
762
|
-
continue
|
|
768
|
+
skipped++;
|
|
769
|
+
continue;
|
|
763
770
|
}
|
|
764
771
|
|
|
765
|
-
const resourceType = this._inferResourceTypeFromArn(arn)
|
|
772
|
+
const resourceType = this._inferResourceTypeFromArn(arn);
|
|
766
773
|
if (!resourceType) {
|
|
767
|
-
skipped
|
|
768
|
-
continue
|
|
774
|
+
skipped++;
|
|
775
|
+
continue;
|
|
769
776
|
}
|
|
770
777
|
|
|
771
|
-
const project = this._inferProjectFromTags(tagged.Tags) || 'unknown'
|
|
778
|
+
const project = this._inferProjectFromTags(tagged.Tags) || 'unknown';
|
|
772
779
|
|
|
773
780
|
try {
|
|
774
781
|
assetManager.addResource({
|
|
@@ -779,34 +786,34 @@ export default class BootstrapCommandHandler {
|
|
|
779
786
|
project,
|
|
780
787
|
status: 'active',
|
|
781
788
|
metadata: { discoveredBy: 'scan' }
|
|
782
|
-
})
|
|
783
|
-
added
|
|
789
|
+
});
|
|
790
|
+
added++;
|
|
784
791
|
} catch {
|
|
785
|
-
skipped
|
|
792
|
+
skipped++;
|
|
786
793
|
}
|
|
787
794
|
}
|
|
788
795
|
} catch {
|
|
789
|
-
console.log(' ⚠️ Could not query tagged resources (credentials or API unavailable)')
|
|
796
|
+
console.log(' ⚠️ Could not query tagged resources (credentials or API unavailable)');
|
|
790
797
|
}
|
|
791
798
|
|
|
792
799
|
// 2. Query ECR for images in ml-container-creator repository
|
|
793
800
|
try {
|
|
794
|
-
console.log(' Checking ECR images...')
|
|
801
|
+
console.log(' Checking ECR images...');
|
|
795
802
|
const ecrResult = this._execAws(
|
|
796
803
|
`ecr describe-images --repository-name ml-container-creator --region ${profile.config.awsRegion}`,
|
|
797
804
|
profile.config.awsProfile
|
|
798
|
-
)
|
|
805
|
+
);
|
|
799
806
|
|
|
800
|
-
const images = ecrResult.imageDetails || []
|
|
807
|
+
const images = ecrResult.imageDetails || [];
|
|
801
808
|
for (const image of images) {
|
|
802
|
-
const tags = image.imageTags || []
|
|
809
|
+
const tags = image.imageTags || [];
|
|
803
810
|
for (const tag of tags) {
|
|
804
|
-
discovered
|
|
805
|
-
const imageUri = `${profile.config.accountId}.dkr.ecr.${profile.config.awsRegion}.amazonaws.com/ml-container-creator:${tag}
|
|
806
|
-
const existing = assetManager.getResource(imageUri)
|
|
811
|
+
discovered++;
|
|
812
|
+
const imageUri = `${profile.config.accountId}.dkr.ecr.${profile.config.awsRegion}.amazonaws.com/ml-container-creator:${tag}`;
|
|
813
|
+
const existing = assetManager.getResource(imageUri);
|
|
807
814
|
if (existing) {
|
|
808
|
-
skipped
|
|
809
|
-
continue
|
|
815
|
+
skipped++;
|
|
816
|
+
continue;
|
|
810
817
|
}
|
|
811
818
|
|
|
812
819
|
try {
|
|
@@ -823,33 +830,33 @@ export default class BootstrapCommandHandler {
|
|
|
823
830
|
region: profile.config.awsRegion,
|
|
824
831
|
discoveredBy: 'scan'
|
|
825
832
|
}
|
|
826
|
-
})
|
|
827
|
-
added
|
|
833
|
+
});
|
|
834
|
+
added++;
|
|
828
835
|
} catch {
|
|
829
|
-
skipped
|
|
836
|
+
skipped++;
|
|
830
837
|
}
|
|
831
838
|
}
|
|
832
839
|
}
|
|
833
840
|
} catch {
|
|
834
|
-
console.log(' ⚠️ Could not query ECR images (credentials or API unavailable)')
|
|
841
|
+
console.log(' ⚠️ Could not query ECR images (credentials or API unavailable)');
|
|
835
842
|
}
|
|
836
843
|
|
|
837
844
|
// 3. Query CodeBuild for *-build-* projects
|
|
838
845
|
try {
|
|
839
|
-
console.log(' Checking CodeBuild projects...')
|
|
846
|
+
console.log(' Checking CodeBuild projects...');
|
|
840
847
|
const cbResult = this._execAws(
|
|
841
848
|
`codebuild list-projects --region ${profile.config.awsRegion}`,
|
|
842
849
|
profile.config.awsProfile
|
|
843
|
-
)
|
|
850
|
+
);
|
|
844
851
|
|
|
845
|
-
const projects = (cbResult.projects || []).filter(name => name.includes('-build-'))
|
|
852
|
+
const projects = (cbResult.projects || []).filter(name => name.includes('-build-'));
|
|
846
853
|
for (const projectName of projects) {
|
|
847
|
-
discovered
|
|
848
|
-
const arn = `arn:aws:codebuild:${profile.config.awsRegion}:${profile.config.accountId}:project/${projectName}
|
|
849
|
-
const existing = assetManager.getResource(arn)
|
|
854
|
+
discovered++;
|
|
855
|
+
const arn = `arn:aws:codebuild:${profile.config.awsRegion}:${profile.config.accountId}:project/${projectName}`;
|
|
856
|
+
const existing = assetManager.getResource(arn);
|
|
850
857
|
if (existing) {
|
|
851
|
-
skipped
|
|
852
|
-
continue
|
|
858
|
+
skipped++;
|
|
859
|
+
continue;
|
|
853
860
|
}
|
|
854
861
|
|
|
855
862
|
try {
|
|
@@ -865,21 +872,21 @@ export default class BootstrapCommandHandler {
|
|
|
865
872
|
region: profile.config.awsRegion,
|
|
866
873
|
discoveredBy: 'scan'
|
|
867
874
|
}
|
|
868
|
-
})
|
|
869
|
-
added
|
|
875
|
+
});
|
|
876
|
+
added++;
|
|
870
877
|
} catch {
|
|
871
|
-
skipped
|
|
878
|
+
skipped++;
|
|
872
879
|
}
|
|
873
880
|
}
|
|
874
881
|
} catch {
|
|
875
|
-
console.log(' ⚠️ Could not query CodeBuild projects (credentials or API unavailable)')
|
|
882
|
+
console.log(' ⚠️ Could not query CodeBuild projects (credentials or API unavailable)');
|
|
876
883
|
}
|
|
877
884
|
|
|
878
885
|
// Display summary
|
|
879
|
-
console.log(`\n Scan complete: ${discovered} discovered, ${added} added, ${skipped} skipped (duplicates or unsupported)`)
|
|
886
|
+
console.log(`\n Scan complete: ${discovered} discovered, ${added} added, ${skipped} skipped (duplicates or unsupported)`);
|
|
880
887
|
|
|
881
888
|
if (discovered === 0) {
|
|
882
|
-
console.log(' No MLCC-managed resources were discovered.')
|
|
889
|
+
console.log(' No MLCC-managed resources were discovered.');
|
|
883
890
|
}
|
|
884
891
|
}
|
|
885
892
|
|
|
@@ -888,36 +895,36 @@ export default class BootstrapCommandHandler {
|
|
|
888
895
|
* 'deleted' or 'unknown' that are no longer useful.
|
|
889
896
|
*/
|
|
890
897
|
async _handlePrune() {
|
|
891
|
-
const profile = this.config.getActiveProfile()
|
|
898
|
+
const profile = this.config.getActiveProfile();
|
|
892
899
|
if (!profile) {
|
|
893
|
-
console.log('No active bootstrap profile found.')
|
|
894
|
-
return
|
|
900
|
+
console.log('No active bootstrap profile found.');
|
|
901
|
+
return;
|
|
895
902
|
}
|
|
896
903
|
|
|
897
|
-
const assetManager = new AssetManager(profile.name)
|
|
904
|
+
const assetManager = new AssetManager(profile.name);
|
|
898
905
|
|
|
899
906
|
if (!existsSync(assetManager.manifestPath)) {
|
|
900
|
-
console.log('No deployment tracking data to prune.')
|
|
901
|
-
return
|
|
907
|
+
console.log('No deployment tracking data to prune.');
|
|
908
|
+
return;
|
|
902
909
|
}
|
|
903
910
|
|
|
904
|
-
const before = assetManager.listResources()
|
|
905
|
-
const toRemove = before.filter(r => r.status === 'deleted' || r.status === 'unknown')
|
|
911
|
+
const before = assetManager.listResources();
|
|
912
|
+
const toRemove = before.filter(r => r.status === 'deleted' || r.status === 'unknown');
|
|
906
913
|
|
|
907
914
|
if (toRemove.length === 0) {
|
|
908
|
-
console.log('Nothing to prune — no deleted or unknown records found.')
|
|
909
|
-
return
|
|
915
|
+
console.log('Nothing to prune — no deleted or unknown records found.');
|
|
916
|
+
return;
|
|
910
917
|
}
|
|
911
918
|
|
|
912
|
-
console.log(`\n🧹 Pruning ${toRemove.length} stale record${toRemove.length === 1 ? '' : 's'}:\n`)
|
|
919
|
+
console.log(`\n🧹 Pruning ${toRemove.length} stale record${toRemove.length === 1 ? '' : 's'}:\n`);
|
|
913
920
|
|
|
914
921
|
for (const resource of toRemove) {
|
|
915
|
-
assetManager.removeResource(resource.resourceId)
|
|
916
|
-
console.log(` 🗑️ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`)
|
|
922
|
+
assetManager.removeResource(resource.resourceId);
|
|
923
|
+
console.log(` 🗑️ [${resource.status}] ${resource.resourceType}: ${resource.resourceId}`);
|
|
917
924
|
}
|
|
918
925
|
|
|
919
|
-
const after = assetManager.listResources()
|
|
920
|
-
console.log(`\n Done. ${toRemove.length} removed, ${after.length} remaining.`)
|
|
926
|
+
const after = assetManager.listResources();
|
|
927
|
+
console.log(`\n Done. ${toRemove.length} removed, ${after.length} remaining.`);
|
|
921
928
|
}
|
|
922
929
|
|
|
923
930
|
/**
|
|
@@ -928,81 +935,87 @@ export default class BootstrapCommandHandler {
|
|
|
928
935
|
* @param {object} [options] - Parsed CLI options (e.g., --ci to force CI update)
|
|
929
936
|
*/
|
|
930
937
|
async _handleUpdate(options = {}) {
|
|
931
|
-
const profile = this.config.getActiveProfile()
|
|
938
|
+
const profile = this.config.getActiveProfile();
|
|
932
939
|
if (!profile) {
|
|
933
|
-
console.log('No active bootstrap profile found.')
|
|
934
|
-
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure first.')
|
|
935
|
-
return
|
|
940
|
+
console.log('No active bootstrap profile found.');
|
|
941
|
+
console.log('Run `ml-container-creator bootstrap` to set up shared infrastructure first.');
|
|
942
|
+
return;
|
|
936
943
|
}
|
|
937
944
|
|
|
938
|
-
const { name, config: profileConfig } = profile
|
|
939
|
-
console.log(`\n🔄 Updating bootstrap infrastructure for profile "${name}"`)
|
|
940
|
-
console.log(` Region: ${profileConfig.awsRegion}`)
|
|
941
|
-
console.log(` Account: ${profileConfig.accountId}`)
|
|
945
|
+
const { name, config: profileConfig } = profile;
|
|
946
|
+
console.log(`\n🔄 Updating bootstrap infrastructure for profile "${name}"`);
|
|
947
|
+
console.log(` Region: ${profileConfig.awsRegion}`);
|
|
948
|
+
console.log(` Account: ${profileConfig.accountId}`);
|
|
942
949
|
|
|
943
950
|
// Re-deploy the CloudFormation bootstrap stack
|
|
944
|
-
const stackName = profileConfig.stackName || `${STACK_NAME_PREFIX}-${name}
|
|
945
|
-
this._displayProgress('☁️', 'Updating bootstrap stack...')
|
|
951
|
+
const stackName = profileConfig.stackName || `${STACK_NAME_PREFIX}-${name}`;
|
|
952
|
+
this._displayProgress('☁️', 'Updating bootstrap stack...');
|
|
946
953
|
|
|
947
954
|
try {
|
|
948
955
|
const stackOutputs = this._deployStack(stackName, {
|
|
949
956
|
CreateS3Buckets: (profileConfig.asyncS3Bucket || profileConfig.batchS3Bucket) ? 'true' : 'false',
|
|
950
957
|
UseExistingRoleArn: ''
|
|
951
|
-
}, profileConfig.awsProfile, profileConfig.awsRegion)
|
|
958
|
+
}, profileConfig.awsProfile, profileConfig.awsRegion);
|
|
952
959
|
|
|
953
960
|
// Update profile with any new outputs
|
|
954
|
-
if (stackOutputs.RoleArn) profileConfig.roleArn = stackOutputs.RoleArn
|
|
955
|
-
if (stackOutputs.EcrRepositoryName) profileConfig.ecrRepositoryName = stackOutputs.EcrRepositoryName
|
|
956
|
-
if (stackOutputs.AsyncS3BucketName) profileConfig.asyncS3Bucket = stackOutputs.AsyncS3BucketName
|
|
957
|
-
if (stackOutputs.BatchS3BucketName) profileConfig.batchS3Bucket = stackOutputs.BatchS3BucketName
|
|
958
|
-
profileConfig.stackName = stackName
|
|
961
|
+
if (stackOutputs.RoleArn) profileConfig.roleArn = stackOutputs.RoleArn;
|
|
962
|
+
if (stackOutputs.EcrRepositoryName) profileConfig.ecrRepositoryName = stackOutputs.EcrRepositoryName;
|
|
963
|
+
if (stackOutputs.AsyncS3BucketName) profileConfig.asyncS3Bucket = stackOutputs.AsyncS3BucketName;
|
|
964
|
+
if (stackOutputs.BatchS3BucketName) profileConfig.batchS3Bucket = stackOutputs.BatchS3BucketName;
|
|
965
|
+
profileConfig.stackName = stackName;
|
|
959
966
|
|
|
960
|
-
console.log(' ✅ Bootstrap stack updated')
|
|
967
|
+
console.log(' ✅ Bootstrap stack updated');
|
|
961
968
|
} catch (error) {
|
|
962
|
-
console.log(` ❌ Stack update failed: ${error.message}`)
|
|
969
|
+
console.log(` ❌ Stack update failed: ${error.message}`);
|
|
963
970
|
}
|
|
964
971
|
|
|
965
972
|
// Re-deploy CI stack if it was provisioned or --ci flag is set
|
|
966
|
-
const shouldUpdateCi = profileConfig.ciInfraProvisioned || options.ci
|
|
973
|
+
const shouldUpdateCi = profileConfig.ciInfraProvisioned || options.ci;
|
|
967
974
|
if (shouldUpdateCi) {
|
|
968
|
-
this._displayProgress('🧪', 'Updating CI harness stack...')
|
|
975
|
+
this._displayProgress('🧪', 'Updating CI harness stack...');
|
|
969
976
|
|
|
970
977
|
try {
|
|
971
|
-
const ciHarnessDir = path.resolve(__dirname, '../../infra/ci-harness')
|
|
978
|
+
const ciHarnessDir = path.resolve(__dirname, '../../infra/ci-harness');
|
|
972
979
|
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
execSync(
|
|
981
|
-
`npx cdk deploy MlccCiHarnessStack --require-approval never`,
|
|
982
|
-
{
|
|
980
|
+
// CI harness source is not bundled in the npm package — only available from git clone
|
|
981
|
+
if (!existsSync(ciHarnessDir)) {
|
|
982
|
+
console.log(' ⏭️ CI harness source not available (npm install does not include infra/)');
|
|
983
|
+
console.log(' To update the CI stack, run from a git clone: git clone https://github.com/awslabs/ml-container-creator && cd ml-container-creator && npx cdk deploy -c region=REGION');
|
|
984
|
+
} else {
|
|
985
|
+
// Ensure dependencies are installed (handles cold starts / fresh clones)
|
|
986
|
+
execSync('npm install --silent', {
|
|
983
987
|
cwd: ciHarnessDir,
|
|
984
988
|
encoding: 'utf8',
|
|
985
|
-
stdio: '
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
989
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
execSync(
|
|
993
|
+
'npx cdk deploy MlccCiHarnessStack --require-approval never',
|
|
994
|
+
{
|
|
995
|
+
cwd: ciHarnessDir,
|
|
996
|
+
encoding: 'utf8',
|
|
997
|
+
stdio: 'inherit',
|
|
998
|
+
env: {
|
|
999
|
+
...process.env,
|
|
1000
|
+
CDK_DEFAULT_REGION: profileConfig.awsRegion,
|
|
1001
|
+
CDK_DEFAULT_ACCOUNT: profileConfig.accountId,
|
|
1002
|
+
AWS_PROFILE: profileConfig.awsProfile
|
|
1003
|
+
}
|
|
991
1004
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1005
|
+
);
|
|
1006
|
+
profileConfig.ciInfraProvisioned = true;
|
|
1007
|
+
console.log(' ✅ CI harness stack updated');
|
|
1008
|
+
}
|
|
996
1009
|
} catch (error) {
|
|
997
|
-
console.log(` ❌ CI stack update failed: ${error.message}`)
|
|
1010
|
+
console.log(` ❌ CI stack update failed: ${error.message}`);
|
|
998
1011
|
}
|
|
999
1012
|
} else {
|
|
1000
|
-
console.log(' ⏭️ CI stack skipped (not provisioned — use --ci to force)')
|
|
1013
|
+
console.log(' ⏭️ CI stack skipped (not provisioned — use --ci to force)');
|
|
1001
1014
|
}
|
|
1002
1015
|
|
|
1003
1016
|
// Save updated profile
|
|
1004
|
-
this.config.setProfile(name, profileConfig)
|
|
1005
|
-
console.log(`\n✅ Update complete for profile "${name}"`)
|
|
1017
|
+
this.config.setProfile(name, profileConfig);
|
|
1018
|
+
console.log(`\n✅ Update complete for profile "${name}"`);
|
|
1006
1019
|
}
|
|
1007
1020
|
|
|
1008
1021
|
/**
|
|
@@ -1011,15 +1024,15 @@ export default class BootstrapCommandHandler {
|
|
|
1011
1024
|
* @returns {string|null} Resource type or null if not recognized
|
|
1012
1025
|
*/
|
|
1013
1026
|
_inferResourceTypeFromArn(arn) {
|
|
1014
|
-
if (arn.includes(':endpoint/')) return 'sagemaker-endpoint'
|
|
1015
|
-
if (arn.includes(':endpoint-config/')) return 'sagemaker-endpoint-config'
|
|
1016
|
-
if (arn.includes(':model/')) return 'sagemaker-model'
|
|
1017
|
-
if (arn.includes(':inference-component/')) return 'sagemaker-inference-component'
|
|
1018
|
-
if (arn.includes(':transform-job/')) return 'sagemaker-transform-job'
|
|
1019
|
-
if (arn.includes(':project/')) return 'codebuild-project'
|
|
1020
|
-
if (arn.includes(':role/')) return 'iam-role'
|
|
1021
|
-
if (arn.includes(':topic')) return 'sns-topic'
|
|
1022
|
-
return null
|
|
1027
|
+
if (arn.includes(':endpoint/')) return 'sagemaker-endpoint';
|
|
1028
|
+
if (arn.includes(':endpoint-config/')) return 'sagemaker-endpoint-config';
|
|
1029
|
+
if (arn.includes(':model/')) return 'sagemaker-model';
|
|
1030
|
+
if (arn.includes(':inference-component/')) return 'sagemaker-inference-component';
|
|
1031
|
+
if (arn.includes(':transform-job/')) return 'sagemaker-transform-job';
|
|
1032
|
+
if (arn.includes(':project/')) return 'codebuild-project';
|
|
1033
|
+
if (arn.includes(':role/')) return 'iam-role';
|
|
1034
|
+
if (arn.includes(':topic')) return 'sns-topic';
|
|
1035
|
+
return null;
|
|
1023
1036
|
}
|
|
1024
1037
|
|
|
1025
1038
|
/**
|
|
@@ -1028,9 +1041,9 @@ export default class BootstrapCommandHandler {
|
|
|
1028
1041
|
* @returns {string|null} Project name or null
|
|
1029
1042
|
*/
|
|
1030
1043
|
_inferProjectFromTags(tags) {
|
|
1031
|
-
if (!tags) return null
|
|
1032
|
-
const projectTag = tags.find(t => t.Key === 'mlcc:project' || t.Key === 'project')
|
|
1033
|
-
return projectTag ? projectTag.Value : null
|
|
1044
|
+
if (!tags) return null;
|
|
1045
|
+
const projectTag = tags.find(t => t.Key === 'mlcc:project' || t.Key === 'project');
|
|
1046
|
+
return projectTag ? projectTag.Value : null;
|
|
1034
1047
|
}
|
|
1035
1048
|
|
|
1036
1049
|
/**
|
|
@@ -1041,7 +1054,7 @@ export default class BootstrapCommandHandler {
|
|
|
1041
1054
|
_inferProjectFromImageTag(tag) {
|
|
1042
1055
|
// Tags often follow pattern: project-name-suffix
|
|
1043
1056
|
// Best effort: use the tag itself as project identifier
|
|
1044
|
-
return tag.replace(/-latest$/, '').replace(/-\d+$/, '') || 'unknown'
|
|
1057
|
+
return tag.replace(/-latest$/, '').replace(/-\d+$/, '') || 'unknown';
|
|
1045
1058
|
}
|
|
1046
1059
|
|
|
1047
1060
|
/**
|
|
@@ -1051,8 +1064,8 @@ export default class BootstrapCommandHandler {
|
|
|
1051
1064
|
*/
|
|
1052
1065
|
_inferProjectFromCodeBuildName(name) {
|
|
1053
1066
|
// Pattern: {project}-build-{suffix}
|
|
1054
|
-
const match = name.match(/^(.+?)-build-/)
|
|
1055
|
-
return match ? match[1] : name
|
|
1067
|
+
const match = name.match(/^(.+?)-build-/);
|
|
1068
|
+
return match ? match[1] : name;
|
|
1056
1069
|
}
|
|
1057
1070
|
|
|
1058
1071
|
// ── Provisioning steps ──────────────────────────────────────────
|
|
@@ -1062,15 +1075,15 @@ export default class BootstrapCommandHandler {
|
|
|
1062
1075
|
* @param {object} options - Parsed CLI options
|
|
1063
1076
|
* @returns {Promise<string>} Selected AWS profile name
|
|
1064
1077
|
*/
|
|
1065
|
-
async _selectProfile(
|
|
1066
|
-
const profiles = this.profileParser.getProfiles()
|
|
1078
|
+
async _selectProfile(_options) {
|
|
1079
|
+
const profiles = this.profileParser.getProfiles();
|
|
1067
1080
|
|
|
1068
1081
|
if (profiles.length === 0) {
|
|
1069
|
-
console.log('❌ No AWS profiles found. Run `aws configure` first.')
|
|
1070
|
-
throw new Error('No AWS profiles found. Run `aws configure` first.')
|
|
1082
|
+
console.log('❌ No AWS profiles found. Run `aws configure` first.');
|
|
1083
|
+
throw new Error('No AWS profiles found. Run `aws configure` first.');
|
|
1071
1084
|
}
|
|
1072
1085
|
|
|
1073
|
-
const defaultProfile = profiles.includes('default') ? 'default' : profiles[0]
|
|
1086
|
+
const defaultProfile = profiles.includes('default') ? 'default' : profiles[0];
|
|
1074
1087
|
|
|
1075
1088
|
const { awsProfile } = await this._promptFn([{
|
|
1076
1089
|
type: 'list',
|
|
@@ -1078,9 +1091,9 @@ export default class BootstrapCommandHandler {
|
|
|
1078
1091
|
message: 'Select an AWS profile:',
|
|
1079
1092
|
choices: profiles,
|
|
1080
1093
|
default: defaultProfile
|
|
1081
|
-
}])
|
|
1094
|
+
}]);
|
|
1082
1095
|
|
|
1083
|
-
return awsProfile
|
|
1096
|
+
return awsProfile;
|
|
1084
1097
|
}
|
|
1085
1098
|
|
|
1086
1099
|
/**
|
|
@@ -1090,23 +1103,23 @@ export default class BootstrapCommandHandler {
|
|
|
1090
1103
|
* @returns {Promise<object>} Object with accountId and region
|
|
1091
1104
|
*/
|
|
1092
1105
|
async _validateCredentials(profile, providedRegion) {
|
|
1093
|
-
const identity = this._execAws('sts get-caller-identity', profile)
|
|
1094
|
-
const accountId = identity.Account
|
|
1106
|
+
const identity = this._execAws('sts get-caller-identity', profile);
|
|
1107
|
+
const accountId = identity.Account;
|
|
1095
1108
|
|
|
1096
|
-
let region
|
|
1109
|
+
let region;
|
|
1097
1110
|
if (providedRegion) {
|
|
1098
|
-
region = providedRegion
|
|
1111
|
+
region = providedRegion;
|
|
1099
1112
|
} else {
|
|
1100
1113
|
const answer = await this._promptFn([{
|
|
1101
1114
|
type: 'input',
|
|
1102
1115
|
name: 'region',
|
|
1103
1116
|
message: 'AWS region for resources:',
|
|
1104
1117
|
default: 'us-east-1'
|
|
1105
|
-
}])
|
|
1106
|
-
region = answer.region
|
|
1118
|
+
}]);
|
|
1119
|
+
region = answer.region;
|
|
1107
1120
|
}
|
|
1108
1121
|
|
|
1109
|
-
return { accountId, region }
|
|
1122
|
+
return { accountId, region };
|
|
1110
1123
|
}
|
|
1111
1124
|
|
|
1112
1125
|
/**
|
|
@@ -1114,8 +1127,8 @@ export default class BootstrapCommandHandler {
|
|
|
1114
1127
|
* @param {object} options - Parsed CLI options
|
|
1115
1128
|
* @returns {Promise<string>} Role ARN
|
|
1116
1129
|
*/
|
|
1117
|
-
async _setupIamRole(
|
|
1118
|
-
const roleName = 'mlcc-sagemaker-execution-role'
|
|
1130
|
+
async _setupIamRole(_options) {
|
|
1131
|
+
const roleName = 'mlcc-sagemaker-execution-role';
|
|
1119
1132
|
|
|
1120
1133
|
// Define trust policy for SageMaker
|
|
1121
1134
|
const trustPolicy = {
|
|
@@ -1129,7 +1142,7 @@ export default class BootstrapCommandHandler {
|
|
|
1129
1142
|
Action: 'sts:AssumeRole'
|
|
1130
1143
|
}
|
|
1131
1144
|
]
|
|
1132
|
-
}
|
|
1145
|
+
};
|
|
1133
1146
|
|
|
1134
1147
|
// Define execution policy with least-privilege permissions
|
|
1135
1148
|
const executionPolicy = {
|
|
@@ -1199,92 +1212,92 @@ export default class BootstrapCommandHandler {
|
|
|
1199
1212
|
]
|
|
1200
1213
|
}
|
|
1201
1214
|
]
|
|
1202
|
-
}
|
|
1215
|
+
};
|
|
1203
1216
|
|
|
1204
1217
|
// Check if role already exists
|
|
1205
1218
|
const roleExists = this._resourceExists(
|
|
1206
1219
|
`iam get-role --role-name ${roleName}`,
|
|
1207
1220
|
this._currentProfile
|
|
1208
|
-
)
|
|
1221
|
+
);
|
|
1209
1222
|
|
|
1210
1223
|
if (roleExists) {
|
|
1211
1224
|
const existingRole = this._execAws(
|
|
1212
1225
|
`iam get-role --role-name ${roleName}`,
|
|
1213
1226
|
this._currentProfile
|
|
1214
|
-
)
|
|
1215
|
-
const roleArn = existingRole.Role.Arn
|
|
1216
|
-
console.log(` ✅ IAM role "${roleName}" already exists — reused`)
|
|
1227
|
+
);
|
|
1228
|
+
const roleArn = existingRole.Role.Arn;
|
|
1229
|
+
console.log(` ✅ IAM role "${roleName}" already exists — reused`);
|
|
1217
1230
|
|
|
1218
1231
|
// Always update the inline policy and tags to ensure they're current
|
|
1219
1232
|
try {
|
|
1220
|
-
const execPolicyFile = this._writeJsonTempFile(executionPolicy, 'exec-policy')
|
|
1233
|
+
const execPolicyFile = this._writeJsonTempFile(executionPolicy, 'exec-policy');
|
|
1221
1234
|
this._execAws(
|
|
1222
1235
|
`iam put-role-policy --role-name ${roleName} --policy-name mlcc-execution-policy --policy-document ${execPolicyFile}`,
|
|
1223
1236
|
this._currentProfile
|
|
1224
|
-
)
|
|
1225
|
-
console.log(
|
|
1237
|
+
);
|
|
1238
|
+
console.log(' ✅ IAM policy "mlcc-execution-policy" — updated');
|
|
1226
1239
|
} catch (err) {
|
|
1227
|
-
console.log(` ⚠️ Could not update inline policy: ${err.message}`)
|
|
1240
|
+
console.log(` ⚠️ Could not update inline policy: ${err.message}`);
|
|
1228
1241
|
}
|
|
1229
1242
|
|
|
1230
1243
|
try {
|
|
1231
|
-
const tags = this._buildResourceTags()
|
|
1244
|
+
const tags = this._buildResourceTags();
|
|
1232
1245
|
this._execAws(
|
|
1233
1246
|
`iam tag-role --role-name ${roleName} --tags ${this._formatTagsForCli(tags)}`,
|
|
1234
1247
|
this._currentProfile
|
|
1235
|
-
)
|
|
1236
|
-
console.log(
|
|
1248
|
+
);
|
|
1249
|
+
console.log(' ✅ IAM role tags — updated');
|
|
1237
1250
|
} catch (err) {
|
|
1238
|
-
console.log(` ⚠️ Could not update role tags: ${err.message}`)
|
|
1251
|
+
console.log(` ⚠️ Could not update role tags: ${err.message}`);
|
|
1239
1252
|
}
|
|
1240
1253
|
|
|
1241
|
-
return roleArn
|
|
1254
|
+
return roleArn;
|
|
1242
1255
|
}
|
|
1243
1256
|
|
|
1244
1257
|
// Display policies to user before creation
|
|
1245
|
-
console.log('\n Trust Policy:')
|
|
1246
|
-
console.log(JSON.stringify(trustPolicy, null, 2))
|
|
1247
|
-
console.log('\n Execution Policy:')
|
|
1248
|
-
console.log(JSON.stringify(executionPolicy, null, 2))
|
|
1249
|
-
console.log('')
|
|
1258
|
+
console.log('\n Trust Policy:');
|
|
1259
|
+
console.log(JSON.stringify(trustPolicy, null, 2));
|
|
1260
|
+
console.log('\n Execution Policy:');
|
|
1261
|
+
console.log(JSON.stringify(executionPolicy, null, 2));
|
|
1262
|
+
console.log('');
|
|
1250
1263
|
|
|
1251
1264
|
try {
|
|
1252
1265
|
// Create the IAM role — write policy to temp file to avoid shell escaping issues
|
|
1253
|
-
const trustPolicyFile = this._writeJsonTempFile(trustPolicy, 'trust-policy')
|
|
1266
|
+
const trustPolicyFile = this._writeJsonTempFile(trustPolicy, 'trust-policy');
|
|
1254
1267
|
const createRoleResult = this._execAws(
|
|
1255
1268
|
`iam create-role --role-name ${roleName} --assume-role-policy-document ${trustPolicyFile}`,
|
|
1256
1269
|
this._currentProfile
|
|
1257
|
-
)
|
|
1258
|
-
const roleArn = createRoleResult.Role.Arn
|
|
1270
|
+
);
|
|
1271
|
+
const roleArn = createRoleResult.Role.Arn;
|
|
1259
1272
|
|
|
1260
1273
|
// Attach inline execution policy
|
|
1261
|
-
const execPolicyFile = this._writeJsonTempFile(executionPolicy, 'exec-policy')
|
|
1274
|
+
const execPolicyFile = this._writeJsonTempFile(executionPolicy, 'exec-policy');
|
|
1262
1275
|
this._execAws(
|
|
1263
1276
|
`iam put-role-policy --role-name ${roleName} --policy-name mlcc-execution-policy --policy-document ${execPolicyFile}`,
|
|
1264
1277
|
this._currentProfile
|
|
1265
|
-
)
|
|
1278
|
+
);
|
|
1266
1279
|
|
|
1267
1280
|
// Apply resource tags
|
|
1268
|
-
const tags = this._buildResourceTags()
|
|
1281
|
+
const tags = this._buildResourceTags();
|
|
1269
1282
|
this._execAws(
|
|
1270
1283
|
`iam tag-role --role-name ${roleName} --tags ${this._formatTagsForCli(tags)}`,
|
|
1271
1284
|
this._currentProfile
|
|
1272
|
-
)
|
|
1285
|
+
);
|
|
1273
1286
|
|
|
1274
|
-
console.log(` ✅ IAM role "${roleName}" — created`)
|
|
1275
|
-
return roleArn
|
|
1287
|
+
console.log(` ✅ IAM role "${roleName}" — created`);
|
|
1288
|
+
return roleArn;
|
|
1276
1289
|
} catch (error) {
|
|
1277
|
-
const errorMessage = error.message || ''
|
|
1290
|
+
const errorMessage = error.message || '';
|
|
1278
1291
|
if (errorMessage.includes('AccessDenied') || errorMessage.includes('UnauthorizedAccess')) {
|
|
1279
|
-
console.log(' ⚠️ Permission denied for iam:CreateRole. Please provide an existing role ARN.')
|
|
1292
|
+
console.log(' ⚠️ Permission denied for iam:CreateRole. Please provide an existing role ARN.');
|
|
1280
1293
|
const { roleArn } = await this._promptFn([{
|
|
1281
1294
|
type: 'input',
|
|
1282
1295
|
name: 'roleArn',
|
|
1283
1296
|
message: 'Enter an existing IAM role ARN for SageMaker execution:'
|
|
1284
|
-
}])
|
|
1285
|
-
return roleArn
|
|
1297
|
+
}]);
|
|
1298
|
+
return roleArn;
|
|
1286
1299
|
}
|
|
1287
|
-
throw error
|
|
1300
|
+
throw error;
|
|
1288
1301
|
}
|
|
1289
1302
|
}
|
|
1290
1303
|
|
|
@@ -1293,27 +1306,27 @@ export default class BootstrapCommandHandler {
|
|
|
1293
1306
|
* @returns {Promise<string>} ECR repository name
|
|
1294
1307
|
*/
|
|
1295
1308
|
async _setupEcrRepository() {
|
|
1296
|
-
const repoName = 'ml-container-creator'
|
|
1309
|
+
const repoName = 'ml-container-creator';
|
|
1297
1310
|
|
|
1298
1311
|
// Check if repository already exists
|
|
1299
1312
|
const repoExists = this._resourceExists(
|
|
1300
1313
|
`ecr describe-repositories --repository-names ${repoName} --region ${this._currentRegion}`,
|
|
1301
1314
|
this._currentProfile
|
|
1302
|
-
)
|
|
1315
|
+
);
|
|
1303
1316
|
|
|
1304
1317
|
if (repoExists) {
|
|
1305
|
-
console.log(` ✅ ECR repository "${repoName}" already exists — reused`)
|
|
1306
|
-
return repoName
|
|
1318
|
+
console.log(` ✅ ECR repository "${repoName}" already exists — reused`);
|
|
1319
|
+
return repoName;
|
|
1307
1320
|
}
|
|
1308
1321
|
|
|
1309
1322
|
// Build resource tags
|
|
1310
|
-
const tags = this._buildResourceTags()
|
|
1323
|
+
const tags = this._buildResourceTags();
|
|
1311
1324
|
|
|
1312
1325
|
// Create the ECR repository with image scanning and AES256 encryption
|
|
1313
1326
|
this._execAws(
|
|
1314
1327
|
`ecr create-repository --repository-name ${repoName} --image-scanning-configuration scanOnPush=true --encryption-configuration encryptionType=AES256 --region ${this._currentRegion} --tags ${this._formatTagsForCli(tags)}`,
|
|
1315
1328
|
this._currentProfile
|
|
1316
|
-
)
|
|
1329
|
+
);
|
|
1317
1330
|
|
|
1318
1331
|
// Apply lifecycle policy to expire untagged images after 30 days
|
|
1319
1332
|
const lifecyclePolicy = {
|
|
@@ -1332,16 +1345,16 @@ export default class BootstrapCommandHandler {
|
|
|
1332
1345
|
}
|
|
1333
1346
|
}
|
|
1334
1347
|
]
|
|
1335
|
-
}
|
|
1348
|
+
};
|
|
1336
1349
|
|
|
1337
|
-
const lifecyclePolicyFile = this._writeJsonTempFile(lifecyclePolicy, 'ecr-lifecycle')
|
|
1350
|
+
const lifecyclePolicyFile = this._writeJsonTempFile(lifecyclePolicy, 'ecr-lifecycle');
|
|
1338
1351
|
this._execAws(
|
|
1339
1352
|
`ecr put-lifecycle-policy --repository-name ${repoName} --lifecycle-policy-text ${lifecyclePolicyFile} --region ${this._currentRegion}`,
|
|
1340
1353
|
this._currentProfile
|
|
1341
|
-
)
|
|
1354
|
+
);
|
|
1342
1355
|
|
|
1343
|
-
console.log(` ✅ ECR repository "${repoName}" — created`)
|
|
1344
|
-
return repoName
|
|
1356
|
+
console.log(` ✅ ECR repository "${repoName}" — created`);
|
|
1357
|
+
return repoName;
|
|
1345
1358
|
}
|
|
1346
1359
|
|
|
1347
1360
|
/**
|
|
@@ -1354,20 +1367,20 @@ export default class BootstrapCommandHandler {
|
|
|
1354
1367
|
name: 'useS3',
|
|
1355
1368
|
message: 'Will you use async inference or batch transform?',
|
|
1356
1369
|
default: false
|
|
1357
|
-
}])
|
|
1370
|
+
}]);
|
|
1358
1371
|
|
|
1359
1372
|
if (!useS3) {
|
|
1360
|
-
return null
|
|
1373
|
+
return null;
|
|
1361
1374
|
}
|
|
1362
1375
|
|
|
1363
|
-
const asyncBucketName = `ml-container-creator-async-${this._currentRegion}-${this._currentAccountId}
|
|
1364
|
-
const batchBucketName = `ml-container-creator-batch-${this._currentRegion}-${this._currentAccountId}
|
|
1376
|
+
const asyncBucketName = `ml-container-creator-async-${this._currentRegion}-${this._currentAccountId}`;
|
|
1377
|
+
const batchBucketName = `ml-container-creator-batch-${this._currentRegion}-${this._currentAccountId}`;
|
|
1365
1378
|
|
|
1366
|
-
const tags = this._buildResourceTags()
|
|
1367
|
-
const asyncS3Bucket = await this._createS3Bucket(asyncBucketName, tags)
|
|
1368
|
-
const batchS3Bucket = await this._createS3Bucket(batchBucketName, tags)
|
|
1379
|
+
const tags = this._buildResourceTags();
|
|
1380
|
+
const asyncS3Bucket = await this._createS3Bucket(asyncBucketName, tags);
|
|
1381
|
+
const batchS3Bucket = await this._createS3Bucket(batchBucketName, tags);
|
|
1369
1382
|
|
|
1370
|
-
return { asyncS3Bucket, batchS3Bucket }
|
|
1383
|
+
return { asyncS3Bucket, batchS3Bucket };
|
|
1371
1384
|
}
|
|
1372
1385
|
|
|
1373
1386
|
/**
|
|
@@ -1381,45 +1394,45 @@ export default class BootstrapCommandHandler {
|
|
|
1381
1394
|
const bucketExists = this._resourceExists(
|
|
1382
1395
|
`s3api head-bucket --bucket ${bucketName}`,
|
|
1383
1396
|
this._currentProfile
|
|
1384
|
-
)
|
|
1397
|
+
);
|
|
1385
1398
|
|
|
1386
1399
|
if (bucketExists) {
|
|
1387
|
-
console.log(` ✅ S3 bucket "${bucketName}" already exists — reused`)
|
|
1388
|
-
return bucketName
|
|
1400
|
+
console.log(` ✅ S3 bucket "${bucketName}" already exists — reused`);
|
|
1401
|
+
return bucketName;
|
|
1389
1402
|
}
|
|
1390
1403
|
|
|
1391
1404
|
// Build create-bucket command with region-appropriate configuration
|
|
1392
|
-
let createCommand = `s3api create-bucket --bucket ${bucketName} --region ${this._currentRegion}
|
|
1405
|
+
let createCommand = `s3api create-bucket --bucket ${bucketName} --region ${this._currentRegion}`;
|
|
1393
1406
|
if (this._currentRegion !== 'us-east-1') {
|
|
1394
|
-
createCommand += ` --create-bucket-configuration LocationConstraint=${this._currentRegion}
|
|
1407
|
+
createCommand += ` --create-bucket-configuration LocationConstraint=${this._currentRegion}`;
|
|
1395
1408
|
}
|
|
1396
1409
|
|
|
1397
|
-
this._execAws(createCommand, this._currentProfile)
|
|
1410
|
+
this._execAws(createCommand, this._currentProfile);
|
|
1398
1411
|
|
|
1399
1412
|
// Enable versioning
|
|
1400
1413
|
this._execAws(
|
|
1401
1414
|
`s3api put-bucket-versioning --bucket ${bucketName} --versioning-configuration Status=Enabled`,
|
|
1402
1415
|
this._currentProfile
|
|
1403
|
-
)
|
|
1416
|
+
);
|
|
1404
1417
|
|
|
1405
1418
|
// Enable AES256 server-side encryption
|
|
1406
|
-
const encryptionConfig = { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' } }] }
|
|
1407
|
-
const encryptionFile = this._writeJsonTempFile(encryptionConfig, 's3-encryption')
|
|
1419
|
+
const encryptionConfig = { Rules: [{ ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256' } }] };
|
|
1420
|
+
const encryptionFile = this._writeJsonTempFile(encryptionConfig, 's3-encryption');
|
|
1408
1421
|
this._execAws(
|
|
1409
1422
|
`s3api put-bucket-encryption --bucket ${bucketName} --server-side-encryption-configuration ${encryptionFile}`,
|
|
1410
1423
|
this._currentProfile
|
|
1411
|
-
)
|
|
1424
|
+
);
|
|
1412
1425
|
|
|
1413
1426
|
// Apply resource tags
|
|
1414
|
-
const tagging = { TagSet: tags }
|
|
1415
|
-
const taggingFile = this._writeJsonTempFile(tagging, 's3-tagging')
|
|
1427
|
+
const tagging = { TagSet: tags };
|
|
1428
|
+
const taggingFile = this._writeJsonTempFile(tagging, 's3-tagging');
|
|
1416
1429
|
this._execAws(
|
|
1417
1430
|
`s3api put-bucket-tagging --bucket ${bucketName} --tagging ${taggingFile}`,
|
|
1418
1431
|
this._currentProfile
|
|
1419
|
-
)
|
|
1432
|
+
);
|
|
1420
1433
|
|
|
1421
|
-
console.log(` ✅ S3 bucket "${bucketName}" — created`)
|
|
1422
|
-
return bucketName
|
|
1434
|
+
console.log(` ✅ S3 bucket "${bucketName}" — created`);
|
|
1435
|
+
return bucketName;
|
|
1423
1436
|
}
|
|
1424
1437
|
|
|
1425
1438
|
// ── AWS CLI helpers ─────────────────────────────────────────────
|
|
@@ -1431,13 +1444,13 @@ export default class BootstrapCommandHandler {
|
|
|
1431
1444
|
* @returns {object} Parsed JSON output
|
|
1432
1445
|
*/
|
|
1433
1446
|
_execAws(command, profile) {
|
|
1434
|
-
const fullCommand = `aws ${command} --profile ${profile} --output json
|
|
1435
|
-
const output = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1436
|
-
const trimmed = output.trim()
|
|
1447
|
+
const fullCommand = `aws ${command} --profile ${profile} --output json`;
|
|
1448
|
+
const output = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1449
|
+
const trimmed = output.trim();
|
|
1437
1450
|
if (!trimmed) {
|
|
1438
|
-
return {}
|
|
1451
|
+
return {};
|
|
1439
1452
|
}
|
|
1440
|
-
return JSON.parse(trimmed)
|
|
1453
|
+
return JSON.parse(trimmed);
|
|
1441
1454
|
}
|
|
1442
1455
|
|
|
1443
1456
|
/**
|
|
@@ -1458,7 +1471,7 @@ export default class BootstrapCommandHandler {
|
|
|
1458
1471
|
// Build parameter overrides string
|
|
1459
1472
|
const paramOverrides = Object.entries(parameters)
|
|
1460
1473
|
.map(([key, value]) => `${key}=${value}`)
|
|
1461
|
-
.join(' ')
|
|
1474
|
+
.join(' ');
|
|
1462
1475
|
|
|
1463
1476
|
const deployCommand = [
|
|
1464
1477
|
'aws cloudformation deploy',
|
|
@@ -1468,18 +1481,18 @@ export default class BootstrapCommandHandler {
|
|
|
1468
1481
|
`--parameter-overrides ${paramOverrides}`,
|
|
1469
1482
|
`--profile ${profile}`,
|
|
1470
1483
|
`--region ${region}`
|
|
1471
|
-
].join(' ')
|
|
1484
|
+
].join(' ');
|
|
1472
1485
|
|
|
1473
1486
|
try {
|
|
1474
|
-
execSync(deployCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1487
|
+
execSync(deployCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1475
1488
|
} catch (error) {
|
|
1476
1489
|
// "No changes to deploy" is a success case — CloudFormation deploy
|
|
1477
1490
|
// exits with code 255 when there's nothing to update
|
|
1478
|
-
const stderr = error.stderr || error.message || ''
|
|
1491
|
+
const stderr = error.stderr || error.message || '';
|
|
1479
1492
|
if (stderr.includes('No changes to deploy')) {
|
|
1480
|
-
console.log(' ℹ️ Stack is up to date — no changes needed')
|
|
1493
|
+
console.log(' ℹ️ Stack is up to date — no changes needed');
|
|
1481
1494
|
} else {
|
|
1482
|
-
throw error
|
|
1495
|
+
throw error;
|
|
1483
1496
|
}
|
|
1484
1497
|
}
|
|
1485
1498
|
|
|
@@ -1487,19 +1500,19 @@ export default class BootstrapCommandHandler {
|
|
|
1487
1500
|
const describeResult = this._execAws(
|
|
1488
1501
|
`cloudformation describe-stacks --stack-name ${stackName} --region ${region}`,
|
|
1489
1502
|
profile
|
|
1490
|
-
)
|
|
1503
|
+
);
|
|
1491
1504
|
|
|
1492
|
-
const stack = describeResult.Stacks && describeResult.Stacks[0]
|
|
1505
|
+
const stack = describeResult.Stacks && describeResult.Stacks[0];
|
|
1493
1506
|
if (!stack) {
|
|
1494
|
-
throw new Error(`Stack "${stackName}" not found after deployment`)
|
|
1507
|
+
throw new Error(`Stack "${stackName}" not found after deployment`);
|
|
1495
1508
|
}
|
|
1496
1509
|
|
|
1497
|
-
const outputs = {}
|
|
1510
|
+
const outputs = {};
|
|
1498
1511
|
for (const output of (stack.Outputs || [])) {
|
|
1499
|
-
outputs[output.OutputKey] = output.OutputValue
|
|
1512
|
+
outputs[output.OutputKey] = output.OutputValue;
|
|
1500
1513
|
}
|
|
1501
1514
|
|
|
1502
|
-
return outputs
|
|
1515
|
+
return outputs;
|
|
1503
1516
|
}
|
|
1504
1517
|
|
|
1505
1518
|
/**
|
|
@@ -1511,13 +1524,13 @@ export default class BootstrapCommandHandler {
|
|
|
1511
1524
|
* @returns {string} The `file://` path to the temp file
|
|
1512
1525
|
*/
|
|
1513
1526
|
_writeJsonTempFile(jsonObj, prefix = 'mlcc-policy') {
|
|
1514
|
-
const dir = path.join(tmpdir(), 'mlcc-bootstrap')
|
|
1527
|
+
const dir = path.join(tmpdir(), 'mlcc-bootstrap');
|
|
1515
1528
|
if (!existsSync(dir)) {
|
|
1516
|
-
mkdirSync(dir, { recursive: true })
|
|
1529
|
+
mkdirSync(dir, { recursive: true });
|
|
1517
1530
|
}
|
|
1518
|
-
const filePath = path.join(dir, `${prefix}-${Date.now()}.json`)
|
|
1519
|
-
writeFileSync(filePath, JSON.stringify(jsonObj))
|
|
1520
|
-
return `file://${filePath}
|
|
1531
|
+
const filePath = path.join(dir, `${prefix}-${Date.now()}.json`);
|
|
1532
|
+
writeFileSync(filePath, JSON.stringify(jsonObj));
|
|
1533
|
+
return `file://${filePath}`;
|
|
1521
1534
|
}
|
|
1522
1535
|
|
|
1523
1536
|
/**
|
|
@@ -1528,10 +1541,10 @@ export default class BootstrapCommandHandler {
|
|
|
1528
1541
|
*/
|
|
1529
1542
|
_resourceExists(checkCommand, profile) {
|
|
1530
1543
|
try {
|
|
1531
|
-
this._execAws(checkCommand, profile)
|
|
1532
|
-
return true
|
|
1544
|
+
this._execAws(checkCommand, profile);
|
|
1545
|
+
return true;
|
|
1533
1546
|
} catch {
|
|
1534
|
-
return false
|
|
1547
|
+
return false;
|
|
1535
1548
|
}
|
|
1536
1549
|
}
|
|
1537
1550
|
|
|
@@ -1542,13 +1555,13 @@ export default class BootstrapCommandHandler {
|
|
|
1542
1555
|
* @returns {Array<{Key: string, Value: string}>} Tag array
|
|
1543
1556
|
*/
|
|
1544
1557
|
_buildResourceTags() {
|
|
1545
|
-
const packageJsonPath = path.resolve(__dirname, '../../package.json')
|
|
1546
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
1558
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
1559
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
1547
1560
|
return [
|
|
1548
1561
|
{ Key: 'mlcc:managed-by', Value: 'ml-container-creator' },
|
|
1549
1562
|
{ Key: 'mlcc:created-by', Value: 'bootstrap' },
|
|
1550
1563
|
{ Key: 'mlcc:version', Value: packageJson.version }
|
|
1551
|
-
]
|
|
1564
|
+
];
|
|
1552
1565
|
}
|
|
1553
1566
|
|
|
1554
1567
|
/**
|
|
@@ -1560,7 +1573,7 @@ export default class BootstrapCommandHandler {
|
|
|
1560
1573
|
* @returns {string} file:// path to the tags JSON file
|
|
1561
1574
|
*/
|
|
1562
1575
|
_formatTagsForCli(tags) {
|
|
1563
|
-
return this._writeJsonTempFile(tags, 'tags')
|
|
1576
|
+
return this._writeJsonTempFile(tags, 'tags');
|
|
1564
1577
|
}
|
|
1565
1578
|
|
|
1566
1579
|
// ── Display helpers ─────────────────────────────────────────────
|
|
@@ -1619,7 +1632,7 @@ EXAMPLES:
|
|
|
1619
1632
|
ml-container-creator bootstrap --non-interactive --profile my-aws-profile --role-arn arn:aws:iam::123456789012:role/MyRole --skip-s3
|
|
1620
1633
|
ml-container-creator bootstrap --non-interactive --profile my-aws-profile --region us-west-2 --ci
|
|
1621
1634
|
ml-container-creator bootstrap --non-interactive --profile my-aws-profile --region us-west-2 --skip-ci
|
|
1622
|
-
`)
|
|
1635
|
+
`);
|
|
1623
1636
|
}
|
|
1624
1637
|
|
|
1625
1638
|
/**
|
|
@@ -1628,12 +1641,12 @@ EXAMPLES:
|
|
|
1628
1641
|
* @param {object} profileConfig - Profile configuration object
|
|
1629
1642
|
*/
|
|
1630
1643
|
_displaySummary(profileName, profileConfig) {
|
|
1631
|
-
console.log(`\n📋 Bootstrap Profile: ${profileName}`)
|
|
1632
|
-
console.log('─'.repeat(40))
|
|
1644
|
+
console.log(`\n📋 Bootstrap Profile: ${profileName}`);
|
|
1645
|
+
console.log('─'.repeat(40));
|
|
1633
1646
|
for (const [key, value] of Object.entries(profileConfig)) {
|
|
1634
|
-
console.log(` ${key}: ${value}`)
|
|
1647
|
+
console.log(` ${key}: ${value}`);
|
|
1635
1648
|
}
|
|
1636
|
-
console.log('─'.repeat(40))
|
|
1649
|
+
console.log('─'.repeat(40));
|
|
1637
1650
|
}
|
|
1638
1651
|
|
|
1639
1652
|
/**
|
|
@@ -1642,6 +1655,6 @@ EXAMPLES:
|
|
|
1642
1655
|
* @param {string} message - Progress message
|
|
1643
1656
|
*/
|
|
1644
1657
|
_displayProgress(emoji, message) {
|
|
1645
|
-
console.log(`${emoji} ${message}`)
|
|
1658
|
+
console.log(`${emoji} ${message}`);
|
|
1646
1659
|
}
|
|
1647
1660
|
}
|