@fishawack/lab-env 2.1.0 → 2.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## Changelog
2
2
 
3
+ ### 2.2.0 (2022-08-10)
4
+ * [Feature] Can now skip diagnose in `fw diagnose`
5
+ * [Feature] Can now provision AWS environments using `fw provision`
6
+ * [Change] Now requires node > 16
7
+ * [Change] Auditted npm dependencies
8
+ * [Misc] Added test coverage for new AWS service
9
+
3
10
  ### 2.1.0 (2022-05-27)
4
11
  * [Change] Bumped core `0.0.21` to `0.1.0`
5
12
 
File without changes
@@ -0,0 +1,10 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const host = "ftp-fishawack.egnyte.com";
4
+ const creds = JSON.parse(fs.readFileSync(`${os.homedir()}/targets/.ftppass`))[host];
5
+
6
+ module.exports = {
7
+ opts: {encoding: 'utf8', stdio: process.argv.includes('--publish') ? 'pipe' : 'inherit'},
8
+ host,
9
+ creds
10
+ };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const expect = require('chai').expect;
4
+ const aws = require("../commands/create/services/aws/index.js");
5
+ var fetch;
6
+
7
+ describe('provision', async () => {
8
+ let config;
9
+ let repo = 'lab-env-test-suite';
10
+ let account = 'fishawack';
11
+
12
+ before(async () => {
13
+ fetch = (await import('node-fetch')).default;
14
+
15
+ config = await aws.static(repo, account);
16
+
17
+ await aws.s3.addFileToS3Bucket(repo, account, 'index.html', new TextEncoder().encode("test"));
18
+ });
19
+
20
+ it('Should provision s3 bucket', async () => {
21
+ expect((await fetch(config.url)).status).to.be.equal(200);
22
+ });
23
+
24
+ after(async () => {
25
+ await aws.s3.removeFileToS3Bucket(repo, account, 'index.html');
26
+
27
+ await aws.s3.removeS3Bucket(repo, account);
28
+
29
+ await aws.cloudfront.removeCloudFrontDistribution(config.cloudfront, account);
30
+
31
+ await aws.cloudfront.removeCloudFrontFunction(repo, account);
32
+ });
33
+ });
package/cli.js CHANGED
@@ -4,10 +4,6 @@ process.env.CWD = process.cwd();
4
4
 
5
5
  const _ = require('./globals.js');
6
6
 
7
- const updateNotifier = require('update-notifier');
8
- const pkg = require('./package.json');
9
- updateNotifier({pkg, updateCheckInterval: 0}).notify();
10
-
11
7
  const execSync = require('child_process').execSync;
12
8
 
13
9
  const yargs = require('yargs/yargs');
@@ -16,9 +12,13 @@ const { hideBin } = require('yargs/helpers');
16
12
  const args = hideBin(process.argv);
17
13
 
18
14
  // Stop here if docker process not running and command isn't version or origin which don't require docker
19
- if(!_.services && !(args[0] === 'origin' || args[0] === '--version')) return;
15
+ if(!_.services && !(args[0] === 'origin' || args[0] === '--version')) process.exit();
20
16
 
