Commit 43609106 authored by Spencer Williams's avatar Spencer Williams
Browse files

Merge branch 'develop' into 'master'

Develop

See merge request cascade/rigado-node-hello-world-frontend!1
parents b6a6a679 745bfb39
REACT_APP_DEBUG=false
REACT_APP_MQTT_TOPIC=/gateway/localhost.localdomain/sensors
REACT_APP_MQTT_URL=ws://broker.mqttdashboard.com:8000/mqtt
\ No newline at end of file
{
"extends": [
"react-app"
]
}
\ No newline at end of file
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.idea/
.vagrant/
\ No newline at end of file
{
"processors": [
"stylelint-processor-styled-components"
],
"extends": [
"stylelint-config-recommended",
"stylelint-config-styled-components"
]
}
\ No newline at end of file
Get the project started
\ No newline at end of file
Cascade Hello World (Node.js) Frontend Application
====
See the tutorial: https://deviceops.rigado.com/projects/cascade/en/latest/prototyping/custom-cloud.html
### Deploy to AWS
1. [Configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) AWS CLI SDK.
2. Install [NodeJs >= 8](https://nodejs.org/en/) and [Yarn >= 1](https://yarnpkg.com/en/docs/install)
3. Run in console.
```bash
yarn install
yarn run deploy --key <Name of an existing EC2 KeyPair>
```
4. Wait for CloudFormation Stack creation.
```bash
yarn run publish
```
To see deploy information execute.
```bash
yarn run describe-stacks
```
### Run in develop mode
Run in console.
```bash
yarn run install
yarn run start
```
### Command API
`yarn run deploy`
Creates AWS CloudFormation Stack.
Options
- *stackName* - AWS CloudFormation Stack Name default rigado-node-hello-world-frontend
- *region* - AWS region **default** *us-east-1*
- *key* - Name of an existing EC2 KeyPair to enable SSH access to the instances **required**
- *instanceType* - EC2 Instance Type **default** *t2.micro*
`yarn run publish`
Sync Web to s3.
Options
- *stackName* - AWS CloudFormation Stack Name default rigado-node-hello-world-frontend
- *updateEnv* - Updates .env file by stack data **default** *true*
`yarn describe-stacks`
Returns the stack description.
Options
- *stackName* - AWS CloudFormation Stack Name **default** *rigado-node-hello-world-frontend*
- *region* - AWS region **default** *us-east-1*
`yarn delete`
Deletes stack. Once the call completes successfully, stack deletion starts.
Options
- *stackName* - AWS CloudFormation Stack Name **default** *rigado-node-hello-world-frontend*
- *region* - AWS region **default** *us-east-1*
# -*- mode: ruby -*-
# vi: set ft=ruby :
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box = "ubuntu/trusty64"
config.vm.hostname = "dev"
# Disable automatic box update checking. If you disable this, then
# boxes will only be checked for updates when the user runs
# `vagrant box outdated`. This is not recommended.
# config.vm.box_check_update = false
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine. In the example below,
# accessing "localhost:8080" will access port 80 on the guest machine.
# NOTE: This will enable public access to the opened port
# config.vm.network "forwarded_port", guest: 80, host: 8080
# Create a forwarded port mapping which allows access to a specific port
# within the machine from a port on the host machine and only allow access
# via 127.0.0.1 to disable public access
# config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"
config.vm.network "forwarded_port", guest: 3000, host: 3001
# Create a private network, which allows host-only access to the machine
# using a specific IP.
config.vm.network "private_network", ip: "192.168.33.10"
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
# config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
# config.vm.synced_folder "../data", "/vagrant_data"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
#
# View the documentation for the provider you are using for more
# information on available options.
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# apt-get update
# apt-get install -y apache2
# SHELL
config.vm.provision "shell", inline: <<-SHELL
sudo apt-get update
SHELL
config.vm.provision "install-nodejs",
type: "shell",
privileged: true,
path: "bin/install_nodejs.sh"
config.vm.provision "install-yarn",
type: "shell",
privileged: true,
path: "bin/install_yarn.sh"
config.vm.provision "shell", inline: <<-SHELL
echo "Change initial path of vagrant ssh..."
echo "cd /vagrant" >> /home/vagrant/.bashrc
SHELL
end
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
KeyName:
ConstraintDescription: must be the name of an existing EC2 KeyPair.
Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
Type: AWS::EC2::KeyPair::KeyName
InstanceType:
AllowedValues:
- t2.micro
ConstraintDescription: must be a valid EC2 instance type.
Default: t2.micro
Description: WebServer EC2 instance type
Type: String
Mappings:
RegionMap:
us-east-1:
AMI: ami-43a15f3e
us-east-2:
AMI: ami-916f59f4
us-west-1:
AMI: ami-925144f2
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
WebsiteConfiguration:
IndexDocument: index.html
ErrorDocument: index.html
EC2Instance:
Type: AWS::EC2::Instance
Properties:
UserData: !Base64
Fn::Join:
- ''
- [IPAddress=, !Ref 'IPAddress']
InstanceType: !Ref 'InstanceType'
SecurityGroups: [!Ref 'InstanceSecurityGroup']
KeyName: !Ref 'KeyName'
ImageId: !FindInMap
- RegionMap
- !Ref 'AWS::Region'
- AMI
UserData:
Fn::Base64:
Fn::Sub: |
#!/bin/sh
add-apt-repository ppa:mosquitto-dev/mosquitto-ppa -y
apt-get update -y
apt-get install mosquitto -y
echo "mosquitto installed"
service mosquitto stop
echo "
# /etc/mosquitto/mosquitto.conf
# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example
pid_file /var/run/mosquitto.pid
log_type all
connection_messages true
log_timestamp true
log_dest file /var/log/mosquitto/mosquitto.log
# Standard MQTT
listener 1883
protocol mqtt
# MQTT over websockets
listener 8000
protocol websockets
include_dir /etc/mosquitto/conf.d
max_inflight_messages 0
max_queued_messages 0
max_connections -1
" > /etc/mosquitto/mosquitto.conf
echo "/etc/mosquitto/mosquitto.conf has been changed"
service mosquitto start
echo "mosquitto installed and configured"
InstanceSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: SSH, MQTT, WS
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: '22'
ToPort: '22'
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: '1883'
ToPort: '1883'
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: '8000'
ToPort: '8000'
CidrIp: 0.0.0.0/0
IPAddress:
Type: AWS::EC2::EIP
IPAssoc:
Type: AWS::EC2::EIPAssociation
Properties:
InstanceId: !Ref 'EC2Instance'
EIP: !Ref 'IPAddress'
Outputs:
InstanceId:
Description: InstanceId of the newly created EC2 instance
Value: !Ref 'EC2Instance'
InstanceIPAddress:
Description: IP address of the newly created EC2 instance
Value: !Ref 'IPAddress'
WebsiteURL:
Value: !GetAtt [S3Bucket, WebsiteURL]
Description: URL for website hosted on S3
S3BucketName:
Value: !Join ['', ['s3://', !Ref 'S3Bucket']]
Description: Name of S3 bucket to hold website content
StackId:
Description: StackId
Value: !Ref "AWS::StackId"
StackName:
Description: StackName
Value: !Ref "AWS::StackName"
#!/bin/sh
add-apt-repository ppa:mosquitto-dev/mosquitto-ppa -y
apt-get update -y
apt-get install mosquitto -y
echo "mosquitto installed"
service mosquitto stop
echo "
# /etc/mosquitto/mosquitto.conf
# Place your local configuration in /etc/mosquitto/conf.d/
#
# A full description of the configuration file is at
# /usr/share/doc/mosquitto/examples/mosquitto.conf.example
pid_file /var/run/mosquitto.pid
log_type all
connection_messages true
log_timestamp true
log_dest file /var/log/mosquitto/mosquitto.log
# Standard MQTT
listener 1883
protocol mqtt
# MQTT over websockets
listener 8000
protocol websockets
include_dir /etc/mosquitto/conf.d
max_inflight_messages 0
max_queued_messages 0
max_connections -1
" > /etc/mosquitto/mosquitto.conf
echo "/etc/mosquitto/mosquitto.conf has been changed"
service mosquitto start
echo "mosquitto installed and configured"
#!/usr/bin/env bash
set -e
echo "Installing Node.js v9.x..."
apt-get install -y g++
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
apt-get install -y nodejs
npm -v >/dev/null || {
echo >&2 'Node.js installation failed =('
exit 1
}
echo -e "\n==> All good!"
#!/usr/bin/env bash
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update -y
sudo apt-get install yarn -y
#!/usr/bin/env node
const fs = require('fs');
const prog = require('caporal');
const {
describeStacks, createOrUpdateStack, changeRegion, syncUIWithStackData, build, updateEnvVariables, deleteStack,
DEFAULT_INSTANCE_TYPE, DEFAULT_STACK_NAME, DEFAULT_REGION
} = require('./s3-service');
const NO_VALIDATE = null;
const REGION_VALIDATE = ['us-east-1', 'us-east-2', 'us-west-1']; //RegionMap in CloudFormation template
prog
.command('deploy', 'Deploy CloudFormation template to AWS')
.option('--stackName <stackName>', 'Stack Name', NO_VALIDATE, DEFAULT_STACK_NAME)
.option('--region <region>', 'AWS region', REGION_VALIDATE, DEFAULT_REGION, false)
.option('--key <key>', 'Name of an existing EC2 KeyPair to enable SSH access to the instances', NO_VALIDATE, null, true)
.option('--instanceType <instanceType>', 'EC2 Instance Type', NO_VALIDATE, DEFAULT_INSTANCE_TYPE, false)
.action((args, options) => {
changeRegion(options.region);
createOrUpdateStack({
stackName: options.stackName,
key: options.key,
instanceType: options.instanceType,
template: fs.readFileSync(`${__dirname}/../aws/cloudformation/template.yaml`, 'utf-8')
}).then((data) => {
console.log(JSON.stringify(data))
}).catch((err) => {
console.error(err.message)
});
});
prog
.command('publish', 'Sync Web to s3')
.option('--stackName <stackName>', 'Stack Name', NO_VALIDATE, DEFAULT_STACK_NAME)
.option('--updateEnv <updateEnv>', 'Updates .env file by stack data', NO_VALIDATE, true)
.action((args, options) => {
syncUIWithStackData(options.stackName, options.updateEnv).then(({websiteURL}) => {
console.log(`build has been synced to s3 bucket`);
console.log(`website URL: ${websiteURL}`);
}).catch((err) => {
console.error(err.message)
});
});
prog
.command('describe-stacks', 'Deploy CloudFormation template to AWS')
.option('--stackName <stackName>', 'Stack Name', NO_VALIDATE, DEFAULT_STACK_NAME)
.option('--region <region>', 'AWS region', NO_VALIDATE, DEFAULT_REGION, false)
.action((args, options) => {
changeRegion(options.region);
describeStacks(options.stackName)
.then((data) => {
console.log(JSON.stringify(data))
})
.catch((err) => {
console.error(err.message)
});
});
prog
.command('delete', 'Delete CloudFormation template')
.option('--stackName <stackName>', 'Stack Name', NO_VALIDATE, DEFAULT_STACK_NAME)
.option('--region <region>', 'AWS region', REGION_VALIDATE, DEFAULT_REGION, false)
.action((args, options) => {
changeRegion(options.region);
deleteStack(options.stackName)
.then((data) => {
console.log(JSON.stringify(data))
})
.catch((err) => {
console.error(err.message)
});
});
prog.parse(process.argv);
/* global process */
/* global Promise */
const fs = require('fs');
const {exec} = require('child_process');
const AWS = require('aws-sdk');
const _ = require('lodash');
const execa = require('execa');
const DEFAULT_STACK_NAME = 'rigado-node-hello-world-frontend';
const DEFAULT_INSTANCE_TYPE = 't2.micro';
const DEFAULT_REGION = 'us-east-1';
const REACT_APP_MQTT_TOPIC = '/gateway/localhost.localdomain/sensors';
const REACT_APP_MQTT_URL = 'ws://broker.mqttdashboard.com:8000/mqtt';
let cloudformation = new AWS.CloudFormation({region: DEFAULT_REGION});
const changeRegion = (region) => {
cloudformation = new AWS.CloudFormation({region})
};
const describeStacks = (stackName = DEFAULT_STACK_NAME) => {
return new Promise((resolve, reject) => {
cloudformation.describeStacks({
StackName: stackName
}, (err, data) => {
if (err) {
return reject(err);
}
return resolve(data);
})
});
};
const syncUIWithStackData = (stackName, updateEnv) => {
return describeStacks(stackName)
.then((data) => {
const stackOutputs = _.get(data, 'Stacks[0].Outputs', []);
const s3BucketNameOutput = _.find(stackOutputs, {OutputKey: 'S3BucketName'});
const s3BucketName = _.get(s3BucketNameOutput, 'OutputValue');
const websiteURLOutpup = _.find(stackOutputs, {OutputKey: 'WebsiteURL'});
const websiteURL = _.get(websiteURLOutpup, 'OutputValue');
const instanceIPAddressOutpup = _.find(stackOutputs, {OutputKey: 'InstanceIPAddress'});
const instanceIPAddress = _.get(instanceIPAddressOutpup, 'OutputValue');
const mqttUrl = `ws://${instanceIPAddress}:8000/`;
if (!s3BucketName) return Promise.reject('no S3BucketSecureURL in template output');
const updateThenSync = () => build()
.then(() => sync(`${__dirname}/../build`, s3BucketName).then(() => {
return {websiteURL}
}));
if (updateEnv) {
return updateEnvVariables(false, mqttUrl)
.then(() => updateThenSync())
} else {
return updateThenSync();
}
})
};
const build = () => {
const run = execa('react-scripts', ['build']);
run.stdout.pipe(process.stdout);
run.stderr.pipe(process.stderr);
return run;
};
const sync = (dirPath, bucket) => {
const run = execa(`aws`, ['s3', 'sync', dirPath, bucket, '--acl', 'public-read', '--delete']);
run.stdout.pipe(process.stdout);
run.stderr.pipe(process.stderr);
return run;
};
const updateEnvVariables = (debug = false, mqttUrl = REACT_APP_MQTT_URL, mqttTopic = REACT_APP_MQTT_TOPIC) => {
return new Promise((resolve, reject) => {
fs.writeFile(`${__dirname}/../.env`,
`REACT_APP_DEBUG=${debug}\nREACT_APP_MQTT_TOPIC=${mqttTopic}\nREACT_APP_MQTT_URL=${mqttUrl}`,
(err) => err ? reject(err) : resolve())
});
};
const createOrUpdateStack = ({stackName, key, instanceType, template}) => {
return describeStacks(stackName)
.then((data) => {
return updateStack({stackName, key, instanceType, template});
})
.catch((err) => {
if (_.includes(err.message, 'does not exist')) {
return createStack({stackName, key, instanceType, template});
} else {
return Promise.reject(err);
}
});
};
const createStack = ({stackName, key, instanceType, template}) => {
return new Promise((resolve, reject) => {
const params = {
StackName: stackName,
Parameters: [{
ParameterKey: 'KeyName',
ParameterValue: key
}, {
ParameterKey: 'InstanceType',
ParameterValue: instanceType
}],
TemplateBody: template
};
cloudformation.createStack(params, (err, data) => {
if (err) reject(err);
resolve(data);
});
});
};
const updateStack = ({stackName, key, instanceType, template}) => {
return new Promise((resolve, reject) => {
const params = {
StackName: stackName,
Parameters: [{
ParameterKey: 'KeyName',
ParameterValue: key
}, {
ParameterKey: 'InstanceType',
ParameterValue: instanceType
}],