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.
- package/commands/create_cdn.js +10 -0
- package/commands/delete.js +3 -0
- package/commands/helpers/apps.js +79 -1
- package/commands/index.html +328 -0
- package/commands/launch_app.js +72 -13
- package/commands/new_app.js +6 -1
- package/commands/refresh_cdn.js +43 -0
- package/commands/update_assets.js +80 -0
- package/main.js +17 -0
- package/package.json +1 -1
package/commands/create_cdn.js
CHANGED
|
@@ -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 });
|
package/commands/delete.js
CHANGED
|
@@ -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
|
}
|
package/commands/helpers/apps.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const
|
|
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>
|
package/commands/launch_app.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
172
|
+
async function getStack(client, stackName){
|
|
147
173
|
try {
|
|
148
174
|
const r = await client.send(new DescribeStacksCommand({ StackName: stackName }));
|
|
149
|
-
return
|
|
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
|
+
|
package/commands/new_app.js
CHANGED
|
@@ -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': {
|