cloudmason2 1.3.0 → 1.5.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,36 @@
1
+ const path = require('path');
2
+
3
+ const TYPES = {
4
+ '.html': 'text/html',
5
+ '.css': 'text/css',
6
+ '.js': 'application/javascript',
7
+ '.mjs': 'application/javascript',
8
+ '.json': 'application/json',
9
+ '.txt': 'text/plain',
10
+ '.md': 'text/markdown',
11
+ '.csv': 'text/csv',
12
+ '.xml': 'application/xml',
13
+ '.yaml': 'text/yaml',
14
+ '.yml': 'text/yaml',
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.svg': 'image/svg+xml',
20
+ '.webp': 'image/webp',
21
+ '.ico': 'image/x-icon',
22
+ '.woff': 'font/woff',
23
+ '.woff2': 'font/woff2',
24
+ '.ttf': 'font/ttf',
25
+ '.otf': 'font/otf',
26
+ '.mp4': 'video/mp4',
27
+ '.webm': 'video/webm',
28
+ '.pdf': 'application/pdf',
29
+ '.zip': 'application/zip',
30
+ '.gz': 'application/gzip'
31
+ };
32
+
33
+ // Content type from a file path or s3 key
34
+ exports.contentType = function(p){
35
+ return TYPES[path.extname(p).toLowerCase()] || 'application/octet-stream';
36
+ }
@@ -0,0 +1,329 @@
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
+ .name { text-transform: uppercase; letter-spacing: 0.5px; }
231
+ .count { margin-left: auto; font-size: 12px; font-weight: 400; color: #525252; }
232
+ .chev { transition: transform 0.15s; color: #525252; }
233
+ .open .chev { transform: rotate(180deg); }
234
+ .versions { display: none; }
235
+ .open .versions { display: block; }
236
+ .row {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 12px;
240
+ padding: 10px 16px 10px 32px;
241
+ font-size: 14px;
242
+ border-bottom: 1px solid #e0e0e0;
243
+ background: #ffffff;
244
+ }
245
+ .row:hover { background: #f4f4f4; }
246
+ .ver { font-weight: 500; position: relative; }
247
+ .full {
248
+ position: absolute;
249
+ bottom: calc(100% + 6px);
250
+ left: 0;
251
+ padding: 4px 8px;
252
+ background: #161616;
253
+ color: #ffffff;
254
+ font-size: 12px;
255
+ font-weight: 400;
256
+ white-space: nowrap;
257
+ opacity: 0;
258
+ visibility: hidden;
259
+ transition: opacity 0.1s 0s, visibility 0s 0.1s;
260
+ pointer-events: none;
261
+ z-index: 1;
262
+ }
263
+ .ver:hover .full {
264
+ opacity: 1;
265
+ visibility: visible;
266
+ transition-delay: 5s, 5s;
267
+ }
268
+ .date { margin-left: auto; color: #525252; }
269
+ .copy {
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ padding: 5px;
274
+ background: none;
275
+ border: none;
276
+ cursor: pointer;
277
+ color: #161616;
278
+ }
279
+ .copy:hover { background: #e8e8e8; }
280
+ .copy:focus { outline: 2px solid #0f62fe; outline-offset: -2px; }
281
+ .ic-check { display: none; color: #24a148; }
282
+ .done .ic-copy { display: none; }
283
+ .done .ic-check { display: block; }
284
+ .empty { padding: 12px 16px 12px 32px; font-size: 14px; color: #525252; border-bottom: 1px solid #e0e0e0; }
285
+ `);
286
+
287
+ }
288
+
289
+ ELMNT.Define(MasonApp);
290
+ </script>
291
+
292
+ <script>
293
+ (async function(){
294
+ const list = document.getElementById('list');
295
+ const filter = new URLSearchParams(location.search).get('app');
296
+
297
+ const apps = [];
298
+ document.querySelectorAll('meta[name="mason-app"]').forEach(m=>{
299
+ const path = m.getAttribute('content');
300
+ const name = path.split('/').slice(-2, -1)[0].replace(/^_/, '');
301
+ if (filter && name.toLowerCase() !== filter.toLowerCase()){ return }
302
+ apps.push({ path: path, name: name });
303
+ });
304
+
305
+ const loaded = await Promise.all(apps.map(a=>{
306
+ return fetch(a.path)
307
+ .then(r=>{ if (!r.ok){ throw new Error(r.status) } return r.json() })
308
+ .then(manifest=>{ return { name: a.name, manifest: manifest } })
309
+ .catch(()=>{ return null });
310
+ }));
311
+
312
+ await customElements.whenDefined('mason-app');
313
+ let count = 0;
314
+ for (const a of loaded){
315
+ if (!a){ continue }
316
+ const el = document.createElement('mason-app');
317
+ el.app = { name: a.manifest.name || a.name, manifest: a.manifest };
318
+ list.appendChild(el);
319
+ count += 1;
320
+ }
321
+ if (count === 0){
322
+ list.innerHTML = '<p class="none">No apps found</p>';
323
+ } else if (filter && count === 1){
324
+ list.querySelector('mason-app').expand();
325
+ }
326
+ })();
327
+ </script>
328
+ </body>
329
+ </html>
@@ -5,7 +5,11 @@ 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();
@@ -110,6 +114,11 @@ exports.main = async function(args){
110
114
  }
111
115
  }
112
116
  console.log('Stack:', stackId);
117
+
118
+ // --- III AWAIT ---
119
+ if (args.await !== undefined){
120
+ await awaitStack(client, stackName);
121
+ }
113
122
  return true;
114
123
  }
115
124
 
@@ -163,13 +172,41 @@ async function waitForAMI(amiId, region){
163
172
  async function getStack(client, stackName){
164
173
  try {
165
174
  const r = await client.send(new DescribeStacksCommand({ StackName: stackName }));
166
- return { status: r.Stacks[0].StackStatus, parameters: r.Stacks[0].Parameters || [] };
175
+ return {
176
+ status: r.Stacks[0].StackStatus,
177
+ reason: r.Stacks[0].StackStatusReason || '',
178
+ parameters: r.Stacks[0].Parameters || []
179
+ };
167
180
  } catch (e){
168
181
  if (e.name === 'ValidationError'){ return null }
169
182
  throw e;
170
183
  }
171
184
  }
172
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
+
173
210
  async function getTemplateKeys(client, templateUrl){
174
211
  const r = await client.send(new GetTemplateSummaryCommand({ TemplateURL: templateUrl }));
175
212
  return new Set((r.Parameters || []).map(p=>{ return p.ParameterKey }));
@@ -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 ---
@@ -3,6 +3,7 @@ const { S3Client, PutObjectCommand, GetObjectCommand } = require("@aws-sdk/clien
3
3
  const { SSMClient, GetParameterCommand } = require("@aws-sdk/client-ssm");
4
4
  const OrgConfig = require('./helpers/org_config');
5
5
  const Apps = require('./helpers/apps');
6
+ const Mime = require('./helpers/mime');
6
7
 
7
8
  // The Marketplace Catalog API is only available in us-east-1
8
9
  const CATALOG_REGION = 'us-east-1';
@@ -53,7 +54,7 @@ exports.main = async function(args){
53
54
  const amiAlias = `resolve:ssm:/aws/service/marketplace/${manifest.productId}/${fullVersion}`;
54
55
  const upgradeText = buildUpgradeTemplate(stackText, amiAlias);
55
56
  const upgradeKey = `${appPrefix}/versions/${vFolder}/upgrade.yaml`;
56
- await s3.send(new PutObjectCommand({ Bucket: bucket, Key: upgradeKey, Body: upgradeText }));
57
+ await s3.send(new PutObjectCommand({ Bucket: bucket, Key: upgradeKey, Body: upgradeText, ContentType: Mime.contentType(upgradeKey) }));
57
58
  console.log(`Uploaded upgrade template s3://${bucket}/${upgradeKey}`);
58
59
 
59
60
  // --- III START CHANGE SET ---
@@ -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
+ }
@@ -5,6 +5,7 @@ const { S3Client, PutObjectCommand, GetObjectCommand } = require("@aws-sdk/clien
5
5
  const { EC2Client, DescribeRegionsCommand, DescribeImagesCommand, DeregisterImageCommand, DeleteSnapshotCommand } = require("@aws-sdk/client-ec2");
6
6
  const OrgConfig = require('./helpers/org_config');
7
7
  const Apps = require('./helpers/apps');
8
+ const Mime = require('./helpers/mime');
8
9
  const { buildAMI } = require('./ssh_build');
9
10
 
10
11
  exports.main = async function(args){
@@ -93,7 +94,7 @@ exports.main = async function(args){
93
94
  console.log('No AmiId parameter found in stack. Template unchanged');
94
95
  }
95
96
  }
96
- await s3.send(new PutObjectCommand({ Bucket: bucket, Key: stackKey, Body: finalStack }));
97
+ await s3.send(new PutObjectCommand({ Bucket: bucket, Key: stackKey, Body: finalStack, ContentType: Mime.contentType(stackKey) }));
97
98
  console.log(`Uploaded s3://${bucket}/${stackKey}`);
98
99
 
99
100
  // --- V UPDATE MANIFEST ---
@@ -162,7 +163,8 @@ async function uploadFile(s3, bucket, key, localPath){
162
163
  await s3.send(new PutObjectCommand({
163
164
  Bucket: bucket,
164
165
  Key: key,
165
- Body: fs.createReadStream(localPath)
166
+ Body: fs.createReadStream(localPath),
167
+ ContentType: Mime.contentType(key)
166
168
  }));
167
169
  }
168
170
 
@@ -3,14 +3,7 @@ const path = require('path');
3
3
  const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
4
4
  const OrgConfig = require('./helpers/org_config');
5
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
- };
6
+ const Mime = require('./helpers/mime');
14
7
 
15
8
  // update-assets: upload a file or folder to the app's cdn folder
16
9
  exports.main = async function(args){
@@ -45,7 +38,7 @@ exports.main = async function(args){
45
38
  Bucket: bucket,
46
39
  Key: key,
47
40
  Body: fs.createReadStream(f),
48
- ContentType: CONTENT_TYPES[path.extname(f).toLowerCase()] || 'application/octet-stream'
41
+ ContentType: Mime.contentType(f)
49
42
  }));
50
43
  console.log(`Uploaded s3://${bucket}/${key}`);
51
44
  uploaded += 1;
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
4
4
  const OrgConfig = require('./helpers/org_config');
5
5
  const Apps = require('./helpers/apps');
6
+ const Mime = require('./helpers/mime');
6
7
 
7
8
  // Marketplace listing text files stored as top-level manifest attributes.
8
9
  // Names match the keys the AWS Marketplace Catalog API expects
@@ -107,7 +108,7 @@ async function applyAssets(s3, bucket, appPrefix, region, assetsPath, manifest){
107
108
  Bucket: bucket,
108
109
  Key: key,
109
110
  Body: fs.createReadStream(imgPath),
110
- ContentType: 'image/png'
111
+ ContentType: Mime.contentType(img.file)
111
112
  }));
112
113
  manifest[img.attr] = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
113
114
  console.log(`Uploaded ${img.file}: ${manifest[img.attr]}`);
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,
@@ -62,6 +69,7 @@ const Commands = {
62
69
  {n: 'v', desc: 'Version to launch', pattern: `[0-9]{1,20}`, r: true},
63
70
  {n: 'title', desc: 'Title of the version', pattern: `^[A-Za-z][A-Za-z0-9-]{1,30}$`, r: true},
64
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},
65
73
  ]
66
74
  },
67
75
  'publish': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [