├── dev-lib └── .gitkeep ├── cookbooks └── dromedary │ ├── chefignore │ ├── CHANGELOG.md │ ├── Berksfile │ ├── attributes-noprereqs.json │ ├── Gemfile │ ├── attributes.json │ ├── test │ └── integration │ │ └── default │ │ ├── bats │ │ └── nginx.bats │ │ └── serverspec │ │ ├── codedeploy_spec.rb │ │ ├── ruby_spec.rb │ │ ├── dromedary_spec.rb │ │ ├── nodejs_spec.rb │ │ └── nginx_spec.rb │ ├── recipes │ ├── yum_packages.rb │ ├── default.rb │ ├── nginx_config.rb │ ├── code_deploy.rb │ ├── prereqs.rb │ ├── install_dromedary.rb │ ├── nodejs.rb │ └── ssl_nginx_config.rb │ ├── metadata.rb │ ├── files │ └── default │ │ └── nginx │ │ ├── dromedary-site.cfg │ │ ├── ssl-dromedary-site.cfg │ │ └── generate-cert.sh │ ├── Berksfile.lock │ ├── .kitchen.yml │ ├── packer.json │ ├── README.md │ ├── Vagrantfile │ └── Gemfile.lock ├── deploy └── scripts │ ├── validateService.sh │ ├── beforeInstall.sh │ ├── applicationStop.sh │ ├── afterInstall.sh │ └── applicationStart.sh ├── .DS_Store ├── pipeline ├── .DS_Store ├── jobs │ ├── scripts │ │ ├── drom-unit-test.sh │ │ ├── drom-staticcode-anal.sh │ │ ├── drom-create-env.sh │ │ ├── drom-promote-env.sh │ │ ├── drom-infra-test.sh │ │ ├── drom-acceptance-test.sh │ │ └── drom-build.sh │ ├── dsl │ │ ├── codepipeline.groovy │ │ ├── views.groovy │ │ ├── dummyselfservice.groovy │ │ └── dummypipeline.groovy │ └── xml │ │ ├── drom-unit-test │ │ └── config.xml │ │ ├── drom-promote-env │ │ └── config.xml │ │ ├── drom-staticcode-anal │ │ └── config.xml │ │ ├── job-seed │ │ └── config.xml │ │ ├── drom-create-env │ │ └── config.xml │ │ ├── drom-acceptance-test │ │ └── config.xml │ │ ├── drom-infra-test │ │ └── config.xml │ │ └── drom-build │ │ └── config.xml └── cfn │ ├── params-example-lambda.json │ ├── params-example.json │ ├── dynamodb.json │ ├── codepipeline-custom-actions.json │ ├── app-eni.json │ ├── pipeline-store.json │ ├── iam.json │ ├── jenkins-instance.json │ ├── codepipeline-cfn.json │ ├── vpc.json │ └── app-instance.json ├── public ├── stelogo.png ├── style.css ├── index.html └── charthandler.js ├── test-infra ├── spec │ ├── spec_helper.rb │ └── security_group_spec.rb └── bootstrap │ └── features │ ├── bootstrap-teardown.feature │ ├── bootstrap.feature │ └── step_definitions │ └── bootstrap.rb ├── bin ├── configure-jenkins-2.sh ├── cfn-wait-for-stack.sh ├── eni-detach.sh ├── packer-create-ami.sh ├── eni-attach-to-app.sh ├── cfn-create-app.sh └── configure-jenkins.sh ├── lib ├── sha.js ├── requestThrottle.js ├── inMemoryStorage.js └── dynamoDbPersist.js ├── index.js ├── appspec.yml ├── test-functional ├── endpoint-index.js ├── endpoint-config.js ├── endpoint-increment.js └── endpoint-data.js ├── .gitignore ├── LICENSE.md ├── package.json ├── .jshintrc ├── test ├── requestThrottle.js └── inMemoryStorage.js ├── swagger.json ├── app.js ├── gulpfile.js └── README.md /dev-lib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cookbooks/dromedary/chefignore: -------------------------------------------------------------------------------- 1 | .kitchen 2 | -------------------------------------------------------------------------------- /deploy/scripts/validateService.sh: -------------------------------------------------------------------------------- 1 | wget localhost:80 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelligent/dromedary/HEAD/.DS_Store -------------------------------------------------------------------------------- /cookbooks/dromedary/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | Initial release of dromedary 4 | -------------------------------------------------------------------------------- /cookbooks/dromedary/Berksfile: -------------------------------------------------------------------------------- 1 | 2 | source "https://supermarket.chef.io" 3 | 4 | metadata 5 | -------------------------------------------------------------------------------- /pipeline/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelligent/dromedary/HEAD/pipeline/.DS_Store -------------------------------------------------------------------------------- /public/stelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stelligent/dromedary/HEAD/public/stelogo.png -------------------------------------------------------------------------------- /deploy/scripts/beforeInstall.sh: -------------------------------------------------------------------------------- 1 | # validate preqreqs exist maybe? 2 | # check node, forever are installed 3 | -------------------------------------------------------------------------------- /deploy/scripts/applicationStop.sh: -------------------------------------------------------------------------------- 1 | # assumes we're the only node process running. 2 | /usr/bin/forever stopall 3 | -------------------------------------------------------------------------------- /deploy/scripts/afterInstall.sh: -------------------------------------------------------------------------------- 1 | # this space intentionally left blank 2 | pushd /dromedary 3 | npm install 4 | popd 5 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-unit-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | npm install 6 | gulp test 7 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-staticcode-anal.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | npm install 6 | gulp lint 7 | -------------------------------------------------------------------------------- /cookbooks/dromedary/attributes-noprereqs.json: -------------------------------------------------------------------------------- 1 | { 2 | "run_list": [ 3 | "recipe[dromedary::default]" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /deploy/scripts/applicationStart.sh: -------------------------------------------------------------------------------- 1 | mkdir -p /dromedary/log 2 | /usr/bin/forever /dromedary/app.js > /dromedary/log/server.log 2>&1 & 3 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-create-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | bash "$(dirname $0)/../../../bin/cfn-create-app.sh" 6 | -------------------------------------------------------------------------------- /cookbooks/dromedary/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'berkshelf' 4 | 5 | gem "test-kitchen" 6 | gem "kitchen-vagrant" 7 | gem "kitchen-ec2" 8 | -------------------------------------------------------------------------------- /cookbooks/dromedary/attributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "run_list": [ 3 | "recipe[dromedary::prereqs]", 4 | "recipe[dromedary::default]" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /cookbooks/dromedary/test/integration/default/bats/nginx.bats: -------------------------------------------------------------------------------- 1 | @test "nginx service running" { 2 | run service nginx status 3 | [ "$status" -eq 0 ] 4 | } 5 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-promote-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | . environment.sh 6 | 7 | bash "$(dirname $0)/../../../bin/eni-attach-to-app.sh" $dromedary_app_stack_name 8 | -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/yum_packages.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: node_modules 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | package "ruby" do 11 | action :install 12 | end -------------------------------------------------------------------------------- /test-infra/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.expect_with :rspec do |expectations| 3 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 4 | end 5 | 6 | config.mock_with :rspec do |mocks| 7 | mocks.verify_partial_doubles = true 8 | end 9 | end -------------------------------------------------------------------------------- /cookbooks/dromedary/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'dromedary' 2 | maintainer 'YOUR_NAME' 3 | maintainer_email 'YOUR_EMAIL' 4 | license 'All rights reserved' 5 | description 'Installs/Configures dromedary' 6 | long_description 'Installs/Configures dromedary' 7 | version '0.1.0' 8 | 9 | depends 'nginx' 10 | -------------------------------------------------------------------------------- /cookbooks/dromedary/test/integration/default/serverspec/codedeploy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | 3 | set :backend, :exec 4 | 5 | # for some reason CodeDeploy doesn't work with the service matcher 6 | describe command('service codedeploy-agent status') do 7 | its(:stdout) { should_not match /The AWS CodeDeploy agent is running as/ } 8 | end 9 | -------------------------------------------------------------------------------- /bin/configure-jenkins-2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "In configure-jenkins.sh" 5 | script_dir="$(dirname "$0")" 6 | bin_dir="$(dirname $0)/../bin" 7 | 8 | echo The value of arg 0 = $0 9 | echo The value of arg 1 = $1 10 | echo The value of arg script_dir = $script_dir 11 | 12 | uuid=$(date +%s) 13 | 14 | sleep 60 15 | 16 | -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: default 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | include_recipe 'dromedary::nginx_config' 11 | # include_recipe 'dromedary::ssl_nginx_config' 12 | 13 | include_recipe 'dromedary::install_dromedary' 14 | -------------------------------------------------------------------------------- /cookbooks/dromedary/test/integration/default/serverspec/ruby_spec.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | 3 | set :backend, :exec 4 | 5 | describe package('ruby') do 6 | it { should be_installed } 7 | end 8 | 9 | describe command('which ruby') do 10 | its(:stdout) { should match /\/bin\/ruby/ } 11 | end 12 | 13 | describe command('ruby -v') do 14 | its(:stdout) { should match /2\.0/ } 15 | end 16 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-infra-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | . ./environment.sh 6 | 7 | gem install rspec aws-sdk 8 | pushd test-infra 9 | export dromedary_security_group="$(aws cloudformation describe-stack-resources --region us-east-1 --stack-name $dromedary_app_stack_name --query StackResources[?LogicalResourceId==\`InstanceSecurityGroup\`].PhysicalResourceId --output text)" 10 | rspec 11 | popd 12 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-acceptance-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | . environment.sh 6 | 7 | dest_host="$(aws cloudformation describe-stacks --stack-name "$dromedary_app_stack_name" --output text --query 'Stacks[0].Outputs[?OutputKey==`PublicDns`].OutputValue')" 8 | if [ -z "$dest_host" ]; then 9 | echo "Empty destination host!" >&2 10 | exit 1 11 | fi 12 | export TARGET_URL=http://$dest_host:8080 13 | 14 | npm install 15 | gulp test-functional 16 | -------------------------------------------------------------------------------- /cookbooks/dromedary/test/integration/default/serverspec/dromedary_spec.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | 3 | set :backend, :exec 4 | 5 | describe file('/dromedary') do 6 | it { should be_directory } 7 | end 8 | 9 | describe file('/dromedary/app.js') do 10 | it { should be_a_file } 11 | end 12 | 13 | describe port(8080) do 14 | it { should be_listening } 15 | end 16 | 17 | describe command("/usr/local/bin/forever list") do 18 | its(:stdout) { should match /dromedary\/app.js/ } 19 | end 20 | -------------------------------------------------------------------------------- /cookbooks/dromedary/test/integration/default/serverspec/nodejs_spec.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | 3 | set :backend, :exec 4 | 5 | describe command('/usr/local/bin/node -v') do 6 | its(:stdout) { should match /v0\.12\.7/ } 7 | end 8 | 9 | describe command('/usr/local/bin/npm -v') do 10 | its(:stdout) { should match /2\.11\.3/ } 11 | end 12 | 13 | describe command('/usr/local/bin/npm list --depth=0 -g 2> /dev/null | grep forever@') do 14 | its(:stdout) { should match /0\.15\.1/ } 15 | end 16 | 17 | -------------------------------------------------------------------------------- /lib/sha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var git = require('git-rev'); 4 | var moment = require('moment'); 5 | 6 | var sha; 7 | 8 | module.exports = function(callback) { 9 | if(!sha) { 10 | git.long(function (rtn) { 11 | if (!rtn) { 12 | sha = moment().format('YYYYMMDD-HHmmss'); 13 | } else { 14 | sha = rtn; 15 | } 16 | 17 | callback(sha); 18 | }); 19 | } else { 20 | callback(sha); 21 | } 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var lambdaExpress = require('lambda-express'); 3 | 4 | // AWS Lambda handler that direct traffic to express app.js 5 | exports.handler = lambdaExpress.appHandler(function(event,context) { 6 | process.env.DROMEDARY_DDB_TABLE_NAME = event.ddbTableName; 7 | console.log("DROMEDARY_DDB_TABLE_NAME = "+event.ddbTableName); 8 | 9 | process.env.AWS_DEFAULT_REGION = AWS.config.region; 10 | console.log("AWS ENV = "+AWS.config.region); 11 | 12 | var app = require('./app.js'); 13 | return app; 14 | }); -------------------------------------------------------------------------------- /cookbooks/dromedary/files/default/nginx/dromedary-site.cfg: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | server_name localhost; 5 | 6 | location / { 7 | proxy_pass http://127.0.0.1:8080; 8 | proxy_http_version 1.1; 9 | proxy_set_header Upgrade $http_upgrade; 10 | proxy_set_header Connection 'upgrade'; 11 | proxy_set_header Host $host; 12 | proxy_set_header X-Real-IP $remote_addr; 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_cache_bypass $http_upgrade; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pipeline/cfn/params-example-lambda.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotificationArn": "arn:aws:sns:us-east-1:ACCTID:TOPICNAME", 3 | "TemplateURL": "https://s3.amazonaws.com/stelligent-training-public/master/dromedary-master.json", 4 | "KeyName": "YOURKEYPAIRNAME", 5 | "Branch": "master", 6 | "GitHubUser": "stelligent", 7 | "BaseTemplateURL": "https://s3.amazonaws.com/stelligent-training-public/master/", 8 | "GitHubToken": "YourGitHubToken", 9 | "DDBTableName": "YourUniqueDDBTableName", 10 | "ProdHostedZone": ".oneclickdeployment.com", 11 | "Domain": "oneclickdeployment.com." 12 | } -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/nginx_config.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: nginx_config 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | cookbook_file '/etc/nginx/sites-available/dromedary' do 11 | source 'nginx/dromedary-site.cfg' 12 | owner 'root' 13 | group 'root' 14 | mode '0644' 15 | action :create 16 | end 17 | 18 | link '/etc/nginx/sites-enabled/000-default' do 19 | to '/etc/nginx/sites-available/dromedary' 20 | end 21 | 22 | service 'nginx' do 23 | action [ :start, :enable ] 24 | end 25 | -------------------------------------------------------------------------------- /bin/cfn-wait-for-stack.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | stack_name="$1" 4 | stack_status='UNKNOWN_IN_PROGRESS' 5 | 6 | echo "Waiting for $stack_name to settle ..." >&2 7 | while [[ $stack_status =~ IN_PROGRESS$ ]]; do 8 | sleep 5 9 | stack_status="$(aws cloudformation describe-stacks --stack-name "$1" --output text --query 'Stacks[0].StackStatus')" 10 | echo " ... $stack_name - $stack_status" >&2 11 | done 12 | echo $stack_status 13 | # if status is failed or we'd rolled back, assume bad things happened 14 | if [[ $stack_status =~ _FAILED$ ]] || [[ $stack_status =~ ROLLBACK ]]; then 15 | exit 1 16 | fi 17 | exit 0 18 | -------------------------------------------------------------------------------- /cookbooks/dromedary/Berksfile.lock: -------------------------------------------------------------------------------- 1 | DEPENDENCIES 2 | dromedary 3 | path: . 4 | metadata: true 5 | 6 | GRAPH 7 | apt (2.8.2) 8 | bluepill (2.3.1) 9 | rsyslog (>= 0.0.0) 10 | build-essential (2.2.3) 11 | dromedary (0.1.0) 12 | nginx (>= 0.0.0) 13 | nginx (2.7.6) 14 | apt (~> 2.2) 15 | bluepill (~> 2.3) 16 | build-essential (~> 2.0) 17 | ohai (~> 2.0) 18 | runit (~> 1.2) 19 | yum-epel (~> 0.3) 20 | ohai (2.0.1) 21 | packagecloud (0.1.0) 22 | rsyslog (2.1.0) 23 | runit (1.7.2) 24 | packagecloud (>= 0.0.0) 25 | yum (3.7.1) 26 | yum-epel (0.6.2) 27 | yum (~> 3.2) 28 | -------------------------------------------------------------------------------- /test-infra/bootstrap/features/bootstrap-teardown.feature: -------------------------------------------------------------------------------- 1 | @teardown 2 | Feature: AWS Test Drive Dromedary Bootstrapper Self-Termination 3 | 4 | Background: 5 | Given I am the bootstrapping instance 6 | 7 | Scenario: 8 | When I have finished bootstrapping Dromedary teardown 9 | Then I should not see a "iam" cloudformation stack 10 | And I should not see a "vpc" cloudformation stack 11 | And I should not see a "ddb" cloudformation stack 12 | And I should not see a "jenkins" cloudformation stack 13 | And I should not see a "pipeline" cloudformation stack 14 | And I should no longer have an environment file from the bootstrapper -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/code_deploy.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: node_modules 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | directory '/tmp/codedeploy' do 11 | owner 'root' 12 | group 'root' 13 | mode '0755' 14 | action :create 15 | end 16 | 17 | remote_file '/tmp/codedeploy/install' do 18 | source "https://s3.amazonaws.com/aws-codedeploy-#{node[:region]}/latest/install" 19 | owner 'root' 20 | group 'root' 21 | mode '0755' 22 | action :create 23 | end 24 | 25 | execute 'codedeploy' do 26 | command '/tmp/codedeploy/install auto' 27 | end 28 | 29 | -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/prereqs.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: prereqs 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | if ['rhel', 'amazon'].include?(node['platform']) 11 | execute 'yum upgrade -y' 12 | end 13 | if ['ubuntu'].include?(node['platform']) 14 | execute 'aptitude update' 15 | execute 'aptitude upgrade -y' 16 | end 17 | 18 | include_recipe 'nginx' 19 | include_recipe 'dromedary::nodejs' 20 | include_recipe 'dromedary::yum_packages' 21 | 22 | service 'nginx' do 23 | action [ :stop, :disable ] 24 | end 25 | 26 | execute 'touch /.dromedary-prereqs-installed' 27 | -------------------------------------------------------------------------------- /appspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.0 2 | os: linux 3 | files: 4 | - source: / 5 | destination: /dromedary 6 | hooks: 7 | # BeforeInstall: 8 | # - location: deploy/scripts/beforeInstall.sh 9 | # timeout: 30 10 | # runas: root 11 | AfterInstall: 12 | - location: deploy/scripts/afterInstall.sh 13 | timeout: 300 14 | runas: root 15 | ApplicationStart: 16 | - location: deploy/scripts/applicationStart.sh 17 | timeout: 30 18 | runas: root 19 | ApplicationStop: 20 | - location: deploy/scripts/applicationStop.sh 21 | timeout: 30 22 | runas: root 23 | ValidateService: 24 | - location: deploy/scripts/validateService.sh 25 | timeout: 30 26 | runas: root 27 | -------------------------------------------------------------------------------- /cookbooks/dromedary/.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: ec2 4 | require_chef_omnibus: true 5 | 6 | security_group_ids: <%= ENV['DROMEDARY_TESTKITCHEN_SG'] %> 7 | aws_ssh_key_id: <%= ENV['DROMEDARY_EC2_KEY'] %> 8 | region: us-east-1 9 | availability_zone: a 10 | instance_type: m4.large 11 | 12 | provisioner: 13 | name: chef_solo 14 | 15 | 16 | platforms: 17 | - name: amazon-linux 18 | driver: 19 | image_id: ami-e3106686 20 | transport: 21 | username: ec2-user 22 | # ssh_key: "/users/jonny/pem/jonny-labs.pem" 23 | 24 | suites: 25 | - name: default 26 | run_list: 27 | - recipe[dromedary::prereqs] 28 | - recipe[dromedary] 29 | attributes: 30 | region: us-east-1 31 | -------------------------------------------------------------------------------- /cookbooks/dromedary/test/integration/default/serverspec/nginx_spec.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | 3 | set :backend, :exec 4 | 5 | describe package('nginx') do 6 | it { should be_installed } 7 | end 8 | 9 | describe service('nginx') do 10 | it { should be_enabled } 11 | it { should be_running } 12 | end 13 | 14 | describe port(80) do 15 | it { should be_listening } 16 | end 17 | 18 | describe port(443) do 19 | # it { should_not be_listening } 20 | it { should be_listening } 21 | end 22 | 23 | # TODO Use a gem for this instead of fork & exec'ing curl 24 | describe command('curl -s http://localhost') do 25 | # describe command('curl --insecure -s https://localhost') do 26 | its(:stdout) { should match /Dromedary/ } 27 | end 28 | -------------------------------------------------------------------------------- /pipeline/cfn/params-example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey":"KeyName", 4 | "ParameterValue":"stelligent-dev" 5 | }, 6 | { 7 | "ParameterKey":"Branch", 8 | "ParameterValue":"master" 9 | }, 10 | { 11 | "ParameterKey":"BaseTemplateURL", 12 | "ParameterValue":"https://s3.amazonaws.com/stelligent-training-public/master/" 13 | }, 14 | { 15 | "ParameterKey":"GitHubUser", 16 | "ParameterValue":"YOURGITHUBUSER" 17 | }, 18 | { 19 | "ParameterKey":"GitHubToken", 20 | "ParameterValue":"YOURGITHUBTOKEN" 21 | }, 22 | { 23 | "ParameterKey":"DDBTableName", 24 | "ParameterValue":"YOURUNIQUEDDBTABLENAME" 25 | }, 26 | { 27 | "ParameterKey":"ProdHostedZone", 28 | "ParameterValue":".YOURHOSTEDZONE" 29 | } 30 | ] -------------------------------------------------------------------------------- /bin/eni-detach.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | script_dir="$(dirname "$0")" 5 | ENVIRONMENT_FILE="$script_dir/../environment.sh" 6 | if [ ! -f "$ENVIRONMENT_FILE" ]; then 7 | echo "Fatal: environment file $ENVIRONMENT_FILE does not exist!" 2>&1 8 | exit 1 9 | fi 10 | 11 | . $ENVIRONMENT_FILE 12 | 13 | eni_id="$(aws cloudformation describe-stacks --stack-name $dromedary_eni_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`EniId`].OutputValue')" 14 | attachment_id="$(aws ec2 describe-network-interfaces --network-interface-ids $eni_id --output text --query 'NetworkInterfaces[0].Attachment.AttachmentId')" 15 | 16 | if [ -n "$attachment_id" -a "$attachment_id" != 'None' ]; then 17 | aws ec2 detach-network-interface --attachment-id $attachment_id 18 | fi 19 | -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/install_dromedary.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: node_modules 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | remote_directory '/dromedary' do 11 | source 'app' 12 | owner 'root' 13 | group 'root' 14 | mode '0755' 15 | action :create 16 | end 17 | 18 | directory '/dromedary/log' do 19 | owner 'root' 20 | group 'root' 21 | mode '0755' 22 | action :create 23 | end 24 | 25 | bash 'dromedary' do 26 | user 'root' 27 | flags '-ex' 28 | code <<-EOH 29 | if /usr/local/bin/forever list | grep -q '^data:'; then 30 | /usr/local/bin/forever stopall 31 | sleep 1 32 | fi 33 | /usr/local/bin/forever /dromedary/app.js >> /dromedary/log/server.log 2>&1 & 34 | EOH 35 | end 36 | -------------------------------------------------------------------------------- /test-infra/spec/security_group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'aws-sdk' 2 | 3 | describe('dromedary_security_group') do 4 | it 'will not allow all traffic on 22' do 5 | ec2 = Aws::EC2::Client.new(region: 'us-east-1') 6 | sg = ENV["dromedary_security_group"] 7 | 8 | expect(sg).to be 9 | 10 | group = ec2.describe_security_groups(filters: [{name: "group-id", values: [ sg ], }, ]).security_groups.first 11 | 12 | twentytwo = group.ip_permissions.select do |perm| 13 | perm.from_port == 22 14 | end 15 | 16 | expect(twentytwo).to be 17 | expect(twentytwo.size).to eq 1 18 | 19 | expect(twentytwo.first.ip_ranges).to be 20 | expect(twentytwo.first.ip_ranges.size).to eq 1 21 | 22 | cidr = twentytwo.first.ip_ranges.first.cidr_ip 23 | 24 | expect(cidr).not_to eq "0.0.0.0/0" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test-infra/bootstrap/features/bootstrap.feature: -------------------------------------------------------------------------------- 1 | @build 2 | Feature: AWS Test Drive Dromedary Bootstrapper 3 | 4 | Background: 5 | Given I am the bootstrapping instance 6 | 7 | Scenario: 8 | When I have finished bootstrapping Dromedary 9 | Then I should see a "iam" cloudformation stack with status "CREATE_COMPLETE" 10 | And I should see a "vpc" cloudformation stack with status "CREATE_COMPLETE" 11 | And I should see a "ddb" cloudformation stack with status "CREATE_COMPLETE" 12 | And I should see a "jenkins" cloudformation stack with status "CREATE_COMPLETE" 13 | And I should see a "pipeline" cloudformation stack with status "CREATE_COMPLETE" 14 | And I should see the dromedary s3 bucket created 15 | And I should have an environment file from the bootstrapper 16 | And the bootstrapping instance should be waiting to self-terminate -------------------------------------------------------------------------------- /test-functional/endpoint-index.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var fs = require('fs'); 3 | var rp = require('request-promise'); 4 | var targetUrl = process.env.hasOwnProperty('TARGET_URL') ? process.env.TARGET_URL : 'http://localhost:8080'; 5 | 6 | describe("/", function() { 7 | var expectedIndex; 8 | before(function() { 9 | expectedIndex = fs.readFileSync(__dirname+'/../public/index.html').toString('utf-8'); 10 | }); 11 | 12 | var servedIndex; 13 | beforeEach(function(done) { 14 | rp({ uri: targetUrl+'/'}) 15 | .then(function(data) { 16 | servedIndex = data; 17 | done(); 18 | }).catch(function(err) { 19 | throw err; 20 | }) 21 | }); 22 | 23 | it("serves ./public/index.html", function() { 24 | expect(servedIndex).to.equal(expectedIndex); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /cookbooks/dromedary/files/default/nginx/ssl-dromedary-site.cfg: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | return 301 https://$host$request_uri; 4 | } 5 | 6 | server { 7 | listen 443 ssl; 8 | server_name localhost; 9 | ssl_certificate /etc/nginx/ssl/nginx.crt; 10 | ssl_certificate_key /etc/nginx/ssl/nginx.key; 11 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 12 | ssl_ciphers HIGH:!aNULL:!MD5; 13 | location / { 14 | proxy_pass http://127.0.0.1:8080; 15 | proxy_http_version 1.1; 16 | proxy_set_header Upgrade $http_upgrade; 17 | proxy_set_header Connection 'upgrade'; 18 | proxy_set_header Host $host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_cache_bypass $http_upgrade; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # Project ignores 30 | dist/ 31 | environment.sh 32 | .kitchen/ 33 | .kitchen.local.yml 34 | ddb-local/ 35 | 36 | # you might want to take this out if you ever start using files in a non-hacky way. 37 | cookbooks/dromedary/files/default/app 38 | 39 | # IDE 40 | .idea/ 41 | *.iml 42 | -------------------------------------------------------------------------------- /test-functional/endpoint-config.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var rp = require('request-promise'); 3 | var targetUrl = process.env.hasOwnProperty('TARGET_URL') ? process.env.TARGET_URL : 'http://localhost:8080'; 4 | var shaRegex = /^([0-9a-f]{40}|[0-9]{8}-[0-9]{6})$/; 5 | 6 | describe("/config.json", function() { 7 | var resp; 8 | beforeEach(function(done) { 9 | rp({ uri: targetUrl+'/config.json', json:true}) 10 | .then(function (data) { 11 | resp = data; 12 | done(); 13 | }) 14 | .catch(function (err) { 15 | throw err; 16 | }); 17 | 18 | console.log(resp) 19 | }); 20 | 21 | it("response contains version key", function() { 22 | expect(resp).to.include.keys('version'); 23 | }); 24 | it("response contains apiBaseurl key", function() { 25 | expect(resp).to.include.keys('apiBaseurl'); 26 | }); 27 | it("version value matches regex: " + shaRegex.toString(), function() { 28 | expect(resp.version).to.match(shaRegex); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /cookbooks/dromedary/packer.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "ami_name": "", 4 | "vpc_id": "", 5 | "subnet_id": "", 6 | "sg_id": "", 7 | "dist_dir": "" 8 | }, 9 | "builders": [ 10 | { 11 | "type": "amazon-ebs", 12 | "associate_public_ip_address": true, 13 | "ssh_pty": "true", 14 | "region": "us-east-1", 15 | "source_ami": "ami-e3106686", 16 | "instance_type": "m4.large", 17 | "ssh_username": "ec2-user", 18 | "ami_name": "{{user `ami_name`}}", 19 | "subnet_id": "{{user `subnet_id`}}", 20 | "vpc_id": "{{user `vpc_id`}}", 21 | "security_group_id": "{{user `sg_id`}}" 22 | } 23 | ], 24 | "provisioners": [ 25 | { 26 | "type": "chef-solo", 27 | "cookbook_paths": [ 28 | "{{user `dist_dir`}}" 29 | ], 30 | "run_list": [ 31 | "dromedary::prereqs" 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /pipeline/jobs/dsl/codepipeline.groovy: -------------------------------------------------------------------------------- 1 | job('example') { 2 | triggers { 3 | scm("* * * * *") 4 | } 5 | steps { 6 | shell("echo hello world") 7 | } 8 | 9 | configure { project -> 10 | project.remove(project / scm) // remove the existing 'scm' element 11 | 12 | project / scm(class: 'com.amazonaws.codepipeline.jenkinsplugin.AWSCodePipelineSCM', plugin: 'codepipeline@0.8') { 13 | clearWorkspace true 14 | actionTypeCategory 'Build' 15 | actionTypeProvider "JenkinsJPSTUE564bc1e4" 16 | projectName 'example' 17 | awsAccessKey '' 18 | awsSecretKey '' 19 | actionTypeVersion 1 20 | proxyHost '' 21 | proxyPort 0 22 | region "us-east-1" 23 | awsClientFactory {} 24 | } 25 | project.remove(project / publishers) 26 | project / publishers / "com.amazonaws.codepipeline.jenkinsplugin.AWSCodePipelinePublisher"(plugin:'codepipeline@0.8') { 27 | buildOutputs { 28 | "com.amazonaws.codepipeline.jenkinsplugin.AWSCodePipelinePublisher_-OutputTuple" { 29 | outputString "" 30 | } 31 | } 32 | awsClientFactory {} 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/nodejs.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: nodejs 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | 9 | ## NOTE: Moved nodejs install to this cookbook 10 | ## It'd be nice to use an open source cookbook, but the one Jonny found 11 | ## installed a very old version of node. Thie version matches what is installed 12 | ## on our Macbooks via Homebrew. 13 | remote_file '/tmp/node-install.tar.gz' do 14 | source "https://nodejs.org/dist/v0.12.7/node-v0.12.7-linux-x64.tar.gz" 15 | owner 'root' 16 | group 'root' 17 | mode '0644' 18 | action :create 19 | end 20 | 21 | bash 'install nodejs' do 22 | user 'root' 23 | flags '-ex' 24 | cwd '/usr/local' 25 | code 'tar --strip-components 1 -xzf /tmp/node-install.tar.gz' 26 | end 27 | 28 | bash 'symlink nodejs' do 29 | user 'root' 30 | flags '-ex' 31 | cwd '/usr/local' 32 | code 'test -L /usr/bin/node || ln -s /usr/local/bin/node /usr/bin/node' 33 | end 34 | 35 | bash 'install forever' do 36 | user 'root' 37 | flags '-ex' 38 | code '/usr/local/bin/npm install -g forever@0.15.1' 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2020 Stelligent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/packer-create-ami.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | script_dir="$(dirname "$0")" 5 | ENVIRONMENT_FILE="$script_dir/../environment.sh" 6 | if [ ! -f "$ENVIRONMENT_FILE" ]; then 7 | echo "Fatal: environment file $ENVIRONMENT_FILE does not exist!" 2>&1 8 | exit 1 9 | fi 10 | 11 | . $ENVIRONMENT_FILE 12 | 13 | vpc_id="$(aws cloudformation describe-stacks --stack-name $dromedary_vpc_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`VPC`].OutputValue')" 14 | subnet_id="$(aws cloudformation describe-stacks --stack-name $dromedary_vpc_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`SubnetId`].OutputValue')" 15 | # borrowing security group from Jenkins 16 | sg_id="$(aws cloudformation describe-stacks --stack-name $dromedary_jenkins_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`SecurityGroup`].OutputValue')" 17 | 18 | set -x 19 | 20 | cd "$(dirname "$0")/.." 21 | npm install 22 | gulp dist 23 | packer build \ 24 | -var "vpc_id=$vpc_id" \ 25 | -var "subnet_id=$subnet_id" \ 26 | -var "sg_id=$sg_id" \ 27 | -var "ami_name=dromedary_ami_created_`date +%Y%m%d%H%M%S`" \ 28 | -var "dist_dir=dist/" \ 29 | cookbooks/dromedary/packer.json 30 | -------------------------------------------------------------------------------- /cookbooks/dromedary/recipes/ssl_nginx_config.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: dromedary 3 | # Recipe:: nginx_config 4 | # 5 | # Copyright (C) 2015 Stelligent 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | cookbook_file '/etc/nginx/sites-available/dromedary' do 11 | source 'nginx/ssl-dromedary-site.cfg' 12 | owner 'root' 13 | group 'root' 14 | mode '0644' 15 | action :create 16 | end 17 | 18 | cookbook_file '/tmp/generate-cert.sh' do 19 | source 'nginx/generate-cert.sh' 20 | owner 'root' 21 | group 'root' 22 | mode '0700' 23 | action :create 24 | end 25 | 26 | execute 'generate-cert' do 27 | cwd '/tmp' 28 | command '/tmp/generate-cert.sh dromedary' 29 | end 30 | 31 | directory '/etc/nginx/ssl/' do 32 | owner 'root' 33 | group 'root' 34 | mode '0755' 35 | action :create 36 | end 37 | 38 | remote_file '/etc/nginx/ssl/nginx.crt' do 39 | source 'file:///tmp/dromedary.crt' 40 | owner 'root' 41 | group 'root' 42 | mode '0755' 43 | action :create 44 | end 45 | 46 | remote_file '/etc/nginx/ssl/nginx.key' do 47 | source 'file:///tmp/dromedary.key' 48 | owner 'root' 49 | group 'root' 50 | mode '0755' 51 | action :create 52 | end 53 | 54 | link '/etc/nginx/sites-enabled/000-default' do 55 | to '/etc/nginx/sites-available/dromedary' 56 | end 57 | 58 | service 'nginx' do 59 | action [ :start, :enable ] 60 | end 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dromedary", 3 | "version": "0.1.0", 4 | "description": "- Stelligent Demo Chart API", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "gulp test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/stelligent/dromedary.git" 12 | }, 13 | "author": { 14 | "name": "Stelligent Systems, LLC" 15 | }, 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/stelligent/dromedary/issues" 19 | }, 20 | "homepage": "https://github.com/stelligent/dromedary#readme", 21 | "devDependencies": { 22 | "chai": "^3.2.0", 23 | "del": "^1.2.1", 24 | "gulp": "^3.9.0", 25 | "gulp-bg": "0.0.7", 26 | "gulp-download": "0.0.1", 27 | "gulp-gunzip": "0.0.3", 28 | "gulp-gzip": "^1.2.0", 29 | "gulp-install": "^0.5.0", 30 | "gulp-jshint": "^1.11.2", 31 | "gulp-live-server": "0.0.28", 32 | "gulp-mocha": "^2.1.3", 33 | "gulp-tar": "^1.4.0", 34 | "gulp-untar": "0.0.4", 35 | "gulp-util": "^3.0.6", 36 | "gulp-zip": "^3.2.0", 37 | "mocha": "^2.2.5", 38 | "request-promise": "^2.0.1", 39 | "run-sequence": "^1.1.2", 40 | "yargs": "^3.19.0" 41 | }, 42 | "dependencies": { 43 | "aws-sdk": "^2.2.3", 44 | "express": "^4.13.3", 45 | "git-rev": "^0.2.1", 46 | "lambda-express": "^0.1.2", 47 | "moment": "^2.10.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/requestThrottle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Module = (function () { 4 | 5 | var requestThrottle = {}; 6 | var blackoutPeriod = 100; 7 | var ipMap = {}; 8 | 9 | requestThrottle.clearIpMap = function () { 10 | ipMap = {}; 11 | }; 12 | 13 | requestThrottle.gcMap = function () { 14 | var ip; 15 | var t = Date.now(); 16 | for (ip in ipMap) { 17 | if (ipMap.hasOwnProperty(ip) && t > ipMap[ip]) { 18 | console.log('Deleted ip from throttle map: ' + ip); 19 | delete ipMap[ip]; 20 | } 21 | } 22 | }; 23 | 24 | requestThrottle.setBlackoutPeriod = function (milliseconds) { 25 | blackoutPeriod = milliseconds; 26 | return blackoutPeriod; 27 | }; 28 | 29 | requestThrottle.getBlackoutPeriod = function () { 30 | return blackoutPeriod; 31 | }; 32 | 33 | requestThrottle.logIp = function (ipAddress, expiration) { 34 | if (!expiration) { 35 | expiration = Date.now() + blackoutPeriod; 36 | } 37 | ipMap[ipAddress] = expiration; 38 | }; 39 | 40 | requestThrottle.ipIsInMap = function (ipAddress) { 41 | return ipMap.hasOwnProperty(ipAddress); 42 | }; 43 | 44 | requestThrottle.checkIp = function (ipAddress) { 45 | if (ipMap.hasOwnProperty(ipAddress)) { 46 | if (Date.now() < ipMap[ipAddress]) { 47 | return false; 48 | } 49 | } 50 | return true; 51 | }; 52 | 53 | return requestThrottle; 54 | }()); 55 | 56 | module.exports = Module; 57 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | html{ 2 | font-family: "futura-pt",sans-serif; 3 | } 4 | 5 | body{ 6 | margin-top: 40px; 7 | font-family: "futura-pt",sans-serif; 8 | } 9 | 10 | .logo{ 11 | margin-bottom:40px; 12 | margin-top: 60px; 13 | } 14 | 15 | p{ 16 | font-size:20px; 17 | font-family: "futura-pt",sans-serif; 18 | } 19 | 20 | h2{ 21 | font-size:40px; 22 | padding-bottom:12px; 23 | font-weight: bold; 24 | font-family: "futura-pt",sans-serif; 25 | } 26 | 27 | h1{ 28 | font-family: "futura-pt",sans-serif; 29 | } 30 | 31 | ul{ 32 | list-style: none; 33 | } 34 | 35 | li{ 36 | font-size: 20px; 37 | display:block; 38 | padding: 16px; 39 | margin-bottom:2px; 40 | border-radius: 2px; 41 | font-family: "futura-pt",sans-serif; 42 | } 43 | 44 | .timestamp{ 45 | font-size:13px; 46 | display:block; 47 | font-weight: bold; 48 | color: #666; 49 | text-transform: uppercase; 50 | padding-top:12px; 51 | } 52 | 53 | .fixed-height{ 54 | max-height: 590px; 55 | overflow:hidden; 56 | } 57 | 58 | .totnum{ 59 | font-size: 26px; 60 | } 61 | 62 | .padtop{ 63 | padding-top: 60px; 64 | clear:both; 65 | } 66 | 67 | .border-right{ 68 | border-right: 1px solid #666; 69 | } 70 | 71 | .bordered{ 72 | padding-top: 20px; 73 | padding-bottom: 20px; 74 | border-top:1px solid #666; 75 | border-bottom: 1px solid #666; 76 | margin-top: 40px; 77 | } 78 | 79 | /* Small Devices, Tablets */ 80 | @media only screen and (max-width : 1000px) { 81 | .border-right{ 82 | border:none; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | * ENVIRONMENTS 4 | * ================= 5 | */ 6 | 7 | // Define globals exposed by modern browsers. 8 | "browser": true, 9 | 10 | // Define globals exposed by jQuery. 11 | "jquery": true, 12 | 13 | // Define globals exposed by Node.js. 14 | "node": true, 15 | 16 | // Allow ES6. 17 | "esnext": true, 18 | 19 | /* 20 | * ENFORCING OPTIONS 21 | * ================= 22 | */ 23 | 24 | // Force all variable names to use either camelCase style or UPPER_CASE 25 | // with underscores. 26 | "camelcase": true, 27 | 28 | // Prohibit use of == and != in favor of === and !==. 29 | "eqeqeq": true, 30 | 31 | // Enforce tab width of 2 spaces. 32 | "indent": 2, 33 | 34 | // Prohibit use of a variable before it is defined. 35 | "latedef": true, 36 | 37 | // Enforce line length to 80 characters 38 | "maxlen": 80, 39 | 40 | // Require capitalized names for constructor functions. 41 | "newcap": true, 42 | 43 | // Enforce use of single quotation marks for strings. 44 | "quotmark": "single", 45 | 46 | // Enforce placing 'use strict' at the top function scope 47 | "strict": true, 48 | 49 | // Prohibit use of explicitly undeclared variables. 50 | "undef": true, 51 | 52 | // Warn when variables are defined but never used. 53 | "unused": true, 54 | 55 | /* 56 | * RELAXING OPTIONS 57 | * ================= 58 | */ 59 | 60 | // Suppress warnings about == null comparisons. 61 | "eqnull": true 62 | } 63 | -------------------------------------------------------------------------------- /pipeline/jobs/scripts/drom-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . /etc/profile 3 | set -ex 4 | 5 | # setup environment.sh 6 | if [ -n "$AWS_DEFAULT_REGION" ]; then 7 | echo "export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION" > environment.sh 8 | else 9 | echo "export AWS_DEFAULT_REGION=us-east-1" > environment.sh 10 | fi 11 | echo "export dromedary_s3_bucket=$DROMEDARY_S3_BUCKET" >> environment.sh 12 | echo "export dromedary_vpc_stack_name=$DROMEDARY_VPC_STACK" >> environment.sh 13 | echo "export dromedary_iam_stack_name=$DROMEDARY_IAM_STACK" >> environment.sh 14 | echo "export dromedary_ddb_stack_name=$DROMEDARY_DDB_STACK" >> environment.sh 15 | echo "export dromedary_eni_stack_name=$DROMEDARY_ENI_STACK" >> environment.sh 16 | echo "export dromedary_ec2_key=$DROMEDARY_EC2_KEY" >> environment.sh 17 | echo "export dromedary_hostname=$DROMEDARY_HOSTNAME" >> environment.sh 18 | echo "export dromedary_domainname=$DROMEDARY_DOMAINNAME" >> environment.sh 19 | echo "export dromedary_zone_id=$DROMEDARY_ZONE_ID" >> environment.sh 20 | echo "export dromedary_artifact=dromedary-$(date +%Y%m%d-%H%M%S).tar.gz" >> environment.sh 21 | echo "export dromedary_custom_action_provider=$DROMEDARY_ACTION_PROVIDER" >> environment.sh 22 | 23 | . environment.sh 24 | 25 | # since the workspace is maintained throughout the build, 26 | # install dependencies now in a clear workspace 27 | rm -rf node_modules dist 28 | npm install 29 | 30 | # build and upload artifact 31 | gulp dist 32 | aws s3 cp dist/archive.tar.gz s3://$dromedary_s3_bucket/$dromedary_artifact 33 | -------------------------------------------------------------------------------- /pipeline/jobs/dsl/views.groovy: -------------------------------------------------------------------------------- 1 | listView('ops') { 2 | description('ops') 3 | jobs { 4 | name('job-seed') 5 | name('DA-commit-poll-scm') 6 | name('DA-selfservice-init') 7 | } 8 | columns { 9 | status() 10 | weather() 11 | name() 12 | lastSuccess() 13 | lastFailure() 14 | lastDuration() 15 | buildButton() 16 | } 17 | } 18 | 19 | listView('Dromedary') { 20 | description('Dromedary CodePipeline Jobs') 21 | jobs { 22 | regex('drom-.+') 23 | } 24 | columns { 25 | status() 26 | weather() 27 | name() 28 | lastSuccess() 29 | lastFailure() 30 | lastDuration() 31 | buildButton() 32 | } 33 | } 34 | 35 | deliveryPipelineView('Dummy App CD Pipeline') { 36 | pipelineInstances(5) 37 | columns(1) 38 | updateInterval(5) 39 | enableManualTriggers() 40 | pipelines { 41 | component('Dummy Application', 'DA-commit-poll-scm') 42 | } 43 | } 44 | 45 | deliveryPipelineView('Dummy App Self-Service') { 46 | pipelineInstances(5) 47 | columns(1) 48 | updateInterval(5) 49 | enableManualTriggers() 50 | pipelines { 51 | component('Dummy Application', 'DA-selfservice-init') 52 | } 53 | } 54 | 55 | listView('Dummy App Jobs') { 56 | description('Dummy Application') 57 | jobs { 58 | regex('DA.+') 59 | } 60 | columns { 61 | status() 62 | weather() 63 | name() 64 | lastSuccess() 65 | lastFailure() 66 | lastDuration() 67 | buildButton() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cookbooks/dromedary/files/default/nginx/generate-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # shamelessly stolen from https://gist.github.com/bradland/1690807. 4 | 5 | # Script accepts a single argument, the fqdn for the cert 6 | DOMAIN="$1" 7 | if [ -z "$DOMAIN" ]; then 8 | echo "Usage: $(basename $0) " 9 | exit 11 10 | fi 11 | 12 | fail_if_error() { 13 | [ $1 != 0 ] && { 14 | unset PASSPHRASE 15 | exit 10 16 | } 17 | } 18 | 19 | # Generate a passphrase 20 | export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo) 21 | 22 | # Certificate details; replace items in angle brackets with your own info 23 | subj=" 24 | C=US 25 | ST=VA 26 | O=Stelligent 27 | localityName=Reston 28 | commonName=$DOMAIN 29 | organizationalUnitName=Funny Hats 30 | emailAddress=paul@stelligent.com 31 | " 32 | 33 | # Generate the server private key 34 | openssl genrsa -des3 -out $DOMAIN.key -passout env:PASSPHRASE 2048 35 | fail_if_error $? 36 | 37 | # Generate the CSR 38 | openssl req \ 39 | -new \ 40 | -batch \ 41 | -subj "$(echo -n "$subj" | tr "\n" "/")" \ 42 | -key $DOMAIN.key \ 43 | -out $DOMAIN.csr \ 44 | -passin env:PASSPHRASE 45 | fail_if_error $? 46 | cp $DOMAIN.key $DOMAIN.key.org 47 | fail_if_error $? 48 | 49 | # Strip the password so we don't have to type it every time we restart Apache 50 | openssl rsa -in $DOMAIN.key.org -out $DOMAIN.key -passin env:PASSPHRASE 51 | fail_if_error $? 52 | echo password stripped 53 | 54 | # Generate the cert (good for 10 years) 55 | openssl x509 -req -days 3650 -in $DOMAIN.csr -signkey $DOMAIN.key -out $DOMAIN.crt 56 | fail_if_error $? 57 | echo cert generated 58 | -------------------------------------------------------------------------------- /cookbooks/dromedary/README.md: -------------------------------------------------------------------------------- 1 | # dromedary-cookbook 2 | 3 | Cookbook for setting up the Dromedary application and any preqrequisites. 4 | 5 | ## Supported Platforms 6 | 7 | Tested with AWS Linux. Should work on CentOS as well. Ubuntu no promises. 8 | 9 | ## Attributes 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
KeyTypeDescriptionDefault
['dromedary']['S3Bucket']StringThe name of the S3 bucket that artifacts are stored in by the scripts and pipelinetrue
['dromedary']['ArtifactPath']StringThe name of the artifact in the S3 bucket to deploy.true
31 | 32 | ## Usage 33 | 34 | ### dromedary::default 35 | 36 | Include `dromedary` in your node's `run_list`: 37 | 38 | ```json 39 | { 40 | "run_list": [ 41 | "recipe[dromedary::default]" 42 | "recipe[dromedary::install_dromedary]"" 43 | 44 | ] 45 | } 46 | ``` 47 | 48 | `default` will install all prereqs. 49 | `install_dromedary` will install the app. 50 | 51 | ## Testing 52 | 53 | Test Kitchen should work straight away with only minor edits to your .kitchen.yml. You'll need to update the S3Bucket name and the Artifact Path. 54 | 55 | Commands: 56 | 57 | `kitchen create` to start your Vagrant VM. 58 | `kitchen converge` to run your cookbooks. 59 | `kitchen verify` to run your tests. 60 | `kitchen destroy` to destroy your Vagrant VM. 61 | `kitchen test` to do all those things. -------------------------------------------------------------------------------- /test/requestThrottle.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var reqThrottle = require("../lib/requestThrottle.js"); 3 | 4 | describe("requestThrottle", function() { 5 | describe(".logIp()", function() { 6 | beforeEach(function() { 7 | reqThrottle.clearIpMap(); 8 | reqThrottle.logIp('127.0.0.1'); 9 | }); 10 | 11 | it("adds ip to map", function() { 12 | expect(reqThrottle.ipIsInMap('127.0.0.1')).to.be.true; 13 | }); 14 | }); 15 | 16 | describe(".checkIp()", function() { 17 | beforeEach(function() { 18 | reqThrottle.clearIpMap(); 19 | reqThrottle.logIp('127.0.0.1', Date.now() + 1000); 20 | reqThrottle.logIp('127.0.0.2', Date.now() - 1000); 21 | }); 22 | 23 | it("does throttle mapped ip", function() { 24 | expect(reqThrottle.checkIp('127.0.0.1')).to.be.false; 25 | }); 26 | 27 | it("does not throttle mapped ip after blackoutPeriod", function() { 28 | expect(reqThrottle.checkIp('127.0.0.2')).to.be.true; 29 | }); 30 | 31 | it("does not throttle unmapped ip", function() { 32 | expect(reqThrottle.checkIp('127.0.0.3')).to.be.true; 33 | }); 34 | }); 35 | 36 | describe(".gcMap()", function() { 37 | beforeEach(function() { 38 | reqThrottle.clearIpMap(); 39 | reqThrottle.logIp('127.0.0.1', Date.now() + 1000); 40 | reqThrottle.logIp('127.0.0.2', Date.now() - 1000); 41 | reqThrottle.gcMap(); 42 | }); 43 | it("removes expired ip", function() { 44 | expect(reqThrottle.ipIsInMap('127.0.0.2')).to.be.false; 45 | }); 46 | it("does not remove unexpired ip", function() { 47 | expect(reqThrottle.ipIsInMap('127.0.0.1')).to.be.true; 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test-infra/bootstrap/features/step_definitions/bootstrap.rb: -------------------------------------------------------------------------------- 1 | require 'cucumber' 2 | require 'rspec' 3 | require 'aws-sdk' 4 | 5 | cfn = Aws::CloudFormation::Client.new 6 | s3 = Aws::S3::Client.new 7 | environment_file = "#{ENV["ENVFILE"]}" 8 | 9 | Given(/^I am the bootstrapping instance$/) do 10 | bootstrapper = %x[/opt/aws/bin/ec2-metadata | grep bootstrapper] 11 | expect(bootstrapper).to_not be_nil 12 | end 13 | 14 | When(/^I have finished bootstrapping Dromedary teardown$/) do 15 | expect(File.file?(environment_file)).to be false 16 | end 17 | 18 | Then(/^I should see a "([^"]*)" cloudformation stack with status "([^"]*)"$/) do |arg1, arg2| 19 | stack_status = cfn.describe_stacks(:stack_name => "#{ENV["PROD"]}-#{arg1}").stacks[0].stack_status 20 | expect(stack_status).to eq(arg2) 21 | end 22 | 23 | Then(/^I should not see a "([^"]*)" cloudformation stack$/) do |arg1| 24 | expect{cfn.describe_stacks(:stack_name => "#{ENV["PROD"]}-#{arg1}")}.to raise_error(Aws::CloudFormation::Errors::ValidationError) 25 | end 26 | 27 | Then(/^I should no longer have an environment file from the bootstrapper$/) do 28 | expect(File.file?(environment_file)).to be false 29 | end 30 | 31 | When(/^I have finished bootstrapping Dromedary$/) do 32 | expect(File.file?(environment_file)).to be true 33 | end 34 | 35 | Then(/^I should see the dromedary s3 bucket created$/) do 36 | bucket = s3.head_bucket({ bucket: "dromedary-#{ENV["ACCTID"]}" }) 37 | expect(bucket).to be_empty 38 | end 39 | 40 | Then(/^I should have an environment file from the bootstrapper$/) do 41 | expect(File.file?(environment_file)).to be true 42 | end 43 | 44 | Then(/^the bootstrapping instance should be waiting to self\-terminate$/) do 45 | sleeping = %x[ps aux | grep 'sleep\ ' | wc -l] 46 | expect(sleeping.to_i).to be >= 2 47 | end -------------------------------------------------------------------------------- /pipeline/cfn/dynamodb.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Dromedary demo - DynamoDB Table", 4 | "Parameters":{ 5 | "ReadCapacityUnits":{ 6 | "Type":"Number", 7 | "Description":"Provisioned Read Capacity Units", 8 | "Default":"5" 9 | }, 10 | "WriteCapacityUnits":{ 11 | "Type":"Number", 12 | "Description":"Provisioned Write Capacity Units", 13 | "Default":"5" 14 | }, 15 | "DDBTableName":{ 16 | "Type":"String", 17 | "Description":"Unique name for the Dromedary Dynamo DB table" 18 | } 19 | }, 20 | "Resources":{ 21 | "Table":{ 22 | "Type":"AWS::DynamoDB::Table", 23 | "Properties":{ 24 | "TableName":{ 25 | "Ref":"DDBTableName" 26 | }, 27 | "AttributeDefinitions":[ 28 | { 29 | "AttributeName":"site_name", 30 | "AttributeType":"S" 31 | }, 32 | { 33 | "AttributeName":"color_name", 34 | "AttributeType":"S" 35 | } 36 | ], 37 | "KeySchema":[ 38 | { 39 | "AttributeName":"site_name", 40 | "KeyType":"HASH" 41 | }, 42 | { 43 | "AttributeName":"color_name", 44 | "KeyType":"RANGE" 45 | } 46 | ], 47 | "ProvisionedThroughput":{ 48 | "ReadCapacityUnits":{ 49 | "Ref":"ReadCapacityUnits" 50 | }, 51 | "WriteCapacityUnits":{ 52 | "Ref":"WriteCapacityUnits" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "Outputs":{ 59 | "StackName":{ 60 | "Value":{ 61 | "Ref":"AWS::StackName" 62 | } 63 | }, 64 | "TableName":{ 65 | "Description":"Name of DynamoDB Table", 66 | "Value":{ 67 | "Ref":"Table" 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /lib/inMemoryStorage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Constructor() { 4 | 5 | var chartData = { 6 | values: { 7 | darkblue: { 8 | label: 'DarkBlue', 9 | value: 10, 10 | color:'#000066', 11 | highlight: '#6F6F6F' 12 | }, 13 | red: { 14 | label: 'Red', 15 | value: 10, 16 | color: '#CC0000', 17 | highlight: '#C9DF6E' 18 | }, 19 | yellow: { 20 | label: 'Yellow', 21 | value: 10, 22 | color:'#FF9900', 23 | highlight: '#FFB75E' 24 | } 25 | } 26 | }; 27 | 28 | this.getForChartJs = function () { 29 | var returnList = []; 30 | var k; 31 | for (k in chartData.values) { 32 | if (chartData.values.hasOwnProperty(k)) { 33 | returnList.push(chartData.values[k]); 34 | } 35 | } 36 | return returnList; 37 | }; 38 | 39 | this.getAllCounts = function() { 40 | var allCounts = {}; 41 | var k; 42 | for (k in chartData.values) { 43 | if (chartData.values.hasOwnProperty(k)) { 44 | allCounts[k] = chartData.values[k].value; 45 | } 46 | } 47 | return allCounts; 48 | }; 49 | 50 | this.getCount = function(color) { 51 | if (chartData.values.hasOwnProperty(color)) { 52 | return chartData.values[color].value; 53 | } 54 | return -1; 55 | }; 56 | 57 | this.incrementCount = function(color) { 58 | if (chartData.values.hasOwnProperty(color)) { 59 | chartData.values[color].value++; 60 | } 61 | }; 62 | 63 | this.setCounts = function(counts) { 64 | var k; 65 | for (k in counts) { 66 | if (counts.hasOwnProperty(k) && chartData.values.hasOwnProperty(k)) { 67 | chartData.values[k].value = counts[k]; 68 | } 69 | } 70 | }; 71 | 72 | this.colorExists = function(color) { 73 | return chartData.values.hasOwnProperty(color); 74 | }; 75 | } 76 | 77 | module.exports = Constructor; 78 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-unit-test/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | drom-unit-test 10 | Test 11 | DromedaryJenkins 12 | 1 13 | us-east-1 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | 26 | * * * * * 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | bash ./pipeline/jobs/scripts/drom-unit-test.sh 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | xterm 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-promote-env/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | drom-promote-env 10 | Test 11 | DromedaryJenkins 12 | 1 13 | us-east-1 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | 26 | * * * * * 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | bash ./pipeline/jobs/scripts/drom-promote-env.sh 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | xterm 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-staticcode-anal/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | drom-staticcode-anal 10 | Test 11 | DromedaryJenkins 12 | 1 13 | us-east-1 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | 26 | * * * * * 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | bash ./pipeline/jobs/scripts/drom-staticcode-anal.sh 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | xterm 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/job-seed/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Runs the Job DSL generator to create the dummy pipeline. 5 | false 6 | 7 | 8 | 2 9 | 10 | 11 | https://github.com/stelligent/dromedary.git 12 | 13 | 14 | 15 | 16 | */BRANCH_PLACEHOLDER 17 | 18 | 19 | false 20 | 21 | 22 | 23 | true 24 | false 25 | false 26 | false 27 | 28 | 29 | */5 * * * * 30 | false 31 | 32 | 33 | false 34 | 35 | 36 | pipeline/jobs/dsl/*.groovy 37 | false 38 | false 39 | DELETE 40 | DELETE 41 | JENKINS_ROOT 42 | 43 | 44 | 45 | 46 | 47 | 48 | xterm 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-create-env/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | drom-create-env 10 | Test 11 | DromedaryJenkins 12 | 1 13 | us-east-1 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | 26 | * * * * * 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | bash ./pipeline/jobs/scripts/drom-create-env.sh 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | xterm 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /pipeline/jobs/dsl/dummyselfservice.groovy: -------------------------------------------------------------------------------- 1 | // Dummy Application (DA)(DummySelfService) 2 | 3 | freeStyleJob ('DA-selfservice-init') { 4 | steps { 5 | shell('sleep 1') 6 | } 7 | publishers { 8 | downstream('DA-selfservice-create-env', 'SUCCESS') 9 | } 10 | deliveryPipelineConfiguration('Self-Service Pipeline', 'request environment') 11 | } 12 | 13 | freeStyleJob ('DA-selfservice-create-env') { 14 | steps { 15 | customWorkspace('dummypipeline') 16 | shell('sleep 1.5') 17 | } 18 | publishers { 19 | downstream('DA-selfservice-node-config-env', 'SUCCESS') 20 | } 21 | deliveryPipelineConfiguration('Self-Service Pipeline', 'provision environment') 22 | } 23 | 24 | freeStyleJob ('DA-selfservice-node-config-env') { 25 | steps { 26 | customWorkspace('dummypipeline') 27 | shell('sleep 1.5') 28 | } 29 | publishers { 30 | downstream('DA-selfservice-load-db', 'SUCCESS') 31 | } 32 | deliveryPipelineConfiguration('Self-Service Pipeline', 'configure environment') 33 | } 34 | 35 | freeStyleJob ('DA-selfservice-load-db') { 36 | steps { 37 | customWorkspace('dummypipeline') 38 | shell('sleep 1.5') 39 | } 40 | publishers { 41 | downstream('DA-selfservice-migrate-db', 'SUCCESS') 42 | } 43 | deliveryPipelineConfiguration('Self-Service Pipeline', 'load test db') 44 | } 45 | 46 | freeStyleJob ('DA-selfservice-migrate-db') { 47 | steps { 48 | customWorkspace('dummypipeline') 49 | shell('sleep 1.5') 50 | } 51 | publishers { 52 | downstream('DA-selfservice-deploy-app', 'SUCCESS') 53 | } 54 | deliveryPipelineConfiguration('Self-Service Pipeline', 'migrate test db') 55 | } 56 | 57 | freeStyleJob ('DA-selfservice-deploy-app') { 58 | steps { 59 | customWorkspace('dummypipeline') 60 | shell('sleep 1.5') 61 | } 62 | publishers { 63 | downstream('DA-selfservice-smoketest', 'SUCCESS') 64 | } 65 | deliveryPipelineConfiguration('Self-Service Pipeline', 'deploy application') 66 | } 67 | 68 | freeStyleJob ('DA-selfservice-smoketest') { 69 | steps { 70 | customWorkspace('dummypipeline') 71 | shell('sleep 1.5') 72 | } 73 | deliveryPipelineConfiguration('Self-Service Pipeline', 'smoke test environment') 74 | } 75 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-acceptance-test/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | drom-acceptance-test 10 | Test 11 | DromedaryJenkins 12 | 1 13 | us-east-1 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | 26 | * * * * * 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | bash ./pipeline/jobs/scripts/drom-acceptance-test.sh 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | xterm 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /test-functional/endpoint-increment.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var rp = require('request-promise'); 3 | var targetUrl = process.env.hasOwnProperty('TARGET_URL') ? process.env.TARGET_URL : 'http://localhost:8080'; 4 | 5 | describe("/increment", function() { 6 | var chartData, initialColorCount, color, expectedNewColorCount; 7 | var incrementResponse, newColorCounts, badIncrementResponse; 8 | 9 | this.timeout(15000); 10 | 11 | before(function(done) { 12 | var apiBaseurl; 13 | 14 | rp({ uri: targetUrl+'/config.json', json:true}) 15 | .then(function (data) { 16 | if(!data.apiBaseurl || data.apiBaseurl == '/') { 17 | apiBaseurl = targetUrl; 18 | } else { 19 | apiBaseurl = data.apiBaseurl; 20 | } 21 | 22 | return rp({ uri: apiBaseurl+'/data', qs: {nocache:true}, json:true}); 23 | }) 24 | .then(function(data) { 25 | chartData = data; 26 | initialColorCount = chartData[0].value; 27 | color = chartData[0].label.toLowerCase(); 28 | expectedNewColorCount = initialColorCount + 1; 29 | 30 | return rp({ uri: apiBaseurl+'/increment',qs: {color: color}, json:true}); 31 | }) 32 | .then(function(data) { 33 | incrementResponse = data; 34 | return rp({ uri: apiBaseurl+'/data',qs: {nocache:true, countsOnly: true}, json:true}); 35 | }) 36 | .then(function(data) { 37 | newColorCounts = data; 38 | return rp({ uri: apiBaseurl+'/increment',qs: {color: 'UKNOWN'}, json:true}); 39 | }) 40 | .then(function(data) { 41 | badIncrementResponse = data; 42 | done(); 43 | }) 44 | .catch(function (err) { 45 | throw err; 46 | }); 47 | }); 48 | 49 | it("returns new count", function() { 50 | expect(incrementResponse.count).to.equal(expectedNewColorCount); 51 | }); 52 | 53 | it("new count matches expected value", function() { 54 | expect(newColorCounts[color]).to.equal(expectedNewColorCount); 55 | }); 56 | 57 | it("bad color produces error", function() { 58 | expect(badIncrementResponse.hasOwnProperty('error')).to.be.true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /bin/eni-attach-to-app.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | script_dir="$(dirname "$0")" 5 | ENVIRONMENT_FILE="$script_dir/../environment.sh" 6 | if [ ! -f "$ENVIRONMENT_FILE" ]; then 7 | echo "Fatal: environment file $ENVIRONMENT_FILE does not exist!" 2>&1 8 | exit 1 9 | fi 10 | 11 | . $ENVIRONMENT_FILE 12 | 13 | app_stack=$1 14 | if [ -z "$app_stack" ]; then 15 | echo "Usage: $(basename $0) " >&2 16 | exit 1 17 | fi 18 | 19 | set -x 20 | 21 | eni_id="$(aws cloudformation describe-stacks --stack-name $dromedary_eni_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`EniId`].OutputValue')" 22 | attachment_id="$(aws ec2 describe-network-interfaces --network-interface-ids $eni_id --output text --query 'NetworkInterfaces[0].Attachment.AttachmentId')" 23 | instance_id="$(aws cloudformation describe-stacks --stack-name $app_stack --output text --query 'Stacks[0].Outputs[?OutputKey==`InstanceId`].OutputValue')" 24 | sec_grp_id="$(aws cloudformation describe-stacks --stack-name $app_stack --output text --query 'Stacks[0].Outputs[?OutputKey==`InstanceSecurityGroup`].OutputValue')" 25 | 26 | # update ENI stack with new security-group 27 | aws cloudformation update-stack \ 28 | --stack-name $dromedary_eni_stack_name \ 29 | --template-body file://$script_dir/../pipeline/cfn/app-eni.json \ 30 | --parameters ParameterKey=Hostname,UsePreviousValue=true \ 31 | ParameterKey=Domain,UsePreviousValue=true \ 32 | ParameterKey=SubnetId,UsePreviousValue=true \ 33 | ParameterKey=SecurityGroupId,ParameterValue=$sec_grp_id 34 | 35 | eni_stack_status="$(bash $script_dir/cfn-wait-for-stack.sh $dromedary_eni_stack_name)" 36 | if [ $? -ne 0 ]; then 37 | echo "Fatal: Jenkins stack $dromedary_eni_stack_name ($eni_stack_status) failed to create properly" >&2 38 | exit 1 39 | fi 40 | 41 | # detach from existing instance 42 | if [ -n "$attachment_id" -a "$attachment_id" != 'None' ]; then 43 | aws ec2 detach-network-interface --attachment-id $attachment_id 44 | fi 45 | # wait for detachment 46 | while [ -n "$attachment_id" -a "$attachment_id" != 'None' ]; do 47 | sleep 1 48 | attachment_id="$(aws ec2 describe-network-interfaces --network-interface-ids $eni_id --output text --query 'NetworkInterfaces[0].Attachment.AttachmentId')" 49 | done 50 | 51 | # attach to new instance 52 | aws ec2 attach-network-interface --network-interface-id $eni_id --instance-id $instance_id --device-index 1 --output=json 53 | -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-infra-test/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | true 9 | drom-infra-test 10 | Test 11 | DromedaryJenkins 12 | 1 13 | us-east-1 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | true 21 | false 22 | false 23 | false 24 | 25 | 26 | * * * * * 27 | false 28 | 29 | 30 | true 31 | 32 | 33 | bash ./pipeline/jobs/scripts/drom-infra-test.sh 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | xterm 49 | 50 | 51 | 52 | 53 | 2.2.2 54 | 55 | rvm 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /bin/cfn-create-app.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | script_dir="$(dirname "$0")" 5 | ENVIRONMENT_FILE="$script_dir/../environment.sh" 6 | if [ ! -f "$ENVIRONMENT_FILE" ]; then 7 | echo "Fatal: environment file $ENVIRONMENT_FILE does not exist!" 2>&1 8 | exit 1 9 | fi 10 | 11 | . $ENVIRONMENT_FILE 12 | 13 | if [ -n "$1" ]; then 14 | dromedary_artifact="$1" 15 | fi 16 | 17 | if [ -z "$dromedary_artifact" ]; then 18 | echo "Fatal: \$dromedary_artifact not specified" >&2 19 | exit 1 20 | fi 21 | 22 | app_subnet_id="$(aws cloudformation describe-stacks --stack-name $dromedary_vpc_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`SubnetId`].OutputValue')" 23 | vpc="$(aws cloudformation describe-stacks --stack-name $dromedary_vpc_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`VPC`].OutputValue')" 24 | app_instance_profile="$(aws cloudformation describe-stacks --stack-name $dromedary_iam_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`InstanceProfile`].OutputValue')" 25 | app_instance_role="$(aws cloudformation describe-stacks --stack-name $dromedary_iam_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`InstanceRole`].OutputValue')" 26 | app_ddb_table="$(aws cloudformation describe-stacks --stack-name $dromedary_ddb_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`TableName`].OutputValue')" 27 | app_custom_action_provider_name="DromedaryJnkns$(date +%s)" 28 | 29 | dromedary_app_stack_name="$dromedary_hostname-$(basename $dromedary_artifact .tar.gz)" 30 | aws cloudformation create-stack \ 31 | --disable-rollback \ 32 | --stack-name $dromedary_app_stack_name \ 33 | --template-body file://./pipeline/cfn/app-instance.json \ 34 | --parameters ParameterKey=Ec2Key,ParameterValue=$dromedary_ec2_key \ 35 | ParameterKey=SubnetId,ParameterValue=$app_subnet_id \ 36 | ParameterKey=VPC,ParameterValue=$vpc \ 37 | ParameterKey=InstanceProfile,ParameterValue=$app_instance_profile \ 38 | ParameterKey=CfnInitRole,ParameterValue=$app_instance_role \ 39 | ParameterKey=S3Bucket,ParameterValue=$dromedary_s3_bucket \ 40 | ParameterKey=ArtifactPath,ParameterValue=$dromedary_artifact \ 41 | ParameterKey=DynamoDbTable,ParameterValue=$app_ddb_table \ 42 | --tags Key=BuiltBy,Value=$dromedary_custom_action_provider 43 | 44 | app_stack_status="$(bash $script_dir/cfn-wait-for-stack.sh $dromedary_app_stack_name)" 45 | if [ $? -ne 0 ]; then 46 | echo "Fatal: Jenkins stack $dromedary_app_stack_name ($app_stack_status) failed to create properly" >&2 47 | exit 1 48 | fi 49 | 50 | echo "export dromedary_app_stack_name=$dromedary_app_stack_name" >> "$ENVIRONMENT_FILE" 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | Stelligent Demo - Dromedary 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 |
27 |

