cloudmason2 1.7.9 → 1.8.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,8 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command } = require("@aws-sdk/client-s3");
4
- const { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand, GetOpenIDConnectProviderCommand, CreateOpenIDConnectProviderCommand } = require("@aws-sdk/client-iam");
5
- const { STSClient, GetCallerIdentityCommand } = require("@aws-sdk/client-sts");
3
+ const { S3Client, PutObjectCommand, ListObjectsV2Command } = require("@aws-sdk/client-s3");
6
4
  const OrgConfig = require('./helpers/org_config');
7
5
  const Apps = require('./helpers/apps');
8
6
 
@@ -16,8 +14,8 @@ const MANIFEST_TEMPLATE = {
16
14
  versions: []
17
15
  };
18
16
 
19
- // new-app: create the app's core assets in s3, the local template folder,
20
- // and (if -repo) the GitHub Actions CI role. Listing content is set by update-listing
17
+ // new-app: create the app's core assets in s3 and the local template folder.
18
+ // Listing content is set by update-listing; the CI role is managed by set-repo
21
19
  exports.main = async function(args){
22
20
  const cfg = OrgConfig.read();
23
21
  if (!cfg){ throw new Error('No org config found. Run set-org') }
@@ -45,13 +43,6 @@ exports.main = async function(args){
45
43
  await Apps.addToIndex(s3, bucket, prefix, appPrefix);
46
44
  }
47
45
  createLocalMarketplace();
48
-
49
- // --- III REPO ROLE ---
50
- if (args.repo){
51
- if (!/^[\w.-]+\/[\w.-]+$/.test(args.repo)){ throw new Error('Invalid repo. Use org/repo format') }
52
- const manifest = await getManifest(s3, bucket, appPrefix);
53
- await ensureGithubRole(args.repo, bucket, appPrefix, appName, manifest.productId);
54
- }
55
46
  return true;
56
47
  }
57
48
 
@@ -104,187 +95,3 @@ function createLocalMarketplace(){
104
95
  });
105
96
  console.log(`Created ${destDir}`);
