cloudmason2 1.2.0 → 1.3.0

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,4 +1,4 @@
1
- const { CloudFormationClient, CreateStackCommand, UpdateStackCommand, DescribeStacksCommand } = require('@aws-sdk/client-cloudformation');
1
+ const { CloudFormationClient, CreateStackCommand, UpdateStackCommand, DescribeStacksCommand, GetTemplateSummaryCommand } = require('@aws-sdk/client-cloudformation');
2
2
  const { EC2Client, DescribeImagesCommand, CopyImageCommand } = require('@aws-sdk/client-ec2');
3
3
  const { S3Client } = require("@aws-sdk/client-s3");
4
4
  const OrgConfig = require('./helpers/org_config');
@@ -44,12 +44,31 @@ exports.main = async function(args){
44
44
  }
45
45
 
46
46
  // --- II DEPLOY CF STACK ---
47
- // CF params: AmiId from the manifest version (unless noami) + any extra CLI args.
48
- // No previous values are reused
49
- const cfParams = manifest.noami ? [] : [{ ParameterKey: 'AmiId', ParameterValue: amiId }];
47
+ const client = new CloudFormationClient({ region: args.region });
48
+ const existing = await getStack(client, stackName);
49
+
50
+ // CLI-provided params: AmiId from the manifest version (unless noami) + any extra CLI args
51
+ const cliParams = {};
50
52
  Object.keys(args).forEach(k=>{
51
53
  if (CLI_ARGS.includes(k) || k === 'AmiId'){ return }
52
- cfParams.push({ ParameterKey: k, ParameterValue: args[k] });
54
+ cliParams[k] = args[k];
55
+ });
56
+ if (!manifest.noami){ cliParams.AmiId = amiId }
57
+
58
+ // On update, carry forward previous stack values unless overridden on the CLI,
59
+ // dropping any that are no longer declared in the new template.
60
+ // UsePreviousValue avoids round-tripping NoEcho params (which read back masked)
61
+ const cfParams = [];
62
+ if (existing){
63
+ const templateKeys = await getTemplateKeys(client, targetVersion.stack_url);
64
+ existing.parameters.forEach(p=>{
65
+ if (!templateKeys.has(p.ParameterKey)){ return }
66
+ if (cliParams[p.ParameterKey] !== undefined){ return }
67
+ cfParams.push({ ParameterKey: p.ParameterKey, UsePreviousValue: true });
68
+ });
69
+ }
70
+ Object.keys(cliParams).forEach(k=>{
71
+ cfParams.push({ ParameterKey: k, ParameterValue: cliParams[k] });
53
72
  });
54
73
 
55
74
  const input = {
@@ -67,17 +86,15 @@ exports.main = async function(args){
67
86
  };
68
87
 
69
88
  // Create if the stack doesn't exist, update if it does
70
- const client = new CloudFormationClient({ region: args.region });
71
- const status = await stackStatus(client, stackName);
72
89
  let stackId;
73
- if (status === null){
90
+ if (existing === null){
74
91
  console.log(`Creating stack ${stackName}`);
75
92
  input.OnFailure = 'DELETE';
76
93
  const r = await client.send(new CreateStackCommand(input));
77
94
  stackId = r.StackId;
78
95
  } else {
79
- if (status.endsWith('_IN_PROGRESS')){
80
- console.log(`Stack ${stackName} is ${status}. Wait for completion before relaunching`);
96
+ if (existing.status.endsWith('_IN_PROGRESS')){
97
+ console.log(`Stack ${stackName} is ${existing.status}. Wait for completion before relaunching`);
81
98
  throw new Error('Stack operation in progress');
82
99
  }
83
100
  console.log(`Updating stack ${stackName}`);
@@ -143,13 +160,18 @@ async function waitForAMI(amiId, region){
143
160
  throw new Error('AMI not available after 20 minutes. Try again in a few minutes.');
144
161
  }
145
162
 
146
- async function stackStatus(client, stackName){
163
+ async function getStack(client, stackName){
147
164
  try {
148
165
  const r = await client.send(new DescribeStacksCommand({ StackName: stackName }));
149
- return r.Stacks[0].StackStatus;
166
+ return { status: r.Stacks[0].StackStatus, parameters: r.Stacks[0].Parameters || [] };
150
167
  } catch (e){
151
168
  if (e.name === 'ValidationError'){ return null }
152
169
  throw e;
153
170
  }
154
171
  }
155
172
 
173
+ async function getTemplateKeys(client, templateUrl){
174
+ const r = await client.send(new GetTemplateSummaryCommand({ TemplateURL: templateUrl }));
175
+ return new Set((r.Parameters || []).map(p=>{ return p.ParameterKey }));
176
+ }
177
+
@@ -0,0 +1,80 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
4
+ const OrgConfig = require('./helpers/org_config');
5
+ const Apps = require('./helpers/apps');
6
+
7
+ const CONTENT_TYPES = {
8
+ '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
9
+ '.json': 'application/json', '.txt': 'text/plain', '.md': 'text/markdown',
10
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
11
+ '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.csv': 'text/csv',
12
+ '.yaml': 'text/yaml', '.yml': 'text/yaml', '.zip': 'application/zip', '.pdf': 'application/pdf'
13
+ };
14
+
15
+ // update-assets: upload a file or folder to the app's cdn folder
16
+ exports.main = async function(args){
17
+ const cfg = OrgConfig.read();
18
+ if (!cfg){ throw new Error('No org config found. Run set-org') }
19
+ const { bucket, prefix } = parseCdn(cfg.cdn);
20
+ const s3 = new S3Client({ region: cfg.region });
21
+
22
+ const app = await Apps.resolveApp(s3, bucket, prefix, args.app.toLowerCase());
23
+ if (!app){ throw new Error('No app named ' + args.app + '. Run new-app') }
24
+
25
+ const srcPath = path.resolve(args.src);
26
+ if (!fs.existsSync(srcPath)){ throw new Error('Source path does not exist: ' + srcPath) }
27
+
28
+ const target = (args.t || '').replace(/^\/+|\/+$/g, '');
29
+ const base = target ? `${app.appPrefix}/${target}` : app.appPrefix;
30
+
31
+ const isFile = fs.statSync(srcPath).isFile();
32
+ const files = isFile ? [srcPath] : walkFiles(srcPath);
33
+ if (files.length === 0){ throw new Error('No files found at ' + srcPath) }
34
+
35
+ let uploaded = 0;
36
+ for (const f of files){
37
+ const rel = isFile ? path.basename(f) : path.relative(srcPath, f).split(path.sep).join('/');
38
+ const key = `${base}/${rel}`;
39
+ // The manifest is managed by commands only - never overwrite it with an asset
40
+ if (key === `${app.appPrefix}/manifest.json`){
41
+ console.log('Skipping manifest.json: managed by update-listing/update-app');
42
+ continue;
43
+ }
44
+ await s3.send(new PutObjectCommand({
45
+ Bucket: bucket,
46
+ Key: key,
47
+ Body: fs.createReadStream(f),
48
+ ContentType: CONTENT_TYPES[path.extname(f).toLowerCase()] || 'application/octet-stream'
49
+ }));
50
+ console.log(`Uploaded s3://${bucket}/${key}`);
51
+ uploaded += 1;
52
+ }
53
+ console.log(`${uploaded} file(s) uploaded`);
54
+ return true;
55
+ }
56
+
57
+
58
+ /////////////////////////////////////////
59
+ ////////////// FUNCS ////////////////////
60
+ ////////////////////////////////////////
61
+
62
+ function parseCdn(cdn){
63
+ const p = cdn.trim().replace(/^s3:\/\//i, '').replace(/\/+$/, '');
64
+ const parts = p.split('/');
65
+ return { bucket: parts[0], prefix: parts.slice(1).join('/') };
66
+ }
67
+
68
+ function walkFiles(dir){
69
+ const files = [];
70
+ fs.readdirSync(dir, { withFileTypes: true }).forEach(e=>{
71
+ if (e.name.startsWith('.')){ return } // skip .DS_Store etc
72
+ const full = path.join(dir, e.name);
73
+ if (e.isDirectory()){
74
+ files.push(...walkFiles(full));
75
+ } else {
76
+ files.push(full);
77
+ }
78
+ });
79
+ return files;
80
+ }
package/main.js CHANGED
@@ -35,6 +35,15 @@ const Commands = {
35
35
  {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false}
36
36
  ]
37
37
  },
38
+ 'update-assets': {
39
+ desc: 'Upload assets to the app cdn folder',
40
+ exec: require('./commands/update_assets').main,
41
+ args: [
42
+ {n: 'app', desc: 'Name of existing app', pattern: `^[A-Za-z]{2,20}$`, r: true},
43
+ {n: 'src', desc: 'Local file or folder to upload', r: true},
44
+ {n: 't', desc: 'Target path within the app folder (default: root, same structure as local)', r: false}
45
+ ]
46
+ },
38
47
  'update-app': {
39
48
  desc: 'Update application',
40
49
  exec: require('./commands/update_app').main,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [