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.
- package/commands/launch_app.js +34 -12
- package/commands/update_assets.js +80 -0
- package/main.js +9 -0
- package/package.json +1 -1
package/commands/launch_app.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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,
|