cloudmason 1.7.22 → 1.9.32

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.
@@ -1,27 +1,42 @@
1
1
  name: Publish Node.js Package
2
-
3
2
  on:
4
- pull_request:
5
- types:
6
- - closed
3
+ push:
7
4
  branches:
8
5
  - 'release/**'
9
6
 
10
7
  jobs:
11
- if_merged:
12
- if: github.event.pull_request.merged == true
8
+ publish:
13
9
  runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
14
13
  steps:
15
- - uses: actions/checkout@v3
16
- - uses: actions/setup-node@v3
14
+ - run: echo "Workflow version 3 - push trigger"
15
+
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 24
20
+ - name: Upgrade npm for OIDC support
21
+ run: npm install -g npm@latest
22
+ - name: Debug OIDC - check actual token request
23
+ uses: actions/github-script@v7
17
24
  with:
18
- node-version: 16
19
- registry-url: https://registry.npmjs.org/
25
+ script: |
26
+ try {
27
+ const token = await core.getIDToken('https://registry.npmjs.org');
28
+ console.log('OIDC token retrieved successfully, length:', token.length);
29
+ } catch (err) {
30
+ console.log('Failed to get OIDC token:', err.message);
31
+ }
32
+ - name: Debug OIDC token
33
+ run: |
34
+ echo "ACTIONS_ID_TOKEN_REQUEST_URL exists: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL != '' }}"
35
+ echo "ACTIONS_ID_TOKEN_REQUEST_TOKEN exists: ${{ env.ACTIONS_ID_TOKEN_REQUEST_TOKEN != '' }}"
36
+
37
+ - run: npm ci
20
38
  - run: npm run build
21
39
  env:
22
- branch: ${{ github.base_ref }}
40
+ branch: ${{ github.ref_name }}
23
41
  build: ${{ github.run_number }}
24
- - run: npm ci
25
- - run: npm publish
26
- env:
27
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
42
+ - run: npm publish --provenance --access public
@@ -95,6 +95,9 @@ async function getSubnets(region){
95
95
  const subnetsData = await ec2Client.send(new DescribeSubnetsCommand({}));
96
96
  const subNets = subnetsData.Subnets.filter(s=>{ return s.DefaultForAz === true });
97
97
  const subnetList = subNets.map(subnet => subnet.SubnetId );
98
+ if (subnetList.length > 3){
99
+ subnetList.length = 3; // Limit to 3 subnets
100
+ }
98
101
  return subnetList;
99
102
  }
100
103
 