106
97
  }
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
-
127
- // Create a role GitHub Actions can assume via OIDC from the given repo.
128
- // Permissions cover update-listing, update-app, launch, and publish - not new-app
129
- async function ensureGithubRole(repo, bucket, appPrefix, appName, productId){
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
- const accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
134
-
135
- const oidcArn = await ensureOidcProvider(iam, accountId);
136
-
137
- // Reuse the role if it already exists - the permissions policy below is
138
- // still re-applied, so re-running new-app refreshes an existing role
139
- let roleArn = null;
140
- try {
141
- const existing = await iam.send(new GetRoleCommand({ RoleName: roleName }));
142
- console.log(`Role ${roleName} already exists (${existing.Role.Arn})`);
143
- roleArn = existing.Role.Arn;
144
- } catch (e){
145
- if (!/NoSuchEntity/.test(e)){ throw e }
146
- }
147
-
148
- if (!roleArn){
149
- // Trust: only GitHub Actions workflows from this repo
150
- const trustPolicy = exports.githubTrustPolicy(oidcArn, repo);
151
- const created = await iam.send(new CreateRoleCommand({
152
- RoleName: roleName,
153
- AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
154
- Description: `Mason CI role for ${appName} (github.com/${repo})`
155
- }));
156
- roleArn = created.Role.Arn;
157
- }
158
-
159
- // Scoped to the app prefix, product id, and app stack names where the
160
- // services support it. EC2 build permissions cannot be scoped
161
- if (!productId){ console.log('WARNING: No productId in manifest. Marketplace access is scoped to all products. Re-create the role after update-listing -pid to scope it') }
162
- const marketplaceEntity = productId
163
- ? `arn:aws:aws-marketplace:us-east-1:${accountId}:AWSMarketplace/AmiProduct/${productId}`
164
- : `arn:aws:aws-marketplace:us-east-1:${accountId}:AWSMarketplace/AmiProduct/*`;
165
- const policy = {
166
- Version: "2012-10-17",
167
- Statement: [
168
- {
169
- Sid: "AppPrefixWrite",
170
- Effect: "Allow",
171
- Action: ["s3:GetObject", "s3:PutObject"],
172
- Resource: `arn:aws:s3:::${bucket}/${appPrefix}/*`
173
- },
174
- {
175
- Sid: "AppPrefixList",
176
- Effect: "Allow",
177
- Action: "s3:ListBucket",
178
- Resource: `arn:aws:s3:::${bucket}`,
179
- Condition: { StringLike: { "s3:prefix": `${appPrefix}/*` } }
180
- },
181
- {
182
- Sid: "MarketplacePublish",
183
- Effect: "Allow",
184
- Action: ["aws-marketplace:StartChangeSet", "aws-marketplace:DescribeChangeSet", "aws-marketplace:DescribeEntity"],
185
- Resource: [
186
- marketplaceEntity,
187
- `arn:aws:aws-marketplace:us-east-1:${accountId}:AWSMarketplace/ChangeSet/*`
188
- ]
189
- },
190
- {
191
- Sid: "AppStacks",
192
- Effect: "Allow",
193
- Action: ["cloudformation:CreateStack", "cloudformation:UpdateStack", "cloudformation:DescribeStacks"],
194
- Resource: `arn:aws:cloudformation:*:${accountId}:stack/${appName}-*/*`
195
- },
196
- {
197
- Sid: "ReadTemplateSummary",
198
- Effect: "Allow",
199
- // launch calls GetTemplateSummary with a TemplateURL, which IAM
200
- // evaluates against * rather than a stack ARN
201
- Action: "cloudformation:GetTemplateSummary",
202
- Resource: "*"
203
- },
204
- {
205
- Sid: "LaunchStackServices",
206
- Effect: "Allow",
207
- // CloudFormation provisions stack resources with the caller's
208
- // credentials. Certs, DNS records, and user pools get
209
- // unpredictable ARNs, so these cannot be resource-scoped
210
- Action: ["acm:*", "route53:*", "cognito-idp:*"],
211
- Resource: "*"
212
- },
213
- {
214
- Sid: "AppStackIam",
215
- Effect: "Allow",
216
- Action: [
217
- "iam:CreateRole", "iam:DeleteRole", "iam:GetRole", "iam:UpdateRole", "iam:TagRole", "iam:UntagRole",
218
- "iam:PutRolePolicy", "iam:DeleteRolePolicy", "iam:GetRolePolicy", "iam:ListRolePolicies",
219
- "iam:AttachRolePolicy", "iam:DetachRolePolicy", "iam:ListAttachedRolePolicies", "iam:PassRole",
220
- "iam:CreateInstanceProfile", "iam:DeleteInstanceProfile", "iam:GetInstanceProfile",
221
- "iam:AddRoleToInstanceProfile", "iam:RemoveRoleFromInstanceProfile", "iam:TagInstanceProfile"
222
- ],
223
- // CFN-generated role/profile names start with the stack name,
224
- // and launch names every stack <app>-<title>
225
- Resource: [
226
- `arn:aws:iam::${accountId}:role/${appName}-*`,
227
- `arn:aws:iam::${accountId}:instance-profile/${appName}-*`
228
- ]
229
- },
230
- {
231
- Sid: "AppStackBuckets",
232
- Effect: "Allow",
233
- Action: "s3:*",
234
- Resource: [`arn:aws:s3:::${appName}-*`, `arn:aws:s3:::${appName}-*/*`]
235
- },
236
- {
237
- Sid: "AppStackTables",
238
- Effect: "Allow",
239
- Action: "dynamodb:*",
240
- Resource: `arn:aws:dynamodb:*:${accountId}:table/${appName}-*`
241
- },
242
- {
243
- Sid: "ReadRoleParam",
244
- Effect: "Allow",
245
- Action: "ssm:GetParameter",
246
- Resource: `arn:aws:ssm:us-east-1:${accountId}:parameter/mason-cdn-${bucket}`
247
- },
248
- {
249
- Sid: "AmiBuildAndLaunch",
250
- Effect: "Allow",
251
- Action: ["ec2:*", "autoscaling:*", "elasticloadbalancing:*"],
252
- Resource: "*"
253
- }
254
- ]
255
- };
256
- await iam.send(new PutRolePolicyCommand({
257
- RoleName: roleName,
258
- PolicyName: 'mason-app-access',
259
- PolicyDocument: JSON.stringify(policy)
260
- }));
261
-
262
- console.log(`Configured role ${roleName} (${roleArn})`);
263
- console.log(`Use in GitHub Actions with: role-to-assume: ${roleArn}`);
264
- return roleArn;
265
- }
266
-
267
- async function ensureOidcProvider(iam, accountId){
268
- const arn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
269
- try {
270
- await iam.send(new GetOpenIDConnectProviderCommand({ OpenIDConnectProviderArn: arn }));
271
- return arn;
272
- } catch (e){
273
- if (!/NoSuchEntity/.test(e)){ throw e }
274
- }
275
- await iam.send(new CreateOpenIDConnectProviderCommand({
276
- Url: 'https://token.actions.githubusercontent.com',
277
- ClientIDList: ['sts.amazonaws.com'],
278
- ThumbprintList: ['6938fd4d98bab03faadb97b34396831e3780aea1']
279
- }));
280
- console.log('Created GitHub OIDC identity provider');
281
- return arn;
282
- }
283
-
284
- async function getManifest(s3, bucket, appPrefix){
285
- const r = await s3.send(new GetObjectCommand({
286
- Bucket: bucket,
287
- Key: `${appPrefix}/manifest.json`
288
- }));
289
- return JSON.parse(await r.Body.transformToString());
290
- }
@@ -0,0 +1,271 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");
4
+ const { CloudFormationClient, GetTemplateSummaryCommand, DescribeTypeCommand } = require("@aws-sdk/client-cloudformation");
5
+ const { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand, UpdateAssumeRolePolicyCommand, GetOpenIDConnectProviderCommand, CreateOpenIDConnectProviderCommand } = require("@aws-sdk/client-iam");
6
+ const { STSClient, GetCallerIdentityCommand } = require("@aws-sdk/client-sts");
7
+ const OrgConfig = require('./helpers/org_config');
8
+ const Apps = require('./helpers/apps');
9
+
10
+ // Inline role policies cap at 10240 chars; collapse derived actions if we get close
11
+ const POLICY_CHAR_LIMIT = 10000;
12
+
13
+ // set-repo: create or update the app's GitHub Actions CI role. Sets the trust
14
+ // policy to the given repo and derives the stack template's required permissions
15
+ // from the CloudFormation registry. Run locally under elevated permissions -
16
+ // the CI role deliberately cannot modify itself
17
+ exports.main = async function(args){
18
+ if (!/^[\w.-]+\/[\w.-]+$/.test(args.repo)){ throw new Error('Invalid repo. Use org/repo format') }
19
+
20
+ const cfg = OrgConfig.read();
21
+ if (!cfg){ throw new Error('No org config found. Run set-org') }
22
+ const { bucket, prefix } = parseCdn(cfg.cdn);
23
+ const s3 = new S3Client({ region: cfg.region });
24
+
25
+ const appName = args.app.toLowerCase();
26
+ const app = await Apps.resolveApp(s3, bucket, prefix, appName);
27
+ if (!app){ throw new Error('No app named ' + args.app + '. Run new-app') }
28
+ const { appPrefix, manifest } = app;
29
+
30
+ // --- I GET TEMPLATE ---
31
+ let stackText = null;
32
+ if (args.stack){
33
+ const stackPath = path.resolve(args.stack);
34
+ if (!fs.existsSync(stackPath)){ throw new Error('Stack not found: ' + stackPath) }
35
+ stackText = fs.readFileSync(stackPath, 'utf-8');
36
+ } else {
37
+ const latest = [...(manifest.versions || [])]
38
+ .sort((a, b)=>{ return String(b.updated_on || '').localeCompare(String(a.updated_on || '')) })[0];
39
+ if (latest){
40
+ const vFolder = latest.version.split('.').slice(0, 2).join('.');
41
+ stackText = await getFileText(s3, bucket, `${appPrefix}/versions/${vFolder}/stack.yaml`);
42
+ }
43
+ }
44
+ if (!stackText){
45
+ console.log('No stack template found. Applying baseline permissions only. Pass -stack to derive template permissions');
46
+ }
47
+
48
+ // --- II DERIVE PERMISSIONS FROM THE CF REGISTRY ---
49
+ let derived = [];
50
+ if (stackText){
51
+ derived = await derivePermissions(cfg.region, stackText);
52
+ console.log(`Derived ${derived.length} actions from the template`);
53
+ }
54
+
55
+ // --- III ENSURE ROLE + TRUST ---
56
+ const iam = new IAMClient({ region: 'us-east-1' });
57
+ const sts = new STSClient({ region: 'us-east-1' });
58
+ const accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
59
+ const oidcArn = await ensureOidcProvider(iam, accountId);
60
+
61
+ const roleName = `mason-gha-${appName}`;
62
+ const trustPolicy = githubTrustPolicy(oidcArn, args.repo);
63
+ let roleArn;
64
+ try {
65
+ const existing = await iam.send(new GetRoleCommand({ RoleName: roleName }));
66
+ roleArn = existing.Role.Arn;
67
+ await iam.send(new UpdateAssumeRolePolicyCommand({
68
+ RoleName: roleName,
69
+ PolicyDocument: JSON.stringify(trustPolicy)
70
+ }));
71
+ console.log(`Updated ${roleName} trust to repo ${args.repo}`);
72
+ } catch (e){
73
+ if (!/NoSuchEntity/.test(e)){ throw e }
74
+ const created = await iam.send(new CreateRoleCommand({
75
+ RoleName: roleName,
76
+ AssumeRolePolicyDocument: JSON.stringify(trustPolicy),
77
+ Description: `Mason CI role for ${appName} (github.com/${args.repo})`
78
+ }));
79
+ roleArn = created.Role.Arn;
80
+ console.log(`Created role ${roleName} for repo ${args.repo}`);
81
+ }
82
+
83
+ // --- IV APPLY PERMISSIONS ---
84
+ if (!manifest.productId){
85
+ console.log('WARNING: No productId in manifest. Marketplace access is scoped to all products. Re-run set-repo after update-listing -pid to scope it');
86
+ }
87
+ const policy = buildPolicy(accountId, bucket, appPrefix, appName, manifest.productId, derived);
88
+ await iam.send(new PutRolePolicyCommand({
89
+ RoleName: roleName,
90
+ PolicyName: 'mason-app-access',
91
+ PolicyDocument: JSON.stringify(policy)
92
+ }));
93
+
94
+ console.log(`Configured role ${roleName} (${roleArn})`);
95
+ console.log(`Use in GitHub Actions with: role-to-assume: ${roleArn}`);
96
+ return true;
97
+ }
98
+
99
+
100
+ /////////////////////////////////////////
101
+ ////////////// FUNCS ////////////////////
102
+ ////////////////////////////////////////
103
+
104
+ function parseCdn(cdn){
105
+ const p = cdn.trim().replace(/^s3:\/\//i, '').replace(/\/+$/, '');
106
+ const parts = p.split('/');
107
+ return { bucket: parts[0], prefix: parts.slice(1).join('/') };
108
+ }
109
+
110
+ // Union the registry schema handler permissions for every resource type in the
111
+ // template, plus property-conditional extras the schemas cannot express
112
+ async function derivePermissions(region, stackText){
113
+ const cf = new CloudFormationClient({ region });
114
+ const summary = await cf.send(new GetTemplateSummaryCommand({ TemplateBody: stackText }));
115
+ const types = [...new Set(summary.ResourceTypes || [])];
116
+ console.log('Template resources: ' + types.join(', '));
117
+
118
+ const actions = new Set();
119
+ for (const t of types){
120
+ try {
121
+ const r = await cf.send(new DescribeTypeCommand({ Type: 'RESOURCE', TypeName: t }));
122
+ const schema = JSON.parse(r.Schema);
123
+ ['create', 'read', 'update', 'delete', 'list'].forEach(h=>{
124
+ const perms = schema.handlers && schema.handlers[h] && schema.handlers[h].permissions;
125
+ (perms || []).forEach(p=>{ actions.add(p) });
126
+ });
127
+ } catch (e){
128
+ console.log(`WARNING: No registry schema for ${t} (${e.name}). Add its permissions manually if needed`);
129
+ }
130
+ }
131
+
132
+ // Property-conditional permissions the registry schemas do not declare
133
+ if (/InstanceProfile|AWS::IAM::Role|RoleArn/i.test(stackText)){
134
+ actions.add('iam:PassRole');
135
+ actions.add('iam:GetRole');
136
+ }
137
+ if (/kms/i.test(stackText)){
138
+ ['kms:DescribeKey', 'kms:CreateGrant', 'kms:Decrypt', 'kms:GenerateDataKey'].forEach(a=>{ actions.add(a) });
139
+ }
140
+ return [...actions].sort();
141
+ }
142
+
143
+ function buildPolicy(accountId, bucket, appPrefix, appName, productId, derived){
144
+ const marketplaceEntity = productId
145
+ ? `arn:aws:aws-marketplace:us-east-1:${accountId}:AWSMarketplace/AmiProduct/${productId}`
146
+ : `arn:aws:aws-marketplace:us-east-1:${accountId}:AWSMarketplace/AmiProduct/*`;
147
+
148
+ const statements = [
149
+ {
150
+ Sid: "AppPrefixWrite",
151
+ Effect: "Allow",
152
+ Action: ["s3:GetObject", "s3:PutObject"],
153
+ Resource: `arn:aws:s3:::${bucket}/${appPrefix}/*`
154
+ },
155
+ {
156
+ Sid: "AppPrefixList",
157
+ Effect: "Allow",
158
+ Action: "s3:ListBucket",
159
+ Resource: `arn:aws:s3:::${bucket}`,
160
+ Condition: { StringLike: { "s3:prefix": `${appPrefix}/*` } }
161
+ },
162
+ {
163
+ Sid: "MarketplacePublish",
164
+ Effect: "Allow",
165
+ Action: ["aws-marketplace:StartChangeSet", "aws-marketplace:DescribeChangeSet", "aws-marketplace:DescribeEntity"],
166
+ Resource: [
167
+ marketplaceEntity,
168
+ `arn:aws:aws-marketplace:us-east-1:${accountId}:AWSMarketplace/ChangeSet/*`
169
+ ]
170
+ },
171
+ {
172
+ Sid: "AppStacks",
173
+ Effect: "Allow",
174
+ Action: ["cloudformation:CreateStack", "cloudformation:UpdateStack", "cloudformation:DescribeStacks"],
175
+ Resource: `arn:aws:cloudformation:*:${accountId}:stack/${appName}-*/*`
176
+ },
177
+ {
178
+ Sid: "ReadTemplateSummary",
179
+ Effect: "Allow",
180
+ // launch calls GetTemplateSummary with a TemplateURL, which IAM
181
+ // evaluates against * rather than a stack ARN
182
+ Action: "cloudformation:GetTemplateSummary",
183
+ Resource: "*"
184
+ },
185
+ {
186
+ Sid: "ReadRoleParam",
187
+ Effect: "Allow",
188
+ Action: "ssm:GetParameter",
189
+ Resource: `arn:aws:ssm:us-east-1:${accountId}:parameter/mason-cdn-${bucket}`
190
+ },
191
+ {
192
+ // The SSH AMI build creates untagged temporary resources and
193
+ // prunes images in every region - it cannot be scoped
194
+ Sid: "AmiBuild",
195
+ Effect: "Allow",
196
+ Action: "ec2:*",
197
+ Resource: "*"
198
+ }
199
+ ];
200
+
201
+ if (derived.length){
202
+ // ec2 is already covered wholesale by the build statement
203
+ let actions = derived.filter(a=>{ return !a.startsWith('ec2:') });
204
+ let policy = wrap(statements, actions);
205
+ if (JSON.stringify(policy).length > POLICY_CHAR_LIMIT){
206
+ // Collapse to per-service wildcards to stay under the inline policy size cap
207
+ actions = [...new Set(actions.map(a=>{ return a.split(':')[0] + ':*' }))].sort();
208
+ console.log('Derived actions exceed the policy size limit. Collapsed to service wildcards: ' + actions.join(', '));
209
+ policy = wrap(statements, actions);
210
+ }
211
+ return policy;
212
+ }
213
+ return { Version: "2012-10-17", Statement: statements };
214
+ }
215
+
216
+ function wrap(statements, derivedActions){
217
+ const all = [...statements];
218
+ if (derivedActions.length){
219
+ all.push({
220
+ Sid: "TemplateResources",
221
+ Effect: "Allow",
222
+ Action: derivedActions,
223
+ Resource: "*"
224
+ });
225
+ }
226
+ return { Version: "2012-10-17", Statement: all };
227
+ }
228
+
229
+ function githubTrustPolicy(oidcArn, repo){
230
+ return {
231
+ Version: "2012-10-17",
232
+ Statement: [
233
+ {
234
+ Effect: "Allow",
235
+ Principal: { Federated: oidcArn },
236
+ Action: "sts:AssumeRoleWithWebIdentity",
237
+ Condition: {
238
+ StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
239
+ StringLike: { "token.actions.githubusercontent.com:sub": `repo:${repo}:*` }
240
+ }
241
+ }
242
+ ]
243
+ };
244
+ }
245
+
246
+ async function ensureOidcProvider(iam, accountId){
247
+ const arn = `arn:aws:iam::${accountId}:oidc-provider/token.actions.githubusercontent.com`;
248
+ try {
249
+ await iam.send(new GetOpenIDConnectProviderCommand({ OpenIDConnectProviderArn: arn }));
250
+ return arn;
251
+ } catch (e){
252
+ if (!/NoSuchEntity/.test(e)){ throw e }
253
+ }
254
+ await iam.send(new CreateOpenIDConnectProviderCommand({
255
+ Url: 'https://token.actions.githubusercontent.com',
256
+ ClientIDList: ['sts.amazonaws.com'],
257
+ ThumbprintList: ['6938fd4d98bab03faadb97b34396831e3780aea1']
258
+ }));
259
+ console.log('Created GitHub OIDC identity provider');
260
+ return arn;
261
+ }
262
+
263
+ async function getFileText(s3, bucket, key){
264
+ try {
265
+ const r = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
266
+ return await r.Body.transformToString();
267
+ } catch (e){
268
+ if (/NoSuchKey|NotFound/.test(e)){ return null }
269
+ throw e;
270
+ }
271
+ }
@@ -1,12 +1,9 @@
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");
6
4
  const OrgConfig = require('./helpers/org_config');
7
5
  const Apps = require('./helpers/apps');
8
6
  const Mime = require('./helpers/mime');
9
- const { githubTrustPolicy } = require('./new_app');
10
7
 
11
8
  // Marketplace listing text files stored as top-level manifest attributes.
12
9
  // Names match the keys the AWS Marketplace Catalog API expects
@@ -30,32 +27,25 @@ exports.main = async function(args){
30
27
  if (!app){ throw new Error('No app named ' + args.app + '. Run new-app') }
31
28
  const { appPrefix, manifest } = app;
32
29
 
33
- if (!args.assets && !args.pid && !args.ec2 && !args.repo){
34
- console.log('Nothing to update. Pass -assets, -pid, -ec2, or -repo');
30
+ if (!args.assets && !args.pid && !args.ec2){
31
+ console.log('Nothing to update. Pass -assets, -pid, or -ec2');
35
32
  return true;
36
33
  }
37
34
 
38
35
  // Apply all manifest updates in one download/update/upload pass
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');
36
+ if (args.assets){
37
+ await applyAssets(s3, bucket, appPrefix, cfg.region, args.assets, manifest);
53
38
  }
54
-
55
- // Replace the CI role trust policy with one for the updated repo
56
- if (args.repo){
57
- await updateRepoTrust(appName, args.repo);
39
+ if (args.pid){
40
+ manifest.productId = args.pid;
41
+ console.log('Set productId');
42
+ }
43
+ if (args.ec2){
44
+ manifest.RecommendedInstanceType = args.ec2;
45
+ console.log('Set RecommendedInstanceType');
58
46
  }
47
+ await putManifest(s3, bucket, appPrefix, manifest);
48
+ console.log('Updated manifest');
59
49
  return true;
60
50
  }
61
51
 
@@ -125,28 +115,6 @@ async function applyAssets(s3, bucket, appPrefix, region, assetsPath, manifest){
125
115
  }
126
116
  }
127
117
 
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
-
150
118
  async function putManifest(s3, bucket, appPrefix, manifest){
151
119
  await s3.send(new PutObjectCommand({
152
120
  Bucket: bucket,
package/main.js CHANGED
@@ -27,7 +27,6 @@ const Commands = {
27
27
  exec: require('./commands/new_app').main,
28
28
  args: [
29
29
  {n: 'name', desc: 'Application name (letters only)', pattern: `^[A-Za-z]{2,20}$`, r: true},
30
- {n: 'repo', desc: 'GitHub repo (org/repo). Creates a CI role assumable by GitHub Actions from that repo', r: false},
31
30
  {n: 'noami', desc: 'Serverless app: update-app skips the AMI build and launch passes no AmiId', r: false},
32
31
  {n: 'private', desc: 'Store the app under a private _ folder (not publicly accessible)', r: false}
33
32
  ]
@@ -39,8 +38,16 @@ const Commands = {
39
38
  {n: 'app', desc: 'Name of existing app', pattern: `^[A-Za-z]{2,20}$`, r: true},
40
39
  {n: 'assets', desc: 'Path to folder with application assets', r: false},
41
40
  {n: 'pid', desc: 'AWS Product ID', 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}
41
+ {n: 'ec2', desc: 'Instance type (default r8g.medium)', r: false}
42
+ ]
43
+ },
44
+ 'set-repo': {
45
+ desc: 'Create/update the GitHub Actions CI role: repo trust + permissions derived from the stack template',
46
+ exec: require('./commands/set_repo').main,
47
+ args: [
48
+ {n: 'app', desc: 'Name of existing app', pattern: `^[A-Za-z]{2,20}$`, r: true},
49
+ {n: 'repo', desc: 'GitHub repo (org/repo) allowed to assume the role', r: true},
50
+ {n: 'stack', desc: 'Path to stack template for permission derivation (default: latest version stack in s3)', r: false}
44
51
  ]
45
52
  },
46
53
  'update-assets': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudmason2",
3
- "version": "1.7.9",
3
+ "version": "1.8.0",
4
4
  "description": "",
5
5
  "main": "main.js",
6
6
  "files": [