diff --git a/.gitignore b/.gitignore index 84dc368..1ad262f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ Thumbs.db # generated documents /doc + +coverage diff --git a/.travis.yml b/.travis.yml index 7cf566e..6f15102 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,33 +1,29 @@ sudo: required dist: trusty language: node_js -cache: - directories: - - $HOME/.yarn-cache +cache: yarn node_js: -- '6.7.0' +- '7.4.0' +services: +- docker before_install: - export CHROME_BIN=chromium-browser - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - # Repo for yarn - - sudo apt-key adv --keyserver pgp.mit.edu --recv D101F7899D41F3C3 - - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - - sudo apt-get update -qq - - sudo apt-get install -y -qq yarn -install: - - yarn install script: - yarn test +after_success: + - yarn coveralls + before_deploy: # Parse branch name and determine an environment to deploy - export ENV=$(echo "${TRAVIS_BRANCH}" | perl -ne "print $& if /(?<=deploy\/).*/") # install aws cli - - sudo apt-get -y install python-pip curl - - sudo pip install awscli + - sudo apt-get -y install python-pip + - pip install awscli - aws --version deploy: - provider: script diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3aa31e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:alpine + +COPY docker/default.conf /etc/nginx/conf.d + +ADD dist /usr/share/nginx/html diff --git a/README.md b/README.md index b0cbede..d2947b4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Angular2 Tutorial [![Build Status][travis-image]][travis-url] +# Angular2 Tutorial + +[![Build Status](https://travis-ci.org/springboot-angular2-tutorial/angular2-app.svg?branch=master)](https://travis-ci.org/springboot-angular2-tutorial/angular2-app) +[![Coverage Status](https://coveralls.io/repos/github/springboot-angular2-tutorial/angular2-app/badge.svg?branch=master)](https://coveralls.io/github/springboot-angular2-tutorial/angular2-app?branch=master) This repository is an example application for angular2 tutorial. @@ -7,6 +10,8 @@ This repository is an example application for angular2 tutorial. * Ahead-of-time compilation * Lazy Loading * Preloading +* [CSS in JS](https://speakerdeck.com/vjeux/react-css-in-js) by using [Aphrodite](https://github.com/Khan/aphrodite) +* Hot module reload ## Getting Started @@ -21,7 +26,7 @@ mvn spring-boot:run Serve frontend app by webpack-dev-server. ``` -npm install -g yarn@">=0.16.0" +npm install -g yarn yarn install yarn start open http://localhost:4200 @@ -36,7 +41,7 @@ yarn test Production build. ``` -yarn run build:prod +yarn run build yarn run server:prod open http://localhost:4200 ``` @@ -49,8 +54,8 @@ Under construction... * [Spring Boot app](https://github.com/springboot-angular2-tutorial/boot-app) * [Android app](https://github.com/springboot-angular2-tutorial/android-app) -* [Server provisioning by Ansible](https://github.com/springboot-angular2-tutorial/micropost-provisionings) * [Infrastructure by Terraform](https://github.com/springboot-angular2-tutorial/micropost-formation) +* [Lambda functions by Serverless](https://github.com/springboot-angular2-tutorial/micropost-functions) ## Credits diff --git a/config/helpers.js b/config/helpers.js index 09228b6..94fdf2e 100644 --- a/config/helpers.js +++ b/config/helpers.js @@ -1,5 +1,5 @@ -var path = require('path'); -var _root = path.resolve(__dirname, '..'); +const path = require('path'); +const _root = path.resolve(__dirname, '..'); function hasProcessFlag(flag) { return process.argv.join('').indexOf(flag) > -1; @@ -10,5 +10,10 @@ function root(args) { return path.join.apply(path, [_root].concat(args)); } +function prod() { + return process.env.NODE_ENV === 'production'; +} + exports.hasProcessFlag = hasProcessFlag; exports.root = root; +exports.prod = prod; diff --git a/config/karma.conf.js b/config/karma.conf.js index b0b72b7..62c7b0f 100644 --- a/config/karma.conf.js +++ b/config/karma.conf.js @@ -8,13 +8,19 @@ module.exports = function (config) { {pattern: './config/spec-bundle.js', watched: false}, ], preprocessors: { - './config/spec-bundle.js': ['webpack', 'sourcemap'], + './config/spec-bundle.js': ['coverage', 'webpack', 'sourcemap'], }, webpack: testWebpackConfig, - webpackServer: { - noInfo: true, + coverageReporter: { + type: 'in-memory' }, - reporters: ['mocha'], + remapCoverageReporter: { + 'text-summary': null, + html: './coverage/html', + lcovonly: './coverage/lcov.info', + }, + webpackMiddleware: {stats: 'errors-only'}, + reporters: ['mocha', 'coverage', 'remap-coverage'], port: 9876, colors: true, logLevel: config.LOG_INFO, @@ -29,7 +35,7 @@ module.exports = function (config) { singleRun: true, }; - if (process.env.TRAVIS){ + if (process.env.TRAVIS) { configuration.browsers = [ 'ChromeTravisCi' ]; diff --git a/config/webpack.aot.js b/config/webpack.aot.js index 3a9e4eb..f48bf37 100644 --- a/config/webpack.aot.js +++ b/config/webpack.aot.js @@ -1,10 +1,13 @@ const helpers = require('./helpers'); -const webpack = require('webpack'); const webpackMerge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); +const DefinePlugin = require('webpack/lib/DefinePlugin'); +const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); +const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); + const ENV = process.env.NODE_ENV = process.env.ENV = 'production'; -const PUBLIC_PATH = (process.env.PUBLIC_PATH || '') + '/' ; +const PUBLIC_PATH = (process.env.PUBLIC_PATH || '') + '/'; module.exports = webpackMerge(commonConfig, { entry: { @@ -29,20 +32,34 @@ module.exports = webpackMerge(commonConfig, { publicPath: PUBLIC_PATH, }, plugins: [ - new webpack.DefinePlugin({ + new DefinePlugin({ 'ENV': JSON.stringify(ENV), }), - new webpack.LoaderOptionsPlugin({ + new LoaderOptionsPlugin({ minimize: true, debug: false }), - new webpack.optimize.UglifyJsPlugin({ + new UglifyJsPlugin({ + beautify: false, + output: { + comments: false + }, + mangle: { + screw_ie8: true + }, compress: { - warnings: false + screw_ie8: true, + warnings: false, + conditionals: true, + unused: true, + comparisons: true, + sequences: true, + dead_code: true, + evaluate: true, + if_return: true, + join_vars: true, + negate_iife: false, // we need this for lazy v8 }, - beautify: false, - comments: false, - sourceMap: true }), ], node: { diff --git a/config/webpack.common.js b/config/webpack.common.js index 0928cc2..332dbeb 100644 --- a/config/webpack.common.js +++ b/config/webpack.common.js @@ -1,8 +1,10 @@ -const webpack = require('webpack'); const helpers = require('./helpers'); +const {CheckerPlugin} = require('awesome-typescript-loader'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin; +const ExtractTextPlugin = require("extract-text-webpack-plugin"); +const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); +const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); module.exports = { entry: { @@ -13,7 +15,8 @@ module.exports = { resolve: { extensions: ['.ts', '.js'], alias: { - "lodash": "lodash-es", + lodash: 'lodash-es', + aphrodite: 'aphrodite/no-important', }, }, module: { @@ -21,13 +24,22 @@ module.exports = { { test: /\.ts$/, loaders: [ + '@angularclass/hmr-loader', 'awesome-typescript-loader', 'angular2-template-loader', 'angular2-router-loader?loader=system', ], exclude: [/\.spec\.ts$/] }, - {test: /\.css$/, loader: 'raw-loader'}, + // global css + { + test: /\.css$/, + exclude: [helpers.root('src')], + loader: ExtractTextPlugin.extract({ + fallbackLoader: "style-loader", + loader: "css-loader", + }), + }, {test: /\.html$/, loader: 'raw-loader'}, ] }, @@ -36,20 +48,16 @@ module.exports = { template: 'src/index.html', chunksSortMode: 'dependency', }), - new ForkCheckerPlugin(), - new webpack.ContextReplacementPlugin( + new CheckerPlugin(), + new ContextReplacementPlugin( + // The (\\|\/) piece accounts for path separators in *nix and Windows /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, - __dirname + helpers.root('src') // location of your src ), - new webpack.optimize.CommonsChunkPlugin({ + new CommonsChunkPlugin({ name: ['polyfills', 'vendor'].reverse() }), - new webpack.ProvidePlugin({ - jQuery: 'jquery', - $: 'jquery', - Util: "exports?Util!bootstrap/js/dist/util", - Collapse: "exports?Collapse!bootstrap/js/dist/collapse", - }), + new ExtractTextPlugin('[name].[chunkhash].css'), ], node: { global: true, diff --git a/config/webpack.dev.js b/config/webpack.dev.js index e797721..8d7a5da 100644 --- a/config/webpack.dev.js +++ b/config/webpack.dev.js @@ -1,8 +1,10 @@ const helpers = require('./helpers'); -const webpack = require('webpack'); const webpackMerge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); +const DefinePlugin = require('webpack/lib/DefinePlugin'); +const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); + const ENV = process.env.ENV = process.env.NODE_ENV = 'development'; module.exports = webpackMerge(commonConfig, { @@ -12,20 +14,15 @@ module.exports = webpackMerge(commonConfig, { filename: '[name].js', sourceMapFilename: '[name].map', chunkFilename: '[id].chunk.js', - publicPath: '/' + // required for hot module replacement + publicPath: 'http://localhost:4200/', }, plugins: [ - new webpack.DefinePlugin({ + new DefinePlugin({ 'ENV': JSON.stringify(ENV), }), - new webpack.LoaderOptionsPlugin({ - options: { - tslint: { - emitErrors: false, - failOnHint: false, - resourcePath: 'src' - }, - }, + new LoaderOptionsPlugin({ + debug: true, }), ], devServer: { diff --git a/config/webpack.test.js b/config/webpack.test.js index 1b3f204..bbfea3a 100644 --- a/config/webpack.test.js +++ b/config/webpack.test.js @@ -1,5 +1,9 @@ +const path = require('path'); const helpers = require('./helpers'); -const webpack = require('webpack'); + +const DefinePlugin = require('webpack/lib/DefinePlugin'); +const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); +const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); const ENV = process.env.ENV = process.env.NODE_ENV = 'test'; @@ -7,6 +11,7 @@ module.exports = { devtool: 'inline-source-map', resolve: { extensions: ['.ts', '.js'], + modules: [path.resolve(__dirname, 'src'), 'node_modules'], alias: { "lodash": "lodash-es", }, @@ -17,7 +22,7 @@ module.exports = { enforce: 'pre', test: /\.ts$/, loader: 'tslint-loader', - exclude: [helpers.root('node_modules')] + exclude: [helpers.root('node_modules')], }, { enforce: 'pre', @@ -25,27 +30,54 @@ module.exports = { loader: 'source-map-loader', exclude: [ helpers.root('node_modules/rxjs'), - helpers.root('node_modules/@angular') + helpers.root('node_modules/@angular'), ] }, { test: /\.ts$/, - loaders: ['awesome-typescript-loader', 'angular2-template-loader'] + loader: 'awesome-typescript-loader', + query: { + sourceMap: false, + inlineSourceMap: true, + module: 'commonjs', // required for coverage https://github.com/AngularClass/angular2-webpack-starter/issues/1021 + }, + }, + { + test: /\.ts$/, + loader: 'angular2-template-loader', }, {test: /\.css$/, loader: 'raw-loader'}, {test: /\.html$/, loader: 'raw-loader'}, + { + enforce: 'post', + test: /\.(js|ts)$/, + loader: 'istanbul-instrumenter-loader', + include: [ + helpers.root('src'), + ], + exclude: [ + /\.spec\.ts$/, + /node_modules/, + ] + }, ] }, plugins: [ - new webpack.DefinePlugin({ + new DefinePlugin({ 'ENV': JSON.stringify(ENV), }), - new webpack.LoaderOptionsPlugin({ + new ContextReplacementPlugin( + // The (\\|\/) piece accounts for path separators in *nix and Windows + /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, + helpers.root('src') // location of your src + ), + new LoaderOptionsPlugin({ + debug: true, options: { tslint: { emitErrors: false, failOnHint: false, - resourcePath: 'src' + resourcePath: 'src', }, }, }), diff --git a/docker/default.conf b/docker/default.conf new file mode 100644 index 0000000..d446325 --- /dev/null +++ b/docker/default.conf @@ -0,0 +1,12 @@ +server { + listen 80; + + root /usr/share/nginx/html; + + location ~ \.(js|css)$ { + } + + location / { + rewrite (.*) /index.html break; + } +} diff --git a/karma.conf.js b/karma.conf.js index d0d7837..9649b15 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,2 +1 @@ module.exports = require('./config/karma.conf.js'); - diff --git a/package.json b/package.json index 7ce0d35..fcdb595 100644 --- a/package.json +++ b/package.json @@ -2,31 +2,20 @@ "name": "angular2-tutorial", "version": "0.1.0", "description": "Angular2 tutorial sample application", - "main": "", "scripts": { - "clean": "yarn cache clean && rimraf -- node_modules doc coverage dist aot src/**/*.css", + "clean": "rimraf -- node_modules doc coverage dist aot", + "clean:install": "yarn run clean && yarn install", + "clean:start": "yarn run clean && yarn start", "clean:dist": "rimraf -- dist aot", - "preclean:install": "yarn run clean", - "clean:install": "yarn set progress=false && yarn install", - "preclean:start": "yarn run clean", - "clean:start": "yarn start", - "build": "yarn run build:dev", - "prebuild:dev": "yarn run clean:dist", - "build:dev": "webpack --config config/webpack.dev.js --progress --profile --colors --display-error-details --display-cached", - "prebuild:prod": "yarn run clean:dist", - "build:prod": "yarn run sass && yarn run ngc && webpack --config config/webpack.aot.js --progress --profile --bail", - "server": "yarn run server:dev", - "server:dev": "webpack-dev-server --config config/webpack.dev.js --progress --profile --watch --content-base src/", + "build": "yarn run clean:dist && yarn run ngc && webpack --config config/webpack.aot.js --progress --profile --bail", + "server:dev": "webpack-dev-server --config config/webpack.dev.js --progress --profile --hot --watch --content-base src/", "server:prod": "node server.js", - "test": "yarn run sass && karma start", - "test:watch": "npm-run-all -p -r sass:watch karma:watch", - "karma:watch": "karma start --browsers Chrome --reporters dots,notify --single-run false", - "docs": "typedoc --options typedoc.json src/**/*.ts", - "start": "npm-run-all -p -r sass:watch server:dev", + "test": "yarn run clean:dist && karma start", + "test:watch": "yarn run clean:dist && karma start --reporters dots,notify --single-run false", + "docs": "typedoc --options typedoc.json src/**/*.ts", + "start": "yarn run server:dev", "ngc": "ngc -p tsconfig.aot.json --locale=en-US", - "presass": "rimraf -- src/**/*.css", - "sass": "node-sass src -o src --include-path node_modules --output-style compressed -q", - "sass:watch": "node-sass src -o src && node-sass -w src -o src" + "coveralls": "cat ./coverage/lcov.info | coveralls" }, "repository": { "type": "git", @@ -39,64 +28,65 @@ }, "homepage": "https://github.com/springboot-angular2-tutorial/angular2-app", "dependencies": { - "@angular/common": "^2.1.0", - "@angular/compiler": "^2.1.0", - "@angular/core": "^2.1.0", - "@angular/forms": "^2.1.0", - "@angular/http": "^2.1.0", - "@angular/platform-browser": "^2.1.0", - "@angular/platform-browser-dynamic": "^2.1.0", - "@angular/router": "^3.1.0", - "bootstrap": "^4.0.0-alpha.4", + "@angular/common": "~2.4.9", + "@angular/compiler": "~2.4.9", + "@angular/core": "~2.4.9", + "@angular/forms": "~2.4.9", + "@angular/http": "~2.4.9", + "@angular/platform-browser": "~2.4.9", + "@angular/platform-browser-dynamic": "~2.4.9", + "@angular/router": "~3.4.9", + "@angularclass/hmr": "^1.2.2", + "@angularclass/hmr-loader": "^3.0.2", + "aphrodite": "^1.1.0", + "bootstrap": "4.0.0-alpha.6", "core-js": "^2.4.1", - "jquery": "^2.2.4", - "lodash-es": "^4.16.4", - "md5-hex": "^1.1.0", - "pluralize": "~3.0.0", - "rxjs": "5.0.0-beta.12", + "jwt-decode": "^2.1.0", + "lodash-es": "^4.16.6", + "rxjs": "~5.2.0", "time-ago": "^0.1.0", - "toastr": "^2.1.2", - "zone.js": "~0.6.17" + "zone.js": "~0.7.6" }, "devDependencies": { - "@angular/compiler-cli": "^2.1.0", - "@angular/platform-server": "^2.1.0", - "@types/jasmine": "^2.5.35", - "@types/lodash": "^4.14.37", - "@types/node": "^6.0.45", + "@angular/compiler-cli": "~2.4.9", + "@angular/platform-server": "~2.4.9", + "@types/jasmine": "^2.5.37", + "@types/lodash": "^4.14.38", + "@types/node": "^7.0.0", "@types/pluralize": "^0.0.27", - "@types/source-map": "^0.1.28", - "@types/toastr": "^2.1.27", - "@types/webpack": "^1.12.35", - "angular2-router-loader": "^0.3.2", - "angular2-template-loader": "^0.5.0", - "awesome-typescript-loader": "^2.2.4", - "codelyzer": "~1.0.0-beta.0", - "css-loader": "^0.25.0", - "exports-loader": "^0.6.3", + "@types/source-map": "^0.5.0", + "@types/webpack": "^2.0.0", + "angular2-router-loader": "^0.3.3", + "angular2-template-loader": "^0.6.0", + "awesome-typescript-loader": "^3.0.0-beta.17", + "codelyzer": "^2.0.0-beta.4", + "coveralls": "^2.11.15", + "css-loader": "^0.26.0", "express": "^4.14.0", - "html-webpack-plugin": "^2.22.0", - "http-proxy": "^1.15.1", - "jasmine-core": "^2.4.1", - "karma": "^1.2.0", + "extract-text-webpack-plugin": "^2.0.0-beta.4", + "html-webpack-plugin": "^2.24.1", + "http-proxy": "^1.15.2", + "istanbul-instrumenter-loader": "^2.0.0", + "jasmine-core": "^2.5.2", + "karma": "^1.4.0", "karma-chrome-launcher": "^2.0.0", - "karma-jasmine": "~1.0.2", + "karma-coverage": "^1.1.1", + "karma-jasmine": "^1.0.2", "karma-mocha-reporter": "^2.0.2", - "karma-notify-reporter": "^1.0.0", + "karma-notify-reporter": "^1.0.1", + "karma-remap-coverage": "^0.1.2", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^1.8.0", - "node-sass": "^3.5.3", - "npm-run-all": "^3.1.0", + "karma-webpack": "^2.0.1", "raw-loader": "^0.5.1", - "rimraf": "2.5.4", + "rimraf": "^2.5.4", "source-map-loader": "^0.1.5", - "style-loader": "^0.13.0", - "ts-helpers": "^1.1.1", - "tslint": "^3.15.1", - "tslint-loader": "^2.1.3", - "typescript": "^2.0.3", - "webpack": "2.1.0-beta.25", - "webpack-dev-server": "^2.1.0-beta.9", - "webpack-merge": "~0.15.0" + "style-loader": "^0.13.1", + "ts-helpers": "^1.1.2", + "tslint": "^4.0.2", + "tslint-loader": "^3.3.0", + "typescript": "~2.2.1", + "webpack": "~2.2.0", + "webpack-dev-server": "^2.4.1", + "webpack-merge": "^4.0.0" } } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 17d2d6c..a5e76d9 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,29 +1,27 @@ -#!/bin/sh +#!/usr/bin/env bash -if [ -z "${ENV}" ]; then - echo "ENV is required." - exit 1 -fi +set -u -# switch role if production -if [ "${ENV}" = "prod" ]; then - source scripts/switch-production-role.sh +if [ ! -v AWS_SESSION_TOKEN ]; then + source ./scripts/switch-role.sh fi -# set variables -CDN_URL="https://cdn-${ENV}.hana053.com" -S3_CDN_URL="s3://cdn-${ENV}.hana053.com" -if [ "${ENV}" = "prod" ]; then - MAIN_URL="https://micropost.hana053.com" -else - MAIN_URL="https://micropost-${ENV}.hana053.com" -fi +readonly DOCKER_NAME=micropost/frontend +readonly AWS_ACCOUNT_NUMBER=$(aws sts get-caller-identity --output text --query 'Account') +readonly IMAGE_URL=${AWS_ACCOUNT_NUMBER}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${DOCKER_NAME} + +# Build +PUBLIC_PATH="https://cdn-${ENV}.hana053.com" yarn run build -# build -PUBLIC_PATH=${CDN_URL} yarn run build:prod +# Ensure docker repository exists +aws ecr describe-repositories --repository-names ${DOCKER_NAME} > /dev/null 2>&1 || \ + aws ecr create-repository --repository-name ${DOCKER_NAME} -# deploy -aws s3 sync --delete --acl public-read dist ${S3_CDN_URL} +# Push to docker repository +eval $(aws ecr get-login) +docker build -t ${DOCKER_NAME} . +docker tag ${DOCKER_NAME}:latest ${IMAGE_URL}:latest +docker push ${IMAGE_URL}:latest -# invalidate index.html -curl -I "${MAIN_URL}/index.html" -H 'Cache-Purge: 1' +# Deploy +./scripts/ecs-deploy -c micropost -n frontend -i ${IMAGE_URL}:latest diff --git a/scripts/ecs-deploy b/scripts/ecs-deploy new file mode 100755 index 0000000..b3a7021 --- /dev/null +++ b/scripts/ecs-deploy @@ -0,0 +1,408 @@ +#!/usr/bin/env bash +set -o errexit +set -o pipefail +set -u + +function usage() { + set -e + cat < /dev/null 2>&1 || { + echo "Some of the required software is not installed:" + echo " please install $1" >&2; + exit 1; + } +} + +# Check for AWS, AWS Command Line Interface +require aws +# Check for jq, Command-line JSON processor +require jq + +# Setup default values for variables +CLUSTER=false +SERVICE=false +TASK_DEFINITION=false +MAX_DEFINITIONS=0 +IMAGE=false +MIN=false +MAX=false +TIMEOUT=90 +VERBOSE=false +TAGVAR=false +AWS_CLI=$(which aws) +AWS_ECS="$AWS_CLI --output json ecs" + +# Loop through arguments, two at a time for key and value +while [[ $# -gt 0 ]] +do + key="$1" + + case $key in + -k|--aws-access-key) + AWS_ACCESS_KEY_ID="$2" + shift # past argument + ;; + -s|--aws-secret-key) + AWS_SECRET_ACCESS_KEY="$2" + shift # past argument + ;; + -r|--region) + AWS_DEFAULT_REGION="$2" + shift # past argument + ;; + -p|--profile) + AWS_PROFILE="$2" + shift # past argument + ;; + --aws-instance-profile) + echo "--aws-instance-profile is not yet in use" + AWS_IAM_ROLE=true + ;; + -c|--cluster) + CLUSTER="$2" + shift # past argument + ;; + -n|--service-name) + SERVICE="$2" + shift # past argument + ;; + -d|--task-definition) + TASK_DEFINITION="$2" + shift + ;; + -i|--image) + IMAGE="$2" + shift + ;; + -t|--timeout) + TIMEOUT="$2" + shift + ;; + -m|--min) + MIN="$2" + shift + ;; + -M|--max) + MAX="$2" + shift + ;; + -D|--desired-count) + DESIRED="$2" + shift + ;; + -e|--tag-env-var) + TAGVAR="$2" + shift + ;; + --max-definitions) + MAX_DEFINITIONS="$2" + shift + ;; + -v|--verbose) + VERBOSE=true + ;; + *) + usage + exit 2 + ;; + esac + shift # past argument or value +done + +# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION and AWS_PROFILE can be set as environment variables +if [ -z ${AWS_ACCESS_KEY_ID+x} ]; then unset AWS_ACCESS_KEY_ID; fi +if [ -z ${AWS_SECRET_ACCESS_KEY+x} ]; then unset AWS_SECRET_ACCESS_KEY; fi +if [ -z ${AWS_DEFAULT_REGION+x} ]; + then unset AWS_DEFAULT_REGION + else + AWS_ECS="$AWS_ECS --region $AWS_DEFAULT_REGION" +fi +if [ -z ${AWS_PROFILE+x} ]; + then unset AWS_PROFILE + else + AWS_ECS="$AWS_ECS --profile $AWS_PROFILE" +fi + +if [ $VERBOSE == true ]; then + set -x +fi + +if [ $SERVICE == false ] && [ $TASK_DEFINITION == false ]; then + echo "One of SERVICE or TASK DEFINITON is required. You can pass the value using -n / --service-name for a service, or -d / --task-definiton for a task" + exit 1 +fi +if [ $SERVICE != false ] && [ $TASK_DEFINITION != false ]; then + echo "Only one of SERVICE or TASK DEFINITON may be specified, but you supplied both" + exit 1 +fi +if [ $SERVICE != false ] && [ $CLUSTER == false ]; then + echo "CLUSTER is required. You can pass the value using -c or --cluster" + exit 1 +fi +if [ $IMAGE == false ]; then + echo "IMAGE is required. You can pass the value using -i or --image" + exit 1 +fi +if ! [[ $MAX_DEFINITIONS =~ ^-?[0-9]+$ ]]; then + echo "MAX_DEFINITIONS must be numeric, or not defined." + exit 1 +fi + +# Define regex for image name +# This regex will create groups for: +# - domain +# - port +# - repo +# - image +# - tag +# If a group is missing it will be an empty string +imageRegex="^([a-zA-Z0-9.\-]+):?([0-9]+)?/([a-zA-Z0-9._-]+)/?([a-zA-Z0-9._-]+)?:?([a-zA-Z0-9\._-]+)?$" + +if [[ $IMAGE =~ $imageRegex ]]; then + # Define variables from matching groups + domain=${BASH_REMATCH[1]} + port=${BASH_REMATCH[2]} + repo=${BASH_REMATCH[3]} + img=${BASH_REMATCH[4]} + tag=${BASH_REMATCH[5]} + + # Validate what we received to make sure we have the pieces needed + if [[ "x$domain" == "x" ]]; then + echo "Image name does not contain a domain or repo as expected. See usage for supported formats." && exit 1; + fi + if [[ "x$repo" == "x" ]]; then + echo "Image name is missing the actual image name. See usage for supported formats." && exit 1; + fi + + # When a match for image is not found, the image name was picked up by the repo group, so reset variables + if [[ "x$img" == "x" ]]; then + img=$repo + repo="" + fi + +else + # check if using root level repo with format like mariadb or mariadb:latest + rootRepoRegex="^([a-zA-Z0-9\-]+):?([a-zA-Z0-9\.\-]+)?$" + if [[ $IMAGE =~ $rootRepoRegex ]]; then + img=${BASH_REMATCH[1]} + if [[ "x$img" == "x" ]]; then + echo "Invalid image name. See usage for supported formats." && exit 1 + fi + tag=${BASH_REMATCH[2]} + else + echo "Unable to parse image name: $IMAGE, check the format and try again" && exit 1 + fi +fi + +# If tag is missing make sure we can get it from env var, or use latest as default +if [[ "x$tag" == "x" ]]; then + if [[ $TAGVAR == false ]]; then + tag="latest" + else + tag=${!TAGVAR} + if [[ "x$tag" == "x" ]]; then + tag="latest" + fi + fi +fi + +# Reassemble image name +useImage="" +if [[ ! -z ${domain+undefined-guard} ]]; then + useImage="$domain" +fi +if [[ ! -z ${port} ]]; then + useImage="$useImage:$port" +fi +if [[ ! -z ${repo+undefined-guard} ]]; then + if [[ ! "x$repo" == "x" ]]; then + useImage="$useImage/$repo" + fi +fi +if [[ ! -z ${img+undefined-guard} ]]; then + if [[ "x$useImage" == "x" ]]; then + useImage="$img" + else + useImage="$useImage/$img" + fi +fi +imageWithoutTag="$useImage" +if [[ ! -z ${tag+undefined-guard} ]]; then + useImage="$useImage:$tag" +fi + +echo "Using image name: $useImage" + +if [ $SERVICE != false ]; then + # Get current task definition name from service + TASK_DEFINITION=`$AWS_ECS describe-services --services $SERVICE --cluster $CLUSTER | jq -r .services[0].taskDefinition` +fi + +echo "Current task definition: $TASK_DEFINITION"; + +# Get a JSON representation of the current task definition +# + Update definition to use new image name +# + Filter the def +DEF=$( $AWS_ECS describe-task-definition --task-def $TASK_DEFINITION \ + | sed -e "s|\"image\": *\"${imageWithoutTag}:.*\"|\"image\": \"${useImage}\"|g" \ + | sed -e "s|\"image\": *\"${imageWithoutTag}\"|\"image\": \"${useImage}\"|g" \ + | jq '.taskDefinition' ) + +# Default JQ filter for new task definition +NEW_DEF_JQ_FILTER="family: .family, volumes: .volumes, containerDefinitions: .containerDefinitions" + +# Some options in task definition should only be included in new definition if present in +# current definition. If found in current definition, append to JQ filter. +CONDITIONAL_OPTIONS=(networkMode taskRoleArn) +for i in "${CONDITIONAL_OPTIONS[@]}"; do + re=".*${i}.*" + if [[ "$DEF" =~ $re ]]; then + NEW_DEF_JQ_FILTER="${NEW_DEF_JQ_FILTER}, ${i}: .${i}" + fi +done + +# Build new DEF with jq filter +NEW_DEF=$(echo $DEF | jq "{${NEW_DEF_JQ_FILTER}}") + +# Register the new task definition, and store its ARN +NEW_TASKDEF=`$AWS_ECS register-task-definition --cli-input-json "$NEW_DEF" | jq -r .taskDefinition.taskDefinitionArn` +echo "New task definition: $NEW_TASKDEF"; + +if [ $SERVICE == false ]; then + echo "Task definition updated successfully" +else + DEPLOYMENT_CONFIG="" + if [ $MAX != false ]; then + DEPLOYMENT_CONFIG=",maximumPercent=$MAX" + fi + if [ $MIN != false ]; then + DEPLOYMENT_CONFIG="$DEPLOYMENT_CONFIG,minimumHealthyPercent=$MIN" + fi + if [ ! -z "$DEPLOYMENT_CONFIG" ]; then + DEPLOYMENT_CONFIG="--deployment-configuration ${DEPLOYMENT_CONFIG:1}" + fi + + DESIRED_COUNT="" + if [ ! -z ${DESIRED+undefined-guard} ]; then + DESIRED_COUNT="--desired-count $DESIRED" + fi + + # Update the service + UPDATE=`$AWS_ECS update-service --cluster $CLUSTER --service $SERVICE $DESIRED_COUNT --task-definition $NEW_TASKDEF $DEPLOYMENT_CONFIG` + + # Only excepts RUNNING state from services whose desired-count > 0 + SERVICE_DESIREDCOUNT=`$AWS_ECS describe-services --cluster $CLUSTER --service $SERVICE | jq '.services[]|.desiredCount'` + if [ $SERVICE_DESIREDCOUNT -gt 0 ]; then + # See if the service is able to come up again + every=10 + i=0 + while [ $i -lt $TIMEOUT ] + do + # Scan the list of running tasks for that service, and see if one of them is the + # new version of the task definition + + RUNNING_TASKS=$($AWS_ECS list-tasks --cluster "$CLUSTER" --service-name "$SERVICE" --desired-status RUNNING \ + | jq -r '.taskArns[]') + + if [[ ! -z $RUNNING_TASKS ]] ; then + RUNNING=$($AWS_ECS describe-tasks --cluster "$CLUSTER" --tasks $RUNNING_TASKS \ + | jq ".tasks[]| if .taskDefinitionArn == \"$NEW_TASKDEF\" then . else empty end|.lastStatus" \ + | grep -e "RUNNING") || : + + if [ "$RUNNING" ]; then + echo "Service updated successfully, new task definition running."; + + if [[ $MAX_DEFINITIONS -gt 0 ]]; then + FAMILY_PREFIX=${TASK_DEFINITION##*:task-definition/} + FAMILY_PREFIX=${FAMILY_PREFIX%*:[0-9]*} + TASK_REVISIONS=`$AWS_ECS list-task-definitions --family-prefix $FAMILY_PREFIX --status ACTIVE --sort ASC` + + NUM_ACTIVE_REVISIONS=$(echo "$TASK_REVISIONS" | jq ".taskDefinitionArns|length") + + if [[ $NUM_ACTIVE_REVISIONS -gt $MAX_DEFINITIONS ]]; then + LAST_OUTDATED_INDEX=$(($NUM_ACTIVE_REVISIONS - $MAX_DEFINITIONS - 1)) + for i in $(seq 0 $LAST_OUTDATED_INDEX); do + OUTDATED_REVISION_ARN=$(echo "$TASK_REVISIONS" | jq -r ".taskDefinitionArns[$i]") + + echo "Deregistering outdated task revision: $OUTDATED_REVISION_ARN" + + $AWS_ECS deregister-task-definition --task-definition "$OUTDATED_REVISION_ARN" > /dev/null + done + fi + fi + + exit 0 + fi + fi + + sleep $every + i=$(( $i + $every )) + done + + # Timeout + echo "ERROR: New task definition not running within $TIMEOUT seconds" + exit 1 + else + echo "Skipping check for running task definition, as desired-count <= 0" + fi +fi diff --git a/scripts/switch-production-role.sh b/scripts/switch-production-role.sh deleted file mode 100755 index 06d3435..0000000 --- a/scripts/switch-production-role.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -credentials=$(aws sts assume-role --role-arn ${PROD_ROLE_ARN} --role-session-name travisci) - -export AWS_ACCESS_KEY_ID=$(echo ${credentials} | jq --raw-output .Credentials.AccessKeyId) -export AWS_SECRET_ACCESS_KEY=$(echo ${credentials} | jq --raw-output .Credentials.SecretAccessKey) -export AWS_SESSION_TOKEN=$(echo ${credentials} | jq --raw-output .Credentials.SessionToken) - -unset credentials diff --git a/scripts/switch-role.sh b/scripts/switch-role.sh new file mode 100755 index 0000000..023abbd --- /dev/null +++ b/scripts/switch-role.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Parse variable such as ROLE_ARN_stg, ROLE_ARN_prod and etc. +ROLE_ARN=$(eval echo '$ROLE_ARN_'${ENV}) + +CREDENTIALS=$(aws sts assume-role --role-arn ${ROLE_ARN} --role-session-name travisci) + +export AWS_ACCESS_KEY_ID=$(echo ${CREDENTIALS} | jq --raw-output .Credentials.AccessKeyId) +export AWS_SECRET_ACCESS_KEY=$(echo ${CREDENTIALS} | jq --raw-output .Credentials.SecretAccessKey) +export AWS_SESSION_TOKEN=$(echo ${CREDENTIALS} | jq --raw-output .Credentials.SessionToken) diff --git a/src/app/app.component.html b/src/app/app.component.html index 530110c..5bbc680 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,8 +1,9 @@ +
-