21
17
  (async () => {
18
+ const updateNotifier = (await import('update-notifier')).default;
19
+ const pkg = require('./package.json');
20
+ updateNotifier({pkg, updateCheckInterval: 0}).notify();
21
+
22
22
  process.env.REPO = _.repo;
23
23
 
24
24
  await _.ports.set();
@@ -59,7 +59,7 @@ if(!_.services && !(args[0] === 'origin' || args[0] === '--version')) return;
59
59
  ['build', 'config', 'down', 'mocha', 'rebuild', 'up', 'volumes', 'compose'].forEach(d => cli.command(...require(`./commands/docker/${d}.js`)));
60
60
 
61
61
  // Create commands
62
- ['new', 'diagnose', 'delete'].forEach(d => cli.command(...require(`./commands/create/cmds/${d}.js`)));
62
+ ['new', 'provision', 'deprovision', 'diagnose', 'delete'].forEach(d => cli.command(...require(`./commands/create/cmds/${d}.js`)));
63
63
 
64
64
  cli.demandCommand(1, '')
65
65
  .wrap(null)
@@ -0,0 +1,53 @@
1
+ const _ = require('../../../globals.js');
2
+ const inquirer = require('inquirer');
3
+ const aws = require('../services/aws/index.js');
4
+
5
+ module.exports = [
6
+ ['deprovision', 'deprov'],
7
+ false,
8
+ yargs => {
9
+ yargs.option('branch', {
10
+ alias: 'b',
11
+ describe: 'Branch to configure',
12
+ type: 'string'
13
+ });
14
+ },
15
+ async argv => {
16
+ let branch = argv.branch || _.branch;
17
+
18
+ let answer = await inquirer.prompt([
19
+ {
20
+ type: 'confirm',
21
+ name: 'check',
22
+ message: `Deprovisioning fw-auto-${_.repo}-${branch}, are you sure you want to continue?`,
23
+ default: 'Y'
24
+ }
25
+ ]);
26
+
27
+ if(!answer.check){
28
+ process.exit(1);
29
+ }
30
+
31
+ const answers = await inquirer.prompt([
32
+ {
33
+ type: 'input',
34
+ name: 'id',
35
+ message: 'What is the Id of the CloudFront distribution?',
36
+ validate: (input) => !!input.length
37
+ },
38
+ {
39
+ type: 'list',
40
+ name: 'client',
41
+ message: 'Which AWS account is this deployed too?',
42
+ choices: ['fishawack', 'abbvie', 'sanofigenzyme', 'gsk', 'janssen', 'astrazeneca', 'training'],
43
+ default: 'fishawack'
44
+ }
45
+ ]);
46
+
47
+ try { await aws.s3.removeS3Bucket(`fw-auto-${_.repo}-${branch}`, answers.client); } catch(e) {}
48
+
49
+ try { await aws.cloudfront.removeCloudFrontDistribution(answers.id, answers.client); } catch(e) {}
50
+
51
+ try { await aws.cloudfront.removeCloudFrontFunction(`fw-auto-${_.repo}-${branch}`, answers.client); } catch(e) {}
52
+ }
53
+ ];
@@ -57,9 +57,11 @@ module.exports = [
57
57
  while(!await test.bitbucket() || !await bitbucket.check()){
58
58
  await guide.bitbucket();
59
59
  }
60
-
61
- while(!await test.gitlab() || !await gitlab.check()){
62
- await guide.gitlab();
60
+
61
+ if(!await guide.gitlabSkip()) {
62
+ while(!await test.gitlab() || !await gitlab.check()){
63
+ await guide.gitlab();
64
+ }
63
65
  }
64
66
 
65
67
  const userRepoName = vars.misc.bitbucket.username.split('@')[0].replace(/\./, '-');
@@ -0,0 +1,120 @@
1
+ const _ = require('../../../globals.js');
2
+ const utilities = require('../libs/utilities');
3
+ const execSync = require('child_process').execSync;
4
+ const inquirer = require('inquirer');
5
+ const aws = require('../services/aws/index.js');
6
+ const generator = require('generate-password');
7
+
8
+ module.exports = [
9
+ ['provision', 'prov'],
10
+ 'Provisions the deployment target',
11
+ yargs => {
12
+ yargs.option('branch', {
13
+ alias: 'b',
14
+ describe: 'Branch to configure',
15
+ type: 'string'
16
+ });
17
+ },
18
+ async argv => {
19
+ let branch = argv.branch || _.branch;
20
+
21
+ let answer = await inquirer.prompt([
22
+ {
23
+ type: 'confirm',
24
+ name: 'check',
25
+ message: `Provisioning the ${utilities.colorize(branch, 'success')} branch, is this correct?`,
26
+ default: 'Y'
27
+ }
28
+ ]);
29
+
30
+ if(!answer.check){
31
+ process.exit(1);
32
+ }
33
+
34
+ const answers = await inquirer.prompt([
35
+ {
36
+ type: 'list',
37
+ name: 'stack',
38
+ message: 'What type of project are you deploying?',
39
+ choices: ['static'],
40
+ default: 'static'
41
+ },
42
+ {
43
+ type: 'list',
44
+ name: 'client',
45
+ message: 'Which AWS account should this be deployed too?',
46
+ choices: ['fishawack', 'abbvie', 'sanofigenzyme', 'gsk', 'janssen', 'astrazeneca', 'training'],
47
+ default: 'fishawack'
48
+ },
49
+ {
50
+ type: 'confirm',
51
+ name: 'protected',
52
+ message: 'Should the site be password protected?',
53
+ default: true
54
+ }
55
+ ]);
56
+
57
+ let credentials = [];
58
+
59
+ if(answers.protected){
60
+ let user = {
61
+ another: true
62
+ };
63
+
64
+ while(user.another){
65
+ user = await inquirer.prompt([
66
+ {
67
+ type: 'input',
68
+ name: 'username',
69
+ message: 'Username',
70
+ default: `${_.repo_safe}User${credentials.length ? `-${credentials.length}` : ''}`,
71
+ validate: (input) => !!input.length
72
+ },
73
+ {
74
+ type: 'input',
75
+ name: 'password',
76
+ message: 'Password (leave empty to generate)',
77
+ default: generator.generate({ length: 10, numbers: true }),
78
+ validate: (input) => !!input.length
79
+ },
80
+ {
81
+ type: 'confirm',
82
+ name: 'another',
83
+ message: 'Add another user?',
84
+ default: false
85
+ }
86
+ ]);
87
+
88
+ credentials.push({username: user.username, password: user.password});
89
+ }
90
+ }
91
+
92
+ let config = {};
93
+ let infastructure;
94
+
95
+ try{
96
+ infastructure = await aws.static(`fw-auto-${_.repo}-${branch}`, answers.client, [{Key: 'repository', Value: _.repo}, {Key: 'environment', Value: branch}], credentials);
97
+ } catch(e){
98
+ console.log(e.message);
99
+ process.exit(1);
100
+ }
101
+
102
+ config[branch] = {
103
+ deploy: {
104
+ "url": infastructure.url,
105
+ "location": infastructure.bucket.slice(1), // Remove / from start
106
+ "aws-s3": answers.client,
107
+ "aws-cloudfront": infastructure.cloudfront
108
+ }
109
+ };
110
+
111
+ if(credentials.length){
112
+ config[branch].deploy.users = credentials;
113
+ }
114
+
115
+ let stringify = JSON.stringify(config, null, 4);
116
+ let output = stringify.substring(1, stringify.length-1).trim();
117
+ execSync(`printf '${output}' | pbcopy`);
118
+ console.log(utilities.colorize(`\n${output}\n\n(copied to clipboard)`, 'title'));
119
+ }
120
+ ];
@@ -0,0 +1,77 @@
1
+ function handler(event) {
2
+ // Redirect if www to non-www
3
+ if (event.request.headers.host.value.includes('www.')) {
4
+ var query = '';
5
+ var index = 0;
6
+
7
+ for(var key in event.request.querystring){
8
+ query += `${index ? '&' : '?'}${key}=${event.request.querystring[key].value}`;
9
+ index++;
10
+ }
11
+
12
+ var response = {
13
+ statusCode: 301,
14
+ statusDescription: 'Moved Permanently',
15
+ headers: { "location": { "value": `${event.request.headers.host.value.split('www.')[1]}${event.request.uri}${query}` } }
16
+ };
17
+
18
+ return response;
19
+ }
20
+
21
+ // Redirect if no trailing slash
22
+ if(!event.request.uri.includes('.') && !event.request.uri.endsWith('/')){
23
+ var query = '';
24
+ var index = 0;
25
+
26
+ for(var key in event.request.querystring){
27
+ query += `${index ? '&' : '?'}${key}=${event.request.querystring[key].value}`;
28
+ index++;
29
+ }
30
+
31
+ return {
32
+ statusCode: 301,
33
+ statusDescription: 'Moved Permanently',
34
+ headers: { "location": { "value": `${event.request.uri}/${query}` } }
35
+ };
36
+ }
37
+
38
+ var authHeaders = event.request.headers.authorization;
39
+
40
+ // The Base64-encoded Auth string that should be present.
41
+ // It is an encoding of `Basic base64([username]:[password])`
42
+ var expected = <%= credentials %>;
43
+
44
+ // If an Authorization header is supplied and it's an exact match, pass the
45
+ // request on through to CF/the origin without any modification.
46
+ if(authHeaders && expected.find(d => d === authHeaders.value)) {
47
+ // Rewrite url to append index.html if not present
48
+ var request = event.request;
49
+ var uri = request.uri;
50
+
51
+ // Check whether the URI is missing a file name.
52
+ if(uri.endsWith('/')) {
53
+ request.uri += 'index.html';
54
+ }
55
+ // Check whether the URI is missing a file extension.
56
+ else if(!uri.includes('.')) {
57
+ request.uri += '/index.html';
58
+ }
59
+
60
+ return request;
61
+ }
62
+
63
+ // But if we get here, we must either be missing the auth header or the
64
+ // credentials failed to match what we expected.
65
+ // Request the browser present the Basic Auth dialog.
66
+ var response = {
67
+ statusCode: 401,
68
+ statusDescription: 'Unauthorized',
69
+ headers: {
70
+ "www-authenticate": {
71
+ value: 'Basic'
72
+ }
73
+ },
74
+ };
75
+
76
+ return response;
77
+ }
@@ -0,0 +1,51 @@
1
+ function handler(event) {
2
+ // Redirect if www to non-www
3
+ if (event.request.headers.host.value.includes('www.')) {
4
+ var query = '';
5
+ var index = 0;
6
+
7
+ for(var key in event.request.querystring){
8
+ query += `${index ? '&' : '?'}${key}=${event.request.querystring[key].value}`;
9
+ index++;
10
+ }
11
+
12
+ var response = {
13
+ statusCode: 301,
14
+ statusDescription: 'Moved Permanently',
15
+ headers: { "location": { "value": `${event.request.headers.host.value.split('www.')[1]}${event.request.uri}${query}` } }
16
+ };
17
+
18
+ return response;
19
+ }
20
+
21
+ // Redirect if no trailing slash
22
+ if(!event.request.uri.includes('.') && !event.request.uri.endsWith('/')){
23
+ var query = '';
24
+ var index = 0;
25
+
26
+ for(var key in event.request.querystring){
27
+ query += `${index ? '&' : '?'}${key}=${event.request.querystring[key].value}`;
28
+ index++;
29
+ }
30
+
31
+ return {
32
+ statusCode: 301,
33
+ statusDescription: 'Moved Permanently',
34
+ headers: { "location": { "value": `${event.request.uri}/${query}` } }
35
+ };
36
+ }
37
+
38
+ var request = event.request;
39
+ var uri = request.uri;
40
+
41
+ // Check whether the URI is missing a file name.
42
+ if(uri.endsWith('/')) {
43
+ request.uri += 'index.html';
44
+ }
45
+ // Check whether the URI is missing a file extension.
46
+ else if(!uri.includes('.')) {
47
+ request.uri += '/index.html';
48
+ }
49
+
50
+ return request;
51
+ }
@@ -23,9 +23,12 @@ const colorize = module.exports.colorize = (str, type) => {
23
23
  }
24
24
  };
25
25
 
26
+ let latestSpinner = null;
27
+
26
28
  module.exports.Spinner = class Spinner {
27
- constructor(startMessage) {
28
- this.ora = ora(colorize(startMessage, 'info')).start();
29
+ constructor(startMessage, simple = false) {
30
+ this.ora = ora(simple ? startMessage : colorize(startMessage, 'info')).start();
31
+ latestSpinner = this;
29
32
  }
30
33
 
31
34
  update(message, status) {
@@ -37,6 +40,50 @@ module.exports.Spinner = class Spinner {
37
40
  this.ora.succeed(colorize(message, status || 'success'));
38
41
  }
39
42
  }
43
+
44
+ action(message, action, success, failure) {
45
+ return new Promise(async (resolve, reject) => {
46
+ let instance = new this.constructor(message);
47
+ let res;
48
+
49
+ try {
50
+ res = await action();
51
+
52
+ instance.update(success);
53
+ } catch (e){
54
+ instance.update(failure, 'fail');
55
+ reject(e);
56
+ }
57
+
58
+ resolve(res);
59
+ });
60
+ }
61
+
62
+ simple(message, action) {
63
+ return new Promise(async (resolve, reject) => {
64
+ let instance = new this.constructor(message, true);
65
+ let res;
66
+
67
+ try {
68
+ res = await action();
69
+
70
+ instance.ora.succeed();
71
+ } catch (e){
72
+ instance.ora.fail();
73
+ reject(e);
74
+ }
75
+
76
+ resolve(res);
77
+ });
78
+ }
79
+
80
+ ping(){
81
+ latestSpinner.ora.color = latestSpinner.ora.color === 'cyan' ? 'magenta' : 'cyan';
82
+ let message = ' - check: ';
83
+ let split = latestSpinner.ora.text.split(message);
84
+ let iteration = split[1] && +split[1].split(':')[0] || 0;
85
+ latestSpinner.ora.text = `${split[0]}${message}${iteration+1}`;
86
+ }
40
87
  };
41
88
 
42
89
  module.exports.encode = (username, password) => {
@@ -0,0 +1,289 @@
1
+ const { CloudFrontClient, CreateDistributionWithTagsCommand, CreateCloudFrontOriginAccessIdentityCommand, DeleteDistributionCommand , DeleteCloudFrontOriginAccessIdentityCommand, GetDistributionCommand, UpdateDistributionCommand, GetCloudFrontOriginAccessIdentityCommand, CreateFunctionCommand, GetFunctionCommand, UpdateFunctionCommand, PublishFunctionCommand, DeleteFunctionCommand, DescribeFunctionCommand } = require("@aws-sdk/client-cloudfront");
2
+ const fs = require('fs');
3
+ const { Spinner } = require('../../libs/utilities');
4
+
5
+ let region = `us-east-1`;
6
+
7
+ const client = new CloudFrontClient({ region });
8
+
9
+ module.exports.createCloudFrontDistribution = async (name, account, tags = []) => {
10
+ process.env.AWS_PROFILE = account;
11
+
12
+ let OAI = await Spinner.prototype.simple(`Creating CloudFront OAI`, () => {
13
+ return client.send(
14
+ new CreateCloudFrontOriginAccessIdentityCommand({
15
+ CloudFrontOriginAccessIdentityConfig: {
16
+ CallerReference: name,
17
+ Comment: `lab-env provisioned CloudFront OAI for s3 bucket ${name}`,
18
+ }
19
+ })
20
+ )
21
+ });
22
+
23
+ let res;
24
+
25
+ try {
26
+ res = await Spinner.prototype.simple(`Creating CloudFront distribution with CloudFront OAI and tags`, () => {
27
+ return client.send(
28
+ new CreateDistributionWithTagsCommand({
29
+ DistributionConfigWithTags: {
30
+ DistributionConfig: {
31
+ Enabled: true,
32
+ CallerReference: name,
33
+ Comment: `lab-env provisioned CloudFront distribution for project ${name}`,
34
+ CustomErrorResponses: {
35
+ Items: [
36
+ {
37
+ ErrorCachingMinTTL: 0,
38
+ ErrorCode: 403,
39
+ ResponseCode: 200,
40
+ ResponsePagePath: '/index.html'
41
+ }
42
+ ],
43
+ Quantity: 1
44
+ },
45
+ DefaultCacheBehavior: {
46
+ Compress: true,
47
+ TargetOriginId: `${name}.s3.${region}.amazonaws.com`,
48
+ ViewerProtocolPolicy: 'redirect-to-https',
49
+ CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6' // Built in, Managed AWS Policy - Cache Optimized
50
+ },
51
+ Origins: {
52
+ Items: [
53
+ {
54
+ DomainName: `${name}.s3.${region}.amazonaws.com`,
55
+ Id: `${name}.s3.${region}.amazonaws.com`,
56
+ S3OriginConfig: {
57
+ OriginAccessIdentity: `origin-access-identity/cloudfront/${OAI.CloudFrontOriginAccessIdentity.Id}`
58
+ }
59
+ }
60
+ ],
61
+ Quantity: 1
62
+ }
63
+ },
64
+ Tags: {
65
+ Items: [{Key: 'client', Value: account}].concat(tags)
66
+ }
67
+ }
68
+ })
69
+ )
70
+ });
71
+
72
+ await Spinner.prototype.simple(`Waiting for CloudFront distribution to deploy`, () => {
73
+ return module.exports.waitForCloudFrontDistribution(res.Distribution.Id, account);
74
+ });
75
+ } catch(e){
76
+ let Id = e.message.split(' ')[e.message.split(' ').length - 1];
77
+
78
+ res = await Spinner.prototype.simple(`Retrieving the already existing CloudFront distribution`, () => {
79
+ return client.send(
80
+ new GetDistributionCommand({ Id })
81
+ );
82
+ });
83
+ }
84
+
85
+ return res;
86
+ }
87
+
88
+ module.exports.removeCloudFrontDistribution = async (Id, account) => {
89
+ process.env.AWS_PROFILE = account;
90
+
91
+ let res = await Spinner.prototype.simple(`Retrieving the CloudFront distribution ${Id}`, () => {
92
+ return client.send(
93
+ new GetDistributionCommand({ Id })
94
+ );
95
+ });
96
+
97
+ let OAI = res.Distribution.DistributionConfig.Origins.Items[0].S3OriginConfig.OriginAccessIdentity.split('origin-access-identity/cloudfront/')[1];
98
+
99
+ res.Distribution.DistributionConfig.Enabled = false;
100
+
101
+ res = await Spinner.prototype.simple(`Disabling the CloudFront distribution`, () => {
102
+ return client.send(
103
+ new UpdateDistributionCommand({ DistributionConfig: res.Distribution.DistributionConfig, Id, IfMatch: res.ETag })
104
+ );
105
+ });
106
+
107
+ await Spinner.prototype.simple(`Waiting for CloudFront distribution to deploy`, () => {
108
+ return module.exports.waitForCloudFrontDistribution(res.Distribution.Id, account);
109
+ });
110
+
111
+ res = await Spinner.prototype.simple(`Deleting the CloudFront distribution`, () => {
112
+ return client.send(
113
+ new DeleteDistributionCommand({ Id, IfMatch: res.ETag })
114
+ );
115
+ });
116
+
117
+ res = await Spinner.prototype.simple(`Retrieving the CloudFront OAI`, () => {
118
+ return client.send(
119
+ new GetCloudFrontOriginAccessIdentityCommand({ Id: OAI })
120
+ );
121
+ });
122
+
123
+ res = await Spinner.prototype.simple(`Deleting the CloudFront OAI`, () => {
124
+ return client.send(
125
+ new DeleteCloudFrontOriginAccessIdentityCommand({ Id: OAI, IfMatch: res.ETag })
126
+ );
127
+ });
128
+ }
129
+
130
+ module.exports.waitForCloudFrontDistribution = async (Id, account) => {
131
+ process.env.AWS_PROFILE = account;
132
+
133
+ let status;
134
+
135
+ do{
136
+ await new Promise((resolve) => setTimeout(() => resolve(), 5000));
137
+
138
+ await Spinner.prototype.ping();
139
+
140
+ let check = await client.send(
141
+ new GetDistributionCommand({ Id })
142
+ );
143
+
144
+ status = check.Distribution.Status;
145
+ } while(status !== 'Deployed')
146
+ }
147
+
148
+ module.exports.createCloudFrontFunction = async (name, account, fn, config) => {
149
+ process.env.AWS_PROFILE = account;
150
+
151
+ let FunctionConfig = {
152
+ Comment: `lab-env provisioned cloudfront function for project ${name} using code snippet ${fn}.js`,
153
+ Runtime: `cloudfront-js-1.0`
154
+ };
155
+
156
+ let res;
157
+
158
+ try{
159
+ res = await Spinner.prototype.simple(`Creating CloudFront function`, () => {
160
+ return client.send(
161
+ new CreateFunctionCommand({
162
+ Name: name,
163
+ FunctionCode: new TextEncoder().encode(" "),
164
+ FunctionConfig
165
+ })
166
+ );
167
+ });
168
+ } catch (e){
169
+ res = await Spinner.prototype.simple(`Retrieving the already existing CloudFront function`, () => {
170
+ return client.send(
171
+ new GetFunctionCommand({
172
+ Name: name
173
+ })
174
+ );
175
+ });
176
+ }
177
+
178
+ let processedFn = fs.readFileSync(`${__dirname}/../../libs/${fn}.js`).toString();
179
+ processedFn = processedFn.replace(/<%=.*%>/g, (el) => JSON.stringify(config[el.slice(3, el.length - 2).trim()], null, 4));
180
+
181
+ res = await Spinner.prototype.simple(`Updating CloudFront function with ${fn}.js code`, () => {
182
+ return client.send(
183
+ new UpdateFunctionCommand({
184
+ Name: name,
185
+ FunctionCode: new TextEncoder().encode(processedFn),
186
+ FunctionConfig,
187
+ IfMatch: res.ETag
188
+ })
189
+ );
190
+ });
191
+
192
+ res = await Spinner.prototype.simple(`Publishing CloudFront function`, () => {
193
+ return client.send(
194
+ new PublishFunctionCommand({
195
+ Name: name,
196
+ IfMatch: res.ETag
197
+ })
198
+ );
199
+ });
200
+
201
+ return res;
202
+ }
203
+
204
+ module.exports.removeCloudFrontFunction = async (name, account) => {
205
+ process.env.AWS_PROFILE = account;
206
+
207
+ let res = await Spinner.prototype.simple(`Retrieving CloudFront function`, () => {
208
+ return client.send(
209
+ new GetFunctionCommand({
210
+ Name: name
211
+ })
212
+ );
213
+ });
214
+
215
+ res = await Spinner.prototype.simple(`Deleting CloudFront function`, () => {
216
+ return client.send(
217
+ new DeleteFunctionCommand({
218
+ Name: name,
219
+ IfMatch: res.ETag
220
+ })
221
+ );
222
+ });
223
+
224
+ return res;
225
+ }
226
+
227
+ module.exports.setCloudFrontFunctionAssociation = async (Id, account) => {
228
+ process.env.AWS_PROFILE = account;
229
+
230
+ let res = await Spinner.prototype.simple(`Retrieving CloudFront distribution`, () => {
231
+ return client.send(
232
+ new GetDistributionCommand({ Id })
233
+ );
234
+ });
235
+
236
+ let { FunctionSummary: { FunctionMetadata: { FunctionARN } } } = await Spinner.prototype.simple(`Retrieving CloudFront function`, () => {
237
+ return client.send(
238
+ new DescribeFunctionCommand({
239
+ Name: res.Distribution.DistributionConfig.CallerReference,
240
+ Stage: 'LIVE'
241
+ })
242
+ );
243
+ });
244
+
245
+ res.Distribution.DistributionConfig.DefaultCacheBehavior.FunctionAssociations = {
246
+ Items: [
247
+ {
248
+ EventType: 'viewer-request',
249
+ FunctionARN: FunctionARN
250
+ }
251
+ ],
252
+ Quantity: 1
253
+ };
254
+
255
+ res = await Spinner.prototype.simple(`Adding CloudFront function to CloudFront distribution`, () => {
256
+ return client.send(
257
+ new UpdateDistributionCommand({ DistributionConfig: res.Distribution.DistributionConfig, Id, IfMatch: res.ETag })
258
+ );
259
+ });
260
+
261
+ await Spinner.prototype.simple(`Waiting for CloudFront distribution to deploy`, () => {
262
+ return module.exports.waitForCloudFrontDistribution(res.Distribution.Id, account);
263
+ });
264
+ };
265
+
266
+ module.exports.removeCloudFrontFunctionAssociation = async (Id, account) => {
267
+ process.env.AWS_PROFILE = account;
268
+
269
+ let res = await Spinner.prototype.simple(`Retrieving CloudFront distribution`, () => {
270
+ return client.send(
271
+ new GetDistributionCommand({ Id })
272
+ );
273
+ });
274
+
275
+ res.Distribution.DistributionConfig.DefaultCacheBehavior.FunctionAssociations = {
276
+ Items: [],
277
+ Quantity: 0
278
+ };
279
+
280
+ res = await Spinner.prototype.simple(`Removing CloudFront function from CloudFront distribution`, () => {
281
+ return client.send(
282
+ new UpdateDistributionCommand({ DistributionConfig: res.Distribution.DistributionConfig, Id, IfMatch: res.ETag })
283
+ );
284
+ });
285
+
286
+ await Spinner.prototype.simple(`Waiting for CloudFront distribution to deploy`, () => {
287
+ return module.exports.waitForCloudFrontDistribution(res.Distribution.Id, account);
288
+ });
289
+ };
@@ -0,0 +1,22 @@
1
+ module.exports.s3 = require("./s3.js");
2
+ module.exports.cloudfront = require("./cloudfront.js");
3
+
4
+ module.exports.static = async (name, account, tags = [], credentials = []) => {
5
+ let s3 = await module.exports.s3.createS3Bucket(name, account, tags);
6
+
7
+ let cloudfront = await module.exports.cloudfront.createCloudFrontDistribution(name, account, tags);
8
+
9
+ await module.exports.s3.setS3BucketPolicy(name, account, cloudfront.Distribution.DistributionConfig.Origins.Items[0].S3OriginConfig.OriginAccessIdentity.split('origin-access-identity/cloudfront/')[1]);
10
+
11
+ await module.exports.cloudfront.createCloudFrontFunction(name, account, credentials.length ? 'aws-cloudfront-auth' : 'aws-cloudfront-simple', {credentials: credentials.map(d => `Basic ${Buffer.from(`${d.username}:${d.password}`).toString('base64')}`)});
12
+
13
+ await module.exports.cloudfront.setCloudFrontFunctionAssociation(cloudfront.Distribution.Id, account);
14
+
15
+ let config = {
16
+ "bucket": s3.Location,
17
+ "url": `https://${cloudfront.Distribution.DomainName}`,
18
+ "cloudfront": cloudfront.Distribution.Id,
19
+ };
20
+
21
+ return config;
22
+ }
@@ -0,0 +1,100 @@
1
+ const { S3Client, CreateBucketCommand, DeleteBucketCommand, PutPublicAccessBlockCommand, PutBucketTaggingCommand, PutBucketPolicyCommand, PutObjectCommand, DeleteObjectCommand } = require("@aws-sdk/client-s3");
2
+ const { Spinner } = require('../../libs/utilities');
3
+
4
+ let region = `us-east-1`;
5
+
6
+ const client = new S3Client({ region });
7
+
8
+ module.exports.createS3Bucket = async (bucket, account, tags = []) => {
9
+ process.env.AWS_PROFILE = account;
10
+
11
+ let res = await Spinner.prototype.simple(`Creating s3 bucket ${bucket}`, () => {
12
+ return client.send(
13
+ new CreateBucketCommand({ Bucket: bucket })
14
+ );
15
+ });
16
+
17
+ await Spinner.prototype.simple(`Blocking all public access to s3 bucket`, () => {
18
+ return client.send(
19
+ new PutPublicAccessBlockCommand({ Bucket: bucket, PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true } })
20
+ );
21
+ });
22
+
23
+ await Spinner.prototype.simple(`Adding tags to s3 bucket`, () => {
24
+ return client.send(
25
+ new PutBucketTaggingCommand({ Bucket: bucket, Tagging: {TagSet: [{Key: 'client', Value: account}].concat(tags)} })
26
+ );
27
+ });
28
+
29
+ return res;
30
+ }
31
+
32
+ module.exports.removeS3Bucket = async (bucket, account) => {
33
+ process.env.AWS_PROFILE = account;
34
+
35
+ await Spinner.prototype.simple(`Removing s3 bucket ${bucket}`, () => {
36
+ return client.send(
37
+ new DeleteBucketCommand({ Bucket: bucket })
38
+ );
39
+ });
40
+ }
41
+
42
+ module.exports.setS3BucketPolicy = async (bucket, account, OAI) => {
43
+ process.env.AWS_PROFILE = account;
44
+
45
+ let res = await Spinner.prototype.simple(`Updating s3 bucket policy`, () => {
46
+ return client.send(
47
+ new PutBucketPolicyCommand({
48
+ Bucket: bucket,
49
+ Policy: JSON.stringify({
50
+ "Version": "2008-10-17",
51
+ "Id": "PolicyForCloudFrontPrivateContent",
52
+ "Statement": [
53
+ {
54
+ "Sid": "1",
55
+ "Effect": "Allow",
56
+ "Principal": {
57
+ "AWS": `arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OAI}`
58
+ },
59
+ "Action": "s3:GetObject",
60
+ "Resource": `arn:aws:s3:::${bucket}/*`
61
+ }
62
+ ]
63
+ })
64
+ })
65
+ );
66
+ });
67
+
68
+ return res;
69
+ }
70
+
71
+ module.exports.addFileToS3Bucket = async (bucket, account, filepath, file) => {
72
+ process.env.AWS_PROFILE = account;
73
+
74
+ let res = await Spinner.prototype.simple(`Adding file to s3 bucket`, () => {
75
+ return client.send(
76
+ new PutObjectCommand({
77
+ Bucket: bucket,
78
+ Body: file,
79
+ Key: filepath
80
+ })
81
+ );
82
+ });
83
+
84
+ return res;
85
+ };
86
+
87
+ module.exports.removeFileToS3Bucket = async (bucket, account, filepath) => {
88
+ process.env.AWS_PROFILE = account;
89
+
90
+ let res = await Spinner.prototype.simple(`Removing file from s3 bucket`, () => {
91
+ return client.send(
92
+ new DeleteObjectCommand({
93
+ Bucket: bucket,
94
+ Key: filepath
95
+ })
96
+ );
97
+ });
98
+
99
+ return res;
100
+ }
@@ -194,6 +194,19 @@ module.exports.preset = async () => {
194
194
  return inputs.preset;
195
195
  }
196
196
 
197
+ module.exports.gitlabSkip = async () => {
198
+ let inputs = await inquirer.prompt([
199
+ {
200
+ type: 'confirm',
201
+ name: 'confirm',
202
+ message: 'Do you want to test gitlab setup? (VPN required)',
203
+ default: true
204
+ }
205
+ ]);
206
+
207
+ return !inputs.confirm;
208
+ }
209
+
197
210
  module.exports.config = async () => {
198
211
  let inputs = await inquirer.prompt([
199
212
  {
package/globals.js CHANGED
@@ -20,8 +20,15 @@ var run;
20
20
  var exec;
21
21
  var running;
22
22
  var services;
23
+ var branch;
23
24
  var opts = {encoding: 'utf8', stdio: 'inherit', shell: '/bin/bash'};
24
25
 
26
+ try{
27
+ branch = process.env.BRANCH || process.env.CI_COMMIT_REF_NAME || require('git-branch').sync();
28
+ } catch(e){
29
+ branch = 'unknown';
30
+ }
31
+
25
32
  try{
26
33
  repo = execSync('basename "$(git rev-parse --show-toplevel)"', {encoding: 'utf8', stdio: 'pipe'}).trim() || path.basename(process.cwd());
27
34
  } catch(e){
@@ -113,6 +120,7 @@ module.exports = {
113
120
  services,
114
121
  platform,
115
122
  repo,
123
+ branch,
116
124
  repo_safe: repo.replace(/\./g, ''),
117
125
  pkg,
118
126
  docker,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {
7
- "test": "rm -rf _Test/_fixtures && mkdir _Test/_fixtures && mocha _Test/*.js --timeout 120s --bail",
7
+ "test": "rm -rf _Test/_fixtures/boilerplate*; mocha _Test/*.js --timeout 1200s --bail",
8
8
  "preversion": "npm test",
9
9
  "postversion": "git push && git push --tags && npm publish",
10
10
  "postpublish": "git checkout development && git merge master && git push"
@@ -16,23 +16,33 @@
16
16
  "author": "",
17
17
  "license": "ISC",
18
18
  "homepage": "https://bitbucket.org/fishawackdigital/lab-env#readme",
19
+ "type": "commonjs",
19
20
  "bin": {
20
21
  "lab-env": "./cli.js",
21
22
  "fw": "./cli.js"
22
23
  },
23
24
  "dependencies": {
24
- "axios": "0.21.1",
25
+ "@aws-sdk/client-cloudfront": "^3.141.0",
26
+ "@aws-sdk/client-s3": "^3.141.0",
27
+ "axios": "^0.21.4",
25
28
  "chalk": "4.1.0",
29
+ "generate-password": "^1.7.0",
26
30
  "get-port": "5.1.1",
31
+ "git-branch": "^2.0.1",
27
32
  "glob": "7.1.7",
28
33
  "inquirer": "8.1.2",
29
34
  "ora": "5.4.1",
30
35
  "semver": "7.3.4",
31
- "update-notifier": "5.1.0",
36
+ "update-notifier": "^6.0.2",
32
37
  "yargs": "16.2.0"
33
38
  },
34
39
  "devDependencies": {
35
40
  "chai": "4.3.4",
36
- "mocha": "9.1.2"
41
+ "mocha": "^9.2.2",
42
+ "node-fetch": "^3.2.10"
43
+ },
44
+ "engines": {
45
+ "npm": ">=8",
46
+ "node": ">=18"
37
47
  }
38
48
  }