@@ -15,7 +15,8 @@ exports.main = async function(args){
15
15
  // -- Get Version & Descriptions
16
16
  const pubArgs = {
17
17
  version: args.v,
18
- changeDescription: args.desc
18
+ changeDescription: args.desc,
19
+ wait: args.wait || false
19
20
  };
20
21
 
21
22
  // -- Get Params
@@ -53,7 +54,7 @@ exports.main = async function(args){
53
54
 
54
55
  // Update AMI Function
55
56
 
56
- const updateAmiVersion = async ({productId, amiId, version, changeDescription}) => {
57
+ const updateAmiVersion = async ({productId, amiId, version, changeDescription, wait}) => {
57
58
  const client = new MarketplaceCatalogClient({ region: process.env.orgRegion }); // Update the region if needed
58
59
  console.log('Updating AMI version:',productId, amiId, version, changeDescription);
59
60
  try {
@@ -137,15 +138,92 @@ const updateAmiVersion = async ({productId, amiId, version, changeDescription})
137
138
  break;
138
139
  } else if (status === "FAILED") {
139
140
  console.error("Change set failed:", describeResponse);
140
- break;
141
+ throw new Error("Change set failed");
141
142
  }
142
143
  }
144
+
145
+ // If wait flag is set, poll entity until version is publicly available
146
+ if (wait && status === "SUCCEEDED") {
147
+ console.log("Waiting for version to become available to consumers...");
148
+ await waitForVersionAvailability(client, productId, version);
149
+ }
150
+
143
151
  } catch (error) {
144
152
  console.error("Error updating AMI version:", error);
153
+ throw error;
145
154
  }
146
155
  };
147
156
 
148
157
 
158
+ // Wait for Version Availability Function
159
+ const waitForVersionAvailability = async (client, productId, version) => {
160
+ const maxAttempts = 1080; // 90 minutes with 5-second intervals (90 * 60 / 5 = 1080)
161
+ let attempts = 0;
162
+
163
+ console.log(`Polling entity for version ${version} availability...`);
164
+ console.log(`Timeout: 90 minutes (will check every 5 seconds)`);
165
+
166
+ while (attempts < maxAttempts) {
167
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
168
+
169
+ try {
170
+ const describeEntityCommand = new DescribeEntityCommand({
171
+ Catalog: "AWSMarketplace",
172
+ EntityId: productId,
173
+ });
174
+
175
+ const entityResponse = await client.send(describeEntityCommand);
176
+
177
+ // Parse the Details field which contains version information
178
+ let details;
179
+ if (typeof entityResponse.Details === 'string') {
180
+ details = JSON.parse(entityResponse.Details);
181
+ } else {
182
+ details = entityResponse.Details;
183
+ }
184
+
185
+ // Check if version exists in Versions array
186
+ const versionInfo = details.Versions?.find(v => v.VersionTitle === version);
187
+
188
+ if (versionInfo) {
189
+ // Check if version has delivery options (indicates it's available)
190
+ const hasDeliveryOptions = versionInfo.DeliveryOptions &&
191
+ versionInfo.DeliveryOptions.length > 0;
192
+
193
+ if (hasDeliveryOptions) {
194
+ // Check if any delivery option has Sources (indicates AMI is accessible)
195
+ const hasActiveSources = versionInfo.DeliveryOptions.some(
196
+ option => option.Details?.AmiDeliveryOptionDetails?.AmiSource ||
197
+ option.Details?.AmiSource
198
+ );
199
+
200
+ if (hasActiveSources) {
201
+ console.log(`āœ“ Version ${version} is now available to consumers`);
202
+ console.log("Version details:", JSON.stringify(versionInfo, null, 2));
203
+ return true;
204
+ }
205
+ }
206
+
207
+ const elapsedMinutes = Math.floor((attempts * 5) / 60);
208
+ console.log(`Version ${version} found but not yet fully available (${elapsedMinutes}m ${(attempts * 5) % 60}s elapsed, attempt ${attempts + 1}/${maxAttempts})`);
209
+ } else {
210
+ const elapsedMinutes = Math.floor((attempts * 5) / 60);
211
+ console.log(`Version ${version} not yet visible in entity (${elapsedMinutes}m ${(attempts * 5) % 60}s elapsed, attempt ${attempts + 1}/${maxAttempts})`);
212
+ }
213
+
214
+ } catch (error) {
215
+ console.error("Error checking entity status:", error.message);
216
+ }
217
+
218
+ attempts++;
219
+ }
220
+
221
+ console.warn(`⚠ Warning: Version availability check timed out after 90 minutes`);
222
+ console.warn("Version may still be under AWS Marketplace review");
223
+ console.warn("The changeset succeeded, but the version is not yet publicly available to consumers");
224
+ return false;
225
+ };
226
+
149
227
 
150
228
  // Get AMI Ids Function
151
229
  const getRegions = async (productId) => {
@@ -0,0 +1,688 @@
1
+ const {
2
+ EC2Client,
3
+ RunInstancesCommand,
4
+ DescribeImagesCommand,
5
+ DescribeInstancesCommand,
6
+ DescribeVpcsCommand,
7
+ CreateSecurityGroupCommand,
8
+ AuthorizeSecurityGroupIngressCommand,
9
+ AuthorizeSecurityGroupEgressCommand,
10
+ RevokeSecurityGroupEgressCommand,
11
+ CreateKeyPairCommand,
12
+ StopInstancesCommand,
13
+ CreateImageCommand,
14
+ TerminateInstancesCommand,
15
+ DeleteSecurityGroupCommand,
16
+ DeleteKeyPairCommand,
17
+ waitUntilInstanceRunning,
18
+ waitUntilInstanceStopped,
19
+ waitUntilImageAvailable,
20
+ waitUntilInstanceTerminated
21
+ } = require('@aws-sdk/client-ec2');
22
+
23
+ const {
24
+ IAMClient,
25
+ CreateRoleCommand,
26
+ PutRolePolicyCommand,
27
+ CreateInstanceProfileCommand,
28
+ AddRoleToInstanceProfileCommand,
29
+ RemoveRoleFromInstanceProfileCommand,
30
+ DeleteInstanceProfileCommand,
31
+ DeleteRolePolicyCommand,
32
+ DeleteRoleCommand
33
+ } = require('@aws-sdk/client-iam');
34
+
35
+ const { Client } = require('ssh2');
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+
39
+ // All SSH setup commands - array of [description, command]
40
+ const SETUP_COMMANDS = [
41
+ ['Updating system packages', 'sudo dnf update -y'],
42
+ ['Installing nodejs', 'sudo dnf install -y nodejs'],
43
+ ['Node version', 'node --version'],
44
+ ['Installing cloudwatch agent', 'sudo dnf install -y amazon-cloudwatch-agent'],
45
+ ['Installing python', 'sudo dnf -y install python3'],
46
+ ['Installing unzip', 'sudo dnf -y install unzip'],
47
+ ['Installing pm2', 'sudo npm install -g pm2'],
48
+ ['Creating app directory', 'sudo mkdir -p /app'],
49
+ ];
50
+
51
+
52
+ class EC2AMIBuilder {
53
+ constructor(amiName, instanceType = 'm6a.large', s3PackageUrl) {
54
+ if (!amiName || !s3PackageUrl) {
55
+ throw new Error('amiName and s3PackageUrl are required parameters');
56
+ }
57
+
58
+ this.amiName = amiName;
59
+ this.instanceType = instanceType;
60
+ this.s3PackageUrl = s3PackageUrl;
61
+
62
+ // AWS clients
63
+ const region = process.env.AWS_REGION || 'us-east-1';
64
+ this.ec2Client = new EC2Client({ region });
65
+ this.iamClient = new IAMClient({ region });
66
+
67
+ // Generate unique names for temporary resources
68
+ this.timestamp = Date.now();
69
+ this.keyPairName = `ec2-builder-keypair-${this.timestamp}`;
70
+ this.securityGroupName = `ec2-builder-sg-${this.timestamp}`;
71
+ this.iamRoleName = `ec2-builder-role-${this.timestamp}`;
72
+ this.instanceProfileName = `ec2-builder-profile-${this.timestamp}`;
73
+ this.privateKeyPath = path.join(__dirname, `${this.keyPairName}.pem`);
74
+
75
+ // Resource tracking for cleanup
76
+ this.createdResources = {
77
+ instanceId: null,
78
+ keyPairName: null,
79
+ securityGroupId: null,
80
+ iamRoleName: null,
81
+ instanceProfileName: null
82
+ };
83
+
84
+ this.sshConnection = null;
85
+ this.publicIp = null;
86
+ }
87
+
88
+ async getLatestAmazonLinuxAMI() {
89
+ console.log('šŸ” Finding latest Amazon Linux AMI...');
90
+
91
+ const command = new DescribeImagesCommand({
92
+ Filters: [
93
+ {
94
+ Name: 'name',
95
+ Values: ['al2023-ami-*-x86_64']
96
+ },
97
+ {
98
+ Name: 'owner-alias',
99
+ Values: ['amazon']
100
+ },
101
+ {
102
+ Name: 'state',
103
+ Values: ['available']
104
+ }
105
+ ],
106
+ Owners: ['amazon']
107
+ });
108
+
109
+ const result = await this.ec2Client.send(command);
110
+
111
+ const latestAMI = result.Images
112
+ .sort((a, b) => new Date(b.CreationDate) - new Date(a.CreationDate))[0];
113
+ // console.log('latestAMI:', latestAMI);
114
+ console.log(`āœ… Found latest AMI: ${latestAMI.ImageId} ${latestAMI.Description} (${latestAMI.Name})`);
115
+ return latestAMI.ImageId;
116
+ }
117
+
118
+ async createKeyPair() {
119
+ console.log('šŸ”‘ Creating temporary key pair...');
120
+
121
+ const command = new CreateKeyPairCommand({
122
+ KeyName: this.keyPairName,
123
+ KeyType: 'rsa',
124
+ KeyFormat: 'pem'
125
+ });
126
+
127
+ const result = await this.ec2Client.send(command);
128
+ this.createdResources.keyPairName = this.keyPairName;
129
+
130
+ // Save private key to file
131
+ fs.writeFileSync(this.privateKeyPath, result.KeyMaterial, { mode: 0o600 });
132
+
133
+ console.log(`āœ… Key pair created: ${this.keyPairName}`);
134
+ return this.keyPairName;
135
+ }
136
+
137
+ async createSecurityGroup() {
138
+ console.log('šŸ›”ļø Creating security group...');
139
+
140
+ // Get default VPC
141
+ const vpcCommand = new DescribeVpcsCommand({
142
+ Filters: [{ Name: 'isDefault', Values: ['true'] }]
143
+ });
144
+
145
+ const vpcs = await this.ec2Client.send(vpcCommand);
146
+ const defaultVpcId = vpcs.Vpcs[0]?.VpcId;
147
+
148
+ if (!defaultVpcId) {
149
+ throw new Error('No default VPC found. Please ensure you have a default VPC in your region.');
150
+ }
151
+
152
+ // Create security group
153
+ const sgCommand = new CreateSecurityGroupCommand({
154
+ GroupName: this.securityGroupName,
155
+ Description: 'Temporary security group for EC2 AMI builder',
156
+ VpcId: defaultVpcId
157
+ });
158
+
159
+ const sgResult = await this.ec2Client.send(sgCommand);
160
+ const securityGroupId = sgResult.GroupId;
161
+ this.createdResources.securityGroupId = securityGroupId;
162
+
163
+ // Add inbound rules (SSH)
164
+ const ingressCommand = new AuthorizeSecurityGroupIngressCommand({
165
+ GroupId: securityGroupId,
166
+ IpPermissions: [
167
+ {
168
+ IpProtocol: 'tcp',
169
+ FromPort: 22,
170
+ ToPort: 22,
171
+ IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'SSH access' }]
172
+ }
173
+ ]
174
+ });
175
+
176
+ await this.ec2Client.send(ingressCommand);
177
+
178
+ // Remove default egress rule
179
+ const revokeEgressCommand = new RevokeSecurityGroupEgressCommand({
180
+ GroupId: securityGroupId,
181
+ IpPermissions: [
182
+ {
183
+ IpProtocol: '-1',
184
+ IpRanges: [{ CidrIp: '0.0.0.0/0' }]
185
+ }
186
+ ]
187
+ });
188
+
189
+ await this.ec2Client.send(revokeEgressCommand);
190
+
191
+ // Add specific outbound rules
192
+ const egressCommand = new AuthorizeSecurityGroupEgressCommand({
193
+ GroupId: securityGroupId,
194
+ IpPermissions: [
195
+ {
196
+ IpProtocol: 'tcp',
197
+ FromPort: 443,
198
+ ToPort: 443,
199
+ IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' }]
200
+ },
201
+ {
202
+ IpProtocol: 'tcp',
203
+ FromPort: 80,
204
+ ToPort: 80,
205
+ IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' }]
206
+ },
207
+ {
208
+ IpProtocol: 'udp',
209
+ FromPort: 53,
210
+ ToPort: 53,
211
+ IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'DNS outbound' }]
212
+ },
213
+ {
214
+ IpProtocol: 'udp',
215
+ FromPort: 123,
216
+ ToPort: 123,
217
+ IpRanges: [{ CidrIp: '0.0.0.0/0', Description: 'NTP outbound' }]
218
+ }
219
+ ]
220
+ });
221
+
222
+ await this.ec2Client.send(egressCommand);
223
+
224
+ console.log(`āœ… Security group created: ${securityGroupId}`);
225
+ return securityGroupId;
226
+ }
227
+
228
+ async createIAMRole() {
229
+ console.log('šŸ‘¤ Creating IAM role for S3 access...');
230
+
231
+ // Trust policy for EC2
232
+ const trustPolicy = {
233
+ Version: '2012-10-17',
234
+ Statement: [
235
+ {
236
+ Effect: 'Allow',
237
+ Principal: { Service: 'ec2.amazonaws.com' },
238
+ Action: 'sts:AssumeRole'
239
+ }
240
+ ]
241
+ };
242
+
243
+ // Create IAM role
244
+ const createRoleCommand = new CreateRoleCommand({
245
+ RoleName: this.iamRoleName,
246
+ AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
247
+ Description: 'Temporary role for EC2 AMI builder to access S3'
248
+ });
249
+
250
+ await this.iamClient.send(createRoleCommand);
251
+ this.createdResources.iamRoleName = this.iamRoleName;
252
+
253
+ // S3 access policy
254
+ const s3Policy = {
255
+ Version: '2012-10-17',
256
+ Statement: [
257
+ {
258
+ Effect: 'Allow',
259
+ Action: [
260
+ 's3:GetObject',
261
+ 's3:GetObjectVersion'
262
+ ],
263
+ Resource: '*'
264
+ }
265
+ ]
266
+ };
267
+
268
+ // Attach inline policy
269
+ const putPolicyCommand = new PutRolePolicyCommand({
270
+ RoleName: this.iamRoleName,
271
+ PolicyName: 'S3AccessPolicy',
272
+ PolicyDocument: JSON.stringify(s3Policy)
273
+ });
274
+
275
+ await this.iamClient.send(putPolicyCommand);
276
+
277
+ // Create instance profile
278
+ const createProfileCommand = new CreateInstanceProfileCommand({
279
+ InstanceProfileName: this.instanceProfileName
280
+ });
281
+
282
+ await this.iamClient.send(createProfileCommand);
283
+ this.createdResources.instanceProfileName = this.instanceProfileName;
284
+
285
+ // Add role to instance profile
286
+ const addRoleCommand = new AddRoleToInstanceProfileCommand({
287
+ InstanceProfileName: this.instanceProfileName,
288
+ RoleName: this.iamRoleName
289
+ });
290
+
291
+ await this.iamClient.send(addRoleCommand);
292
+
293
+ // Wait for IAM propagation
294
+ console.log('ā³ Waiting for IAM role propagation...');
295
+ await new Promise(resolve => setTimeout(resolve, 10000));
296
+
297
+ console.log(`āœ… IAM role created: ${this.iamRoleName}`);
298
+ return this.instanceProfileName;
299
+ }
300
+
301
+ async launchInstance() {
302
+ console.log('šŸš€ Launching EC2 instance...');
303
+
304
+ const amiId = await this.getLatestAmazonLinuxAMI();
305
+ const keyPairName = await this.createKeyPair();
306
+ const securityGroupId = await this.createSecurityGroup();
307
+ const instanceProfileName = await this.createIAMRole();
308
+
309
+ const command = new RunInstancesCommand({
310
+ ImageId: amiId,
311
+ InstanceType: this.instanceType,
312
+ KeyName: keyPairName,
313
+ SecurityGroupIds: [securityGroupId],
314
+ IamInstanceProfile: {
315
+ Name: instanceProfileName
316
+ },
317
+ MinCount: 1,
318
+ MaxCount: 1,
319
+ BlockDeviceMappings: [
320
+ {
321
+ DeviceName: '/dev/xvda', // Root device for Amazon Linux
322
+ Ebs: {
323
+ VolumeSize: 20, // Increase from default 8GB to 20GB
324
+ VolumeType: 'gp3',
325
+ DeleteOnTermination: true
326
+ }
327
+ }
328
+ ],
329
+ TagSpecifications: [
330
+ {
331
+ ResourceType: 'instance',
332
+ Tags: [
333
+ { Key: 'Name', Value: `AMI-Builder-${this.timestamp}` },
334
+ { Key: 'Purpose', Value: 'Temporary AMI Builder' }
335
+ ]
336
+ }
337
+ ]
338
+ });
339
+
340
+ const result = await this.ec2Client.send(command);
341
+ this.createdResources.instanceId = result.Instances[0].InstanceId;
342
+
343
+ console.log(`āœ… Instance launched: ${this.createdResources.instanceId}`);
344
+
345
+ await this.waitForInstanceRunning();
346
+ await this.getInstancePublicIP();
347
+
348
+ console.log(`🌐 Instance public IP: ${this.publicIp}`);
349
+ }
350
+
351
+ async waitForInstanceRunning() {
352
+ console.log('ā³ Waiting for instance to be running...');
353
+
354
+ await waitUntilInstanceRunning(
355
+ { client: this.ec2Client, maxWaitTime: 300 },
356
+ { InstanceIds: [this.createdResources.instanceId] }
357
+ );
358
+
359
+ console.log('āœ… Instance is running');
360
+
361
+ // Wait for SSH service to be ready
362
+ console.log('ā³ Waiting for SSH service to be ready...');
363
+ await new Promise(resolve => setTimeout(resolve, 60000));
364
+ }
365
+
366
+ async getInstancePublicIP() {
367
+ const command = new DescribeInstancesCommand({
368
+ InstanceIds: [this.createdResources.instanceId]
369
+ });
370
+
371
+ const result = await this.ec2Client.send(command);
372
+ this.publicIp = result.Reservations[0].Instances[0].PublicIpAddress;
373
+ }
374
+
375
+ async connectSSH() {
376
+ return new Promise((resolve, reject) => {
377
+ console.log('šŸ”‘ Connecting to instance via SSH...');
378
+
379
+ this.sshConnection = new Client();
380
+
381
+ this.sshConnection.on('ready', () => {
382
+ console.log('āœ… SSH connection established');
383
+ resolve();
384
+ });
385
+
386
+ this.sshConnection.on('error', (err) => {
387
+ console.error('āŒ SSH connection error:', err.message);
388
+ reject(err);
389
+ });
390
+
391
+ this.sshConnection.connect({
392
+ host: this.publicIp,
393
+ username: 'ec2-user',
394
+ privateKey: fs.readFileSync(this.privateKeyPath),
395
+ readyTimeout: 60000
396
+ });
397
+ });
398
+ }
399
+
400
+ async executeCommand(command, description) {
401
+ return new Promise((resolve, reject) => {
402
+ console.log(`\nšŸ”§ ${description}...`);
403
+ console.log(`šŸ“ Command: ${command}`);
404
+ console.log('šŸ“¤ Output:');
405
+ console.log('─'.repeat(50));
406
+
407
+ this.sshConnection.exec(command, (err, stream) => {
408
+ if (err) {
409
+ console.error(`āŒ Error executing command: ${err.message}`);
410
+ reject(err);
411
+ return;
412
+ }
413
+
414
+ let output = '';
415
+ let errorOutput = '';
416
+
417
+ stream.on('close', (code) => {
418
+ console.log('─'.repeat(50));
419
+ if (code === 0) {
420
+ console.log(`āœ… ${description} completed successfully (exit code: ${code})\n`);
421
+ resolve(output);
422
+ } else {
423
+ console.log(`āŒ ${description} failed with exit code ${code}\n`);
424
+ if (errorOutput.trim()) {
425
+ console.error('🚨 Error details:');
426
+ console.error(errorOutput);
427
+ }
428
+ reject(new Error(`Command failed with exit code ${code}`));
429
+ }
430
+ });
431
+
432
+ stream.on('data', (data) => {
433
+ const text = data.toString();
434
+ output += text;
435
+ process.stdout.write(text);
436
+ });
437
+
438
+ stream.stderr.on('data', (data) => {
439
+ const text = data.toString();
440
+ errorOutput += text;
441
+ // Print stderr in red color if possible
442
+ process.stderr.write(`\x1b[31m${text}\x1b[0m`);
443
+ });
444
+ });
445
+ });
446
+ }
447
+
448
+ async setupSystem() {
449
+ console.log('šŸ”§ Setting up system packages...');
450
+
451
+ // Execute all setup commands from the array
452
+ for (const [description, command] of SETUP_COMMANDS) {
453
+ await this.executeCommand(command, description);
454
+ }
455
+
456
+ console.log('āœ… System setup completed');
457
+ }
458
+
459
+ async downloadAndSetupApp() {
460
+ console.log('šŸ“¦ Setting up Node.js application...');
461
+
462
+ // Application setup commands (dynamic based on S3 URL)
463
+ const appCommands = [
464
+ ['Downloading Node.js app package from S3', `aws s3 cp "${this.s3PackageUrl}" ./app-package.zip`],
465
+ ['Extracting application package', 'sudo unzip -o app-package.zip -d /app >/dev/null'],
466
+ ['Cleaning up package archive', 'sudo rm -f app-package.zip'],
467
+ ['Directory files', 'ls -A /app'],
468
+ ['Showing application structure', 'find /app -maxdepth 2 -name "node_modules" -prune -o -print']
469
+ ];
470
+
471
+ // Execute all app setup commands
472
+ for (const [description, command] of appCommands) {
473
+ await this.executeCommand(command, description);
474
+ }
475
+
476
+ console.log('āœ… Application setup completed');
477
+ }
478
+
479
+ async createAMI() {
480
+ console.log('šŸ“ø Creating AMI from instance...');
481
+
482
+ // Cleanup commands before AMI creation
483
+ const cleanupCommands = [
484
+ ['Cleaning up instance before AMI creation', 'sudo dnf clean all && sudo rm -rf /tmp/* /var/tmp/* /var/log/messages* /var/log/secure* ~/.bash_history'],
485
+ ['Checking disk usage', 'df -h && du -sh .']
486
+ ];
487
+
488
+ // Execute cleanup commands
489
+ for (const [description, command] of cleanupCommands) {
490
+ await this.executeCommand(command, description);
491
+ }
492
+
493
+ // Close SSH connection
494
+ if (this.sshConnection) {
495
+ this.sshConnection.end();
496
+ this.sshConnection = null;
497
+ }
498
+
499
+ // Stop the instance
500
+ console.log('šŸ›‘ Stopping instance before AMI creation...');
501
+ const stopCommand = new StopInstancesCommand({
502
+ InstanceIds: [this.createdResources.instanceId]
503
+ });
504
+
505
+ await this.ec2Client.send(stopCommand);
506
+
507
+ await waitUntilInstanceStopped(
508
+ { client: this.ec2Client, maxWaitTime: 300 },
509
+ { InstanceIds: [this.createdResources.instanceId] }
510
+ );
511
+
512
+ console.log('āœ… Instance stopped');
513
+
514
+ // Create AMI
515
+ const createImageCommand = new CreateImageCommand({
516
+ InstanceId: this.createdResources.instanceId,
517
+ Name: this.amiName,
518
+ Description: `AMI with Node.js application - Created ${new Date().toISOString()}`,
519
+ NoReboot: true
520
+ });
521
+
522
+ const result = await this.ec2Client.send(createImageCommand);
523
+ const amiId = result.ImageId;
524
+
525
+ console.log(`āœ… AMI creation started: ${amiId}`);
526
+ console.log('ā³ Waiting for AMI to be available (this may take several minutes)...');
527
+
528
+ await waitUntilImageAvailable(
529
+ { client: this.ec2Client, maxWaitTime: 1800 },
530
+ { ImageIds: [amiId] }
531
+ );
532
+
533
+ console.log(`šŸŽ‰ AMI created successfully: ${amiId}`);
534
+ return amiId;
535
+ }
536
+
537
+ async cleanup() {
538
+ console.log('🧹 Cleaning up temporary resources...');
539
+
540
+ // Close SSH connection
541
+ if (this.sshConnection) {
542
+ this.sshConnection.end();
543
+ }
544
+
545
+ // Delete private key file
546
+ if (fs.existsSync(this.privateKeyPath)) {
547
+ fs.unlinkSync(this.privateKeyPath);
548
+ }
549
+
550
+ try {
551
+ // Terminate instance
552
+ if (this.createdResources.instanceId) {
553
+ console.log('šŸ—‘ļø Terminating instance...');
554
+ const terminateCommand = new TerminateInstancesCommand({
555
+ InstanceIds: [this.createdResources.instanceId]
556
+ });
557
+
558
+ await this.ec2Client.send(terminateCommand);
559
+
560
+ await waitUntilInstanceTerminated(
561
+ { client: this.ec2Client, maxWaitTime: 300 },
562
+ { InstanceIds: [this.createdResources.instanceId] }
563
+ );
564
+ }
565
+
566
+ // Delete security group
567
+ if (this.createdResources.securityGroupId) {
568
+ console.log('šŸ—‘ļø Deleting security group...');
569
+ const deleteSecurityGroupCommand = new DeleteSecurityGroupCommand({
570
+ GroupId: this.createdResources.securityGroupId
571
+ });
572
+
573
+ await this.ec2Client.send(deleteSecurityGroupCommand);
574
+ }
575
+
576
+ // Delete key pair
577
+ if (this.createdResources.keyPairName) {
578
+ console.log('šŸ—‘ļø Deleting key pair...');
579
+ const deleteKeyPairCommand = new DeleteKeyPairCommand({
580
+ KeyName: this.createdResources.keyPairName
581
+ });
582
+
583
+ await this.ec2Client.send(deleteKeyPairCommand);
584
+ }
585
+
586
+ // Clean up IAM resources
587
+ if (this.createdResources.instanceProfileName && this.createdResources.iamRoleName) {
588
+ console.log('šŸ—‘ļø Cleaning up IAM resources...');
589
+
590
+ // Remove role from instance profile
591
+ const removeRoleCommand = new RemoveRoleFromInstanceProfileCommand({
592
+ InstanceProfileName: this.createdResources.instanceProfileName,
593
+ RoleName: this.createdResources.iamRoleName
594
+ });
595
+
596
+ await this.iamClient.send(removeRoleCommand);
597
+
598
+ // Delete instance profile
599
+ const deleteProfileCommand = new DeleteInstanceProfileCommand({
600
+ InstanceProfileName: this.createdResources.instanceProfileName
601
+ });
602
+
603
+ await this.iamClient.send(deleteProfileCommand);
604
+
605
+ // Delete role policy
606
+ const deletePolicyCommand = new DeleteRolePolicyCommand({
607
+ RoleName: this.createdResources.iamRoleName,
608
+ PolicyName: 'S3AccessPolicy'
609
+ });
610
+
611
+ await this.iamClient.send(deletePolicyCommand);
612
+
613
+ // Delete role
614
+ const deleteRoleCommand = new DeleteRoleCommand({
615
+ RoleName: this.createdResources.iamRoleName
616
+ });
617
+
618
+ await this.iamClient.send(deleteRoleCommand);
619
+ }
620
+
621
+ console.log('āœ… Cleanup completed');
622
+
623
+ } catch (error) {
624
+ console.warn('āš ļø Some cleanup operations failed:', error.message);
625
+ }
626
+ }
627
+
628
+ async build() {
629
+ console.log('Starting SSH AMI Build Process...');
630
+ const start = Date.now();
631
+ try {
632
+ await this.launchInstance();
633
+ await this.connectSSH();
634
+ await this.setupSystem();
635
+ await this.downloadAndSetupApp();
636
+ console.log('Build complete after', Math.ceil((Date.now() - start)/1000/60));
637
+ const amiId = await this.createAMI();
638
+ console.log('AMI Created after', Math.ceil((Date.now() - start)/1000/60));
639
+ console.log(`šŸ“‹ Summary:`);
640
+ console.log(` - AMI ID: ${amiId}`);
641
+ console.log(` - AMI Name: ${this.amiName}`);
642
+ console.log(` - Instance Type Used: ${this.instanceType}`);
643
+ console.log(` - S3 Package: ${this.s3PackageUrl}`);
644
+
645
+ return amiId;
646
+
647
+ } catch (error) {
648
+ console.error('āŒ AMI Build failed:', error.message);
649
+ throw error;
650
+ } finally {
651
+ await this.cleanup();
652
+ }
653
+ }
654
+ }
655
+
656
+ async function sshAMI(amiName,s3PackageUrl,instanceType){
657
+ const builder = new EC2AMIBuilder(amiName, instanceType, s3PackageUrl);
658
+ const result = await builder.build();
659
+ console.log('AMI ID:', result);
660
+ return result;
661
+ }
662
+
663
+
664
+ // Convenience function for direct usage
665
+ module.exports.buildAMI = sshAMI;
666
+
667
+ sshAMI('test-ami-2', 's3://coreinfra-infrabucket-qtfrahre6vbl/apps/theorim/3.6/app.zip')
668
+
669
+ // // CLI usage if called directly
670
+ // if (require.main === module) {
671
+ // const [,, amiName, instanceType, s3PackageUrl] = process.argv;
672
+
673
+ // if (!amiName || !s3PackageUrl) {
674
+ // console.error('Usage: node ec2-ami-builder.js <amiName> [instanceType] <s3PackageUrl>');
675
+ // console.error('Example: node ec2-ami-builder.js "my-app-ami" "t3.micro" "s3://mybucket/myapp.zip"');
676
+ // process.exit(1);
677
+ // }
678
+
679
+ // module.exports.buildAMI(amiName, instanceType || 't3.micro', s3PackageUrl)
680
+ // .then(amiId => {
681
+ // console.log(`\nšŸš€ Your new AMI is ready: ${amiId}`);
682
+ // process.exit(0);
683
+ // })
684
+ // .catch(error => {
685
+ // console.error('\nšŸ’„ Build failed:', error.message);
686
+ // process.exit(1);
687
+ // });
688
+ // }
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"cloudmason","version":"1.7.22","description":"","main":"main.js","scripts":{"build":"node build.js"},"bin":{"mason":"./main.js"},"repository":{"type":"git","url":"https://github.com/kai-harvey/secure-saas.git"},"author":"Kai Harvey","license":"ISC","dependencies":{"@aws-sdk/client-acm":"^3.418.0","@aws-sdk/client-auto-scaling":"^3.470.0","@aws-sdk/client-cloudformation":"^3.418.0","@aws-sdk/client-ec2":"^3.416.0","@aws-sdk/client-iam":"^3.418.0","@aws-sdk/client-marketplace-catalog":"^3.716.0","@aws-sdk/client-route-53":"^3.425.0","@aws-sdk/client-s3":"^3.418.0","@aws-sdk/client-ssm":"^3.421.0","adm-zip":"^0.5.10","yaml":"^2.6.1"}}
1
+ {"name":"cloudmason","version":"1.9.32","description":"","main":"main.js","scripts":{"build":"node build.js"},"bin":{"mason":"./main.js"},"repository":{"type":"git","url":"https://github.com/kai-harvey/cloudmason.git"},"author":"Kai Harvey","license":"ISC","dependencies":{"@aws-sdk/client-acm":"^3.418.0","@aws-sdk/client-auto-scaling":"^3.470.0","@aws-sdk/client-cloudformation":"^3.418.0","@aws-sdk/client-ec2":"^3.864.0","@aws-sdk/client-iam":"^3.864.0","@aws-sdk/client-marketplace-catalog":"^3.716.0","@aws-sdk/client-route-53":"^3.425.0","@aws-sdk/client-s3":"^3.418.0","@aws-sdk/client-ssm":"^3.421.0","adm-zip":"^0.5.10","ssh2":"^1.16.0","yaml":"^2.6.1"}}