cloudmason2 1.4.0 → 1.6.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.
@@ -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
+ }
@@ -227,6 +227,7 @@ class MasonApp extends ELMNT
227
227
  background: #ffffff;
228
228
  }
229
229
  .head:hover { background: #e8e8e8; }
230
+ .name { text-transform: uppercase; letter-spacing: 0.5px; }
230
231
  .count { margin-left: auto; font-size: 12px; font-weight: 400; color: #525252; }
231
232
  .chev { transition: transform 0.15s; color: #525252; }
232
233
  .open .chev { transform: rotate(180deg); }
@@ -105,6 +105,25 @@ function createLocalMarketplace(){
105
105
  console.log(`Created ${destDir}`);
106
106
  }
107
107
 
108
+ // Trust policy allowing GitHub Actions OIDC assumption from one repo.
109
+ // Shared with update-listing's -repo trust update
110
+ exports.githubTrustPolicy = function(oidcArn, repo){
111
+ return {
112
+ Version: "2012-10-17",
113
+ Statement: [
114
+ {
115
+ Effect: "Allow",
116
+ Principal: { Federated: oidcArn },
117
+ Action: "sts:AssumeRoleWithWebIdentity",
118
+ Condition: {
119
+ StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
120
+ StringLike: { "token.actions.githubusercontent.com:sub": `repo:${repo}:*` }
121
+ }
122
+ }
123
+ ]
124
+ };
125
+ }
126
+
108
127
  // Create a role GitHub Actions can assume via OIDC from the given repo.
109
128
  // Permissions cover update-listing, update-app, launch, and publish - not new-app
110
129
  async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
@@ -124,20 +143,7 @@ async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
124
143
  }
125
144
 
126
145
  // Trust: only GitHub Actions workflows from this repo
127
- const trustPolicy = {
128
- Version: "2012-10-17",
129
- Statement: [
130
- {
131
- Effect: "Allow",
132
- Principal: { Federated: oidcArn },
133
- Action: "sts:AssumeRoleWithWebIdentity",
134
- Condition: {
135
- StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
136
- StringLike: { "token.actions.githubusercontent.com:sub": `repo:${repo}:*` }
137
- }
138
- }
139
- ]
140
- };
146
+ const trustPolicy = exports.githubTrustPolicy(oidcArn, repo);
141
147
  const created = await iam.send(new CreateRoleCommand({
142
148
  RoleName: roleName,
143
149
  AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
@@ -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 ---
@@ -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;
@@ -1,8 +1,12 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
4
+ const { IAMClient, GetRoleCommand, UpdateAssumeRolePolicyCommand } = require("@aws-sdk/client-iam");
5
+ const { STSClient, GetCallerIdentityCommand } = require("@aws-sdk/client-sts");
4
6
  const OrgConfig = require('./helpers/org_config');
5
7
  const Apps = require('./helpers/apps');
8
+ const Mime = require('./helpers/mime');
9
+ const { githubTrustPolicy } = require('./new_app');
6
10
 
7
11
  // Marketplace listing text files stored as top-level manifest attributes.
8
12
  // Names match the keys the AWS Marketplace Catalog API expects
@@ -26,25 +30,32 @@ exports.main = async function(args){
26
30
  if (!app){ throw new Error('No app named ' + args.app + '. Run new-app') }
27
31
  const { appPrefix, manifest } = app;
28
32
 
29
- if (!args.assets && !args.pid && !args.ec2){
30
- console.log('Nothing to update. Pass -assets, -pid, or -ec2');
33
+ if (!args.assets && !args.pid && !args.ec2 && !args.repo){
34
+ console.log('Nothing to update. Pass -assets, -pid, -ec2, or -repo');
31
35
  return true;
32
36
  }
33
37
 
34
38
  // Apply all manifest updates in one download/update/upload pass
35
- if (args.assets){
36
- await applyAssets(s3, bucket, appPrefix, cfg.region, args.assets, manifest);
37
- }
38
- if (args.pid){
39
- manifest.productId = args.pid;
40
- console.log('Set productId');
39
+ if (args.assets || args.pid || args.ec2){
40
+ if (args.assets){
41
+ await applyAssets(s3, bucket, appPrefix, cfg.region, args.assets, manifest);
42
+ }
43
+ if (args.pid){
44
+ manifest.productId = args.pid;
45
+ console.log('Set productId');
46
+ }
47
+ if (args.ec2){
48
+ manifest.RecommendedInstanceType = args.ec2;
49
+ console.log('Set RecommendedInstanceType');
50
+ }
51
+ await putManifest(s3, bucket, appPrefix, manifest);
52
+ console.log('Updated manifest');
41
53
  }
42
- if (args.ec2){
43
- manifest.RecommendedInstanceType = args.ec2;
44
- console.log('Set RecommendedInstanceType');
54
+
55
+ // Replace the CI role trust policy with one for the updated repo
56
+ if (args.repo){
57
+ await updateRepoTrust(appName, args.repo);
45
58
  }
46
- await putManifest(s3, bucket, appPrefix, manifest);
47
- console.log('Updated manifest');
48
59
  return true;
49
60
  }
50
61
 
@@ -107,13 +118,35 @@ async function applyAssets(s3, bucket, appPrefix, region, assetsPath, manifest){
107
118
  Bucket: bucket,
108
119
  Key: key,
109
120
  Body: fs.createReadStream(imgPath),
110
- ContentType: 'image/png'
121
+ ContentType: Mime.contentType(img.file)
111
122
  }));
112
123
  manifest[img.attr] = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
113
124
  console.log(`Uploaded ${img.file}: ${manifest[img.attr]}`);
114
125
  }
115
126
  }
116
127
 
128
+ async function updateRepoTrust(appName, repo){
129
+ if (!/^[\w.-]+\/[\w.-]+$/.test(repo)){ throw new Error('Invalid repo. Use org/repo format') }
130
+ const roleName = `mason-gha-${appName}`;
131
+ const iam = new IAMClient({ region: 'us-east-1' });
132
+ const sts = new STSClient({ region: 'us-east-1' });
133
+
134
+ try {
135
+ await iam.send(new GetRoleCommand({ RoleName: roleName }));
136
+ } catch (e){
137
+ if (/NoSuchEntity/.test(e)){ throw new Error(`No CI role ${roleName} found. Run new-app with -repo first`) }
138
+ throw e;
139
+ }
140
+
141
+ const accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
142
+ const oidcArn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
143
+ await iam.send(new UpdateAssumeRolePolicyCommand({
144
+ RoleName: roleName,
145
+ PolicyDocument: JSON.stringify(githubTrustPolicy(oidcArn, repo))
146
+ }));
147
+ console.log(`Updated ${roleName} trust policy to repo ${repo}`);
148
+ }
149
+
117
150
  async function putManifest(s3, bucket, appPrefix, manifest){
118
151
  await s3.send(new PutObjectCommand({
119
152
  Bucket: bucket,
package/main.js CHANGED
@@ -39,7 +39,8 @@ const Commands = {
39
39
  {n: 'app', desc: 'Name of existing app', pattern: `^[A-Za-z]{2,20}$`, r: true},
40
40
  {n: 'assets', desc: 'Path to folder with application assets', r: false},
41
41
  {n: 'pid', desc: 'AWS Product ID', r: false},
42
- {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false}
42
+ {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false},
43
+ {n: 'repo', desc: 'GitHub repo (org/repo). Replaces the CI role trust policy with this repo', r: false}
43
44
  ]
44
45
  },
45
46
  'update-assets': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [