cloudmason2 1.2.0 → 1.4.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,6 +1,7 @@
1
1
  const { S3Client, CreateBucketCommand, HeadBucketCommand, PutBucketPolicyCommand, PutPublicAccessBlockCommand } = require("@aws-sdk/client-s3");
2
2
  const { IAMClient, GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand, PutRolePolicyCommand } = require("@aws-sdk/client-iam");
3
3
  const { SSMClient, PutParameterCommand } = require("@aws-sdk/client-ssm");
4
+ const Apps = require('./helpers/apps');
4
5
 
5
6
  // All CDN resources are created in us-east-1
6
7
  const REGION = 'us-east-1';
@@ -45,6 +46,15 @@ exports.main = async function(args){
45
46
  await s3.send(new PutBucketPolicyCommand({ Bucket: bucket, Policy: JSON.stringify(policy) }));
46
47
  console.log('Set public access policy (keys starting with "_" are private)');
47
48
 
49
+ // Root index.html listing the cdn's public apps
50
+ await s3.send(new PutObjectCommand({
51
+ Bucket: bucket,
52
+ Key: 'index.html',
53
+ Body: Apps.indexTemplate(),
54
+ ContentType: 'text/html'
55
+ }));
56
+ console.log('Created index.html');
57
+
48
58
  // --- III MARKETPLACE ROLE ---
49
59
  const roleArn = await createMarketplaceRole(bucket);
50
60
  const ssm = new SSMClient({ region: REGION });
@@ -92,6 +92,9 @@ exports.delete_app = async function(args){
92
92
  // --- IV DELETE IAM ROLE ---
93
93
  await deleteGithubRole(appName);
94
94
 
95
+ // --- V REMOVE FROM INDEX ---
96
+ await Apps.removeFromIndex(s3, bucket, prefix, appPrefix);
97
+
95
98
  console.log(`Deleted app ${args.app}`);
96
99
  return true;
97
100
  }
@@ -1,4 +1,10 @@
1
- const { GetObjectCommand } = require("@aws-sdk/client-s3");
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3");
4
+
5
+ const INDEX_TEMPLATE = path.resolve(__dirname, '..', 'index.html');
6
+ // Marker in the template <head> after which app tags are inserted
7
+ const INDEX_MARKER = '<!-- mason-apps -->';
2
8
 
3
9
  // Resolve an app name to its s3 folder and manifest. Private apps live under
4
10
  // a _ prefixed folder, so both locations are probed - callers always use the
@@ -18,3 +24,75 @@ exports.resolveApp = async function(s3, bucket, orgPrefix, appName){
18
24
  }
19
25
  return null;
20
26
  }
27
+
28
+ // The cdn root index.html carries one head tag per public app pointing at its manifest:
29
+ // <meta name="mason-app" content="<app>/manifest.json">
30
+
31
+ exports.indexTemplate = function(){
32
+ return fs.readFileSync(INDEX_TEMPLATE, 'utf-8');
33
+ }
34
+
35
+ exports.addToIndex = async function(s3, bucket, orgPrefix, appPrefix){
36
+ const key = indexKey(orgPrefix);
37
+ let html = await readIndex(s3, bucket, key);
38
+ if (html === null){ html = exports.indexTemplate() }
39
+
40
+ const tag = appTag(appPrefix);
41
+ if (html.includes(tag)){ return }
42
+ if (!html.includes(INDEX_MARKER)){
43
+ console.log(`WARNING: index.html has no ${INDEX_MARKER} marker. App not added to index`);
44
+ return;
45
+ }
46
+ html = html.replace(INDEX_MARKER, `${INDEX_MARKER}\n ${tag}`);
47
+ await writeIndex(s3, bucket, key, html);
48
+ console.log(`Added ${appPrefix} to index.html`);
49
+ }
50
+
51
+ exports.removeFromIndex = async function(s3, bucket, orgPrefix, appPrefix){
52
+ const key = indexKey(orgPrefix);
53
+ const html = await readIndex(s3, bucket, key);
54
+ if (html === null){ return }
55
+
56
+ const rgx = new RegExp(`[ \\t]*${escapeRegex(appTag(appPrefix))}\\r?\\n?`);
57
+ const updated = html.replace(rgx, '');
58
+ if (updated === html){ return }
59
+ await writeIndex(s3, bucket, key, updated);
60
+ console.log(`Removed ${appPrefix} from index.html`);
61
+ }
62
+
63
+ exports.appTag = function(appPrefix){
64
+ return `<meta name="mason-app" content="${appPrefix}/manifest.json">`;
65
+ }
66
+
67
+ exports.indexMarker = INDEX_MARKER;
68
+
69
+ function indexKey(orgPrefix){
70
+ return orgPrefix ? `${orgPrefix}/index.html` : 'index.html';
71
+ }
72
+
73
+ function appTag(appPrefix){
74
+ return exports.appTag(appPrefix);
75
+ }
76
+
77
+ function escapeRegex(s){
78
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
79
+ }
80
+
81
+ async function readIndex(s3, bucket, key){
82
+ try {
83
+ const r = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
84
+ return await r.Body.transformToString();
85
+ } catch (e){
86
+ if (/NoSuchKey|NotFound/.test(e)){ return null }
87
+ throw e;
88
+ }
89
+ }
90
+
91
+ async function writeIndex(s3, bucket, key, html){
92
+ await s3.send(new PutObjectCommand({
93
+ Bucket: bucket,
94
+ Key: key,
95
+ Body: html,
96
+ ContentType: 'text/html'
97
+ }));
98
+ }
@@ -0,0 +1,328 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Apps</title>
7
+ <!-- mason-apps -->
8
+ <style>
9
+ @font-face {
10
+ font-family: 'Jost';
11
+ font-style: normal;
12
+ font-weight: 400 700;
13
+ font-display: swap;
14
+ src: url(data:font/woff2;base64,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) format('woff2');
15
+ }
16
+ * { box-sizing: border-box; }
17
+ body {
18
+ margin: 0;
19
+ min-height: 100vh;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ background: #f4f4f4;
24
+ font-family: 'Jost', 'Helvetica Neue', Arial, sans-serif;
25
+ color: #161616;
26
+ }
27
+ main#apps {
28
+ width: 600px;
29
+ height: 600px;
30
+ background: #ffffff;
31
+ border: 1px solid #e0e0e0;
32
+ display: flex;
33
+ flex-direction: column;
34
+ overflow: hidden;
35
+ }
36
+ .hint {
37
+ margin: 0;
38
+ padding: 16px;
39
+ font-size: 14px;
40
+ letter-spacing: 0.16px;
41
+ color: #525252;
42
+ border-bottom: 1px solid #e0e0e0;
43
+ background: #ffffff;
44
+ }
45
+ #list {
46
+ flex: 1;
47
+ overflow-y: auto;
48
+ overflow-x: hidden;
49
+ }
50
+ .none {
51
+ padding: 16px;
52
+ font-size: 14px;
53
+ color: #525252;
54
+ }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <main id="apps">
59
+ <p class="hint">Click the copy icon to copy the Cloudformation Stack URL</p>
60
+ <div id="list"></div>
61
+ </main>
62
+
63
+ <script>
64
+ class ELMNT extends HTMLElement
65
+ {
66
+ // Constructor
67
+ constructor(){
68
+ super();
69
+ this.shadow = this.attachShadow({ mode: 'open' });
70
+ this.shadowRoot.adoptedStyleSheets = [this.constructor.Stylesheet];
71
+ this.shadow.innerHTML = this.constructor.Html || '';
72
+ // Map Event
73
+ if (this.constructor['@Events']){
74
+ for (const ev of this.constructor['@Events']){
75
+ const $el = this.shadow.querySelector(ev[0]);
76
+ if (!$el) throw 'Missing element in ' + this.constructor.Tag + ' ' + ev;
77
+ $el.addEventListener(ev[1], this[ev[2]].bind(this))
78
+ }
79
+ }
80
+ // Map Outputs
81
+ this.dispatch = {};
82
+ if (this.constructor['@Outputs']){
83
+ for (const key of this.constructor['@Outputs']){
84
+ const wantsResponse = key.charAt(0) === '@';
85
+ const name = wantsResponse ? key.slice(1) : key;
86
+ this.dispatch[name] = (p)=>{
87
+ const detail = { target: this, params: p };
88
+ let rProm = null;
89
+ if (wantsResponse){
90
+ const { promise, resolve } = Promise.withResolvers();
91
+ rProm = promise;
92
+ detail.respond = resolve;
93
+ }
94
+ this.dispatchEvent(new CustomEvent(name, {
95
+ detail,
96
+ bubbles: true,
97
+ composed: true
98
+ }));
99
+ return rProm;
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ // --- Public Properties --- //
106
+ dispatch = {};
107
+
108
+ attributeChangedCallback(name, oldValue, newValue){
109
+ if (oldValue !== newValue){ this[name] = newValue }
110
+ }
111
+
112
+ map(obj){
113
+ for (const key in obj){ this[key] = obj[key]}
114
+ }
115
+
116
+ static Define(cls){
117
+ cls.Stylesheet = new CSSStyleSheet();
118
+ cls.Stylesheet.replace(cls.Css).then(() => {
119
+ customElements.define(cls.Tag, cls);
120
+ })
121
+ }
122
+ }
123
+ </script>
124
+
125
+ <script>
126
+ class MasonApp extends ELMNT
127
+ {
128
+
129
+ // Constructor
130
+ constructor(){
131
+ super();
132
+ }
133
+
134
+ // --- Static Definitions --- //
135
+ static observedAttributes = [];
136
+ static '@Events' = [
137
+ ['.head', 'click', '_onToggle'],
138
+ ['.versions', 'click', '_onCopy']
139
+ ];
140
+ static '@Outputs' = [];
141
+
142
+ // --- Private Properties --- //
143
+ #app;
144
+
145
+ // --- Setters & Getters --- //
146
+ set app(v){
147
+ this.#app = v;
148
+ this.#_render();
149
+ }
150
+
151
+ // --- Public Methods --- //
152
+ expand(){
153
+ this.shadow.querySelector('.item').classList.add('open');
154
+ }
155
+
156
+ // --- User Triggered Events --- //
157
+ _onToggle(){
158
+ this.shadow.querySelector('.item').classList.toggle('open');
159
+ }
160
+
161
+ _onCopy(e){
162
+ const btn = e.target.closest('.copy');
163
+ if (!btn){ return }
164
+ navigator.clipboard.writeText(btn.dataset.url);
165
+ btn.classList.add('done');
166
+ setTimeout(()=>{ btn.classList.remove('done') }, 1500);
167
+ }
168
+
169
+ // --- Private SubHelpers --- //
170
+ #_render(){
171
+ this.shadow.querySelector('.name').textContent = this.#app.name;
172
+ const versions = MasonApp.PublishedVersions(this.#app.manifest);
173
+ this.shadow.querySelector('.count').textContent = `${versions.length} version${versions.length === 1 ? '' : 's'}`;
174
+ this.shadow.querySelector('.versions').innerHTML = versions.length
175
+ ? versions.map(v=>{ return MasonApp.VersionRow(v) }).join('')
176
+ : '<div class="empty">No published versions</div>';
177
+ }
178
+
179
+ // --- Static Methods --- //
180
+ static PublishedVersions(manifest){
181
+ return (manifest.versions || [])
182
+ .filter(v=>{ return v.published === true })
183
+ .sort((a,b)=>{ return String(b.publish_date || b.version).localeCompare(String(a.publish_date || a.version)) });
184
+ }
185
+
186
+ static VersionRow(v){
187
+ const mm = v.version.split('.').slice(0, 2).join('.');
188
+ return `<div class="row">
189
+ <span class="ver">v${mm}<span class="full">${v.version}</span></span>
190
+ <span class="date">${MasonApp.#FormatDate(v.publish_date)}</span>
191
+ <button class="copy" data-url="${v.stack_url || ''}" aria-label="Copy Cloudformation Stack URL">
192
+ <svg class="ic-copy" viewBox="0 0 32 32" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z"/><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z"/></svg>
193
+ <svg class="ic-check" viewBox="0 0 32 32" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M13 24l-9-9 1.41-1.41L13 21.17 26.59 7.59 28 9 13 24z"/></svg>
194
+ </button>
195
+ </div>`;
196
+ }
197
+
198
+ // --- Static SubHelpers --- //
199
+ static #FormatDate(iso){
200
+ if (!iso){ return '' }
201
+ return new Date(iso).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
202
+ }
203
+
204
+ // --- Static Defs --- //
205
+ static Tag = 'mason-app';
206
+ static Html = (`
207
+ <div class="item">
208
+ <div class="head">
209
+ <span class="name"></span>
210
+ <span class="count"></span>
211
+ <svg class="chev" viewBox="0 0 32 32" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M16 22L6 12l1.4-1.4 8.6 8.6 8.6-8.6L26 12z"/></svg>
212
+ </div>
213
+ <div class="versions"></div>
214
+ </div>
215
+ `);
216
+ static Css = (`
217
+ :host { display: block; font-family: 'Jost', 'Helvetica Neue', Arial, sans-serif; color: #161616; }
218
+ .head {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 12px;
222
+ padding: 14px 16px;
223
+ font-size: 15px;
224
+ font-weight: 500;
225
+ cursor: pointer;
226
+ border-bottom: 1px solid #e0e0e0;
227
+ background: #ffffff;
228
+ }
229
+ .head:hover { background: #e8e8e8; }
230
+ .count { margin-left: auto; font-size: 12px; font-weight: 400; color: #525252; }
231
+ .chev { transition: transform 0.15s; color: #525252; }
232
+ .open .chev { transform: rotate(180deg); }
233
+ .versions { display: none; }
234
+ .open .versions { display: block; }
235
+ .row {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 12px;
239
+ padding: 10px 16px 10px 32px;
240
+ font-size: 14px;
241
+ border-bottom: 1px solid #e0e0e0;
242
+ background: #ffffff;
243
+ }
244
+ .row:hover { background: #f4f4f4; }
245
+ .ver { font-weight: 500; position: relative; }
246
+ .full {
247
+ position: absolute;
248
+ bottom: calc(100% + 6px);
249
+ left: 0;
250
+ padding: 4px 8px;
251
+ background: #161616;
252
+ color: #ffffff;
253
+ font-size: 12px;
254
+ font-weight: 400;
255
+ white-space: nowrap;
256
+ opacity: 0;
257
+ visibility: hidden;
258
+ transition: opacity 0.1s 0s, visibility 0s 0.1s;
259
+ pointer-events: none;
260
+ z-index: 1;
261
+ }
262
+ .ver:hover .full {
263
+ opacity: 1;
264
+ visibility: visible;
265
+ transition-delay: 5s, 5s;
266
+ }
267
+ .date { margin-left: auto; color: #525252; }
268
+ .copy {
269
+ display: flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ padding: 5px;
273
+ background: none;
274
+ border: none;
275
+ cursor: pointer;
276
+ color: #161616;
277
+ }
278
+ .copy:hover { background: #e8e8e8; }
279
+ .copy:focus { outline: 2px solid #0f62fe; outline-offset: -2px; }
280
+ .ic-check { display: none; color: #24a148; }
281
+ .done .ic-copy { display: none; }
282
+ .done .ic-check { display: block; }
283
+ .empty { padding: 12px 16px 12px 32px; font-size: 14px; color: #525252; border-bottom: 1px solid #e0e0e0; }
284
+ `);
285
+
286
+ }
287
+
288
+ ELMNT.Define(MasonApp);
289
+ </script>
290
+
291
+ <script>
292
+ (async function(){
293
+ const list = document.getElementById('list');
294
+ const filter = new URLSearchParams(location.search).get('app');
295
+
296
+ const apps = [];
297
+ document.querySelectorAll('meta[name="mason-app"]').forEach(m=>{
298
+ const path = m.getAttribute('content');
299
+ const name = path.split('/').slice(-2, -1)[0].replace(/^_/, '');
300
+ if (filter && name.toLowerCase() !== filter.toLowerCase()){ return }
301
+ apps.push({ path: path, name: name });
302
+ });
303
+
304
+ const loaded = await Promise.all(apps.map(a=>{
305
+ return fetch(a.path)
306
+ .then(r=>{ if (!r.ok){ throw new Error(r.status) } return r.json() })
307
+ .then(manifest=>{ return { name: a.name, manifest: manifest } })
308
+ .catch(()=>{ return null });
309
+ }));
310
+
311
+ await customElements.whenDefined('mason-app');
312
+ let count = 0;
313
+ for (const a of loaded){
314
+ if (!a){ continue }
315
+ const el = document.createElement('mason-app');
316
+ el.app = { name: a.manifest.name || a.name, manifest: a.manifest };
317
+ list.appendChild(el);
318
+ count += 1;
319
+ }
320
+ if (count === 0){
321
+ list.innerHTML = '<p class="none">No apps found</p>';
322
+ } else if (filter && count === 1){
323
+ list.querySelector('mason-app').expand();
324
+ }
325
+ })();
326
+ </script>
327
+ </body>
328
+ </html>
@@ -1,11 +1,15 @@
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');
5
5
  const Apps = require('./helpers/apps');
6
6
 
7
7
  // CLI args consumed by the command itself; everything else is passed through as a CF stack parameter
8
- const CLI_ARGS = ['app', 'v', 'title', 'region'];
8
+ const CLI_ARGS = ['app', 'v', 'title', 'region', 'await'];
9
+
10
+ // Terminal/settling stack statuses that mean the launch succeeded.
11
+ // UPDATE_COMPLETE_CLEANUP_IN_PROGRESS means the update itself is done
12
+ const SUCCESS_STATUSES = ['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS'];
9
13
 
10
14
  exports.main = async function(args){
11
15
  const cfg = OrgConfig.read();
@@ -44,12 +48,31 @@ exports.main = async function(args){
44
48
  }
45
49
 
46
50
  // --- 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 }];
51
+ const client = new CloudFormationClient({ region: args.region });
52
+ const existing = await getStack(client, stackName);
53
+
54
+ // CLI-provided params: AmiId from the manifest version (unless noami) + any extra CLI args
55
+ const cliParams = {};
50
56
  Object.keys(args).forEach(k=>{
51
57
  if (CLI_ARGS.includes(k) || k === 'AmiId'){ return }
52
- cfParams.push({ ParameterKey: k, ParameterValue: args[k] });
58
+ cliParams[k] = args[k];
59
+ });
60
+ if (!manifest.noami){ cliParams.AmiId = amiId }
61
+
62
+ // On update, carry forward previous stack values unless overridden on the CLI,
63
+ // dropping any that are no longer declared in the new template.
64
+ // UsePreviousValue avoids round-tripping NoEcho params (which read back masked)
65
+ const cfParams = [];
66
+ if (existing){
67
+ const templateKeys = await getTemplateKeys(client, targetVersion.stack_url);
68
+ existing.parameters.forEach(p=>{
69
+ if (!templateKeys.has(p.ParameterKey)){ return }
70
+ if (cliParams[p.ParameterKey] !== undefined){ return }
71
+ cfParams.push({ ParameterKey: p.ParameterKey, UsePreviousValue: true });
72
+ });
73
+ }
74
+ Object.keys(cliParams).forEach(k=>{
75
+ cfParams.push({ ParameterKey: k, ParameterValue: cliParams[k] });
53
76
  });
54
77
 
55
78
  const input = {
@@ -67,17 +90,15 @@ exports.main = async function(args){
67
90
  };
68
91
 
69
92
  // 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
93
  let stackId;
73
- if (status === null){
94
+ if (existing === null){
74
95
  console.log(`Creating stack ${stackName}`);
75
96
  input.OnFailure = 'DELETE';
76
97
  const r = await client.send(new CreateStackCommand(input));
77
98
  stackId = r.StackId;
78
99
  } else {
79
- if (status.endsWith('_IN_PROGRESS')){
80
- console.log(`Stack ${stackName} is ${status}. Wait for completion before relaunching`);
100
+ if (existing.status.endsWith('_IN_PROGRESS')){
101
+ console.log(`Stack ${stackName} is ${existing.status}. Wait for completion before relaunching`);
81
102
  throw new Error('Stack operation in progress');
82
103
  }
83
104
  console.log(`Updating stack ${stackName}`);
@@ -93,6 +114,11 @@ exports.main = async function(args){
93
114
  }
94
115
  }
95
116
  console.log('Stack:', stackId);
117
+
118
+ // --- III AWAIT ---
119
+ if (args.await !== undefined){
120
+ await awaitStack(client, stackName);
121
+ }
96
122
  return true;
97
123
  }
98
124
 
@@ -143,13 +169,46 @@ async function waitForAMI(amiId, region){
143
169
  throw new Error('AMI not available after 20 minutes. Try again in a few minutes.');
144
170
  }
145
171
 
146
- async function stackStatus(client, stackName){
172
+ async function getStack(client, stackName){
147
173
  try {
148
174
  const r = await client.send(new DescribeStacksCommand({ StackName: stackName }));
149
- return r.Stacks[0].StackStatus;
175
+ return {
176
+ status: r.Stacks[0].StackStatus,
177
+ reason: r.Stacks[0].StackStatusReason || '',
178
+ parameters: r.Stacks[0].Parameters || []
179
+ };
150
180
  } catch (e){
151
181
  if (e.name === 'ValidationError'){ return null }
152
182
  throw e;
153
183
  }
154
184
  }
155
185
 
186
+ // Poll every 30s for up to 30 minutes until the stack reaches a success status.
187
+ // Still in progress: CREATE_IN_PROGRESS, UPDATE_IN_PROGRESS. Anything containing
188
+ // FAILED, ROLLBACK, or DELETE means the launch did not succeed
189
+ async function awaitStack(client, stackName){
190
+ console.log('Waiting for stack to complete');
191
+ for (let i=0; i<60; i++){
192
+ await new Promise(res=>{ setTimeout(res, 30000) });
193
+ const stack = await getStack(client, stackName);
194
+ if (!stack){
195
+ // OnFailure: DELETE removes a failed create entirely
196
+ throw new Error('Stack no longer exists. Create failed and was deleted');
197
+ }
198
+ console.log(`\tStack status @${(i+1)*30}s: ${stack.status}`);
199
+ if (SUCCESS_STATUSES.includes(stack.status)){
200
+ console.log(`Stack launch succeeded: ${stack.status}`);
201
+ return;
202
+ }
203
+ if (/FAILED|ROLLBACK|DELETE/.test(stack.status)){
204
+ throw new Error(`Stack launch failed: ${stack.status}${stack.reason ? ' - ' + stack.reason : ''}`);
205
+ }
206
+ }
207
+ throw new Error('Stack not complete after 30 minutes');
208
+ }
209
+
210
+ async function getTemplateKeys(client, templateUrl){
211
+ const r = await client.send(new GetTemplateSummaryCommand({ TemplateURL: templateUrl }));
212
+ return new Set((r.Parameters || []).map(p=>{ return p.ParameterKey }));
213
+ }
214
+
@@ -4,6 +4,7 @@ const { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } = r
4
4
  const { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand, GetOpenIDConnectProviderCommand, CreateOpenIDConnectProviderCommand } = require("@aws-sdk/client-iam");
5
5
  const { STSClient, GetCallerIdentityCommand } = require("@aws-sdk/client-sts");
6
6
  const OrgConfig = require('./helpers/org_config');
7
+ const Apps = require('./helpers/apps');
7
8
 
8
9
  const TEMPLATE_DIR = path.resolve(__dirname, '..', '.marketplace');
9
10
 
@@ -38,7 +39,11 @@ exports.main = async function(args){
38
39
  await createManifest(s3, bucket, appPrefix, args.name, args.noami !== undefined, args.private !== undefined);
39
40
  }
40
41
 
41
- // --- II LOCAL TEMPLATES ---
42
+ // --- II INDEX + LOCAL TEMPLATES ---
43
+ // Public apps are listed in the cdn root index.html
44
+ if (args.private === undefined){
45
+ await Apps.addToIndex(s3, bucket, prefix, appPrefix);
46
+ }
42
47
  createLocalMarketplace();
43
48
 
44
49
  // --- III REPO ROLE ---
@@ -0,0 +1,43 @@
1
+ const { S3Client, ListObjectsV2Command, PutObjectCommand } = require("@aws-sdk/client-s3");
2
+ const Apps = require('./helpers/apps');
3
+
4
+ // All core resources live in us-east-1
5
+ const REGION = 'us-east-1';
6
+
7
+ // refresh-cdn: rebuild the cdn root index.html from all public app manifests
8
+ exports.main = async function(args){
9
+ const bucket = args.cdn;
10
+ const s3 = new S3Client({ region: REGION });
11
+
12
+ // Scan for app manifests: one folder deep, private "_" folders excluded
13
+ const apps = [];
14
+ let token;
15
+ do {
16
+ const r = await s3.send(new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: token }));
17
+ (r.Contents || []).forEach(o=>{
18
+ const parts = o.Key.split('/');
19
+ if (parts.length === 2 && parts[1] === 'manifest.json' && !parts[0].startsWith('_')){
20
+ apps.push(parts[0]);
21
+ }
22
+ });
23
+ token = r.NextContinuationToken;
24
+ } while (token);
25
+ apps.sort();
26
+
27
+ // Rebuild from the boilerplate template. Tags insert at the marker,
28
+ // so add in reverse to end up in alphabetical order
29
+ let html = Apps.indexTemplate();
30
+ [...apps].reverse().forEach(a=>{
31
+ html = html.replace(Apps.indexMarker, `${Apps.indexMarker}\n ${Apps.appTag(a)}`);
32
+ });
33
+
34
+ await s3.send(new PutObjectCommand({
35
+ Bucket: bucket,
36
+ Key: 'index.html',
37
+ Body: html,
38
+ ContentType: 'text/html'
39
+ }));
40
+ apps.forEach(a=>{ console.log(' ' + a) });
41
+ console.log(`Rebuilt index.html with ${apps.length} app(s)`);
42
+ return true;
43
+ }
@@ -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
@@ -8,6 +8,13 @@ const Commands = {
8
8
  {n: 'name', desc: 'CDN bucket name (lowercase letters, numbers, hyphens)', pattern: `^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$`, r: true}
9
9
  ]
10
10
  },
11
+ 'refresh-cdn': {
12
+ desc: "Rebuild the cdn index.html from all public app manifests",
13
+ exec: require('./commands/refresh_cdn').main,
14
+ args: [
15
+ {n: 'cdn', desc: 'CDN bucket name', pattern: `^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$`, r: true}
16
+ ]
17
+ },
11
18
  'set-org': {
12
19
  desc: "Set the organization CDN",
13
20
  exec: require('./commands/init_org').main,
@@ -35,6 +42,15 @@ const Commands = {
35
42
  {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false}
36
43
  ]
37
44
  },
45
+ 'update-assets': {
46
+ desc: 'Upload assets to the app cdn folder',
47
+ exec: require('./commands/update_assets').main,
48
+ args: [
49
+ {n: 'app', desc: 'Name of existing app', pattern: `^[A-Za-z]{2,20}$`, r: true},
50
+ {n: 'src', desc: 'Local file or folder to upload', r: true},
51
+ {n: 't', desc: 'Target path within the app folder (default: root, same structure as local)', r: false}
52
+ ]
53
+ },
38
54
  'update-app': {
39
55
  desc: 'Update application',
40
56
  exec: require('./commands/update_app').main,
@@ -53,6 +69,7 @@ const Commands = {
53
69
  {n: 'v', desc: 'Version to launch', pattern: `[0-9]{1,20}`, r: true},
54
70
  {n: 'title', desc: 'Title of the version', pattern: `^[A-Za-z][A-Za-z0-9-]{1,30}$`, r: true},
55
71
  {n: 'region', desc: 'AWS Region to launch the instance', pattern: `^[a-z]{2}-[a-z]+-[0-9]$`, r: true},
72
+ {n: 'await', desc: 'Wait for the stack launch to complete (up to 30m)', r: false},
56
73
  ]
57
74
  },
58
75
  'publish': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [