cloudmason 1.8.23 ā 1.9.33
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/.github/workflows/main.yml +30 -15
- package/commands/await-ami.js +119 -0
- package/commands/publish.js +9 -3
- package/commands/ssh_build.js +688 -0
- package/main.js +8 -0
- package/package.json +1 -1
|
@@ -1,27 +1,42 @@
|
|
|
1
1
|
name: Publish Node.js Package
|
|
2
|
-
|
|
3
2
|
on:
|
|
4
|
-
|
|
5
|
-
types:
|
|
6
|
-
- closed
|
|
3
|
+
push:
|
|
7
4
|
branches:
|
|
8
5
|
- 'release/**'
|
|
9
6
|
|
|
10
7
|
jobs:
|
|
11
|
-
|
|
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
|
-
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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.
|
|
40
|
+
branch: ${{ github.ref_name }}
|
|
23
41
|
build: ${{ github.run_number }}
|
|
24
|
-
- run: npm
|
|
25
|
-
- run: npm publish
|
|
26
|
-
env:
|
|
27
|
-
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
|
42
|
+
- run: npm publish --provenance --access public
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { MarketplaceCatalogClient, DescribeEntityCommand } = require("@aws-sdk/client-marketplace-catalog");
|
|
2
|
+
const Params = require('./helpers/params');
|
|
3
|
+
|
|
4
|
+
exports.main = async function(args){
|
|
5
|
+
// Get app and product ID
|
|
6
|
+
const app = await Params.getApp(args.app);
|
|
7
|
+
const productId = app.pid;
|
|
8
|
+
const version = args.v;
|
|
9
|
+
|
|
10
|
+
if (!productId) {
|
|
11
|
+
console.log('ERR: No marketplace listing found for app:', args.app);
|
|
12
|
+
throw new Error('No marketplace listing found. Use new-listing first.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log('Waiting for AMI version to become available:');
|
|
16
|
+
console.log('\tProduct ID:', productId);
|
|
17
|
+
console.log('\tVersion:', version);
|
|
18
|
+
console.log('----------');
|
|
19
|
+
|
|
20
|
+
// Create AWS client
|
|
21
|
+
const client = new MarketplaceCatalogClient({ region: process.env.orgRegion });
|
|
22
|
+
|
|
23
|
+
// Wait for version availability
|
|
24
|
+
await waitForVersionAvailability(client, productId, version);
|
|
25
|
+
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Wait for Version Availability Function
|
|
30
|
+
const waitForVersionAvailability = async (client, productId, version) => {
|
|
31
|
+
const maxAttempts = 1080; // 90 minutes with 5-second intervals (90 * 60 / 5 = 1080)
|
|
32
|
+
let attempts = 0;
|
|
33
|
+
|
|
34
|
+
console.log(`Polling entity for version ${version} availability...`);
|
|
35
|
+
console.log(`Timeout: 90 minutes (will check every 5 seconds)`);
|
|
36
|
+
|
|
37
|
+
while (attempts < maxAttempts) {
|
|
38
|
+
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const describeEntityCommand = new DescribeEntityCommand({
|
|
42
|
+
Catalog: "AWSMarketplace",
|
|
43
|
+
EntityId: productId,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const entityResponse = await client.send(describeEntityCommand);
|
|
47
|
+
|
|
48
|
+
// Parse the Details field which contains version information
|
|
49
|
+
let details;
|
|
50
|
+
if (typeof entityResponse.Details === 'string') {
|
|
51
|
+
details = JSON.parse(entityResponse.Details);
|
|
52
|
+
} else {
|
|
53
|
+
details = entityResponse.Details;
|
|
54
|
+
}
|
|
55
|
+
// Check if version exists in Versions array
|
|
56
|
+
const versionInfo = details.Versions?.find(v => v.VersionTitle === version);
|
|
57
|
+
console.log('Version details:', versionInfo);
|
|
58
|
+
|
|
59
|
+
if (versionInfo) {
|
|
60
|
+
// Check 1: Version must have Sources array with AMI
|
|
61
|
+
const hasSources = Array.isArray(versionInfo.Sources) &&
|
|
62
|
+
versionInfo.Sources.length > 0 &&
|
|
63
|
+
versionInfo.Sources.some(source => source.Image && source.Image.startsWith('ami-'));
|
|
64
|
+
|
|
65
|
+
// Check 2: Version must have DeliveryOptions array
|
|
66
|
+
const hasDeliveryOptions = Array.isArray(versionInfo.DeliveryOptions) &&
|
|
67
|
+
versionInfo.DeliveryOptions.length > 0;
|
|
68
|
+
|
|
69
|
+
// Check 3: At least one DeliveryOption must have Visibility set to 'Public'
|
|
70
|
+
const isPubliclyVisible = hasDeliveryOptions &&
|
|
71
|
+
versionInfo.DeliveryOptions.some(option => option.Visibility === 'Public');
|
|
72
|
+
|
|
73
|
+
if (hasSources && hasDeliveryOptions && isPubliclyVisible) {
|
|
74
|
+
console.log(`ā Version ${version} is now available to consumers`);
|
|
75
|
+
|
|
76
|
+
// Extract and display AMI details
|
|
77
|
+
const amiSources = versionInfo.Sources.filter(s => s.Image);
|
|
78
|
+
amiSources.forEach(source => {
|
|
79
|
+
console.log(` AMI ID: ${source.Image}`);
|
|
80
|
+
console.log(` Architecture: ${source.Architecture}`);
|
|
81
|
+
console.log(` Type: ${source.VirtualizationType}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Display public delivery options
|
|
85
|
+
const publicOptions = versionInfo.DeliveryOptions.filter(opt => opt.Visibility === 'Public');
|
|
86
|
+
console.log(` Public Delivery Options: ${publicOptions.length}`);
|
|
87
|
+
publicOptions.forEach(opt => {
|
|
88
|
+
console.log(` - ${opt.Title || opt.Type}`);
|
|
89
|
+
if (opt.AmiAlias) console.log(` SSM Alias: ${opt.AmiAlias}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Provide detailed feedback on what's missing
|
|
96
|
+
const elapsedMinutes = Math.floor((attempts * 5) / 60);
|
|
97
|
+
const reasons = [];
|
|
98
|
+
if (!hasSources) reasons.push('no AMI sources');
|
|
99
|
+
if (!hasDeliveryOptions) reasons.push('no delivery options');
|
|
100
|
+
if (hasDeliveryOptions && !isPubliclyVisible) reasons.push('not publicly visible yet');
|
|
101
|
+
|
|
102
|
+
console.log(`Version ${version} found but not yet fully available: ${reasons.join(', ')} (${elapsedMinutes}m ${(attempts * 5) % 60}s elapsed, attempt ${attempts + 1}/${maxAttempts})`);
|
|
103
|
+
} else {
|
|
104
|
+
const elapsedMinutes = Math.floor((attempts * 5) / 60);
|
|
105
|
+
console.log(`Version ${version} not yet visible in entity (${elapsedMinutes}m ${(attempts * 5) % 60}s elapsed, attempt ${attempts + 1}/${maxAttempts})`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error("Error checking entity status:", error.message);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
attempts++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.warn(`ā Warning: Version availability check timed out after 90 minutes`);
|
|
116
|
+
console.warn("Version may still be under AWS Marketplace review");
|
|
117
|
+
console.warn("The changeset succeeded, but the version is not yet publicly available to consumers");
|
|
118
|
+
return false;
|
|
119
|
+
};
|
package/commands/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { MarketplaceCatalogClient, StartChangeSetCommand,DescribeChangeSetCommand
|
|
1
|
+
const { MarketplaceCatalogClient, StartChangeSetCommand, DescribeChangeSetCommand } = require("@aws-sdk/client-marketplace-catalog");
|
|
2
2
|
const { EC2Client, DescribeImagesCommand } = require("@aws-sdk/client-ec2");
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
@@ -45,6 +45,11 @@ exports.main = async function(args){
|
|
|
45
45
|
const newFileName = path.resolve(args.out);
|
|
46
46
|
console.log('Updating Template:',newFileName);
|
|
47
47
|
fs.writeFileSync(newFileName,stackTxt);
|
|
48
|
+
|
|
49
|
+
// -- Suggest next step
|
|
50
|
+
console.log('\nTo wait for this version to be publicly available in marketplace, run:');
|
|
51
|
+
console.log(` mason await-ami -app ${args.app} -v ${args.v}`);
|
|
52
|
+
|
|
48
53
|
return true
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -137,16 +142,17 @@ const updateAmiVersion = async ({productId, amiId, version, changeDescription})
|
|
|
137
142
|
break;
|
|
138
143
|
} else if (status === "FAILED") {
|
|
139
144
|
console.error("Change set failed:", describeResponse);
|
|
140
|
-
|
|
145
|
+
throw new Error("Change set failed");
|
|
141
146
|
}
|
|
142
147
|
}
|
|
148
|
+
|
|
143
149
|
} catch (error) {
|
|
144
150
|
console.error("Error updating AMI version:", error);
|
|
151
|
+
throw error;
|
|
145
152
|
}
|
|
146
153
|
};
|
|
147
154
|
|
|
148
155
|
|
|
149
|
-
|
|
150
156
|
// Get AMI Ids Function
|
|
151
157
|
const getRegions = async (productId) => {
|
|
152
158
|
const client = new MarketplaceCatalogClient({ region: process.env.orgRegion }); // Update region if needed
|
|
@@ -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/main.js
CHANGED
|
@@ -118,6 +118,14 @@ const Commands = {
|
|
|
118
118
|
{n: 'out', desc: 'Output path of marketplace stack', r: true}
|
|
119
119
|
]
|
|
120
120
|
},
|
|
121
|
+
'await-ami': {
|
|
122
|
+
desc: 'Wait for an AMI version to be publicly available in marketplace',
|
|
123
|
+
exec: require('./commands/await-ami').main,
|
|
124
|
+
args: [
|
|
125
|
+
{n: 'app', desc: 'Name of existing app', pattern: `[A-Za-z]{2,20}`, r: true},
|
|
126
|
+
{n: 'v', desc: 'Version to wait for', pattern: `[0-9]{1,20}`, r: true}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
121
129
|
'inspect': {
|
|
122
130
|
desc: 'Get stack status and Ec2 console logs for an instance',
|
|
123
131
|
exec: require('./commands/inspect').main,
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"cloudmason","version":"1.
|
|
1
|
+
{"name":"cloudmason","version":"1.9.33","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"}}
|