Vote For Your Favorite Color

28 |

Deployed version:
 

29 | 30 | 31 |
32 |
33 |

THE LATEST

34 |
35 |

-1

36 |

Unknown

37 |
38 |

-1

39 |

Unknown

40 |
41 |

-1

42 |

Unknown

43 |
44 |
45 |
46 |
    47 |
  •  
  • 48 |
49 |
50 |
51 |
52 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "schemes": [ 4 | "https" 5 | ], 6 | "paths": { 7 | "/{subpath}": { 8 | "get": { 9 | "consumes": [ 10 | "application/json" 11 | ], 12 | "produces": [ 13 | "application/json" 14 | ], 15 | "parameters": [ 16 | { 17 | "name": "subpath", 18 | "in": "path", 19 | "required": true, 20 | "type": "string" 21 | } 22 | ], 23 | "responses": { 24 | "200": { 25 | "description": "200 response", 26 | "headers": { 27 | "Access-Control-Allow-Origin": { 28 | "type": "string" 29 | }, 30 | "Access-Control-Allow-Methods": { 31 | "type": "string" 32 | }, 33 | "Content-Type": { 34 | "type": "string" 35 | } 36 | } 37 | } 38 | }, 39 | "x-amazon-apigateway-integration": { 40 | "responses": { 41 | ".*": { 42 | "statusCode": "200", 43 | "responseParameters": { 44 | "method.response.header.Access-Control-Allow-Methods": "'GET, OPTIONS'", 45 | "method.response.header.Content-Type": "integration.response.body.contentType", 46 | "method.response.header.Access-Control-Allow-Origin": "'*'" 47 | }, 48 | "responseTemplates": { 49 | "application/json": "$util.base64Decode( $input.path('$.payload') )" 50 | } 51 | } 52 | }, 53 | "requestTemplates": { 54 | "application/json": "{\n \"stage\": \"$context.stage\",\n \"request-id\": \"$context.requestId\",\n \"api-id\": \"$context.apiId\",\n \"resource-path\": \"$context.resourcePath\",\n \"resource-id\": \"$context.resourceId\",\n \"http-method\": \"$context.httpMethod\",\n \"source-ip\": \"$context.identity.sourceIp\",\n \"user-agent\": \"$context.identity.userAgent\",\n \"account-id\": \"$context.identity.accountId\",\n \"api-key\": \"$context.identity.apiKey\",\n \"caller\": \"$context.identity.caller\",\n \"user\": \"$context.identity.user\",\n \"user-arn\": \"$context.identity.userArn\",\n \"queryString\": \"$input.params().querystring\",\n \"headers\": \"$input.params().header\",\n \"pathParams\": \"$input.params().path\",\n \"allParams\": \"$input.params()\",\n \"ddbTableName\": \"$stageVariables.DDBTableName\"\n}" 55 | }, 56 | "uri": "arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:324320755747:function:${stageVariables.AppFunctionName}:${stageVariables.AppVersion}/invocations", 57 | "httpMethod": "POST", 58 | "requestParameters": { 59 | "integration.request.path.subpath": "method.request.path.subpath" 60 | }, 61 | "type": "aws" 62 | } 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /pipeline/jobs/xml/drom-build/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | 9 | DROMEDARY_S3_BUCKET=S3BUCKET_PLACEHOLDER 10 | DROMEDARY_VPC_STACK=VPC_PLACEHOLDER 11 | DROMEDARY_IAM_STACK=IAM_PLACEHOLDER 12 | DROMEDARY_DDB_STACK=DDB_PLACEHOLDER 13 | DROMEDARY_ENI_STACK=ENI_PLACEHOLDER 14 | DROMEDARY_EC2_KEY=KEY_PLACEHOLDER 15 | DROMEDARY_HOSTNAME=HOSTNAME_PLACEHOLDER 16 | DROMEDARY_DOMAINNAME=DOMAINNAME_PLACEHOLDER 17 | DROMEDARY_ZONE_ID=ZONE_ID_PLACEHOLDER 18 | DROMEDARY_ACTION_PROVIDER=ACTION_PROVIDER_PLACEHOLDER 19 | false 20 | 21 | true 22 | true 23 | true 24 | false 25 | 26 | 27 | 28 | 29 | true 30 | drom-build 31 | Build 32 | DromedaryJenkins 33 | 1 34 | us-east-1 35 | 36 | 37 | 38 | 0 39 | 40 | 41 | true 42 | false 43 | false 44 | false 45 | 46 | 47 | * * * * * 48 | false 49 | 50 | 51 | true 52 | 53 | 54 | bash ./pipeline/jobs/scripts/drom-build.sh 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | xterm 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/inMemoryStorage.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var InMemStor = require("../lib/inMemoryStorage.js"); 3 | 4 | var expectedNumberOfItems = 3; 5 | var expectedProperties = ['value', 'color', 'highlight', 'label']; 6 | var backend = new InMemStor(); 7 | 8 | describe("inMemoryStorage", function() { 9 | describe(".getForChartJs()", function() { 10 | beforeEach(function() { 11 | this.chartData = backend.getForChartJs(); 12 | }); 13 | 14 | it("has exactly " + expectedNumberOfItems + " items", function() { 15 | expect(this.chartData).to.have.length(expectedNumberOfItems); 16 | }); 17 | 18 | it("each item has exactly " + expectedProperties.length + " properties", function() { 19 | var index; 20 | for (index = 0; index < this.chartData.length; index++) { 21 | expect(Object.keys(this.chartData[index])).to.have.length(expectedProperties.length); 22 | } 23 | }); 24 | 25 | it("each item has properties: " + expectedProperties, function() { 26 | var itemProperties; 27 | var itemIndex; 28 | var propIndex; 29 | for (itemIndex = 0; itemIndex < this.chartData.length; itemIndex++) { 30 | itemProperties = Object.keys(this.chartData[itemIndex]); 31 | for (propIndex = 0; propIndex < expectedProperties.length; propIndex++) { 32 | expect(itemProperties).to.contain(expectedProperties[propIndex]); 33 | } 34 | } 35 | }); 36 | }); 37 | 38 | describe(".getAllCounts()", function() { 39 | beforeEach(function() { 40 | this.colorCounts = backend.getAllCounts(); 41 | }); 42 | 43 | it("has exactly " + expectedNumberOfItems + " items", function() { 44 | expect(Object.keys(this.colorCounts)).to.have.length(expectedNumberOfItems); 45 | }); 46 | 47 | it("each item is a number", function() { 48 | var color; 49 | for (color in this.colorCounts) { 50 | expect(this.colorCounts[color]).to.be.a('number'); 51 | } 52 | }); 53 | }); 54 | 55 | describe(".incrementCount()", function() { 56 | beforeEach(function() { 57 | var color; 58 | for (color in this.colorCounts) { 59 | backend.incrementCount(color); 60 | } 61 | }); 62 | 63 | it("increments counts by one", function() { 64 | var color; 65 | for (color in this.colorCounts) { 66 | expect(backend.getCount(color)).to.equal(this.colorCounts[color]+1); 67 | } 68 | }); 69 | }); 70 | 71 | describe(".colorExists()", function() { 72 | beforeEach(function() { 73 | this.colors = Object.keys(backend.getAllCounts()); 74 | this.badcolors = ['', 'UKNOWN', null]; 75 | }); 76 | 77 | it("each color exists", function() { 78 | var i; 79 | var result; 80 | for (i=0; i < this.colors.length; i++) { 81 | result = backend.colorExists(this.colors[i]); 82 | expect(result).to.be.true; 83 | } 84 | }); 85 | 86 | it("bad colors do not exist", function() { 87 | var i; 88 | var result; 89 | for (i=0; i < this.badcolors.length; i++) { 90 | result = backend.colorExists(this.badcolors[i]); 91 | expect(result).to.be.false; 92 | } 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /cookbooks/dromedary/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = '2' 6 | 7 | Vagrant.require_version '>= 1.5.0' 8 | 9 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 10 | # All Vagrant configuration is done here. The most common configuration 11 | # options are documented and commented below. For a complete reference, 12 | # please see the online documentation at vagrantup.com. 13 | 14 | config.vm.hostname = 'dromedary-berkshelf' 15 | 16 | # Set the version of chef to install using the vagrant-omnibus plugin 17 | # NOTE: You will need to install the vagrant-omnibus plugin: 18 | # 19 | # $ vagrant plugin install vagrant-omnibus 20 | # 21 | if Vagrant.has_plugin?("vagrant-omnibus") 22 | config.omnibus.chef_version = 'latest' 23 | end 24 | 25 | # Every Vagrant virtual environment requires a box to build off of. 26 | # If this value is a shorthand to a box in Vagrant Cloud then 27 | # config.vm.box_url doesn't need to be specified. 28 | config.vm.box = 'chef/ubuntu-14.04' 29 | 30 | 31 | # Assign this VM to a host-only network IP, allowing you to access it 32 | # via the IP. Host-only networks can talk to the host machine as well as 33 | # any other machines on the same network, but cannot be accessed (through this 34 | # network interface) by any external networks. 35 | config.vm.network :private_network, type: 'dhcp' 36 | 37 | # Create a forwarded port mapping which allows access to a specific port 38 | # within the machine from a port on the host machine. In the example below, 39 | # accessing "localhost:8080" will access port 80 on the guest machine. 40 | 41 | # Share an additional folder to the guest VM. The first argument is 42 | # the path on the host to the actual folder. The second argument is 43 | # the path on the guest to mount the folder. And the optional third 44 | # argument is a set of non-required options. 45 | # config.vm.synced_folder "../data", "/vagrant_data" 46 | 47 | # Provider-specific configuration so you can fine-tune various 48 | # backing providers for Vagrant. These expose provider-specific options. 49 | # Example for VirtualBox: 50 | # 51 | # config.vm.provider :virtualbox do |vb| 52 | # # Don't boot with headless mode 53 | # vb.gui = true 54 | # 55 | # # Use VBoxManage to customize the VM. For example to change memory: 56 | # vb.customize ["modifyvm", :id, "--memory", "1024"] 57 | # end 58 | # 59 | # View the documentation for the provider you're using for more 60 | # information on available options. 61 | 62 | # The path to the Berksfile to use with Vagrant Berkshelf 63 | # config.berkshelf.berksfile_path = "./Berksfile" 64 | 65 | # Enabling the Berkshelf plugin. To enable this globally, add this configuration 66 | # option to your ~/.vagrant.d/Vagrantfile file 67 | config.berkshelf.enabled = true 68 | 69 | # An array of symbols representing groups of cookbook described in the Vagrantfile 70 | # to exclusively install and copy to Vagrant's shelf. 71 | # config.berkshelf.only = [] 72 | 73 | # An array of symbols representing groups of cookbook described in the Vagrantfile 74 | # to skip installing and copying to Vagrant's shelf. 75 | # config.berkshelf.except = [] 76 | 77 | config.vm.provision :chef_solo do |chef| 78 | chef.json = { 79 | mysql: { 80 | server_root_password: 'rootpass', 81 | server_debian_password: 'debpass', 82 | server_repl_password: 'replpass' 83 | } 84 | } 85 | 86 | chef.run_list = [ 87 | 'recipe[dromedary::default]' 88 | ] 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test-functional/endpoint-data.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var rp = require('request-promise'); 3 | var targetUrl = process.env.hasOwnProperty('TARGET_URL') ? process.env.TARGET_URL : 'http://localhost:8080'; 4 | 5 | var expectedNumberOfItems = 3; 6 | var expectedProperties = ['value', 'color', 'highlight', 'label']; 7 | 8 | describe("/data", function() { 9 | this.timeout(15000); 10 | 11 | var apiBaseurl; 12 | before(function(done) { 13 | rp({ uri: targetUrl+'/config.json', json:true}) 14 | .then(function (data) { 15 | if(!data.apiBaseurl || data.apiBaseurl == '/') { 16 | apiBaseurl = targetUrl; 17 | } else { 18 | apiBaseurl = data.apiBaseurl; 19 | } 20 | done(); 21 | }) 22 | .catch(function (err) { 23 | throw err; 24 | }); 25 | }); 26 | 27 | var chartData; 28 | beforeEach(function(done) { 29 | rp({ uri: apiBaseurl+'/data', json:true}) 30 | .then(function(data) { 31 | chartData = data; 32 | done(); 33 | }) 34 | .catch (function(err) { 35 | throw err; 36 | }); 37 | }); 38 | 39 | it("response has exactly " + expectedNumberOfItems + " items", function() { 40 | expect(chartData).to.have.length(expectedNumberOfItems); 41 | }); 42 | 43 | it("each item has exactly " + expectedProperties.length + " properties", function() { 44 | var index; 45 | for (index = 0; index < chartData.length; index++) { 46 | expect(Object.keys(chartData[index])).to.have.length(expectedProperties.length); 47 | } 48 | }); 49 | 50 | it("each item has properties: " + expectedProperties, function() { 51 | var itemProperties; 52 | var itemIndex; 53 | var propIndex; 54 | for (itemIndex = 0; itemIndex < chartData.length; itemIndex++) { 55 | itemProperties = Object.keys(chartData[itemIndex]); 56 | for (propIndex = 0; propIndex < expectedProperties.length; propIndex++) { 57 | expect(itemProperties).to.contain(expectedProperties[propIndex]); 58 | } 59 | } 60 | }); 61 | }); 62 | 63 | describe("/data?countsOnly", function() { 64 | this.timeout(15000); 65 | 66 | var apiBaseurl; 67 | before(function(done) { 68 | rp({ uri: targetUrl+'/config.json', json:true}) 69 | .then(function (data) { 70 | if(!data.apiBaseurl || data.apiBaseurl == '/') { 71 | apiBaseurl = targetUrl; 72 | } else { 73 | apiBaseurl = data.apiBaseurl; 74 | } 75 | done(); 76 | }) 77 | .catch(function (err) { 78 | throw err; 79 | }); 80 | }); 81 | 82 | var chartData; 83 | var colorCounts; 84 | beforeEach(function(done) { 85 | var replyCount = 0; 86 | rp({ uri: apiBaseurl+'/data', json:true}) 87 | .then(function(data) { 88 | chartData = data; 89 | if(++replyCount == 2) { 90 | done(); 91 | } 92 | }) 93 | .catch (function(err) { 94 | throw err; 95 | }); 96 | 97 | rp({ uri: apiBaseurl+'/data', qs:{countsOnly: true}, json:true}) 98 | .then(function(data) { 99 | colorCounts = data; 100 | if(++replyCount == 2) { 101 | done(); 102 | } 103 | }) 104 | .catch (function(err) { 105 | throw err; 106 | }); 107 | }); 108 | 109 | it("response has exactly " + expectedNumberOfItems + " keys", function() { 110 | expect(Object.keys(colorCounts)).to.have.length(expectedNumberOfItems); 111 | }); 112 | it("matches values in /data response", function() { 113 | var index; 114 | var color; 115 | var value; 116 | for (index = 0; index < chartData.length; index++) { 117 | color = chartData[index].label.toLowerCase(); 118 | value = chartData[index].value; 119 | expect(colorCounts[color]).to.equal(value); 120 | } 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /pipeline/cfn/codepipeline-custom-actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Dromedary CodePipeline Custom Action Provisioning", 4 | "Parameters":{ 5 | "MyInputArtifacts":{ 6 | "Type":"String", 7 | "Default":"DromedarySource" 8 | }, 9 | "MyBuildProvider":{ 10 | "Type":"String" 11 | }, 12 | "MyJenkinsURL":{ 13 | "Type":"String" 14 | } 15 | }, 16 | "Resources":{ 17 | "MyCustomBuildActionType":{ 18 | "Type":"AWS::CodePipeline::CustomActionType", 19 | "Properties":{ 20 | "Category":"Build", 21 | "Provider":{ 22 | "Ref":"MyBuildProvider" 23 | }, 24 | "Version":"1", 25 | "ConfigurationProperties":[ 26 | { 27 | "Description":"The name of the build project must be provided when this action is added to the pipeline.", 28 | "Key":"true", 29 | "Name":"ProjectName", 30 | "Queryable":"true", 31 | "Required":"true", 32 | "Secret":"false", 33 | "Type":"String" 34 | } 35 | ], 36 | "InputArtifactDetails":{ 37 | "MaximumCount":"5", 38 | "MinimumCount":"1" 39 | }, 40 | "OutputArtifactDetails":{ 41 | "MaximumCount":"5", 42 | "MinimumCount":"0" 43 | }, 44 | "Settings":{ 45 | "EntityUrlTemplate":{ 46 | "Fn::Join":[ 47 | "", 48 | [ 49 | { 50 | "Ref":"MyJenkinsURL" 51 | }, 52 | "job/{Config:ProjectName}" 53 | ] 54 | ] 55 | }, 56 | "ExecutionUrlTemplate":{ 57 | "Fn::Join":[ 58 | "", 59 | [ 60 | { 61 | "Ref":"MyJenkinsURL" 62 | }, 63 | "job/{Config:ProjectName}/{ExternalExecutionId}" 64 | ] 65 | ] 66 | } 67 | } 68 | } 69 | }, 70 | "MyCustomTestActionType":{ 71 | "Type":"AWS::CodePipeline::CustomActionType", 72 | "Properties":{ 73 | "Category":"Test", 74 | "Provider":{ 75 | "Ref":"MyBuildProvider" 76 | }, 77 | "Version":"1", 78 | "ConfigurationProperties":[ 79 | { 80 | "Description":"The name of the build project must be provided when this action is added to the pipeline.", 81 | "Key":"true", 82 | "Name":"ProjectName", 83 | "Queryable":"true", 84 | "Required":"true", 85 | "Secret":"false", 86 | "Type":"String" 87 | } 88 | ], 89 | "InputArtifactDetails":{ 90 | "MaximumCount":"5", 91 | "MinimumCount":"1" 92 | }, 93 | "OutputArtifactDetails":{ 94 | "MaximumCount":"5", 95 | "MinimumCount":"0" 96 | }, 97 | "Settings":{ 98 | "EntityUrlTemplate":{ 99 | "Fn::Join":[ 100 | "", 101 | [ 102 | { 103 | "Ref":"MyJenkinsURL" 104 | }, 105 | "job/{Config:ProjectName}" 106 | ] 107 | ] 108 | }, 109 | "ExecutionUrlTemplate":{ 110 | "Fn::Join":[ 111 | "", 112 | [ 113 | { 114 | "Ref":"MyJenkinsURL" 115 | }, 116 | "job/{Config:ProjectName}/{ExternalExecutionId}" 117 | ] 118 | ] 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "Outputs":{ 125 | "StackName":{ 126 | "Value":{ 127 | "Ref":"AWS::StackName" 128 | } 129 | }, 130 | "CustomActionBuildName":{ 131 | "Description":"CodePipeline Build Custom action name", 132 | "Value":{ 133 | "Ref":"MyCustomBuildActionType" 134 | } 135 | }, 136 | "CustomActionTestName":{ 137 | "Description":"CodePipeline Test Custom action name", 138 | "Value":{ 139 | "Ref":"MyCustomTestActionType" 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /pipeline/cfn/app-eni.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Stelligent CloudFormation Sample Template ** This template creates one or more Amazon resources. You will be billed for the AWS resources used if you create a stack from this template.", 4 | "Parameters":{ 5 | "Hostname":{ 6 | "Type":"String", 7 | "Description":"DNS Hostname for prod IP (but not domainname)", 8 | "Default":"" 9 | }, 10 | "Domain":{ 11 | "Type":"String", 12 | "Description":"Route53 Hosted Zone name for prod IP (include trailing .)", 13 | "Default":"" 14 | }, 15 | "SubnetId":{ 16 | "Type":"String", 17 | "Default":"", 18 | "Description":"TODO: Modify this later. VPC subnet id in which to place ENI" 19 | }, 20 | "SecurityGroupId":{ 21 | "Type":"String", 22 | "Description":"Security Group id with which to associate app ENI", 23 | "Default":"" 24 | } 25 | }, 26 | "Conditions":{ 27 | "NoSecurityGroup":{ 28 | "Fn::Equals":[ 29 | { 30 | "Ref":"SecurityGroupId" 31 | }, 32 | "" 33 | ] 34 | }, 35 | "Route53Update":{ 36 | "Fn::And":[ 37 | { 38 | "Fn::Not":[ 39 | { 40 | "Fn::Equals":[ 41 | { 42 | "Ref":"Hostname" 43 | }, 44 | "" 45 | ] 46 | } 47 | ] 48 | }, 49 | { 50 | "Fn::Not":[ 51 | { 52 | "Fn::Equals":[ 53 | { 54 | "Ref":"Domain" 55 | }, 56 | "" 57 | ] 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | }, 64 | "Resources":{ 65 | "EIP":{ 66 | "Type":"AWS::EC2::EIP", 67 | "Properties":{ 68 | "Domain":"vpc" 69 | } 70 | }, 71 | "ProdDNSRecord":{ 72 | "Type":"AWS::Route53::RecordSet", 73 | "Condition":"Route53Update", 74 | "Properties":{ 75 | "HostedZoneName":{ 76 | "Ref":"Domain" 77 | }, 78 | "Comment":"DNS name for Dromedary prod.", 79 | "Name":{ 80 | "Fn::Join":[ 81 | "", 82 | [ 83 | { 84 | "Ref":"Hostname" 85 | }, 86 | ".", 87 | { 88 | "Ref":"Domain" 89 | } 90 | ] 91 | ] 92 | }, 93 | "Type":"A", 94 | "TTL":"120", 95 | "ResourceRecords":[ 96 | { 97 | "Ref":"EIP" 98 | } 99 | ] 100 | } 101 | }, 102 | "ENI":{ 103 | "Type":"AWS::EC2::NetworkInterface", 104 | "Properties":{ 105 | "Description":"Dromedary Prod ENI", 106 | "SubnetId":{ 107 | "Ref":"SubnetId" 108 | }, 109 | "GroupSet":{ 110 | "Fn::If":[ 111 | "NoSecurityGroup", 112 | { 113 | "Ref":"AWS::NoValue" 114 | }, 115 | [ 116 | { 117 | "Ref":"SecurityGroupId" 118 | } 119 | ] 120 | ] 121 | }, 122 | "Tags":[ 123 | { 124 | "Key":"Application", 125 | "Value":{ 126 | "Ref":"AWS::StackId" 127 | } 128 | }, 129 | { 130 | "Key":"Name", 131 | "Value":{ 132 | "Ref":"AWS::StackName" 133 | } 134 | } 135 | ] 136 | } 137 | }, 138 | "EipAssocation":{ 139 | "Type":"AWS::EC2::EIPAssociation", 140 | "Properties":{ 141 | "AllocationId":{ 142 | "Fn::GetAtt":[ 143 | "EIP", 144 | "AllocationId" 145 | ] 146 | }, 147 | "NetworkInterfaceId":{ 148 | "Ref":"ENI" 149 | } 150 | } 151 | } 152 | }, 153 | "Outputs":{ 154 | "StackName":{ 155 | "Value":{ 156 | "Ref":"AWS::StackName" 157 | } 158 | }, 159 | "PublicIp":{ 160 | "Description":"Public IP Address of ENI", 161 | "Value":{ 162 | "Ref":"EIP" 163 | } 164 | }, 165 | "EniId":{ 166 | "Description":"Elastic Network Interface Id", 167 | "Value":{ 168 | "Ref":"ENI" 169 | } 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /pipeline/cfn/pipeline-store.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Store CFN Outputs to consume in downstream CFN stacks", 4 | "Parameters":{ 5 | "UUID":{ 6 | "Type":"String", 7 | "Description":"Unique identifier to uniquely name Resources", 8 | "Default":"EMPTY" 9 | }, 10 | "MasterStackName":{ 11 | "Type":"String", 12 | "Default":"EMPTY", 13 | "Description":"Name of the master.json CFN stack" 14 | }, 15 | "DromedaryS3Bucket":{ 16 | "Type":"String", 17 | "Default":"EMPTY", 18 | "Description":"Name of S3 bucket used to store Jenkins config" 19 | }, 20 | "Branch":{ 21 | "Type":"String", 22 | "Default":"EMPTY", 23 | "Description":"Name of Dromedary Github branch" 24 | }, 25 | "KeyName":{ 26 | "Type":"String", 27 | "Default":"EMPTY", 28 | "Description":"EC2 KeyPair" 29 | }, 30 | "MyBuildProvider":{ 31 | "Type":"String", 32 | "Default":"EMPTY", 33 | "Description":"Jenkins Build Provider Name" 34 | }, 35 | "JobConfigsTarball":{ 36 | "Type":"String", 37 | "Default":"EMPTY", 38 | "Description":"S3 key for DromedaryS3Bucket" 39 | }, 40 | "Hostname":{ 41 | "Type":"String", 42 | "Default":"EMPTY", 43 | "Description":"subdomain name" 44 | }, 45 | "Domain":{ 46 | "Type":"String", 47 | "Description":"Route53 Hosted Zone name for prod IP (include trailing .)", 48 | "Default":"oneclickdeployment.com." 49 | }, 50 | "ProdHostedZone":{ 51 | "Type":"String", 52 | "Description":"Route53 Hosted Zone (e.g. PRODHOST.HOSTED.ZONE)", 53 | "AllowedPattern":"^.*?\\..*?\\..*$" 54 | }, 55 | "VPCStackName":{ 56 | "Type":"String", 57 | "Default":"EMPTY", 58 | "Description":"A stack name reference to vpc.json" 59 | }, 60 | "IAMStackName":{ 61 | "Type":"String", 62 | "Default":"EMPTY", 63 | "Description":"A stack name reference to iam.json" 64 | }, 65 | "DDBStackName":{ 66 | "Type":"String", 67 | "Default":"EMPTY", 68 | "Description":"A stack name reference to dynamodb.json" 69 | }, 70 | "ENIStackName":{ 71 | "Type":"String", 72 | "Default":"EMPTY", 73 | "Description":"A stack name reference to app-eni.json" 74 | }, 75 | "DromedaryAppURL":{ 76 | "Type":"String", 77 | "Default":"EMPTY", 78 | "Description":"The URL users use to launch the Dromedary application" 79 | } 80 | }, 81 | "Resources":{ 82 | "MyQueue":{ 83 | "Type":"AWS::SQS::Queue", 84 | "Properties":{ 85 | "QueueName":{ 86 | "Fn::Join":[ 87 | "", 88 | [ 89 | "PipelineStoreQueue-", 90 | { 91 | "Ref":"UUID" 92 | } 93 | ] 94 | ] 95 | } 96 | } 97 | } 98 | }, 99 | "Outputs":{ 100 | "StackName":{ 101 | "Value":{ 102 | "Ref":"AWS::StackName" 103 | } 104 | }, 105 | "UUID":{ 106 | "Value":{ 107 | "Ref":"UUID" 108 | } 109 | }, 110 | "MasterStackName":{ 111 | "Description":"Name of the Master CFN Stack", 112 | "Value":{ 113 | "Ref":"MasterStackName" 114 | } 115 | }, 116 | "DromedaryS3Bucket":{ 117 | "Description":"Name of S3 bucket used to store Jenkins config", 118 | "Value":{ 119 | "Ref":"DromedaryS3Bucket" 120 | } 121 | }, 122 | "Branch":{ 123 | "Description":"TBD", 124 | "Value":{ 125 | "Ref":"Branch" 126 | } 127 | }, 128 | "KeyName":{ 129 | "Description":"TBD", 130 | "Value":{ 131 | "Ref":"KeyName" 132 | } 133 | }, 134 | "MyBuildProvider":{ 135 | "Description":"TBD", 136 | "Value":{ 137 | "Ref":"MyBuildProvider" 138 | } 139 | }, 140 | "JobConfigsTarball":{ 141 | "Description":"S3 key for DromedaryS3Bucket", 142 | "Value":{ 143 | "Ref":"JobConfigsTarball" 144 | } 145 | }, 146 | "Hostname":{ 147 | "Description":"subdomain name", 148 | "Value":{ 149 | "Ref":"Hostname" 150 | } 151 | }, 152 | "Domain":{ 153 | "Description":"Route53 Hosted Zone name for prod IP (include trailing .)", 154 | "Value":{ 155 | "Ref":"Domain" 156 | } 157 | }, 158 | "ProdHostedZone":{ 159 | "Description":"Route53 Hosted Zone (e.g. PRODHOST.HOSTED.ZONE)", 160 | "Value":{ 161 | "Ref":"ProdHostedZone" 162 | } 163 | }, 164 | "VPCStackName":{ 165 | "Description":"A stack name reference to vpc.json", 166 | "Value":{ 167 | "Ref":"VPCStackName" 168 | } 169 | }, 170 | "IAMStackName":{ 171 | "Description":"A stack name reference to iam.json", 172 | "Value":{ 173 | "Ref":"IAMStackName" 174 | } 175 | }, 176 | "DDBStackName":{ 177 | "Description":"A stack name reference to dynamodb.json", 178 | "Value":{ 179 | "Ref":"DDBStackName" 180 | } 181 | }, 182 | "ENIStackName":{ 183 | "Description":"A stack name reference to app-eni.json", 184 | "Value":{ 185 | "Ref":"ENIStackName" 186 | } 187 | }, 188 | "DromedaryAppURL":{ 189 | "Description":"The URL users use to launch the Dromedary application", 190 | "Value":{ 191 | "Ref":"DromedaryAppURL" 192 | } 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /cookbooks/dromedary/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.5.2) 5 | public_suffix (>= 2.0.2, < 4.0) 6 | aws-sdk (2.1.14) 7 | aws-sdk-resources (= 2.1.14) 8 | aws-sdk-core (2.1.14) 9 | jmespath (~> 1.0) 10 | aws-sdk-resources (2.1.14) 11 | aws-sdk-core (= 2.1.14) 12 | berkshelf (7.0.7) 13 | chef (>= 13.6.52) 14 | chef-config 15 | cleanroom (~> 1.0) 16 | concurrent-ruby (~> 1.0) 17 | minitar (>= 0.6) 18 | mixlib-archive (~> 0.4) 19 | mixlib-config (>= 2.2.5) 20 | mixlib-shellout (~> 2.0) 21 | octokit (~> 4.0) 22 | retryable (~> 2.0) 23 | solve (~> 4.0) 24 | thor (>= 0.20) 25 | builder (3.2.3) 26 | chef (13.10.4) 27 | addressable 28 | bundler (>= 1.10) 29 | chef-config (= 13.10.4) 30 | chef-zero (~> 13.0) 31 | diff-lcs (~> 1.2, >= 1.2.4) 32 | erubis (~> 2.7) 33 | ffi-yajl (~> 2.2) 34 | highline (~> 1.6, >= 1.6.9) 35 | iniparse (~> 1.4) 36 | iso8601 (~> 0.9.1) 37 | mixlib-archive (~> 0.4) 38 | mixlib-authentication (~> 1.4) 39 | mixlib-cli (~> 1.7) 40 | mixlib-log (~> 1.3) 41 | mixlib-shellout (~> 2.0) 42 | net-sftp (~> 2.1, >= 2.1.2) 43 | net-ssh (>= 2.9, < 5.0) 44 | net-ssh-multi (~> 1.2, >= 1.2.1) 45 | ohai (~> 13.0) 46 | plist (~> 3.2) 47 | proxifier (~> 1.0) 48 | rspec-core (~> 3.5, < 3.8) 49 | rspec-expectations (~> 3.5, < 3.8) 50 | rspec-mocks (~> 3.5, < 3.8) 51 | rspec_junit_formatter (~> 0.2.0) 52 | serverspec (~> 2.7) 53 | specinfra (~> 2.10) 54 | syslog-logger (~> 1.6) 55 | uuidtools (~> 2.1.5) 56 | chef-config (13.10.4) 57 | addressable 58 | fuzzyurl 59 | mixlib-config (>= 2.2.12, < 3.0) 60 | mixlib-shellout (~> 2.0) 61 | tomlrb (~> 1.2) 62 | chef-zero (13.1.0) 63 | ffi-yajl (~> 2.2) 64 | hashie (>= 2.0, < 4.0) 65 | mixlib-log (~> 1.3) 66 | rack (~> 2.0) 67 | uuidtools (~> 2.1) 68 | cleanroom (1.0.0) 69 | concurrent-ruby (1.1.4) 70 | diff-lcs (1.3) 71 | erubis (2.7.0) 72 | excon (0.71.1) 73 | faraday (0.15.4) 74 | multipart-post (>= 1.2, < 3) 75 | ffi (1.9.25) 76 | ffi-yajl (2.3.1) 77 | libyajl2 (~> 1.2) 78 | fuzzyurl (0.9.0) 79 | hashie (3.6.0) 80 | highline (1.7.10) 81 | iniparse (1.4.4) 82 | ipaddress (0.8.3) 83 | iso8601 (0.9.1) 84 | jmespath (1.0.2) 85 | multi_json (~> 1.0) 86 | kitchen-ec2 (0.10.0) 87 | aws-sdk (~> 2) 88 | excon 89 | multi_json 90 | test-kitchen (~> 1.4, >= 1.4.1) 91 | kitchen-vagrant (0.18.0) 92 | test-kitchen (~> 1.4) 93 | libyajl2 (1.2.0) 94 | minitar (0.7) 95 | mixlib-archive (0.4.19) 96 | mixlib-log 97 | mixlib-authentication (1.4.2) 98 | mixlib-cli (1.7.0) 99 | mixlib-config (2.2.18) 100 | tomlrb 101 | mixlib-log (1.7.1) 102 | mixlib-shellout (2.2.0) 103 | molinillo (0.6.6) 104 | multi_json (1.11.2) 105 | multipart-post (2.0.0) 106 | net-scp (1.2.1) 107 | net-ssh (>= 2.6.5) 108 | net-sftp (2.1.2) 109 | net-ssh (>= 2.6.5) 110 | net-ssh (2.9.2) 111 | net-ssh-gateway (1.3.0) 112 | net-ssh (>= 2.6.5) 113 | net-ssh-multi (1.2.1) 114 | net-ssh (>= 2.6.5) 115 | net-ssh-gateway (>= 1.2.0) 116 | net-telnet (0.1.1) 117 | octokit (4.13.0) 118 | sawyer (~> 0.8.0, >= 0.5.3) 119 | ohai (13.12.4) 120 | chef-config (>= 12.5.0.alpha.1, < 14) 121 | ffi (~> 1.9) 122 | ffi-yajl (~> 2.2) 123 | ipaddress 124 | mixlib-cli 125 | mixlib-config (~> 2.0) 126 | mixlib-log (>= 1.7.1, < 2.0) 127 | mixlib-shellout (~> 2.0) 128 | plist (~> 3.1) 129 | systemu (~> 2.6.4) 130 | wmi-lite (~> 1.0) 131 | plist (3.5.0) 132 | proxifier (1.0.3) 133 | public_suffix (3.0.3) 134 | rack (2.2.3) 135 | retryable (2.0.4) 136 | rspec (3.7.0) 137 | rspec-core (~> 3.7.0) 138 | rspec-expectations (~> 3.7.0) 139 | rspec-mocks (~> 3.7.0) 140 | rspec-core (3.7.1) 141 | rspec-support (~> 3.7.0) 142 | rspec-expectations (3.7.0) 143 | diff-lcs (>= 1.2.0, < 2.0) 144 | rspec-support (~> 3.7.0) 145 | rspec-its (1.2.0) 146 | rspec-core (>= 3.0.0) 147 | rspec-expectations (>= 3.0.0) 148 | rspec-mocks (3.7.0) 149 | diff-lcs (>= 1.2.0, < 2.0) 150 | rspec-support (~> 3.7.0) 151 | rspec-support (3.7.1) 152 | rspec_junit_formatter (0.2.3) 153 | builder (< 4) 154 | rspec-core (>= 2, < 4, != 2.12.0) 155 | safe_yaml (1.0.4) 156 | sawyer (0.8.1) 157 | addressable (>= 2.3.5, < 2.6) 158 | faraday (~> 0.8, < 1.0) 159 | semverse (3.0.0) 160 | serverspec (2.41.3) 161 | multi_json 162 | rspec (~> 3.0) 163 | rspec-its 164 | specinfra (~> 2.72) 165 | sfl (2.3) 166 | solve (4.0.2) 167 | molinillo (~> 0.6) 168 | semverse (>= 1.1, < 4.0) 169 | specinfra (2.76.6) 170 | net-scp 171 | net-ssh (>= 2.7) 172 | net-telnet (= 0.1.1) 173 | sfl 174 | syslog-logger (1.6.8) 175 | systemu (2.6.5) 176 | test-kitchen (1.4.2) 177 | mixlib-shellout (>= 1.2, < 3.0) 178 | net-scp (~> 1.1) 179 | net-ssh (~> 2.7, < 2.10) 180 | safe_yaml (~> 1.0) 181 | thor (~> 0.18) 182 | thor (0.20.3) 183 | tomlrb (1.2.8) 184 | uuidtools (2.1.5) 185 | wmi-lite (1.0.1) 186 | 187 | PLATFORMS 188 | ruby 189 | 190 | DEPENDENCIES 191 | berkshelf 192 | kitchen-ec2 193 | kitchen-vagrant 194 | test-kitchen 195 | 196 | BUNDLED WITH 197 | 1.16.1 198 | -------------------------------------------------------------------------------- /lib/dynamoDbPersist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var AWS = require('aws-sdk'); 4 | 5 | // if we're running locally, use these params to create the DDB-local table 6 | var createLocalDdbTableParams = { 7 | AttributeDefinitions: [ 8 | { AttributeName: 'site_name', AttributeType: 'S' }, 9 | { AttributeName: 'color_name', AttributeType: 'S' }, 10 | ], 11 | KeySchema: [ 12 | { AttributeName: 'site_name', KeyType: 'HASH' }, 13 | { AttributeName: 'color_name', KeyType: 'RANGE' } 14 | ], 15 | ProvisionedThroughput: { 16 | ReadCapacityUnits: 10, 17 | WriteCapacityUnits: 5 18 | } 19 | }; 20 | 21 | // setup AWS config 22 | if (process.env.hasOwnProperty('AWS_DEFAULT_REGION')) { 23 | AWS.config.region = process.env.AWS_DEFAULT_REGION; 24 | } else { 25 | AWS.config.region = 'us-east-1'; 26 | } 27 | 28 | function Constructor() { 29 | var ddbTableName; 30 | var ddb; 31 | 32 | this.init = function (cb) { 33 | // if DROMEDARY_DDB_TABLE_NAME is specified in the environment, 34 | // assume we're running in EC2 35 | if (process.env.hasOwnProperty('DROMEDARY_DDB_TABLE_NAME')) { 36 | ddbTableName = process.env.DROMEDARY_DDB_TABLE_NAME; 37 | ddb = new AWS.DynamoDB(); 38 | ddb.waitFor('tableExists', { TableName: ddbTableName }, cb); 39 | } else { 40 | // if DROMEDARY_DDB_TABLE_NAME is not set, assume we're running in dev 41 | ddbTableName = 'dromedary_dev'; 42 | ddb = new AWS.DynamoDB({ 43 | endpoint: new AWS.Endpoint('http://localhost:8079') 44 | }); 45 | ddb.describeTable({ TableName: ddbTableName }, function(err, data) { 46 | if (err) { 47 | if (err.code === 'ResourceNotFoundException') { 48 | createLocalDdbTableParams.TableName = ddbTableName; 49 | ddb.createTable(createLocalDdbTableParams, function(err) { 50 | if (err) { 51 | cb(err, null); 52 | } else { 53 | ddb.waitFor('tableExists', { TableName: ddbTableName }, cb); 54 | } 55 | }); 56 | } else { 57 | cb(err, null); 58 | } 59 | } else { 60 | cb(null, data); 61 | } 62 | }); 63 | } 64 | 65 | }; 66 | 67 | /* Fetches color counts from DDB; Also updates DDB if counts are missing */ 68 | this.getSiteCounts = function(siteName, colorCounts, cb) { 69 | var getColor; 70 | var batchGetReqItems = {}; 71 | 72 | console.log('Fetching color counts for ' + siteName); 73 | 74 | batchGetReqItems[ddbTableName] = { 75 | Keys: [], 76 | AttributesToGet: [ 'color_name', 'color_count' ], 77 | ConsistentRead: true, 78 | }; 79 | 80 | for (getColor in colorCounts) { 81 | if (colorCounts.hasOwnProperty(getColor)) { 82 | // Ignore camelCase because of DDB Table Definition 83 | /*jshint -W106 */ 84 | batchGetReqItems[ddbTableName].Keys.push({ 85 | site_name: { S: siteName}, 86 | color_name: { S: getColor } 87 | }); 88 | /*jshint +W106 */ 89 | } 90 | } 91 | 92 | ddb.batchGetItem({ RequestItems: batchGetReqItems }, function(err, data) { 93 | var batchGetResp; 94 | var ddbColorCounts = {}; 95 | var i; 96 | var color; 97 | var batchWriteParams = { RequestItems: {} }; 98 | var ddbBatchWrites = []; 99 | if (err) { 100 | cb(err); 101 | return; 102 | } 103 | batchGetResp = data.Responses[ddbTableName]; 104 | for (i=0; i < batchGetResp.length; i++) { 105 | /*jshint -W106 */ 106 | ddbColorCounts[batchGetResp[i].color_name.S] = 107 | parseInt(batchGetResp[i].color_count.N); 108 | /*jshint +W106 */ 109 | } 110 | 111 | // merge DDB & local values 112 | for (color in colorCounts) { 113 | if (colorCounts.hasOwnProperty(color)) { 114 | // if DDB had a value, update the local value 115 | if (ddbColorCounts.hasOwnProperty(color)) { 116 | colorCounts[color] = ddbColorCounts[color]; 117 | // if DDB did not have a value, push an update 118 | } else { 119 | /*jshint -W106 */ 120 | ddbBatchWrites.push({PutRequest: {Item: { 121 | site_name: {S: siteName}, 122 | color_name: {S: color}, 123 | color_count: {N: colorCounts[color].toString()} 124 | }}}); 125 | /*jshint +W106 */ 126 | } 127 | } 128 | } 129 | 130 | if (ddbBatchWrites.length === 0) { 131 | cb(null, colorCounts); 132 | return; 133 | } 134 | batchWriteParams.RequestItems[ddbTableName] = ddbBatchWrites; 135 | console.log(JSON.stringify(batchWriteParams)); 136 | ddb.batchWriteItem(batchWriteParams, function(err, data) { 137 | if (err) { 138 | cb(err); 139 | } else { 140 | console.log('Performed batch DDB write: ' + JSON.stringify(data)); 141 | cb(null, colorCounts); 142 | } 143 | }); 144 | }); 145 | }; 146 | 147 | /* Increments color for specified site */ 148 | this.incrementCount = function(siteName, colorName, cb) { 149 | /*jshint -W106 */ 150 | var updateParams = { 151 | TableName: ddbTableName, 152 | Key: { 153 | site_name: { S: siteName}, 154 | color_name: { S: colorName } 155 | }, 156 | AttributeUpdates: { 157 | color_count: { Action: 'ADD', Value: { N: '1' } } 158 | } 159 | }; 160 | /*jshint +W106 */ 161 | ddb.updateItem(updateParams, function(err, data) { 162 | if (err) { 163 | cb(err); 164 | } 165 | console.log(colorName + ' incremented in DDB for ' + siteName); 166 | cb(null, data); 167 | }); 168 | }; 169 | } 170 | 171 | module.exports = Constructor; 172 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var app = express(); 5 | var CS = require(__dirname + '/lib/inMemoryStorage.js'); 6 | var sha = require(__dirname + '/lib/sha.js'); 7 | var reqThrottle = require(__dirname + '/lib/requestThrottle.js'); 8 | var DDBP = require(__dirname + '/lib/dynamoDbPersist.js'); 9 | var serverPort = 8080; 10 | var siteChartStore = {}; 11 | var ddbLastFetch = {}; 12 | 13 | module.exports = app; 14 | 15 | var ddbPersist = new DDBP(); 16 | 17 | if (process.env.hasOwnProperty('AUTOMATED_ACCEPTANCE_TEST')) { 18 | serverPort = 0; 19 | } 20 | 21 | /* Helper to refresh in memory store w/ data from DDB */ 22 | function updateColorCountsFromDdb(siteName, cb) { 23 | var chartData = siteChartStore[siteName]; 24 | 25 | /*jshint -W101 */ 26 | ddbPersist.getSiteCounts(siteName, chartData.getAllCounts(), function(err, data) { 27 | if (err) { 28 | cb(err); 29 | } else { 30 | chartData.setCounts(data); 31 | ddbLastFetch[siteName] = Date.now(); 32 | cb(null, chartData); 33 | } 34 | }); 35 | /*jshint +W101 */ 36 | } 37 | 38 | /* Returns in memory store chart data store */ 39 | function getChartData(siteName, nocache, cb) { 40 | if (!siteChartStore.hasOwnProperty(siteName)) { 41 | siteChartStore[siteName] = new CS(siteName); 42 | ddbLastFetch[siteName] = 0; 43 | } 44 | 45 | if (nocache || (Date.now() - ddbLastFetch[siteName] > 1000)) { 46 | // Fetch from DDB if it's been more than a second since last refresh 47 | updateColorCountsFromDdb(siteName, cb); 48 | } else { 49 | cb(null, siteChartStore[siteName]); 50 | } 51 | } 52 | 53 | /* Helper to send responses to frontend */ 54 | function sendJsonResponse(res, obj) { 55 | res.setHeader('Content-Type', 'application/json'); 56 | res.setHeader('Access-Control-Allow-Origin', '*'); 57 | res.send(JSON.stringify(obj)); 58 | } 59 | 60 | /* helper to determine client ip */ 61 | function getClientIp(req) { 62 | var ip = req.ip; 63 | if (req.headers.hasOwnProperty('x-real-ip')) { 64 | ip = req.headers['x-real-ip']; 65 | } 66 | return ip; 67 | } 68 | 69 | /* clean up throttle map every minute to keep it tidy */ 70 | setInterval(reqThrottle.gcMap, 1000); 71 | 72 | /* Host static content from /public */ 73 | app.use(express.static(__dirname + '/public')); 74 | 75 | /* GET requests to /config.json means the site is being served from /public */ 76 | app.get('/config.json', function (req, res) { 77 | console.log('Request received from %s for /config.json', getClientIp(req)); 78 | 79 | sha(function(version) { 80 | sendJsonResponse(res, { apiBaseurl: '', version: version }); 81 | }); 82 | }); 83 | 84 | 85 | /* GET requests to /data return chart data values */ 86 | app.get('/data', function (req, res) { 87 | console.log('Request received from %s for /data', getClientIp(req)); 88 | var nocache = req.query.hasOwnProperty('nocache') ; 89 | getChartData(req.headers.host,nocache,function (err, data) { 90 | var chartData = data; 91 | if (err) { 92 | console.log(err); 93 | sendJsonResponse(res, {error: err}); 94 | } else { 95 | if (req.query.hasOwnProperty('countsOnly')) { 96 | sendJsonResponse(res, chartData.getAllCounts()); 97 | } else { 98 | sendJsonResponse(res, chartData.getForChartJs()); 99 | } 100 | } 101 | }); 102 | }); 103 | 104 | /* GET requests to /increment to increment counts */ 105 | app.get('/increment', function (req, res) { 106 | var ip = getClientIp(req); 107 | if (! reqThrottle.checkIp(ip) ) { 108 | console.log('Request throttled from %s for /increment', ip); 109 | sendJsonResponse(res, {error: 'Request throttled'}); 110 | return; 111 | } 112 | 113 | if (!req.query.hasOwnProperty('color')) { 114 | console.log('No color specified in params'); 115 | sendJsonResponse(res, {count: 0}); 116 | return; 117 | } 118 | 119 | var nocache = req.query.hasOwnProperty('nocache') ; 120 | getChartData(req.headers.host,nocache,function (err, data) { 121 | console.log('Request received from %s for /increment', ip); 122 | reqThrottle.logIp(ip); 123 | if (err) { 124 | console.log(err); 125 | sendJsonResponse(res, {error: err}); 126 | return; 127 | } 128 | if (! data.colorExists(req.query.color)) { 129 | console.log('Increment received for unknown color ' + req.query.color); 130 | sendJsonResponse(res, {error: 'Unknown color'}); 131 | return; 132 | } 133 | 134 | /*jshint -W101 */ 135 | ddbPersist.incrementCount(req.headers.host, req.query.color, function (err) { 136 | console.log('Incrementing count for ' + req.query.color); 137 | if (err) { 138 | console.log(err); 139 | sendJsonResponse( res, {error: 'Failed to increment color count in DDB'}); 140 | return; 141 | } 142 | 143 | updateColorCountsFromDdb(req.headers.host, function (err, data) { 144 | if (err) { 145 | console.log(err); 146 | sendJsonResponse( res, {error: 'Failed to increment color count in DDB'}); 147 | return; 148 | } 149 | sendJsonResponse(res, {count: data.getCount(req.query.color)}); 150 | }); 151 | }); 152 | /*jshint +W101 */ 153 | }); 154 | }); 155 | 156 | ddbPersist.init(function(err) { 157 | var server; 158 | if (err) { 159 | console.log('Failed to init DynamoDB persistence'); 160 | console.log(err); 161 | process.exit(1); 162 | } 163 | 164 | server = app.listen(serverPort, function () { 165 | var host = server.address().address; 166 | var port = server.address().port; 167 | console.log('Listening on %s:%s', host, port); 168 | if (process.env.hasOwnProperty('AUTOMATED_ACCEPTANCE_TEST')) { 169 | require('fs').writeFileSync(__dirname + '/dev-lib/targetPort.js', 170 | 'module.exports = ' + port + ';\n'); 171 | } 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /pipeline/cfn/iam.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Dromedary demo - iam roles & policies and instance-profiles", 4 | "Resources":{ 5 | "InstanceRole":{ 6 | "Type":"AWS::IAM::Role", 7 | "Properties":{ 8 | "AssumeRolePolicyDocument":{ 9 | "Statement":[ 10 | { 11 | "Effect":"Allow", 12 | "Principal":{ 13 | "Service":[ 14 | "ec2.amazonaws.com" 15 | ] 16 | }, 17 | "Action":[ 18 | "sts:AssumeRole" 19 | ] 20 | } 21 | ] 22 | }, 23 | "Path":"/", 24 | "Policies":[ 25 | { 26 | "PolicyName":"AllowAll", 27 | "PolicyDocument":{ 28 | "Statement":[ 29 | { 30 | "Effect":"Allow", 31 | "Action":"*", 32 | "Resource":"*" 33 | } 34 | ] 35 | } 36 | } 37 | ] 38 | } 39 | }, 40 | "InstanceProfile":{ 41 | "Type":"AWS::IAM::InstanceProfile", 42 | "Properties":{ 43 | "Path":"/", 44 | "Roles":[ 45 | { 46 | "Ref":"InstanceRole" 47 | } 48 | ] 49 | } 50 | }, 51 | "CodeDeployTrustRole":{ 52 | "Type":"AWS::IAM::Role", 53 | "Properties":{ 54 | "AssumeRolePolicyDocument":{ 55 | "Statement":[ 56 | { 57 | "Sid":"1", 58 | "Effect":"Allow", 59 | "Principal":{ 60 | "Service":[ 61 | "codedeploy.us-east-1.amazonaws.com", 62 | "codedeploy.us-west-2.amazonaws.com" 63 | ] 64 | }, 65 | "Action":"sts:AssumeRole" 66 | } 67 | ] 68 | }, 69 | "Path":"/", 70 | "Policies":[ 71 | { 72 | "PolicyName":"CodeDeployPolicy", 73 | "PolicyDocument":{ 74 | "Statement":[ 75 | { 76 | "Effect":"Allow", 77 | "Action":[ 78 | "ec2:Describe*" 79 | ], 80 | "Resource":[ 81 | "*" 82 | ] 83 | }, 84 | { 85 | "Effect":"Allow", 86 | "Resource":[ 87 | "*" 88 | ], 89 | "Action":[ 90 | "autoscaling:CompleteLifecycleAction", 91 | "autoscaling:DeleteLifecycleHook", 92 | "autoscaling:DescribeLifecycleHooks", 93 | "autoscaling:DescribeAutoScalingGroups", 94 | "autoscaling:PutLifecycleHook", 95 | "autoscaling:RecordLifecycleActionHeartbeat" 96 | ] 97 | } 98 | ] 99 | } 100 | } 101 | ] 102 | } 103 | }, 104 | "CodePipelineTrustRole":{ 105 | "Type":"AWS::IAM::Role", 106 | "Properties":{ 107 | "AssumeRolePolicyDocument":{ 108 | "Statement":[ 109 | { 110 | "Sid":"1", 111 | "Effect":"Allow", 112 | "Principal":{ 113 | "Service":[ 114 | "codepipeline.amazonaws.com" 115 | ] 116 | }, 117 | "Action":"sts:AssumeRole" 118 | } 119 | ] 120 | }, 121 | "Path":"/", 122 | "Policies":[ 123 | { 124 | "PolicyName":"CodePipelinePolicy", 125 | "PolicyDocument":{ 126 | "Version":"2012-10-17", 127 | "Statement":[ 128 | { 129 | "Action":[ 130 | "s3:GetObject", 131 | "s3:GetObjectVersion", 132 | "s3:GetBucketVersioning" 133 | ], 134 | "Resource":"*", 135 | "Effect":"Allow" 136 | }, 137 | { 138 | "Action":[ 139 | "s3:PutObject" 140 | ], 141 | "Resource":[ 142 | "arn:aws:s3:::codepipeline*", 143 | "arn:aws:s3:::dromedary*", 144 | "arn:aws:s3:::elasticbeanstalk*" 145 | ], 146 | "Effect":"Allow" 147 | }, 148 | { 149 | "Action":[ 150 | "codedeploy:CreateDeployment", 151 | "codedeploy:GetApplicationRevision", 152 | "codedeploy:GetDeployment", 153 | "codedeploy:GetDeploymentConfig", 154 | "codedeploy:RegisterApplicationRevision" 155 | ], 156 | "Resource":"*", 157 | "Effect":"Allow" 158 | }, 159 | { 160 | "Action":[ 161 | "elasticbeanstalk:*", 162 | "ec2:*", 163 | "elasticloadbalancing:*", 164 | "autoscaling:*", 165 | "cloudwatch:*", 166 | "s3:*", 167 | "sns:*", 168 | "cloudformation:*", 169 | "rds:*", 170 | "sqs:*", 171 | "ecs:*", 172 | "iam:PassRole" 173 | ], 174 | "Resource":"*", 175 | "Effect":"Allow" 176 | }, 177 | { 178 | "Action":[ 179 | "lambda:InvokeFunction", 180 | "lambda:ListFunctions" 181 | ], 182 | "Resource":"*", 183 | "Effect":"Allow" 184 | } 185 | ] 186 | } 187 | } 188 | ] 189 | } 190 | } 191 | }, 192 | "Outputs":{ 193 | "StackName":{ 194 | "Value":{ 195 | "Ref":"AWS::StackName" 196 | } 197 | }, 198 | "CodeDeployServiceRoleARN":{ 199 | "Description":"The ARN of the Code Deploy Trust Role, which is needed to configure Code Deploy", 200 | "Value":{ 201 | "Fn::GetAtt":[ 202 | "CodeDeployTrustRole", 203 | "Arn" 204 | ] 205 | } 206 | }, 207 | "CodePipelineTrustRoleARN":{ 208 | "Description":"The ARN of the Code Pipeline Trust Role, which is needed to configure Code Pipeline", 209 | "Value":{ 210 | "Fn::GetAtt":[ 211 | "CodePipelineTrustRole", 212 | "Arn" 213 | ] 214 | } 215 | }, 216 | "InstanceProfile":{ 217 | "Description":"Name if instance-profile for Jenkins and application instances", 218 | "Value":{ 219 | "Ref":"InstanceProfile" 220 | } 221 | }, 222 | "InstanceRole":{ 223 | "Description":"IAM Role for Jenkins and application instance profile", 224 | "Value":{ 225 | "Ref":"InstanceRole" 226 | } 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var bg = require('gulp-bg'); 3 | var download = require('gulp-download'); 4 | var zip = require('gulp-zip'); 5 | var gzip = require('gulp-gzip'); 6 | var gunzip = require('gulp-gunzip'); 7 | var jshint = require('gulp-jshint'); 8 | var gls = require('gulp-live-server'); 9 | var install = require('gulp-install'); 10 | var mocha = require('gulp-mocha'); 11 | var tar = require('gulp-tar'); 12 | var untar = require('gulp-untar'); 13 | var gutil = require('gulp-util'); 14 | var exec = require('child_process').exec; 15 | var del = require('del'); 16 | var fs = require('fs'); 17 | var runSequence = require('run-sequence'); 18 | var argv = require('yargs').argv; 19 | 20 | 21 | // default is 8000, which might be common 22 | var ddbLocalPort = 8079; 23 | 24 | // Delete the dist directory 25 | gulp.task('clean', function (cb) { 26 | del(['cookbooks/dromedary/files/app/*', 'dist'], cb); 27 | }); 28 | 29 | // Execute unit tests 30 | gulp.task('test', function () { 31 | return gulp.src('test/*.js', {read: false}) 32 | .pipe(mocha({reporter: 'spec'})); 33 | }); 34 | 35 | // JSHint 36 | gulp.task('lint-app', function() { 37 | return gulp.src(['./app.js', './lib/*.js']) 38 | .pipe(jshint()) 39 | .pipe(jshint.reporter('default', { verbose: true })) 40 | .pipe(jshint.reporter('fail')); 41 | }); 42 | gulp.task('lint-charthandler', function() { 43 | return gulp.src('public/charthandler.js') 44 | .pipe(jshint({ 'globals': { Chart: true, dromedaryChartHandler: true }})) 45 | .pipe(jshint.reporter('default', { verbose: true })) 46 | .pipe(jshint.reporter('fail')); 47 | }); 48 | gulp.task('lint', function(callback) { 49 | runSequence( 50 | ['lint-app', 'lint-charthandler'], 51 | callback 52 | ); 53 | }); 54 | 55 | // Copy dromedary app to cookbooks/dromedary/files/default/app 56 | gulp.task('cookbookfiles:app', function () { 57 | return gulp.src(['app.js', 'appspec.yml'] ) 58 | .pipe(gulp.dest('cookbooks/dromedary/files/default/app')); 59 | }); 60 | gulp.task('cookbookfiles:lib', function () { 61 | return gulp.src(['lib/*.js'] ) 62 | .pipe(gulp.dest('cookbooks/dromedary/files/default/app/lib')); 63 | }); 64 | gulp.task('cookbookfiles:public', function () { 65 | return gulp.src(['public/*'] ) 66 | .pipe(gulp.dest('cookbooks/dromedary/files/default/app/public')); 67 | }); 68 | gulp.task('cookbookfiles:package', function () { 69 | return gulp.src(['package.json']) 70 | .pipe(gulp.dest('cookbooks/dromedary/files/default/app')) 71 | .pipe(install({production: true})); 72 | }); 73 | 74 | // Alias to run above tasks 75 | gulp.task('copy-to-cookbooks', function(callback) { 76 | runSequence( 77 | [ 'cookbookfiles:app', 78 | 'cookbookfiles:lib', 79 | 'cookbookfiles:public', 80 | 'cookbookfiles:package' ], 81 | callback 82 | ); 83 | }); 84 | 85 | // Copy cookbooks to dist/ 86 | gulp.task('dist:berks-vendor', function (cb) { 87 | exec('cd cookbooks/dromedary/ && berks vendor ../../dist', function (err, stdout, stderr) { 88 | gutil.log(stdout); 89 | gutil.log(stderr); 90 | cb(err); 91 | }); 92 | }); 93 | 94 | // Create tarball 95 | gulp.task('dist:tar', function () { 96 | return gulp.src('dist/**/*') 97 | .pipe(tar('archive.tar')) 98 | .pipe(gzip()) 99 | .pipe(gulp.dest('dist')); 100 | }); 101 | 102 | // 'dist' ties together all dist tasks 103 | gulp.task('dist', function(callback) { 104 | runSequence( 105 | 'clean', 106 | 'copy-to-cookbooks', 107 | 'dist:berks-vendor', 108 | 'dist:tar', 109 | callback 110 | ); 111 | }); 112 | 113 | // Execute functional tests 114 | gulp.task('test-functional', function () { 115 | if (process.env.hasOwnProperty('AUTOMATED_ACCEPTANCE_TEST')) { 116 | process.env.TARGET_URL = 'http://localhost:' + require(__dirname + '/dev-lib/targetPort.js'); 117 | } 118 | return gulp.src('test-functional/*.js', {read: false}) 119 | .pipe(mocha({reporter: 'spec'})); 120 | }); 121 | 122 | // run the node app 123 | gulp.task('app:serve', function() { 124 | var server = gls.new('app.js'); 125 | server.start(); 126 | 127 | //use gulp.watch to trigger server actions(notify, start or stop) 128 | gulp.watch(['public/*'], function (file) { 129 | server.notify.apply(server, [file]); 130 | }); 131 | gulp.watch(['app.js', 'lib/*.js'], function() { 132 | server.start.apply(server); 133 | }); 134 | }); 135 | 136 | // Support for DDB local - tasks to clean ddb dir, download and untar 137 | gulp.task('ddb-local:clean', function (cb) { 138 | del(['ddb-local'], cb); 139 | }); 140 | 141 | gulp.task('ddb-local:download', function() { 142 | return download('http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz') 143 | .pipe(gulp.dest(__dirname + '/ddb-local/')); 144 | }); 145 | gulp.task('ddb-local:untar', function () { 146 | return gulp.src(__dirname + '/ddb-local/dynamodb_local_latest.tar.gz') 147 | .pipe(gunzip()) 148 | .pipe(untar()) 149 | .pipe(gulp.dest('.')); 150 | }); 151 | 152 | // Hacky way to run the ddb-local:download & ddb-local:untar only when we need to 153 | gulp.task('ddb-local:download-wrapper', function(callback) { 154 | fs.stat(__dirname + '/ddb-local/DynamoDBLocal.jar', function(err) { 155 | if (err) { 156 | runSequence( 157 | 'ddb-local:download', 158 | 'ddb-local:untar', 159 | callback 160 | ); 161 | } else { 162 | gutil.log(__dirname + '/ddb-local/DynamoDBLocal.jar exists. Skipping download.'); 163 | callback(); 164 | } 165 | }); 166 | }); 167 | 168 | gulp.task('ddb-local:serve', bg( 169 | 'java', '-Djava.library.path=ddb-local/DynamoDBLocal_lib', '-jar', 'ddb-local/DynamoDBLocal.jar', '-dbPath', 'ddb-local/', '-sharedDb', '-port', ddbLocalPort 170 | )); 171 | 172 | gulp.task('ddb-local', function(callback) { 173 | runSequence( 174 | 'ddb-local:download-wrapper', 175 | 'ddb-local:serve', 176 | callback 177 | ); 178 | }); 179 | 180 | // Default is to serve locally 181 | gulp.task('serve', function(callback) { 182 | runSequence( 183 | 'ddb-local', 184 | 'app:serve', 185 | callback 186 | ); 187 | }); 188 | 189 | gulp.task('default', function(callback) { 190 | runSequence( 191 | 'serve', 192 | callback 193 | ); 194 | }); 195 | 196 | 197 | gulp.task('package-site', ['lint-charthandler'],function () { 198 | return gulp.src('public/**/*') 199 | .pipe(zip('site.zip')) 200 | .pipe(gulp.dest('dist')); 201 | }); 202 | 203 | gulp.task('dist-app', function() { 204 | return gulp.src(['package.json','index.js','app.js','lib{,/*.js}']) 205 | .pipe(gulp.dest('dist/app/')) 206 | .pipe(install({production: true})); 207 | }); 208 | 209 | gulp.task('package-app', ['lint-app','test','dist-app'], function () { 210 | return gulp.src(['!dist/app/package.json','!dist/app/**/aws-sdk{,/**}', 'dist/app/**/*']) 211 | .pipe(zip('lambda.zip')) 212 | .pipe(gulp.dest('dist')); 213 | }); 214 | 215 | gulp.task('package-swagger', function() { 216 | return gulp.src('swagger.json') 217 | .pipe(gulp.dest('dist/')); 218 | }); 219 | 220 | gulp.task('package',['package-site','package-app','package-swagger'], function() { 221 | }); 222 | 223 | 224 | -------------------------------------------------------------------------------- /public/charthandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | dromedaryChartHandler = function () { 4 | var ctx = document.getElementById('myChart').getContext('2d'); 5 | var myPieChart; 6 | var commitSha = 'unknown'; 7 | var updateChart = false; 8 | var lastApiHtml = '\n'; 9 | var colorCounts = {}; 10 | var colors = []; 11 | var apiBaseurl = ''; 12 | 13 | function updateLastApiMessage(message) { 14 | var d = new Date(); 15 | lastApiHtml = '
  • ' + d.toDateString() + ' ' + 16 | d.toLocaleTimeString() + ' ' + message + '
  • \n' + lastApiHtml; 17 | document.getElementById('lastApiResponses').innerHTML = lastApiHtml; 18 | } 19 | 20 | function refreshColorCount() { 21 | var w = Math.floor(12 / colors.length); 22 | var colorCountHtml = ''; 23 | var divClass; 24 | var i; 25 | var color; 26 | for (i = 0; i < colors.length; i++) { 27 | color = colors[i]; 28 | divClass = 'col-sm-' + w + ' border-right'; 29 | if (i === colors.length - 1) { 30 | divClass = 'col-sm-' + w; 31 | } 32 | colorCountHtml += '

    ' + 34 | colorCounts[colors[i]].value + '

    ' + 35 | colorCounts[colors[i]].label + '

    \n'; 36 | } 37 | document.getElementById('colorCounts').innerHTML = colorCountHtml; 38 | } 39 | 40 | function incrementColorViaColorCounts(colorToInc) { 41 | var incUrl = apiBaseurl+'increment?color=' + colorToInc; 42 | $.getJSON(incUrl, {}, function(data, status) { 43 | var segment; 44 | var segmentColor; 45 | var segmentIndex; 46 | 47 | // console.log('Color increment GET status: ' + status); 48 | if (status !== 'success') { 49 | console.log('Failed to fetch /increment?color=' + colorToInc); 50 | } else if (data.hasOwnProperty('error')) { 51 | console.log('/increment error: ' + data.error); 52 | updateLastApiMessage('Vote for ' + colorToInc + 53 | ' failed: ' + data.error); 54 | } else if (data.hasOwnProperty('count') && data.count > 0) { 55 | colorCounts[colorToInc].value = data.count; 56 | for (segmentIndex in myPieChart.segments) { 57 | segment = myPieChart.segments[segmentIndex]; 58 | segmentColor = segment.label.toLowerCase(); 59 | if (segmentColor === colorToInc) { 60 | myPieChart.segments[segmentIndex].value = data.count; 61 | updateChart = true; 62 | updateLastApiMessage('Incremented ' + colorToInc + 63 | ' ... new count is ' + data.count); 64 | } 65 | } 66 | } 67 | }); 68 | } 69 | 70 | function pollForUpdates() { 71 | if (!myPieChart.hasOwnProperty('segments')) { 72 | return; 73 | } 74 | $.getJSON(apiBaseurl+'data?countsOnly=true', {}, function(data, status) { 75 | var segment; 76 | var segmentIndex; 77 | var color; 78 | var doUpdate = false; 79 | 80 | // console.log('Chart counts GET status: ' + status); 81 | // console.log('Chart counts GET: ' + JSON.stringify(data)); 82 | 83 | if (status !== 'success') { 84 | console.log('Failed to fetch /data?countsOnly=true'); 85 | return; 86 | } 87 | 88 | for (segmentIndex in myPieChart.segments) { 89 | segment = myPieChart.segments[segmentIndex]; 90 | color = segment.label.toLowerCase(); 91 | if (segment.value !== data[color]) { 92 | console.log('Updating count for ' + color + ' to ' + data[color]); 93 | myPieChart.segments[segmentIndex].value = data[color]; 94 | doUpdate = true; 95 | } 96 | colorCounts[color].value = data[color]; 97 | } 98 | if (doUpdate) { 99 | updateLastApiMessage('New color counts received from backend'); 100 | updateChart = true; 101 | } 102 | }); 103 | } 104 | 105 | function pollForNewConfig() { 106 | $.getJSON('config.json', {}, function(data, status) { 107 | if (status !== 'success' || ! data.hasOwnProperty('version')) { 108 | return; 109 | } 110 | if (commitSha !== data.version) { 111 | updateLastApiMessage('New commit sha detected!'); 112 | location.reload(true); 113 | } 114 | }); 115 | } 116 | 117 | $.ajaxSetup({ timeout: 750 }); 118 | 119 | $.getJSON('config.json', {}, function(data, status) { 120 | if (status !== 'success' || ! data.hasOwnProperty('version')) { 121 | return; 122 | } 123 | commitSha = data.version; 124 | apiBaseurl = data.apiBaseurl; 125 | document.getElementById('gitCommitSha').innerHTML = commitSha; 126 | updateLastApiMessage('Build version is ' + commitSha); 127 | 128 | // load data now that we have our config info 129 | $.getJSON(apiBaseurl+'data', {}, function(data, status) { 130 | var i; 131 | // console.log('Chart data GET status: ' + status); 132 | // console.log('Chart data GET: ' + JSON.stringify(data)); 133 | if (status !== 'success') { 134 | console.log('Failed to fetch /data'); 135 | return; 136 | } 137 | myPieChart = new Chart(ctx).Pie(data); 138 | updateLastApiMessage('Initial chart data received'); 139 | 140 | for (i = 0; i < data.length; i++) { 141 | colors.push(data[i].label.toLowerCase()); 142 | colorCounts[data[i].label.toLowerCase()] = 143 | {label: data[i].label, value: data[i].value}; 144 | } 145 | refreshColorCount(); 146 | }); 147 | 148 | // check for updates occasionally 149 | setInterval(pollForUpdates, 5000); 150 | setInterval(pollForNewConfig, 1000); 151 | }); 152 | 153 | 154 | $('#colorCounts').click(function(evt) { 155 | var colorMatch = evt.target.id.match(/^([a-z]+)Count(Div|Label)?$/); 156 | if (colorMatch !== null) { 157 | incrementColorViaColorCounts(colorMatch[1]); 158 | } 159 | }); 160 | 161 | $('#myChart').click(function(evt) { 162 | var activePoints = myPieChart.getSegmentsAtEvent(evt); 163 | var colorToInc; 164 | var incUrl; 165 | if (activePoints.length < 1 || ! activePoints[0].hasOwnProperty('label')) { 166 | return; 167 | } 168 | colorToInc = activePoints[0].label.toLowerCase(); 169 | incUrl = apiBaseurl+'increment?color=' + colorToInc; 170 | $.getJSON(incUrl, {}, function(data, status) { 171 | console.log('Color increment GET status: ' + status); 172 | if (status !== 'success') { 173 | console.log('Failed to fetch /increment?color=' + colorToInc); 174 | return; 175 | } 176 | if (data.hasOwnProperty('error')) { 177 | console.log('/increment error: ' + data.error); 178 | updateLastApiMessage('Vote for ' + colorToInc + 179 | ' failed: ' + data.error); 180 | } else if (data.hasOwnProperty('count') && data.count > 0) { 181 | activePoints[0].value = data.count; 182 | colorCounts[colorToInc].value = data.count; 183 | updateChart = true; 184 | updateLastApiMessage('Incremented ' + colorToInc + 185 | ' ... new count is ' + data.count); 186 | } 187 | }); 188 | }); 189 | 190 | 191 | setInterval(function() { 192 | if (updateChart) { 193 | myPieChart.update(); 194 | refreshColorCount(); 195 | updateLastApiMessage('Updating chart'); 196 | updateChart = false; 197 | } 198 | }, 100); 199 | }; 200 | 201 | -------------------------------------------------------------------------------- /bin/configure-jenkins.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | echo "In configure-jenkins.sh" 5 | script_dir="$(dirname "$0")" 6 | bin_dir="$(dirname $0)/../bin" 7 | 8 | echo The value of arg 0 = $0 9 | echo The value of arg 1 = $1 10 | echo The value of arg script_dir = $script_dir 11 | 12 | uuid=$(date +%s) 13 | 14 | pipeline_store_stackname=$1 15 | 16 | VPCStackName="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`VPCStackName`].OutputValue')" 17 | IAMStackName="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`IAMStackName`].OutputValue')" 18 | DDBStackName="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`DDBStackName`].OutputValue')" 19 | ENIStackName="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`ENIStackName`].OutputValue')" 20 | MasterStackName="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`MasterStackName`].OutputValue')" 21 | dromedary_s3_bucket=dromedary-"$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`DromedaryS3Bucket`].OutputValue')" 22 | dromedary_branch="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`Branch`].OutputValue')" 23 | dromedary_ec2_key="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`KeyName`].OutputValue')" 24 | 25 | #prod_dns_param="pmd.oneclickdeployment.com" 26 | 27 | my_prod_dns_param="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`ProdHostedZone`].OutputValue')" 28 | prod_dns_param="$MasterStackName$my_prod_dns_param" 29 | echo "The value of prod_dns_param is $prod_dns_param" 30 | 31 | prod_dns="$(echo $prod_dns_param | sed 's/[.]$//')" 32 | 33 | dromedary_hostname=$(echo $prod_dns | cut -f 1 -d . -s) 34 | dromedary_domainname=$(echo $prod_dns | sed s/^$dromedary_hostname[.]//) 35 | 36 | echo "dromedary_hostname is $dromedary_hostname" 37 | echo "dromedary_domainname is $dromedary_domainname" 38 | 39 | my_domainname="$dromedary_domainname." 40 | 41 | 42 | if [ -z "$dromedary_hostname" -o -z "$dromedary_domainname" ]; then 43 | echo "Fatal: $prod_dns is an invalid hostname" >&2 44 | exit 1 45 | fi 46 | 47 | dromedary_zone_id=$(aws route53 list-hosted-zones --output=text --query "HostedZones[?Name==\`${dromedary_domainname}.\`].Id" | sed 's,^/hostedzone/,,') 48 | if [ -z "$dromedary_zone_id" ]; then 49 | echo "Fatal: unable to find Route53 zone id for $dromedary_domainname." >&2 50 | exit 1 51 | fi 52 | 53 | echo "dromedary_zone_id is $dromedary_zone_id" 54 | 55 | dromedary_vpc_stack_name="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`VPCStackName`].OutputValue')" 56 | dromedary_iam_stack_name="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`IAMStackName`].OutputValue')" 57 | dromedary_ddb_stack_name="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`DDBStackName`].OutputValue')" 58 | dromedary_eni_stack_name="$(aws cloudformation describe-stacks --stack-name $pipeline_store_stackname --output text --query 'Stacks[0].Outputs[?OutputKey==`ENIStackName`].OutputValue')" 59 | #dromedary_eni_stack_name="ENIStack$(echo $uuid)" 60 | jenkins_custom_action_provider_name="Jenkins$(echo $uuid)" 61 | 62 | temp_dir=$(mktemp -d /tmp/dromedary.XXXX) 63 | config_dir="$(dirname $0)/../pipeline/jobs/xml" 64 | config_tar_path="$MasterStackName/jenkins-job-configs-$uuid.tgz" 65 | 66 | echo "The value of VPCStackName is $VPCStackName" 67 | echo "The value of IAMStackName is $IAMStackName" 68 | echo "The value of DDBStackName is $DDBStackName" 69 | echo "The value of ENIStackName is $ENIStackName" 70 | echo "The value of MasterStackName is $MasterStackName" 71 | echo "The value of dromedary_s3_bucket is $dromedary_s3_bucket" 72 | echo "The value of dromedary_branch is $dromedary_branch" 73 | echo "The value of dromedary_domainname is $dromedary_domainname" 74 | echo "The value of dromedary_ec2_key is $dromedary_ec2_key" 75 | echo "The value of dromedary_zone_id is $dromedary_zone_id" 76 | echo "The value of dromedary_iam_stack_name is $dromedary_iam_stack_name" 77 | echo "The value of dromedary_ddb_stack_name is $dromedary_ddb_stack_name" 78 | echo "The value of dromedary_eni_stack_name is $dromedary_eni_stack_name" 79 | echo "The value of jenkins_custom_action_provider_name is $jenkins_custom_action_provider_name" 80 | echo "The value of dromedary_eni_stack_name is $dromedary_eni_stack_name" 81 | echo "The value of my_domainname is $my_domainname" 82 | 83 | eni_subnet_id="$(aws cloudformation describe-stacks --stack-name $dromedary_vpc_stack_name --output text --query 'Stacks[0].Outputs[?OutputKey==`SubnetId`].OutputValue')" 84 | 85 | echo "The value of eni_subnet_id is $eni_subnet_id" 86 | 87 | cp -r $config_dir/* $temp_dir/ 88 | pushd $temp_dir > /dev/null 89 | for f in */config.xml; do 90 | sed s/DromedaryJenkins/$jenkins_custom_action_provider_name/ $f > $f.new && mv $f.new $f 91 | done 92 | sed s/S3BUCKET_PLACEHOLDER/$dromedary_s3_bucket/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 93 | sed s/BRANCH_PLACEHOLDER/$dromedary_branch/ job-seed/config.xml > job-seed/config.xml.new && mv job-seed/config.xml.new job-seed/config.xml 94 | sed s/VPC_PLACEHOLDER/$dromedary_vpc_stack_name/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 95 | sed s/IAM_PLACEHOLDER/$dromedary_iam_stack_name/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 96 | sed s/DDB_PLACEHOLDER/$dromedary_ddb_stack_name/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 97 | sed s/ENI_PLACEHOLDER/$dromedary_eni_stack_name/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 98 | sed s/KEY_PLACEHOLDER/$dromedary_ec2_key/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 99 | sed s/HOSTNAME_PLACEHOLDER/$dromedary_hostname/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 100 | sed s/DOMAINNAME_PLACEHOLDER/$dromedary_domainname/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 101 | sed s/ZONE_ID_PLACEHOLDER/$dromedary_zone_id/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 102 | sed s/ACTION_PROVIDER_PLACEHOLDER/$jenkins_custom_action_provider_name/ drom-build/config.xml > drom-build/config.xml.new && mv drom-build/config.xml.new drom-build/config.xml 103 | 104 | tar czf job-configs.tgz * 105 | aws s3 cp job-configs.tgz s3://$dromedary_s3_bucket/$config_tar_path 106 | popd > /dev/null 107 | rm -rf $temp_dir 108 | 109 | if ! aws s3 ls s3://$dromedary_s3_bucket/$config_tar_path; then 110 | echo "Fatal: Unable to upload Jenkins job configs to s3://$dromedary_s3_bucket/$config_tar_path" >&2 111 | exit 1 112 | fi 113 | 114 | aws cloudformation update-stack \ 115 | --stack-name $pipeline_store_stackname \ 116 | --use-previous-template \ 117 | --capabilities="CAPABILITY_IAM" \ 118 | --parameters ParameterKey=UUID,ParameterValue=$uuid \ 119 | ParameterKey=DromedaryS3Bucket,ParameterValue=$dromedary_s3_bucket \ 120 | ParameterKey=Branch,ParameterValue=$dromedary_branch \ 121 | ParameterKey=MasterStackName,ParameterValue=$MasterStackName \ 122 | ParameterKey=JobConfigsTarball,ParameterValue=$config_tar_path \ 123 | ParameterKey=Hostname,ParameterValue=$dromedary_hostname \ 124 | ParameterKey=Domain,ParameterValue=$my_domainname \ 125 | ParameterKey=MyBuildProvider,ParameterValue=$jenkins_custom_action_provider_name \ 126 | ParameterKey=ProdHostedZone,ParameterValue=$prod_dns_param \ 127 | ParameterKey=VPCStackName,ParameterValue=$VPCStackName \ 128 | ParameterKey=IAMStackName,ParameterValue=$IAMStackName \ 129 | ParameterKey=DDBStackName,ParameterValue=$DDBStackName \ 130 | ParameterKey=ENIStackName,ParameterValue=$dromedary_eni_stack_name \ 131 | ParameterKey=DromedaryAppURL,ParameterValue=$prod_dns_param \ 132 | ParameterKey=KeyName,ParameterValue=$dromedary_ec2_key 133 | 134 | sleep 60 135 | 136 | -------------------------------------------------------------------------------- /pipeline/cfn/jenkins-instance.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Stelligent CloudFormation Template for launching Jenkins on an EC2 instance ** This template creates one or more Amazon resources. You will be billed for the AWS resources used if you create a stack from this template.", 4 | "Parameters":{ 5 | "Ec2Key":{ 6 | "Type":"String", 7 | "Description":"Ec2 key for ssh access", 8 | "Default":"" 9 | }, 10 | "SubnetId":{ 11 | "Type":"String", 12 | "Description":"VPC subnet id in which to place jenkins" 13 | }, 14 | "VPC":{ 15 | "Type":"String", 16 | "Description":"VPC ID in which to place Jenkins" 17 | }, 18 | "CfnInitRole":{ 19 | "Type":"String", 20 | "Description":"IAM Role for cfn-init" 21 | }, 22 | "InstanceProfile":{ 23 | "Type":"String", 24 | "Description":"Instance profile for jenkins instance" 25 | }, 26 | "S3Bucket":{ 27 | "Type":"String", 28 | "Description":"Artifact Bucket" 29 | }, 30 | "JobConfigsTarball":{ 31 | "Type":"String", 32 | "Description":"Path to config tarball in S3Bucket" 33 | }, 34 | "SshCidr":{ 35 | "Type":"String", 36 | "Description":"Whitelisted network CIDR for inbound SSH", 37 | "Default":"0.0.0.0/0" 38 | } 39 | }, 40 | "Conditions":{ 41 | "NoEc2Key":{ 42 | "Fn::Equals":[ 43 | { 44 | "Ref":"Ec2Key" 45 | }, 46 | "" 47 | ] 48 | } 49 | }, 50 | "Mappings":{ 51 | "RegionConfig":{ 52 | "us-east-1":{ 53 | "ami":"ami-dcc2b3b6" 54 | } 55 | } 56 | }, 57 | "Resources":{ 58 | "JenkinsSecurityGroup":{ 59 | "Type":"AWS::EC2::SecurityGroup", 60 | "Properties":{ 61 | "VpcId":{ 62 | "Ref":"VPC" 63 | }, 64 | "GroupDescription":"Open ALL the ports", 65 | "SecurityGroupIngress":[ 66 | { 67 | "IpProtocol":"tcp", 68 | "FromPort":"1", 69 | "ToPort":"65535", 70 | "CidrIp":{ 71 | "Ref":"SshCidr" 72 | } 73 | } 74 | ] 75 | } 76 | }, 77 | "JenkinsInstance":{ 78 | "Type":"AWS::EC2::Instance", 79 | "Metadata":{ 80 | "AWS::CloudFormation::Authentication":{ 81 | "S3AccessCreds":{ 82 | "type":"S3", 83 | "roleName":{ 84 | "Ref":"CfnInitRole" 85 | }, 86 | "buckets":[ 87 | { 88 | "Ref":"S3Bucket" 89 | } 90 | ] 91 | } 92 | }, 93 | "AWS::CloudFormation::Init":{ 94 | "config":{ 95 | "files":{ 96 | "/tmp/job-configs.tgz":{ 97 | "source":{ 98 | "Fn::Join":[ 99 | "", 100 | [ 101 | "https://s3.amazonaws.com/", 102 | { 103 | "Ref":"S3Bucket" 104 | }, 105 | "/", 106 | { 107 | "Ref":"JobConfigsTarball" 108 | } 109 | ] 110 | ] 111 | }, 112 | "authentication":"S3AccessCreds", 113 | "mode":"000644", 114 | "owner":"root", 115 | "group":"root" 116 | }, 117 | "/tmp/node-install.tar.gz":{ 118 | "source":"https://nodejs.org/dist/v0.12.7/node-v0.12.7-linux-x64.tar.gz", 119 | "mode":"000644", 120 | "owner":"root", 121 | "group":"root" 122 | } 123 | }, 124 | "commands":{ 125 | "00-extract-configs":{ 126 | "command":{ 127 | "Fn::Join":[ 128 | "", 129 | [ 130 | "cd /var/lib/jenkins/jobs/\n", 131 | "tar xzf /tmp/job-configs.tgz\n", 132 | "chown -R jenkins:jenkins .\n" 133 | ] 134 | ] 135 | } 136 | }, 137 | "10-install-node":{ 138 | "test":"test \"$(/usr/local/bin/node --version 2>/dev/null)\" != 'v0.12.7'", 139 | "command":{ 140 | "Fn::Join":[ 141 | "", 142 | [ 143 | "yum remove -y nodejs npm\n", 144 | "\n", 145 | "cd /usr/local && tar --strip-components 1 -xzf /tmp/node-install.tar.gz\n", 146 | "if [ ! -e /usr/bin/node ]; then\n", 147 | " ln -s /usr/local/bin/node /usr/bin/node\n", 148 | "fi\n", 149 | "if [ ! -e /usr/bin/npm ]; then\n", 150 | " ln -s /usr/local/bin/npm /usr/bin/npm\n", 151 | "fi\n" 152 | ] 153 | ] 154 | } 155 | }, 156 | "15-install-node-modules":{ 157 | "command":"npm install -g gulp" 158 | } 159 | } 160 | } 161 | } 162 | }, 163 | "Properties":{ 164 | "ImageId":{ 165 | "Fn::FindInMap":[ 166 | "RegionConfig", 167 | { 168 | "Ref":"AWS::Region" 169 | }, 170 | "ami" 171 | ] 172 | }, 173 | "InstanceType":"m4.large", 174 | "IamInstanceProfile":{ 175 | "Ref":"InstanceProfile" 176 | }, 177 | "KeyName":{ 178 | "Fn::If":[ 179 | "NoEc2Key", 180 | { 181 | "Ref":"AWS::NoValue" 182 | }, 183 | { 184 | "Ref":"Ec2Key" 185 | } 186 | ] 187 | }, 188 | "Tags":[ 189 | { 190 | "Key":"Application", 191 | "Value":{ 192 | "Ref":"AWS::StackId" 193 | } 194 | }, 195 | { 196 | "Key":"Name", 197 | "Value":{ 198 | "Ref":"AWS::StackName" 199 | } 200 | } 201 | ], 202 | "NetworkInterfaces":[ 203 | { 204 | "GroupSet":[ 205 | { 206 | "Ref":"JenkinsSecurityGroup" 207 | } 208 | ], 209 | "AssociatePublicIpAddress":"true", 210 | "DeviceIndex":"0", 211 | "DeleteOnTermination":"true", 212 | "SubnetId":{ 213 | "Ref":"SubnetId" 214 | } 215 | } 216 | ], 217 | "UserData":{ 218 | "Fn::Base64":{ 219 | "Fn::Join":[ 220 | "", 221 | [ 222 | "#!/bin/bash -xe\n", 223 | "yum update -y aws-cfn-bootstrap\n", 224 | "yum -y upgrade\n", 225 | "\n", 226 | "service jenkins stop\n", 227 | "/opt/aws/bin/cfn-init -v", 228 | " --stack ", 229 | { 230 | "Ref":"AWS::StackName" 231 | }, 232 | " --resource JenkinsInstance ", 233 | " --role ", 234 | { 235 | "Ref":"CfnInitRole" 236 | }, 237 | " --region ", 238 | { 239 | "Ref":"AWS::Region" 240 | }, 241 | "\n", 242 | "\n", 243 | "service jenkins start\n", 244 | "node -v \n", 245 | "npm -v\n", 246 | "\n", 247 | "/opt/aws/bin/cfn-signal -e $? ", 248 | " --stack ", 249 | { 250 | "Ref":"AWS::StackName" 251 | }, 252 | " --resource JenkinsInstance ", 253 | " --region ", 254 | { 255 | "Ref":"AWS::Region" 256 | }, 257 | "\n" 258 | ] 259 | ] 260 | } 261 | } 262 | }, 263 | "CreationPolicy":{ 264 | "ResourceSignal":{ 265 | "Timeout":"PT15M" 266 | } 267 | } 268 | } 269 | }, 270 | "Outputs":{ 271 | "StackName":{ 272 | "Value":{ 273 | "Ref":"AWS::StackName" 274 | } 275 | }, 276 | "PublicDns":{ 277 | "Description":"Public DNS of Jenkins instance", 278 | "Value":{ 279 | "Fn::GetAtt":[ 280 | "JenkinsInstance", 281 | "PublicIp" 282 | ] 283 | } 284 | }, 285 | "JenkinsURL":{ 286 | "Description":"Jenkins URL", 287 | "Value":{ 288 | "Fn::Join":[ 289 | "", 290 | [ 291 | "http://", 292 | { 293 | "Fn::GetAtt":[ 294 | "JenkinsInstance", 295 | "PublicIp" 296 | ] 297 | }, 298 | ":8080/" 299 | ] 300 | ] 301 | } 302 | }, 303 | "SecurityGroup":{ 304 | "Description":"Jenkins Security Group", 305 | "Value":{ 306 | "Fn::GetAtt":[ 307 | "JenkinsSecurityGroup", 308 | "GroupId" 309 | ] 310 | } 311 | } 312 | } 313 | } -------------------------------------------------------------------------------- /pipeline/jobs/dsl/dummypipeline.groovy: -------------------------------------------------------------------------------- 1 | // Dummy Application (DA)(DummyPipeline) 2 | 3 | // Commit Stage 4 | freeStyleJob ('DA-commit-poll-scm') { 5 | steps { 6 | customWorkspace('dummypipeline') 7 | shell('sleep 1') 8 | } 9 | publishers { 10 | downstream('DA-commit-create-build-artifact', 'SUCCESS') 11 | } 12 | deliveryPipelineConfiguration('Commit', 'poll scm') 13 | } 14 | 15 | freeStyleJob ('DA-commit-create-build-artifact') { 16 | steps { 17 | customWorkspace('dummypipeline') 18 | shell('sleep 1.5') 19 | } 20 | publishers { 21 | downstream('DA-commit-unit-tests', 'SUCCESS') 22 | } 23 | deliveryPipelineConfiguration('Commit', 'create build artifact') 24 | } 25 | 26 | freeStyleJob ('DA-commit-unit-tests') { 27 | steps { 28 | customWorkspace('dummypipeline') 29 | shell('sleep 1.5') 30 | } 31 | publishers { 32 | downstream('DA-commit-static-analysis', 'SUCCESS') 33 | } 34 | deliveryPipelineConfiguration('Commit', 'unit tests') 35 | } 36 | 37 | freeStyleJob ('DA-commit-static-analysis') { 38 | steps { 39 | customWorkspace('dummypipeline') 40 | shell('sleep 1.5') 41 | } 42 | publishers { 43 | downstream('DA-commit-upload-artifact', 'SUCCESS') 44 | } 45 | deliveryPipelineConfiguration('Commit', 'static analysis') 46 | } 47 | 48 | freeStyleJob ('DA-commit-upload-artifact') { 49 | steps { 50 | customWorkspace('dummypipeline') 51 | shell('sleep 1.5') 52 | } 53 | publishers { 54 | downstream('DA-accept-integration-tests', 'SUCCESS') 55 | } 56 | deliveryPipelineConfiguration('Commit', 'upload build artifact') 57 | } 58 | 59 | // Acceptance Testing 60 | freeStyleJob ('DA-accept-integration-tests') { 61 | steps { 62 | customWorkspace('dummypipeline') 63 | shell('sleep 1.5') 64 | } 65 | publishers { 66 | downstream('DA-accept-create-env', 'SUCCESS') 67 | } 68 | deliveryPipelineConfiguration('Acceptance Testing', 'integration tests') 69 | } 70 | 71 | freeStyleJob ('DA-accept-create-env') { 72 | steps { 73 | customWorkspace('dummypipeline') 74 | shell('sleep 1.5') 75 | } 76 | publishers { 77 | downstream('DA-accept-config-env', 'SUCCESS') 78 | } 79 | deliveryPipelineConfiguration('Acceptance Testing', 'provision environment') 80 | } 81 | 82 | freeStyleJob ('DA-accept-config-env') { 83 | steps { 84 | customWorkspace('dummypipeline') 85 | shell('sleep 1.5') 86 | } 87 | publishers { 88 | downstream('DA-accept-infra-tests', 'SUCCESS') 89 | } 90 | deliveryPipelineConfiguration('Acceptance Testing', 'configure environment') 91 | } 92 | 93 | freeStyleJob ('DA-accept-infra-tests') { 94 | steps { 95 | customWorkspace('dummypipeline') 96 | shell('sleep 1.5') 97 | } 98 | publishers { 99 | downstream('DA-accept-load-test-db', 'SUCCESS') 100 | } 101 | deliveryPipelineConfiguration('Acceptance Testing', 'infrastructure tests') 102 | } 103 | 104 | freeStyleJob ('DA-accept-load-test-db') { 105 | steps { 106 | customWorkspace('dummypipeline') 107 | shell('sleep 1.5') 108 | } 109 | publishers { 110 | downstream('DA-accept-test-db-migrations', 'SUCCESS') 111 | } 112 | deliveryPipelineConfiguration('Acceptance Testing', 'load test db') 113 | } 114 | 115 | freeStyleJob ('DA-accept-test-db-migrations') { 116 | steps { 117 | customWorkspace('dummypipeline') 118 | shell('sleep 1.5') 119 | } 120 | publishers { 121 | downstream('DA-accept-deploy-app', 'SUCCESS') 122 | } 123 | deliveryPipelineConfiguration('Acceptance Testing', 'migrate test db') 124 | } 125 | 126 | freeStyleJob ('DA-accept-deploy-app') { 127 | steps { 128 | customWorkspace('dummypipeline') 129 | shell('sleep 1.5') 130 | } 131 | publishers { 132 | downstream('DA-accept-automated-tests', 'SUCCESS') 133 | } 134 | deliveryPipelineConfiguration('Acceptance Testing', 'deploy application') 135 | } 136 | 137 | freeStyleJob ('DA-accept-automated-tests') { 138 | steps { 139 | customWorkspace('dummypipeline') 140 | shell('sleep 1.5') 141 | } 142 | publishers { 143 | downstream('DA-accept-terminate-env', 'SUCCESS') 144 | } 145 | deliveryPipelineConfiguration('Acceptance Testing', 'acceptance tests') 146 | } 147 | 148 | freeStyleJob ('DA-accept-terminate-env') { 149 | steps { 150 | customWorkspace('dummypipeline') 151 | shell('sleep 1.5') 152 | } 153 | publishers { 154 | downstream('DA-cap-create-env', 'SUCCESS') 155 | } 156 | deliveryPipelineConfiguration('Acceptance Testing', 'terminate environment') 157 | } 158 | 159 | // Capacity Testing 160 | freeStyleJob ('DA-cap-create-env') { 161 | steps { 162 | customWorkspace('dummypipeline') 163 | shell('sleep 1.5') 164 | } 165 | publishers { 166 | downstream('DA-cap-node-config-env', 'SUCCESS') 167 | } 168 | deliveryPipelineConfiguration('Capacity Testing', 'provision environment') 169 | } 170 | 171 | freeStyleJob ('DA-cap-node-config-env') { 172 | steps { 173 | customWorkspace('dummypipeline') 174 | shell('sleep 1.5') 175 | } 176 | publishers { 177 | downstream('DA-cap-load-db', 'SUCCESS') 178 | } 179 | deliveryPipelineConfiguration('Capacity Testing', 'configure environment') 180 | } 181 | 182 | freeStyleJob ('DA-cap-load-db') { 183 | steps { 184 | customWorkspace('dummypipeline') 185 | shell('sleep 1.5') 186 | } 187 | publishers { 188 | downstream('DA-cap-db-migrations', 'SUCCESS') 189 | } 190 | deliveryPipelineConfiguration('Capacity Testing', 'load perftest db') 191 | } 192 | 193 | freeStyleJob ('DA-cap-db-migrations') { 194 | steps { 195 | customWorkspace('dummypipeline') 196 | shell('sleep 1.5') 197 | } 198 | publishers { 199 | downstream('DA-cap-deploy-app', 'SUCCESS') 200 | } 201 | deliveryPipelineConfiguration('Capacity Testing', 'migrate perftest db') 202 | } 203 | 204 | freeStyleJob ('DA-cap-deploy-app') { 205 | steps { 206 | customWorkspace('dummypipeline') 207 | shell('sleep 1.5') 208 | } 209 | publishers { 210 | downstream('DA-cap-capacity-tests', 'SUCCESS') 211 | } 212 | deliveryPipelineConfiguration('Capacity Testing', 'deploy application') 213 | } 214 | 215 | freeStyleJob ('DA-cap-capacity-tests') { 216 | steps { 217 | customWorkspace('dummypipeline') 218 | shell('sleep 1.5') 219 | } 220 | publishers { 221 | downstream('DA-cap-terminate-env', 'SUCCESS') 222 | } 223 | deliveryPipelineConfiguration('Capacity Testing', 'capacity tests') 224 | } 225 | 226 | freeStyleJob ('DA-cap-terminate-env') { 227 | steps { 228 | customWorkspace('dummypipeline') 229 | shell('sleep 1.5') 230 | } 231 | publishers { 232 | downstream('DA-preprod-create-rc-manifest', 'SUCCESS') 233 | } 234 | deliveryPipelineConfiguration('Capacity Testing', 'terminate environment') 235 | } 236 | 237 | // Pre-Production 238 | freeStyleJob ('DA-preprod-create-rc-manifest') { 239 | steps { 240 | customWorkspace('dummypipeline') 241 | shell('sleep 1.5') 242 | } 243 | publishers { 244 | downstream('DA-preprod-approve-reject-rc', 'SUCCESS') 245 | } 246 | deliveryPipelineConfiguration('Pre-Production', 'create RC manifest') 247 | } 248 | 249 | freeStyleJob ('DA-preprod-approve-reject-rc') { 250 | steps { 251 | customWorkspace('dummypipeline') 252 | shell('sleep 1.5') 253 | } 254 | publishers { 255 | downstream('DA-prod-create-env', 'SUCCESS') 256 | } 257 | deliveryPipelineConfiguration('Pre-Production', 'approve/reject RC') 258 | } 259 | 260 | // Production 261 | freeStyleJob ('DA-prod-create-env') { 262 | steps { 263 | customWorkspace('dummypipeline') 264 | shell('sleep 1.5') 265 | } 266 | publishers { 267 | downstream('DA-prod-node-config-env', 'SUCCESS') 268 | } 269 | deliveryPipelineConfiguration('Production', 'provision environment') 270 | } 271 | 272 | freeStyleJob ('DA-prod-node-config-env') { 273 | steps { 274 | customWorkspace('dummypipeline') 275 | shell('sleep 1.5') 276 | } 277 | publishers { 278 | downstream('DA-prod-migrate-db', 'SUCCESS') 279 | } 280 | deliveryPipelineConfiguration('Production', 'configure environment') 281 | } 282 | 283 | freeStyleJob ('DA-prod-migrate-db') { 284 | steps { 285 | customWorkspace('dummypipeline') 286 | shell('sleep 1.5') 287 | } 288 | publishers { 289 | downstream('DA-prod-deploy-app', 'SUCCESS') 290 | } 291 | deliveryPipelineConfiguration('Production', 'migrate prod db') 292 | } 293 | 294 | freeStyleJob ('DA-prod-deploy-app') { 295 | steps { 296 | customWorkspace('dummypipeline') 297 | shell('sleep 1.5') 298 | } 299 | publishers { 300 | downstream('DA-prod-smoketest', 'SUCCESS') 301 | } 302 | deliveryPipelineConfiguration('Production', 'deploy application') 303 | } 304 | 305 | freeStyleJob ('DA-prod-smoketest') { 306 | steps { 307 | customWorkspace('dummypipeline') 308 | shell('sleep 1.5') 309 | } 310 | publishers { 311 | downstream('DA-prod-blue-green-deployment', 'SUCCESS') 312 | } 313 | deliveryPipelineConfiguration('Production', 'smoke test environment') 314 | } 315 | 316 | freeStyleJob ('DA-prod-blue-green-deployment') { 317 | steps { 318 | customWorkspace('dummypipeline') 319 | shell('sleep 1.5') 320 | } 321 | publishers { 322 | downstream('DA-prod-approve-reject', 'SUCCESS') 323 | } 324 | deliveryPipelineConfiguration('Production', 'blue/green deployment') 325 | } 326 | 327 | freeStyleJob ('DA-prod-approve-reject') { 328 | steps { 329 | customWorkspace('dummypipeline') 330 | shell('sleep 1.5') 331 | } 332 | publishers { 333 | downstream('DA-prod-term-env', 'SUCCESS') 334 | } 335 | deliveryPipelineConfiguration('Production', 'approve/reject deploy') 336 | } 337 | 338 | freeStyleJob ('DA-prod-term-env') { 339 | steps { 340 | customWorkspace('dummypipeline') 341 | shell('sleep 1.5') 342 | } 343 | deliveryPipelineConfiguration('Production', 'terminate old env') 344 | } 345 | -------------------------------------------------------------------------------- /pipeline/cfn/codepipeline-cfn.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Dromedary CodePipeline provisioning", 4 | "Parameters":{ 5 | "ArtifactStoreBucket":{ 6 | "Type":"String", 7 | "Description":"S3 bucket to use for artifacts. Just bucket Name; not URL. IAM user should have access to the bucket.", 8 | "Default":"codepipeline-us-east-1-XXXXXXXXXXX" 9 | }, 10 | "GitHubToken":{ 11 | "NoEcho":"true", 12 | "Type":"String", 13 | "Description":"Secret. It might look something like 9b189a1654643522561f7b3ebd44a1531a4287af OAuthToken with access to Repo. Go to https://github.com/settings/tokens" 14 | }, 15 | "GitHubUser":{ 16 | "Type":"String", 17 | "Description":"GitHub UserName", 18 | "Default":"stelligent" 19 | }, 20 | "Repo":{ 21 | "Type":"String", 22 | "Description":"GitHub Repo to pull from. Only the Name. not the URL", 23 | "Default":"dromedary" 24 | }, 25 | "Branch":{ 26 | "Type":"String", 27 | "Description":"Branch to use from Repo. Only the Name. not the URL", 28 | "Default":"master" 29 | }, 30 | "MyInputArtifacts":{ 31 | "Type":"String", 32 | "Default":"DromedarySource" 33 | }, 34 | "MyBuildProvider":{ 35 | "Type":"String", 36 | "Description":"Unique identifier for Custom Action" 37 | }, 38 | "MyJenkinsURL":{ 39 | "Type":"String" 40 | }, 41 | "CodePipelineServiceRole":{ 42 | "Type":"String", 43 | "Default":"arn:aws:iam::123456789012:role/AWS-CodePipeline-Service", 44 | "Description":"This IAM role must have proper permissions." 45 | } 46 | }, 47 | "Resources":{ 48 | "AppPipeline":{ 49 | "Type":"AWS::CodePipeline::Pipeline", 50 | "Properties":{ 51 | "RoleArn":{ 52 | "Ref":"CodePipelineServiceRole" 53 | }, 54 | "Stages":[ 55 | { 56 | "Name":"Source", 57 | "Actions":[ 58 | { 59 | "InputArtifacts":[ 60 | 61 | ], 62 | "Name":"Source", 63 | "ActionTypeId":{ 64 | "Category":"Source", 65 | "Owner":"ThirdParty", 66 | "Version":"1", 67 | "Provider":"GitHub" 68 | }, 69 | "OutputArtifacts":[ 70 | { 71 | "Name":{ 72 | "Ref":"MyInputArtifacts" 73 | } 74 | } 75 | ], 76 | "Configuration":{ 77 | "Owner":{ 78 | "Ref":"GitHubUser" 79 | }, 80 | "Repo":{ 81 | "Ref":"Repo" 82 | }, 83 | "Branch":{ 84 | "Ref":"Branch" 85 | }, 86 | "OAuthToken":{ 87 | "Ref":"GitHubToken" 88 | } 89 | }, 90 | "RunOrder":1 91 | } 92 | ] 93 | }, 94 | { 95 | "Name":"Commit", 96 | "Actions":[ 97 | { 98 | "InputArtifacts":[ 99 | { 100 | "Name":{ 101 | "Ref":"MyInputArtifacts" 102 | } 103 | } 104 | ], 105 | "Name":"Build", 106 | "ActionTypeId":{ 107 | "Category":"Build", 108 | "Owner":"Custom", 109 | "Version":"1", 110 | "Provider":{ 111 | "Ref":"MyBuildProvider" 112 | } 113 | }, 114 | "OutputArtifacts":[ 115 | { 116 | "Name":"DromedaryBuild" 117 | } 118 | ], 119 | "Configuration":{ 120 | "ProjectName":"drom-build" 121 | }, 122 | "RunOrder":1 123 | }, 124 | { 125 | "InputArtifacts":[ 126 | { 127 | "Name":{ 128 | "Ref":"MyInputArtifacts" 129 | } 130 | } 131 | ], 132 | "Name":"UnitTest", 133 | "ActionTypeId":{ 134 | "Category":"Test", 135 | "Owner":"Custom", 136 | "Version":"1", 137 | "Provider":{ 138 | "Ref":"MyBuildProvider" 139 | } 140 | }, 141 | "OutputArtifacts":[ 142 | 143 | ], 144 | "Configuration":{ 145 | "ProjectName":"drom-unit-test" 146 | }, 147 | "RunOrder":1 148 | }, 149 | { 150 | "InputArtifacts":[ 151 | { 152 | "Name":{ 153 | "Ref":"MyInputArtifacts" 154 | } 155 | } 156 | ], 157 | "Name":"StaticCodeAnalysis", 158 | "ActionTypeId":{ 159 | "Category":"Test", 160 | "Owner":"Custom", 161 | "Version":"1", 162 | "Provider":{ 163 | "Ref":"MyBuildProvider" 164 | } 165 | }, 166 | "OutputArtifacts":[ 167 | 168 | ], 169 | "Configuration":{ 170 | "ProjectName":"drom-staticcode-anal" 171 | }, 172 | "RunOrder":1 173 | } 174 | ] 175 | }, 176 | { 177 | "Name":"Acceptance", 178 | "Actions":[ 179 | { 180 | "InputArtifacts":[ 181 | { 182 | "Name":"DromedaryBuild" 183 | } 184 | ], 185 | "Name":"CreateEnvironment", 186 | "ActionTypeId":{ 187 | "Category":"Test", 188 | "Owner":"Custom", 189 | "Version":"1", 190 | "Provider":{ 191 | "Ref":"MyBuildProvider" 192 | } 193 | }, 194 | "OutputArtifacts":[ 195 | { 196 | "Name":"DromedaryCreate" 197 | } 198 | ], 199 | "Configuration":{ 200 | "ProjectName":"drom-create-env" 201 | }, 202 | "RunOrder":1 203 | }, 204 | { 205 | "InputArtifacts":[ 206 | { 207 | "Name":"DromedaryCreate" 208 | } 209 | ], 210 | "Name":"AcceptanceTest", 211 | "ActionTypeId":{ 212 | "Category":"Test", 213 | "Owner":"Custom", 214 | "Version":"1", 215 | "Provider":{ 216 | "Ref":"MyBuildProvider" 217 | } 218 | }, 219 | "OutputArtifacts":[ 220 | { 221 | "Name":"DromedaryAccepted" 222 | } 223 | ], 224 | "Configuration":{ 225 | "ProjectName":"drom-acceptance-test" 226 | }, 227 | "RunOrder":2 228 | }, 229 | { 230 | "InputArtifacts":[ 231 | { 232 | "Name":"DromedaryCreate" 233 | } 234 | ], 235 | "Name":"InfrastructureTest", 236 | "ActionTypeId":{ 237 | "Category":"Test", 238 | "Owner":"Custom", 239 | "Version":"1", 240 | "Provider":{ 241 | "Ref":"MyBuildProvider" 242 | } 243 | }, 244 | "OutputArtifacts":[ 245 | { 246 | "Name":"DromedaryInfra" 247 | } 248 | ], 249 | "Configuration":{ 250 | "ProjectName":"drom-infra-test" 251 | }, 252 | "RunOrder":2 253 | } 254 | ] 255 | }, 256 | { 257 | "Name":"Production", 258 | "Actions":[ 259 | { 260 | "InputArtifacts":[ 261 | { 262 | "Name":"DromedaryAccepted" 263 | } 264 | ], 265 | "Name":"PromoteEnvironment", 266 | "ActionTypeId":{ 267 | "Category":"Test", 268 | "Owner":"Custom", 269 | "Version":"1", 270 | "Provider":{ 271 | "Ref":"MyBuildProvider" 272 | } 273 | }, 274 | "OutputArtifacts":[ 275 | 276 | ], 277 | "Configuration":{ 278 | "ProjectName":"drom-promote-env" 279 | }, 280 | "RunOrder":1 281 | } 282 | ] 283 | } 284 | ], 285 | "ArtifactStore":{ 286 | "Type":"S3", 287 | "Location":{ 288 | "Ref":"ArtifactStoreBucket" 289 | } 290 | } 291 | } 292 | } 293 | }, 294 | "Outputs":{ 295 | "StackName":{ 296 | "Value":{ 297 | "Ref":"AWS::StackName" 298 | } 299 | }, 300 | "CodePipelineURL":{ 301 | "Value":{ 302 | "Fn::Join":[ 303 | "", 304 | [ 305 | "https://console.aws.amazon.com/codepipeline/home?region=", 306 | { 307 | "Ref":"AWS::Region" 308 | }, 309 | "#/view/", 310 | { 311 | "Ref":"AppPipeline" 312 | } 313 | ] 314 | ] 315 | } 316 | } 317 | } 318 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dromedary :dromedary_camel: 2 | Sample app to demonstrate a working pipeline using [AWS Code Services](https://aws.amazon.com/awscode/) 3 | 4 | ## Infrastructure as Code 5 | 6 | Dromedary was featured by [Paul Duvall](https://twitter.com/PaulDuvall), 7 | [Stelligent](http://www.stelligent.com/)'s Chief Technology Officer, during the 8 | ARC307: Infrastructure as Code breakout session at 2015 9 | [AWS re:Invent](https://reinvent.awsevents.com/). 10 | 11 | Click [here](https://www.youtube.com/watch?v=WL2xSMVXy5w) to view a recording of the re:Invent breakout session or, to view a shorter 10-minute walkthrough of the demo, click [here](https://stelligent.com/2015/11/17/stelligent-aws-continuous-delivery-demo-screencast/). 12 | 13 | ## The Demo App :dromedary_camel: 14 | 15 | The Dromedary demo app is a simple nodejs application that displays a pie chart to users. The data that 16 | describes the pie chart (i.e. the colors and their values) is served by the application. 17 | 18 | If a user clicks on a particular color segment in the chart, the frontend will send a request to the 19 | backend to increment the value for that color and update the chart with the new value. 20 | 21 | The frontend will also poll the backend for changes to values of the colors of the pie chart and update the chart 22 | appropriately. If it detects that a new version of the app has been deployed, it will reload the page. 23 | 24 | Directions are provided to run this demo in AWS and locally. 25 | 26 | ## Core Demo Requirements 27 | 28 | Given a version-control repository, the bootstrapping and the application must be capable of launching from a single _CloudFormation_ command and a CloudFormation button click - assuming that an [EC2 Key Pair](http://docs.aws.amazon.com/gettingstarted/latest/wah/getting-started-prereq.html#create-a-key-pair) and [Route 53 Hosted Zone](http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/CreatingHostedZone.html) has been configured. The demo should not be required to run from a local environment. 29 | 30 | An application pipeline in CodePipeline must go from commit to production in less than 10 minutes. 31 | 32 | It should be capable of running on a new AWS account without any additional setup. 33 | 34 | ## Feature Backlog :dromedary_camel: 35 | 36 | We plan to add additional features in the coming months. Check the [issues](https://github.com/stelligent/dromedary/issues) and [Feature Backlog](https://github.com/stelligent/dromedary/wiki/Feature-Backlog) for more information. 37 | 38 | ### Running in AWS :dromedary_camel: 39 | 40 | **DISCLAIMER**: Executing the following will create billable AWS resources in your account. Be sure to clean 41 | up Dromedary resources after you are done to minimize charges 42 | 43 | **PLEASE NOTE**: This demo is an exercise in _Infrastructure as Code_, and is meant to demonstrate neither 44 | best practices in highly available nor highly secure deployments in AWS. 45 | 46 | #### CloudFormation Bootstrapping (e.g. for AWS Test Drive) 47 | 48 | You'll need the AWS CLI tools [installed](https://aws.amazon.com/cli/) and [configured](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) to start. 49 | 50 | You'll also need to create a hosted zone in [Route53](https://aws.amazon.com/route53/). This hosted zone does 51 | not necessarily need to be publicly available and a registered domain. 52 | 53 | You can either use the AWS CLI or the AWS web console to launch a new CloudFormation stack. To launch from the console, click the button below (you'll need to login to your AWS account if you have not already done so). 54 | 55 | [![Launch CFN stack](https://s3.amazonaws.com/stelligent-training-public/public/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#cstack=sn~DromedaryStack|turl~https://s3.amazonaws.com/stelligent-training-public/master/dromedary-master.json) 56 | 57 | To launch from the CLI, see this example: 58 | 59 | ``` 60 | aws cloudformation create-stack \ 61 | --stack-name DromedaryStack \ 62 | --template-body https://raw.githubusercontent.com/stelligent/dromedary/master/pipeline/cfn/dromedary-master.json \ 63 | --region us-east-1 \ 64 | --disable-rollback --capabilities="CAPABILITY_IAM" \ 65 | --parameters ParameterKey=KeyName,ParameterValue=YOURKEYPAIR \ 66 | ParameterKey=Branch,ParameterValue=master \ 67 | ParameterKey=BaseTemplateURL,ParameterValue=https://s3.amazonaws.com/stelligent-training-public/master/ \ 68 | ParameterKey=GitHubUser,ParameterValue=YOURGITHUBUSER \ 69 | ParameterKey=GitHubToken,ParameterValue=YOURGITHUBTOKEN \ 70 | ParameterKey=DDBTableName,ParameterValue=YOURUNIQUEDDBTABLENAME \ 71 | ParameterKey=ProdHostedZone,ParameterValue=.YOURHOSTEDZONE 72 | ``` 73 | 74 | In the above example, you'll need to set the `YOURHOSTEDZONE` value to your Route53 hosted zone. See [Hosted Zones](https://console.aws.amazon.com/route53/home?region=us-east-1#hosted-zones:) for the hosted zones configured in your AWS account. 75 | 76 | To integrate with GitHub, AWS CodePipeline uses OAuth tokens. Generate your token at [GitHub](https://github.com/settings/tokens) and ensure you enable the following two scopes: 77 | * `admin:repo_hook`, which is used to detect when you have committed and pushed changes to the repository 78 | * `repo`, which is used to read and pull artifacts from public and private repositories into a pipeline 79 | 80 | Parameters | Description 81 | ---------- | ------------ 82 | KeyName | The EC2 keypair name to use for ssh access to the bootstrapping instance. 83 | GitHubUser | GitHub UserName. This username must be the owner of the Repo. 84 | GitHubToken | Secret. OAuthToken with access to Repo. Go to https://github.com/settings/tokens. 85 | BaseTemplateURL | S3 Base URL of all the CloudFormation templated used in Dromedary (without the file names) 86 | DDBTableName | Unique TableName for the Dromedary DynamoDB database. 87 | ProdHostedZone | Route53 Hosted Zone. You must precede `YOURHOSTEDZONE` with a `.` See [Hosted Zones](https://console.aws.amazon.com/route53/home?region=us-east-1#hosted-zones:) for the hosted zones configured in your AWS account. 88 | 89 | As part of the bootstrapping process, it will automatically launch the Dromedary application stack via CodePipeline. 90 | 91 | #### Outputs 92 | 93 | A few of the most relevant CloudFormation outputs from the master stack are listed in the table below. 94 | 95 | Output | Description 96 | ---------- | ------------ 97 | CodePipelineURL | The URL to the instantiated pipeline 98 | JenkinsURL | The URL to Jenkins server that runs the execution of jobs for CodePipeline 99 | DromedaryAppURL | Link to the working application once the application pipeline is complete 100 | 101 | #### Post-bootstrap steps 102 | 103 | **IMPORTANT**: You will need to manually delete the CloudFormation stack once you've completed usage. You will be charged for AWS resource usage. 104 | 105 | **Bootstrapping Tests** 106 | View the outputs in CloudFormation for links to test reports uploaded to your Dromedary S3 bucket. 107 | 108 | Upon completion of a successful pipeline execution, Dromedary will be available by going to the Outputs tab on the master CloudFormation stack and clicking on the value for the `DromedaryAppURL` Output. If that hosted zone is not a publicly registered domain, you can access Dromedary via IP address. The IP address can be queried by viewing the EIP output of the ENI CloudFormation stack. 109 | 110 | Every time changes are pushed to Github, CodePipeline will build, test, deploy and release those changes. 111 | 112 | #### Configure Jenkins Security 113 | 114 | **IMPORTANT**: It's very important that you enable Jenkins security. 115 | 116 | From CodePipeline, click on any of the Actions to launch Jenkins. From Jenkins, perform the following steps to configure security: 117 | 118 | 1. Manage `Jenkins` > `Configure Global Security` 119 | 1. Check `Enable Security` 120 | 1. Click `Jenkins’ own user database` 121 | 1. Check `Allow users to sign up` 122 | 1. Check `Logged in users can do anything` 123 | 1. Click the `Save` button 124 | 1. Click `Sign Up` in the top right to create an account 125 | 1. Save and login as that user 126 | 1. Manage `Jenkins` > `Configure Global Security` 127 | 1. Check `Matrix Based Security` 128 | 1. Add a line for the user you just created 129 | 1. Check the `Administer` box 130 | 1. Click the `Save` button 131 | 132 | #### Cleanup 133 | To delete (nearly) all Dromedary resources, delete any Dromedary application stacks and delete the master CloudFormation stack. The only resources that remain and require manual deletion is the Dromedary S3 bucket. 134 | 135 | ### Running Locally :dromedary_camel: 136 | 137 | #### Install Prerequisites 138 | 139 | 1. Ensure [nodejs](https://nodejs.org/) and [npm](https://www.npmjs.com/) are installed 140 | * On Mac OS X, this can be done via [Homebrew](http://brew.sh/): `brew install node` 141 | * On Amazon Linux, packages are available via the [EPEL](https://fedoraproject.org/wiki/EPEL) yum repo: `yum install -y nodejs npm --enablerepo=epel` 142 | 1. Java must be installed so that [DynamoDB Local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html) can run 143 | 1. Install dependencies: `npm install` 144 | 145 | NOTE: Dromedary relies on [gulp](http://gulpjs.com/) for local development and build tasks. 146 | You may need to install gulp globally: `npm install -g gulp` 147 | 148 | If gulp is not globally installed, ensure `./node_modules/.bin/` is in your PATH. 149 | 150 | #### Local Server 151 | 152 | The default task will start dynamodb-local on port 8079 and a node server listening on port 8080: 153 | 154 | 1. Run `gulp` - this downloads and starts DynamoDB Local and starts Node 155 | 1. Point your webbrowser to [http://localhost:8080](http://localhost:8080) 156 | 157 | #### Executing Unit Tests 158 | 159 | Unit tests located in `test/` were written using [Mocha](https://mochajs.org/) and [Chai](http://chaijs.com/), 160 | and can be executed using the `test` task: 161 | 162 | 1. Run `gulp test` 163 | 164 | #### Executing Acceptance Tests 165 | 166 | Acceptance tests located in `tests-functional/` require Dromedary to be running (eg: `gulp`), and can be 167 | executed using the `test-functional` task: 168 | 169 | 1. Run `gulp test-functional` 170 | 171 | These tests (which, at this time are closer to integration tests than functional tests) exercise the API 172 | endpoints defined in `app.js`. 173 | 174 | #### Building a Distributable Archive 175 | 176 | The `dist` task copies relevant files to `dist/` and installs only dependencies required to run the standalone 177 | app: 178 | 179 | 1. Run `gulp dist` 180 | 181 | `dist/archive.tar.gz` will be created if this task run successfully. 182 | -------------------------------------------------------------------------------- /pipeline/cfn/vpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description": "Dromedary demo - network infrastructure", 4 | "Resources":{ 5 | "VPC":{ 6 | "Type":"AWS::EC2::VPC", 7 | "Properties":{ 8 | "CidrBlock":"10.0.0.0/16", 9 | "Tags":[ 10 | { 11 | "Key":"Name", 12 | "Value":{ 13 | "Ref":"AWS::StackName" 14 | } 15 | }, 16 | { 17 | "Key":"Application", 18 | "Value":{ 19 | "Ref":"AWS::StackId" 20 | } 21 | } 22 | ] 23 | } 24 | }, 25 | "Subnet":{ 26 | "Type":"AWS::EC2::Subnet", 27 | "Properties":{ 28 | "VpcId":{ 29 | "Ref":"VPC" 30 | }, 31 | "CidrBlock":"10.0.0.0/24", 32 | "Tags":[ 33 | { 34 | "Key":"Name", 35 | "Value":{ 36 | "Ref":"AWS::StackName" 37 | } 38 | }, 39 | { 40 | "Key":"Application", 41 | "Value":{ 42 | "Ref":"AWS::StackId" 43 | } 44 | } 45 | ] 46 | } 47 | }, 48 | "InternetGateway":{ 49 | "Type":"AWS::EC2::InternetGateway", 50 | "Properties":{ 51 | "Tags":[ 52 | { 53 | "Key":"Name", 54 | "Value":{ 55 | "Ref":"AWS::StackName" 56 | } 57 | }, 58 | { 59 | "Key":"Application", 60 | "Value":{ 61 | "Ref":"AWS::StackId" 62 | } 63 | } 64 | ] 65 | } 66 | }, 67 | "AttachGateway":{ 68 | "Type":"AWS::EC2::VPCGatewayAttachment", 69 | "Properties":{ 70 | "VpcId":{ 71 | "Ref":"VPC" 72 | }, 73 | "InternetGatewayId":{ 74 | "Ref":"InternetGateway" 75 | } 76 | } 77 | }, 78 | "RouteTable":{ 79 | "Type":"AWS::EC2::RouteTable", 80 | "Properties":{ 81 | "VpcId":{ 82 | "Ref":"VPC" 83 | }, 84 | "Tags":[ 85 | { 86 | "Key":"Name", 87 | "Value":{ 88 | "Ref":"AWS::StackName" 89 | } 90 | }, 91 | { 92 | "Key":"Application", 93 | "Value":{ 94 | "Ref":"AWS::StackId" 95 | } 96 | } 97 | ] 98 | } 99 | }, 100 | "Route":{ 101 | "Type":"AWS::EC2::Route", 102 | "DependsOn":"AttachGateway", 103 | "Properties":{ 104 | "RouteTableId":{ 105 | "Ref":"RouteTable" 106 | }, 107 | "DestinationCidrBlock":"0.0.0.0/0", 108 | "GatewayId":{ 109 | "Ref":"InternetGateway" 110 | } 111 | } 112 | }, 113 | "SubnetRouteTableAssociation":{ 114 | "Type":"AWS::EC2::SubnetRouteTableAssociation", 115 | "Properties":{ 116 | "SubnetId":{ 117 | "Ref":"Subnet" 118 | }, 119 | "RouteTableId":{ 120 | "Ref":"RouteTable" 121 | } 122 | } 123 | }, 124 | "NetworkAcl":{ 125 | "Type":"AWS::EC2::NetworkAcl", 126 | "Properties":{ 127 | "VpcId":{ 128 | "Ref":"VPC" 129 | }, 130 | "Tags":[ 131 | { 132 | "Key":"Name", 133 | "Value":{ 134 | "Ref":"AWS::StackName" 135 | } 136 | }, 137 | { 138 | "Key":"Application", 139 | "Value":{ 140 | "Ref":"AWS::StackId" 141 | } 142 | } 143 | ] 144 | } 145 | }, 146 | "InboundSSHNetworkAclEntry":{ 147 | "Type":"AWS::EC2::NetworkAclEntry", 148 | "Properties":{ 149 | "NetworkAclId":{ 150 | "Ref":"NetworkAcl" 151 | }, 152 | "RuleNumber":"100", 153 | "Protocol":"6", 154 | "RuleAction":"allow", 155 | "Egress":"false", 156 | "CidrBlock":"0.0.0.0/0", 157 | "PortRange":{ 158 | "From":"22", 159 | "To":"22" 160 | } 161 | } 162 | }, 163 | "InboundHTTPNetworkAclEntry":{ 164 | "Type":"AWS::EC2::NetworkAclEntry", 165 | "Properties":{ 166 | "NetworkAclId":{ 167 | "Ref":"NetworkAcl" 168 | }, 169 | "RuleNumber":"105", 170 | "Protocol":"6", 171 | "RuleAction":"allow", 172 | "Egress":"false", 173 | "CidrBlock":"0.0.0.0/0", 174 | "PortRange":{ 175 | "From":"80", 176 | "To":"80" 177 | } 178 | } 179 | }, 180 | "InboundHTTPNetworkAclEntry2":{ 181 | "Type":"AWS::EC2::NetworkAclEntry", 182 | "Properties":{ 183 | "NetworkAclId":{ 184 | "Ref":"NetworkAcl" 185 | }, 186 | "RuleNumber":"110", 187 | "Protocol":"6", 188 | "RuleAction":"allow", 189 | "Egress":"false", 190 | "CidrBlock":"0.0.0.0/0", 191 | "PortRange":{ 192 | "From":"8080", 193 | "To":"8080" 194 | } 195 | } 196 | }, 197 | "InboundHTTPSNetworkAclEntry":{ 198 | "Type":"AWS::EC2::NetworkAclEntry", 199 | "Properties":{ 200 | "NetworkAclId":{ 201 | "Ref":"NetworkAcl" 202 | }, 203 | "RuleNumber":"130", 204 | "Protocol":"6", 205 | "RuleAction":"allow", 206 | "Egress":"false", 207 | "CidrBlock":"0.0.0.0/0", 208 | "PortRange":{ 209 | "From":"443", 210 | "To":"443" 211 | } 212 | } 213 | }, 214 | "InboundNtpResponseUdpPortNetworkAclEntry":{ 215 | "Type":"AWS::EC2::NetworkAclEntry", 216 | "Properties":{ 217 | "NetworkAclId":{ 218 | "Ref":"NetworkAcl" 219 | }, 220 | "RuleNumber":"120", 221 | "Protocol":"17", 222 | "RuleAction":"allow", 223 | "Egress":"false", 224 | "CidrBlock":"0.0.0.0/0", 225 | "PortRange":{ 226 | "From":"123", 227 | "To":"123" 228 | } 229 | } 230 | }, 231 | "InboundNtpResponseTcpPortNetworkAclEntry":{ 232 | "Type":"AWS::EC2::NetworkAclEntry", 233 | "Properties":{ 234 | "NetworkAclId":{ 235 | "Ref":"NetworkAcl" 236 | }, 237 | "RuleNumber":"125", 238 | "Protocol":"6", 239 | "RuleAction":"allow", 240 | "Egress":"false", 241 | "CidrBlock":"0.0.0.0/0", 242 | "PortRange":{ 243 | "From":"123", 244 | "To":"123" 245 | } 246 | } 247 | }, 248 | "InboundResponsePortsNetworkAclEntry":{ 249 | "Type":"AWS::EC2::NetworkAclEntry", 250 | "Properties":{ 251 | "NetworkAclId":{ 252 | "Ref":"NetworkAcl" 253 | }, 254 | "RuleNumber":"900", 255 | "Protocol":"6", 256 | "RuleAction":"allow", 257 | "Egress":"false", 258 | "CidrBlock":"0.0.0.0/0", 259 | "PortRange":{ 260 | "From":"1024", 261 | "To":"65535" 262 | } 263 | } 264 | }, 265 | "OutBoundHTTPNetworkAclEntry":{ 266 | "Type":"AWS::EC2::NetworkAclEntry", 267 | "Properties":{ 268 | "NetworkAclId":{ 269 | "Ref":"NetworkAcl" 270 | }, 271 | "RuleNumber":"100", 272 | "Protocol":"6", 273 | "RuleAction":"allow", 274 | "Egress":"true", 275 | "CidrBlock":"0.0.0.0/0", 276 | "PortRange":{ 277 | "From":"80", 278 | "To":"80" 279 | } 280 | } 281 | }, 282 | "OutBoundHTTPNetworkAclEntry2":{ 283 | "Type":"AWS::EC2::NetworkAclEntry", 284 | "Properties":{ 285 | "NetworkAclId":{ 286 | "Ref":"NetworkAcl" 287 | }, 288 | "RuleNumber":"105", 289 | "Protocol":"6", 290 | "RuleAction":"allow", 291 | "Egress":"true", 292 | "CidrBlock":"0.0.0.0/0", 293 | "PortRange":{ 294 | "From":"8080", 295 | "To":"8080" 296 | } 297 | } 298 | }, 299 | "OutBoundHTTPSNetworkAclEntry":{ 300 | "Type":"AWS::EC2::NetworkAclEntry", 301 | "Properties":{ 302 | "NetworkAclId":{ 303 | "Ref":"NetworkAcl" 304 | }, 305 | "RuleNumber":"130", 306 | "Protocol":"6", 307 | "RuleAction":"allow", 308 | "Egress":"true", 309 | "CidrBlock":"0.0.0.0/0", 310 | "PortRange":{ 311 | "From":"443", 312 | "To":"443" 313 | } 314 | } 315 | }, 316 | "OutBoundNtpUdpNetworkAclEntry":{ 317 | "Type":"AWS::EC2::NetworkAclEntry", 318 | "Properties":{ 319 | "NetworkAclId":{ 320 | "Ref":"NetworkAcl" 321 | }, 322 | "RuleNumber":"115", 323 | "Protocol":"17", 324 | "RuleAction":"allow", 325 | "Egress":"true", 326 | "CidrBlock":"0.0.0.0/0", 327 | "PortRange":{ 328 | "From":"123", 329 | "To":"123" 330 | } 331 | } 332 | }, 333 | "OutBoundNtpTcpNetworkAclEntry":{ 334 | "Type":"AWS::EC2::NetworkAclEntry", 335 | "Properties":{ 336 | "NetworkAclId":{ 337 | "Ref":"NetworkAcl" 338 | }, 339 | "RuleNumber":"120", 340 | "Protocol":"6", 341 | "RuleAction":"allow", 342 | "Egress":"true", 343 | "CidrBlock":"0.0.0.0/0", 344 | "PortRange":{ 345 | "From":"123", 346 | "To":"123" 347 | } 348 | } 349 | }, 350 | "OutBoundResponsePortsNetworkAclEntry":{ 351 | "Type":"AWS::EC2::NetworkAclEntry", 352 | "Properties":{ 353 | "NetworkAclId":{ 354 | "Ref":"NetworkAcl" 355 | }, 356 | "RuleNumber":"900", 357 | "Protocol":"6", 358 | "RuleAction":"allow", 359 | "Egress":"true", 360 | "CidrBlock":"0.0.0.0/0", 361 | "PortRange":{ 362 | "From":"1024", 363 | "To":"65535" 364 | } 365 | } 366 | }, 367 | "SubnetNetworkAclAssociation":{ 368 | "Type":"AWS::EC2::SubnetNetworkAclAssociation", 369 | "Properties":{ 370 | "SubnetId":{ 371 | "Ref":"Subnet" 372 | }, 373 | "NetworkAclId":{ 374 | "Ref":"NetworkAcl" 375 | } 376 | } 377 | } 378 | }, 379 | "Outputs":{ 380 | "StackName":{ 381 | "Value":{ 382 | "Ref":"AWS::StackName" 383 | } 384 | }, 385 | "SubnetId":{ 386 | "Description":"Id of VPC Subnet", 387 | "Value":{ 388 | "Ref":"Subnet" 389 | } 390 | }, 391 | "VPC":{ 392 | "Description":"VPC ID", 393 | "Value":{ 394 | "Ref":"VPC" 395 | } 396 | } 397 | } 398 | } -------------------------------------------------------------------------------- /pipeline/cfn/app-instance.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Dromedary demo - application instance", 4 | 5 | "Parameters" : { 6 | "Ec2Key": { 7 | "Type": "String", 8 | "Description": "Ec2 key for ssh access", 9 | "Default": "" 10 | }, 11 | "SubnetId": { 12 | "Type": "String", 13 | "Description": "VPC subnet id in which to place instance" 14 | }, 15 | "VPC": { 16 | "Type": "String", 17 | "Description": "VPC id in which to place instance" 18 | }, 19 | "CfnInitRole": { 20 | "Type": "String", 21 | "Description": "IAM Role for cfn-init" 22 | }, 23 | "InstanceProfile": { 24 | "Type": "String", 25 | "Description": "Instance profile for app instance" 26 | }, 27 | "S3Bucket": { 28 | "Type": "String", 29 | "Description": "Artifact Bucket" 30 | }, 31 | "ArtifactPath": { 32 | "Type": "String", 33 | "Description": "Path to tarball in Artifact Bucket", 34 | "Default": "" 35 | }, 36 | "CodeDeployTag": { 37 | "Type": "String", 38 | "Description": "Resource Tags for Deployment Group (non-zero enables CodeDeploy agent)", 39 | "Default": "1" 40 | }, 41 | "DynamoDbTable": { 42 | "Type": "String", 43 | "Description": "DynamoDb table name for persistent storage", 44 | "MinLength": "1", 45 | "MaxLength": "32" 46 | } 47 | }, 48 | 49 | "Conditions": { 50 | "NoEc2Key": { "Fn::Equals" : [ { "Ref" : "Ec2Key" }, "" ]}, 51 | "InstallCodeDeploy": { "Fn::Not" : [{ "Fn::Equals" : [ { "Ref" : "CodeDeployTag" }, "" ]} ]} 52 | }, 53 | 54 | "Mappings": { 55 | "RegionConfig": { 56 | "us-east-1": { 57 | "ami": "ami-2d652448" 58 | } 59 | } 60 | }, 61 | 62 | "Resources": { 63 | "InstanceSecurityGroup": { 64 | "Type": "AWS::EC2::SecurityGroup", 65 | "Properties": { 66 | "VpcId": { "Ref": "VPC" }, 67 | "GroupDescription": "Enable SSH access via port 22", 68 | "SecurityGroupIngress": [ 69 | { "IpProtocol": "tcp", "FromPort": "22", "ToPort": "22", "CidrIp": "152.3.4.5/32" }, 70 | { "IpProtocol": "tcp", "FromPort": "80", "ToPort": "80", "CidrIp": "0.0.0.0/0" }, 71 | { "IpProtocol": "tcp", "FromPort": "443", "ToPort": "443", "CidrIp": "0.0.0.0/0" }, 72 | { "IpProtocol": "tcp", "FromPort": "8080", "ToPort": "8080", "CidrIp": "0.0.0.0/0" } 73 | ] 74 | } 75 | }, 76 | 77 | "WebServerInstance": { 78 | "Type": "AWS::EC2::Instance", 79 | "Metadata": { 80 | "AWS::CloudFormation::Authentication" : { 81 | "S3AccessCreds" : { 82 | "type" : "S3", 83 | "roleName" : { "Ref" : "CfnInitRole" }, 84 | "buckets" : [{ "Ref" : "S3Bucket" }] 85 | } 86 | }, 87 | "AWS::CloudFormation::Init": { 88 | "configSets" : { 89 | "chef" : [ "chef" ], 90 | "base" : [ "base" ], 91 | "noprereqs" : [ "noprereqs" ], 92 | "default" : [ { "ConfigSet" : "base" }] 93 | }, 94 | "chef": { 95 | "files": { 96 | "/tmp/chefdk.rpm": { 97 | "source": "https://opscode-omnibus-packages.s3.amazonaws.com/el/6/x86_64/chefdk-0.7.0-1.el6.x86_64.rpm", 98 | "mode": "000644", 99 | "owner": "root", 100 | "group": "root" 101 | } 102 | }, 103 | "commands": { 104 | "10-install-chef": { 105 | "command": "rpm -ivh /tmp/chefdk.rpm" 106 | } 107 | } 108 | }, 109 | "base": { 110 | "files": { 111 | "/tmp/dromedary.tgz": { 112 | "source": { "Fn::Join": [ "", [ "https://s3.amazonaws.com/", { "Ref" : "S3Bucket" }, "/", { "Ref": "ArtifactPath" } ]]}, 113 | "authentication": "S3AccessCreds", 114 | "mode": "000644", 115 | "owner": "root", 116 | "group": "root" 117 | } 118 | }, 119 | "commands": { 120 | "10-extract-dromedary": { 121 | "command": { "Fn::Join" : [ "", [ 122 | "mkdir -p -m755 /userdata\n", 123 | "cd /userdata\n", 124 | "tar xzf /tmp/dromedary.tgz\n" 125 | ]]} 126 | }, 127 | "20-run-chef": { 128 | "cwd": "/userdata", 129 | "env": { 130 | "HOME": "/root", 131 | "DROMEDARY_DDB_TABLE_NAME": { "Ref": "DynamoDbTable" } 132 | }, 133 | "command": { "Fn::Join" : [ "", [ 134 | "cat > /userdata/solo.rb < /userdata/solo.rb < /dev/null 2>&2; then\n", 229 | " cfn_init -c chef || error_exit 'Failed to run cfn-init chef'\n", 230 | "fi\n", 231 | "\n", 232 | "if [ -e /.dromedary-prereqs-installed ]; then\n", 233 | " cfn_init -c noprereqs || error_exit 'Failed to run cfn-init noprereqs'\n", 234 | "else\n", 235 | " yum -y upgrade\n", 236 | " cfn_init || error_exit 'Failed to run cfn-init'\n", 237 | "fi\n", 238 | "cfn_signal_ok\n", 239 | "\n" 240 | ]]}} 241 | }, 242 | "CreationPolicy": { 243 | "ResourceSignal": { "Timeout": "PT15M" } 244 | } 245 | } 246 | }, 247 | 248 | "Outputs": { 249 | "PublicDns": { 250 | "Description": "Public DNS of Dromedary App instance", 251 | "Value": { "Fn::GetAtt": [ "WebServerInstance", "PublicIp" ]} 252 | }, 253 | "InstanceId": { 254 | "Description": "Dromedary App instance id", 255 | "Value": { "Ref": "WebServerInstance" } 256 | }, 257 | "InstanceSecurityGroup": { 258 | "Description": "Security group id of app instance", 259 | "Value": { "Ref": "InstanceSecurityGroup" } 260 | } 261 | } 262 | } 263 | --------------------------------------------------------------------------------