cloudmason 2.7.49 → 2.7.51

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.
@@ -0,0 +1,26 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.cloudmason');
6
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
7
+ const LEGACY_PATH = path.resolve(__dirname, '..', '..', 'org.txt');
8
+
9
+ exports.read = function () {
10
+ if (fs.existsSync(CONFIG_PATH)) {
11
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
12
+ }
13
+ if (fs.existsSync(LEGACY_PATH)) {
14
+ const [name, region] = fs.readFileSync(LEGACY_PATH, 'utf-8').split(',');
15
+ const cfg = { name, region };
16
+ exports.write(cfg);
17
+ try { fs.unlinkSync(LEGACY_PATH); } catch (_) {}
18
+ return cfg;
19
+ }
20
+ return null;
21
+ };
22
+
23
+ exports.write = function (cfg) {
24
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
25
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf-8');
26
+ };
@@ -1,10 +1,9 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
1
  const { S3Client,HeadBucketCommand } = require("@aws-sdk/client-s3");
4
2
  const { EC2Client, DescribeVpcsCommand } = require("@aws-sdk/client-ec2");
5
3
 
6
4
  const CF = require('./helpers/cf');
7
5
  const Params = require('./helpers/params');
6
+ const OrgConfig = require('./helpers/org_config');
8
7
 
9
8
  exports.main = async function(args){
10
9
  console.log(`Setting up ${args.name}@ in ${args.region} with repo ${args.repo}`)
@@ -23,11 +22,8 @@ exports.main = async function(args){
23
22
  throw new Error('Org already exists')
24
23
  }
25
24
 
26
- // Set org.txt
27
- const orgPath = path.resolve(__dirname,'..','org.txt');
28
- const orgData = `${args.name},${args.region}`;
29
- fs.writeFileSync(orgPath,orgData,'utf-8')
30
- console.log('Set up org:',orgData)
25
+ OrgConfig.write({ name: args.name, region: args.region });
26
+ console.log('Set up org:', args.name, args.region)
31
27
  return true;
32
28
  }
33
29
 
@@ -48,11 +44,8 @@ exports.updateOrgStack = async function(args){
48
44
  }
49
45
 
50
46
  exports.setOrg = async function(args){
51
- // Set org.txt
52
- const orgPath = path.resolve(__dirname,'..','org.txt');
53
- const orgData = `${args.name},${args.region}`;
54
- fs.writeFileSync(orgPath,orgData,'utf-8')
55
- console.log('Set org:',orgData)
47
+ OrgConfig.write({ name: args.name, region: args.region });
48
+ console.log('Set org:', args.name, args.region)
56
49
  return true;
57
50
  }
58
51
 
@@ -1,9 +1,37 @@
1
1
  const { MarketplaceCatalogClient, StartChangeSetCommand, DescribeChangeSetCommand } = require("@aws-sdk/client-marketplace-catalog");
2
2
  const { EC2Client, DescribeImagesCommand } = require("@aws-sdk/client-ec2");
3
+ const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
3
4
  const fs = require('fs');
4
5
  const path = require('path');
5
6
  const Params = require('./helpers/params');
6
7
 
8
+ // Parse path-style, virtual-hosted, or s3:// URLs into {bucket, key, region}
9
+ function parseS3Url(url){
10
+ if (!url){ throw new Error('Missing S3 URL'); }
11
+ let m;
12
+ if ((m = url.match(/^s3:\/\/([^/]+)\/(.+)$/))) { return { bucket: m[1], key: m[2], region: null }; }
13
+ if ((m = url.match(/^https:\/\/s3\.([^.]+)\.amazonaws\.com\/([^/]+)\/(.+)$/))) { return { bucket: m[2], key: m[3], region: m[1] }; }
14
+ if ((m = url.match(/^https:\/\/s3\.amazonaws\.com\/([^/]+)\/(.+)$/))) { return { bucket: m[1], key: m[2], region: null }; }
15
+ if ((m = url.match(/^https:\/\/([^.]+)\.s3\.([^.]+)\.amazonaws\.com\/(.+)$/))) { return { bucket: m[1], key: m[3], region: m[2] }; }
16
+ if ((m = url.match(/^https:\/\/([^.]+)\.s3\.amazonaws\.com\/(.+)$/))) { return { bucket: m[1], key: m[2], region: null }; }
17
+ throw new Error('Unrecognized S3 URL format: ' + url);
18
+ }
19
+
20
+ // AWS Marketplace requires virtual-hosted-style URLs for Template/ArchitectureDiagram
21
+ function toVirtualHostedUrl(url){
22
+ const { bucket, key, region } = parseS3Url(url);
23
+ return region
24
+ ? `https://${bucket}.s3.${region}.amazonaws.com/${key}`
25
+ : `https://${bucket}.s3.amazonaws.com/${key}`;
26
+ }
27
+
28
+ async function readS3Text(url){
29
+ const { bucket, key, region } = parseS3Url(url);
30
+ const client = new S3Client({ region: region || process.env.orgRegion });
31
+ const resp = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
32
+ return await resp.Body.transformToString();
33
+ }
34
+
7
35
  exports.add_listing = async function(args){
8
36
  console.log('Adding Listing>>', args.app, args.pid);
9
37
  await Params.addPid(args.app,args.pid.trim());
@@ -28,24 +56,43 @@ exports.main = async function(args){
28
56
  }
29
57
  pubArgs.amiId = instanceVersion.baseAMI_Id;
30
58
  pubArgs.arch = instanceVersion.arch || 'x86_64';
31
- console.log('Publishing AMI:\n\t',Object.entries(pubArgs).map(([k,v])=>{return `${k}:${v}`}).join('\n\t'));
32
- console.log('----------')
59
+ if (args.cft){
60
+ pubArgs.cftS3Url = instanceVersion.stackURL;
61
+ if (!pubArgs.cftS3Url){
62
+ console.log('ERR: No stack URL found for version',pubArgs.version);
63
+ throw new Error('No stack URL found for version:' + pubArgs.version);
64
+ }
65
+ const missing = ['short','long','diagram'].filter(k => !args[k]);
66
+ if (missing.length){
67
+ throw new Error('Missing required CFT args: ' + missing.map(k=>'-'+k).join(', '));
68
+ }
69
+ pubArgs.shortDescS3Url = args.short;
70
+ pubArgs.longDescS3Url = args.long;
71
+ pubArgs.archDiagramS3Url = args.diagram;
72
+ console.log('Publishing AMI + CFT:\n\t',Object.entries(pubArgs).map(([k,v])=>{return `${k}:${v}`}).join('\n\t'));
73
+ await exports.updateAmiCft(pubArgs);
74
+ } else {
75
+ if (!args.stack){
76
+ throw new Error('Missing required arg -stack for AMI-only publish');
77
+ }
78
+ console.log('Publishing AMI:\n\t',Object.entries(pubArgs).map(([k,v])=>{return `${k}:${v}`}).join('\n\t'));
79
+ await updateAmiVersion(pubArgs);
33
80
 
34
- // -- Publish AMI to Marketplace
35
- await updateAmiVersion(pubArgs);
81
+ // -- Get Marketplace AMI IDs
82
+ // AmiAlias: '/aws/service/marketplace/prod-shmtmk4gqrfge/1.2'
83
+ const amiAlias = `/aws/service/marketplace/${pubArgs.productId}/${pubArgs.version}`;
84
+ console.log('AMI Alias:',amiAlias);
85
+ let stackTxt = fs.readFileSync(path.resolve(args.stack),'utf8');
86
+ // stackTxt = stackTxt.replace(`ImageId: !Ref AmiId`,`ImageId: resolve:ssm:${amiAlias}`);
87
+ stackTxt = stackTxt.replace(/^#-Strip.+#-Strip/ms,'');
36
88
 
37
- // -- Get Marketplace AMI IDs
38
- // AmiAlias: '/aws/service/marketplace/prod-shmtmk4gqrfge/1.2'
39
- const amiAlias = `/aws/service/marketplace/${pubArgs.productId}/${pubArgs.version}`;
40
- console.log('AMI Alias:',amiAlias);
41
- let stackTxt = fs.readFileSync(path.resolve(args.stack),'utf8');
42
- // stackTxt = stackTxt.replace(`ImageId: !Ref AmiId`,`ImageId: resolve:ssm:${amiAlias}`);
43
- stackTxt = stackTxt.replace(/^#-Strip.+#-Strip/ms,'');
89
+ // -- Update CF Template with AMI IDs
90
+ // const newFileName = path.resolve(args.out);
91
+ // console.log('Updating Template:',newFileName);
92
+ // fs.writeFileSync(newFileName,stackTxt);
93
+ }
44
94
 
45
- // -- Update CF Template with AMI IDs
46
- const newFileName = path.resolve(args.out);
47
- console.log('Updating Template:',newFileName);
48
- fs.writeFileSync(newFileName,stackTxt);
95
+ console.log('----------')
49
96
 
50
97
  // -- Suggest next step
51
98
  console.log('\nTo wait for this version to be publicly available in marketplace, run:');
@@ -156,53 +203,102 @@ const updateAmiVersion = async ({productId, amiId, version, changeDescription, a
156
203
  };
157
204
 
158
205
 
159
- // Get AMI Ids Function
160
- const getRegions = async (productId) => {
161
- const client = new MarketplaceCatalogClient({ region: process.env.orgRegion }); // Update region if needed
206
+ // Update AMI + CloudFormation Template Function
162
207
 
208
+ exports.updateAmiCft = async ({productId, amiId, version, changeDescription, arch, cftS3Url, shortDescS3Url, longDescS3Url, archDiagramS3Url}) => {
209
+ const client = new MarketplaceCatalogClient({ region: process.env.orgRegion });
210
+ console.log('Updating AMI+CFT version:', productId, amiId, version, changeDescription, cftS3Url);
163
211
 
164
- const command = new DescribeEntityCommand({
165
- Catalog: "AWSMarketplace",
166
- EntityId: productId,
167
- });
168
- const response = await client.send(command);
169
- console.log('dd',response.DetailsDocument);
170
- console.log('v',response.DetailsDocument.Versions[1].DeliveryOptions[0].AmiAlias);
171
- return response.DetailsDocument.RegionAvailability.Regions;
172
- // console.log("Regions:", response.DetailsDocument.Description.RegionAvailability.Regions);
173
- // console.log("Version:", response.DetailsDocument.Description.RegionAvailability.Regions);
174
- // Extract AMI details by region
175
- }
176
-
177
- // List by Mktplc Listing
178
- const getAMIs = async (region,productName,productCode) => {
179
- const ec2Client = new EC2Client({ region });
212
+ const shortDescription = await readS3Text(shortDescS3Url);
213
+ const longDescription = await readS3Text(longDescS3Url);
214
+ const templateUrl = toVirtualHostedUrl(cftS3Url);
215
+ const diagramUrl = toVirtualHostedUrl(archDiagramS3Url);
180
216
 
181
217
  try {
182
- const command = new DescribeImagesCommand({
183
- Filters: [
184
- {
185
- Name: "name",
186
- Values: [
187
- `${productName}-*-${productCode}` // Adjust the filter to match your product naming pattern
188
- ]
189
- }
190
- ]
218
+ const changeSet = {
219
+ Catalog: "AWSMarketplace",
220
+ Intent: "APPLY",
221
+ ChangeSet: [
222
+ {
223
+ ChangeType: "AddDeliveryOptions",
224
+ Entity: {
225
+ Type: "AmiProduct@1.0",
226
+ Identifier: productId,
227
+ },
228
+ Details: JSON.stringify({
229
+ Version: {
230
+ VersionTitle: version,
231
+ ReleaseNotes: changeDescription,
232
+ },
233
+ DeliveryOptions: [
234
+ {
235
+ DeliveryOptionTitle: "AMI with CloudFormation Template",
236
+ Details: {
237
+ "DeploymentTemplateDeliveryOptionDetails": {
238
+ "ShortDescription": shortDescription,
239
+ "LongDescription": longDescription,
240
+ "UsageInstructions": "Visit Theorim.ai/install for installation instructions",
241
+ "RecommendedInstanceType": arch === 'arm' ? "r8g.medium" : "m6a.large",
242
+ "ArchitectureDiagram": diagramUrl,
243
+ "Template": templateUrl,
244
+ "TemplateSources": [
245
+ {
246
+ "ParameterName": "AmiId",
247
+ "AmiSource": {
248
+ "AmiId": amiId,
249
+ "AccessRoleArn": "arn:aws:iam::590183947985:role/Theorim_MarketPlaceRole",
250
+ "UserName": "ec2-user",
251
+ "OperatingSystemName": "AMAZONLINUX",
252
+ "OperatingSystemVersion": arch === 'arm'
253
+ ? "Amazon Linux 2023 arm64 HVM"
254
+ : "Amazon Linux 2 AMI 2.0.20220207.1 x86_64 HVM gp2"
255
+ }
256
+ }
257
+ ]
258
+ }
259
+ }
260
+ }
261
+ ]
262
+ }),
263
+ },
264
+ ],
265
+ };
266
+
267
+ const startChangeSetCommand = new StartChangeSetCommand(changeSet);
268
+ const startResponse = await client.send(startChangeSetCommand);
269
+ console.log("Change set started:", startResponse);
270
+
271
+ const changeSetId = startResponse.ChangeSetId;
272
+
273
+ let status = "IN_PROGRESS";
274
+ while (status === "IN_PROGRESS") {
275
+ await new Promise(resolve => setTimeout(resolve, 5000));
276
+
277
+ const describeChangeSetCommand = new DescribeChangeSetCommand({
278
+ Catalog: "AWSMarketplace",
279
+ ChangeSetId: changeSetId,
191
280
  });
281
+ const describeResponse = await client.send(describeChangeSetCommand);
192
282
 
193
- const response = await ec2Client.send(command);
283
+ status = describeResponse.Status;
284
+ console.log("Change set status:", status);
194
285
 
195
- console.log(`Found ${response.Images.length} AMIs for product: ${productName} in region: ${region}`);
196
- for (const ami of response.Images) {
197
- console.log('ami',ami);
286
+ if (status === "SUCCEEDED") {
287
+ console.log("Change set succeeded:", describeResponse);
288
+ break;
289
+ } else if (status === "FAILED") {
290
+ console.error("Change set failed:", describeResponse);
291
+ throw new Error("Change set failed");
198
292
  }
293
+ }
199
294
 
200
- return response.Images;
201
295
  } catch (error) {
202
- console.error(`Error fetching AMIs for product: ${productName} in region: ${region}`, error);
203
- throw error;
296
+ console.error("Error updating AMI+CFT version:", error);
297
+ throw error;
204
298
  }
205
- }
299
+ };
300
+
301
+
206
302
 
207
303
 
208
304
 
package/main.js CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const path = require('path')
4
- const fs = require('fs')
5
-
6
3
  const Params = require('./commands/helpers/params');
4
+ const OrgConfig = require('./commands/helpers/org_config');
7
5
 
8
6
  const Commands = {
9
7
  'init-org': {
@@ -115,8 +113,12 @@ const Commands = {
115
113
  {n: 'app', desc: 'Name of existing app', pattern: `[A-Za-z]{2,20}`, r: true},
116
114
  {n: 'desc', desc: 'Description of Changes', r: true},
117
115
  {n: 'v', desc: 'Version to launch', pattern: `[0-9]{1,20}`, r: true},
118
- {n: 'stack', desc: 'Path of stack.yaml', r: true},
119
- {n: 'out', desc: 'Output path of marketplace stack', r: true}
116
+ {n: 'stack', desc: 'Path of stack.yaml (required for AMI-only publish)', r: false},
117
+ {n: 'out', desc: 'Output path of marketplace stack (AMI-only publish)', r: false},
118
+ {n: 'cft', desc: 'Publish as AMI + CloudFormation Template delivery', r: false},
119
+ {n: 'short', desc: 'S3 URL of short-description text file (required when -cft is set)', r: false},
120
+ {n: 'long', desc: 'S3 URL of long-description text file (required when -cft is set)', r: false},
121
+ {n: 'diagram', desc: 'S3 URL of architecture diagram image (required when -cft is set)', r: false}
120
122
  ]
121
123
  },
122
124
  'await-ami': {
@@ -257,16 +259,12 @@ function checkNodeVersion() {
257
259
  }
258
260
 
259
261
  async function readOrgInfo(){
260
- const orgPath = path.resolve(__dirname,'org.txt');
261
- if (fs.existsSync(orgPath)){
262
- const orgInfo = fs.readFileSync(orgPath,'utf-8').split(',');
263
- process.env.orgName = orgInfo[0];
264
- process.env.orgRegion = orgInfo[1];
265
- process.env.orgBucket = await Params.getOrgBucket();
266
- return true;
267
- } else {
268
- return false;
269
- }
262
+ const cfg = OrgConfig.read();
263
+ if (!cfg) return false;
264
+ process.env.orgName = cfg.name;
265
+ process.env.orgRegion = cfg.region;
266
+ process.env.orgBucket = await Params.getOrgBucket();
267
+ return true;
270
268
  }
271
269
 
272
270
  function parseArgs(){
package/org.txt ADDED
@@ -0,0 +1 @@
1
+ theorim,us-east-1
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"cloudmason","version":"2.7.49","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"}}
1
+ {"name":"cloudmason","version":"2.7.51","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"}}