balancer 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7210c71bd43eea249cb93535e4d264dc1ca79aa9c5899c588cb3b44c628e7335
4
- data.tar.gz: 7ce6745e43f8f7d0d96e18c56a697d34c92fe478c0d6aff1891bd174c1444e77
3
+ metadata.gz: 1432379a1ef4e87892a5a90a1fbc50428156124a52b12a64d259cd05c451b10b
4
+ data.tar.gz: 469af119bc5fb4093ce3a84b4cf008167dac9f58ec6041b0d11af5efe3d2fc6b
5
5
  SHA512:
6
- metadata.gz: a306a2be2701c9b876128137fa31b712609e645cf534a17b6289d81bd1935e4e0eb2bfcd5f59f7ed317945866c844c7ab433feac05030d2d5b0ecf5d69ccfb91
7
- data.tar.gz: b1a64e2e011970038894b81f2ac57ee5f16f157cc91276e4cf21f872a114293268697bb239a3fed251396f7891ce2e50e093a004cec120b863988c2a2cef9cae
6
+ metadata.gz: 94760e1fab90cc6762b8508bdc1e01a93a887c7dee4d5a4c8c7d141a802531465901a7da28ea17b6d40c5834f69acd6689e3fb550cd1babef54c08770738624e
7
+ data.tar.gz: 8b83a2318937d7ec0ee8110e6a17cd86048722008ef21500fd57a0499adcb1b5a030e1095969babb4e7d233836a5c364e16fd8812ac1f7284c4b9933419ded24
@@ -0,0 +1,26 @@
1
+ #!/bin/bash -eux
2
+
3
+ # Even though specs also generate docs, lets run again to ensure clean slate
4
+ rake docs
5
+
6
+ out=$(git status docs)
7
+ if [[ "$out" = *"nothing to commit"* ]]; then
8
+ exit
9
+ fi
10
+
11
+ COMMIT_MESSAGE="docs updated by circleci"
12
+
13
+ # If the last commit already updated the docs, then exit.
14
+ # Preventable measure to avoid infinite loop.
15
+ if git log -1 --pretty=oneline | grep "$COMMIT_MESSAGE" ; then
16
+ exit
17
+ fi
18
+
19
+ # If reach here, we have some changes on docs that we should commit.
20
+ # Even though s
21
+ git add docs
22
+ git commit -m "$COMMIT_MESSAGE"
23
+
24
+ # https://makandracards.com/makandra/12107-git-show-current-branch-name-only
25
+ current_branch=$(git rev-parse --abbrev-ref HEAD)
26
+ git push origin "$current_branch"
@@ -0,0 +1,70 @@
1
+ # Ruby CircleCI 2.0 configuration file
2
+ #
3
+ # Check https://circleci.com/docs/2.0/language-ruby/ for more details
4
+ #
5
+ version: 2
6
+ jobs:
7
+ build:
8
+ docker:
9
+ # specify the version you desire here
10
+ - image: circleci/ruby:2.5.1-node-browsers
11
+
12
+ # Specify service dependencies here if necessary
13
+ # CircleCI maintains a library of pre-built images
14
+ # documented at https://circleci.com/docs/2.0/circleci-images/
15
+ # - image: circleci/postgres:9.4
16
+
17
+ working_directory: ~/repo
18
+
19
+ steps:
20
+ - checkout
21
+
22
+ - run:
23
+ name: submodule sync
24
+ command: |
25
+ git submodule sync
26
+ git submodule update --init
27
+
28
+ # Download and cache dependencies
29
+ - restore_cache:
30
+ keys:
31
+ - v1-dependencies-{{ checksum "Gemfile" }}
32
+ # fallback to using the latest cache if no exact match is found
33
+ - v1-dependencies-
34
+
35
+ - run:
36
+ name: install dependencies
37
+ command: |
38
+ bundle install --jobs=4 --retry=3 --path vendor/bundle
39
+
40
+ - save_cache:
41
+ paths:
42
+ - ./vendor/bundle
43
+ key: v1-dependencies-{{ checksum "Gemfile" }}
44
+
45
+ # specs need git configured ad commit_docs.sh required it also
46
+ - run:
47
+ name: configure git
48
+ command: |
49
+ git config --global user.email "tongueroo@gmail.com"
50
+ git config --global user.name "Tung Nguyen"
51
+
52
+ # run tests!
53
+ - run:
54
+ name: run tests
55
+ command: |
56
+ mkdir /tmp/test-results
57
+ bundle exec rspec
58
+
59
+ # - run:
60
+ # name: commit cli reference docs
61
+ # command: |
62
+ # chmod a+x -R .circleci/bin
63
+ # .circleci/bin/commit_docs.sh
64
+
65
+ # collect reports
66
+ - store_test_results:
67
+ path: /tmp/test-results
68
+ - store_artifacts:
69
+ path: /tmp/test-results
70
+ destination: test-results
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
2
  --format documentation
3
+ --require spec_helper
@@ -3,5 +3,10 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
5
 
6
+ ## [0.2.0]
7
+ - balancer create command
8
+ - balancer destroy command
9
+ - auto create security group that opens up the listener port
10
+
6
11
  ## [0.1.0]
7
12
  - Initial release.
@@ -1,9 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- balancer (0.1.0)
4
+ balancer (0.2.0)
5
5
  activesupport
6
+ aws-sdk-ec2
7
+ aws-sdk-elasticloadbalancingv2
6
8
  colorize
9
+ memoist
7
10
  thor
8
11
 
9
12
  GEM
@@ -14,6 +17,20 @@ GEM
14
17
  i18n (>= 0.7, < 2)
15
18
  minitest (~> 5.1)
16
19
  tzinfo (~> 1.1)
20
+ aws-eventstream (1.0.0)
21
+ aws-partitions (1.91.0)
22
+ aws-sdk-core (3.21.2)
23
+ aws-eventstream (~> 1.0)
24
+ aws-partitions (~> 1.0)
25
+ aws-sigv4 (~> 1.0)
26
+ jmespath (~> 1.0)
27
+ aws-sdk-ec2 (1.35.0)
28
+ aws-sdk-core (~> 3)
29
+ aws-sigv4 (~> 1.0)
30
+ aws-sdk-elasticloadbalancingv2 (1.10.0)
31
+ aws-sdk-core (~> 3)
32
+ aws-sigv4 (~> 1.0)
33
+ aws-sigv4 (1.0.2)
17
34
  byebug (10.0.0)
18
35
  cli_markdown (0.1.0)
19
36
  codeclimate-test-reporter (1.0.8)
@@ -24,7 +41,9 @@ GEM
24
41
  docile (1.1.5)
25
42
  i18n (1.0.1)
26
43
  concurrent-ruby (~> 1.0)
44
+ jmespath (1.4.0)
27
45
  json (2.1.0)
46
+ memoist (0.16.0)
28
47
  minitest (5.11.3)
29
48
  rake (12.3.0)
30
49
  rspec (3.7.0)
data/README.md CHANGED
@@ -1,32 +1,61 @@
1
1
  # Balancer
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/GEMNAME.png)](http://badge.fury.io/rb/GEMNAME)
4
- [![CircleCI](https://circleci.com/gh/USER/REPO.svg?style=svg)](https://circleci.com/gh/USER/REPO)
5
- [![Dependency Status](https://gemnasium.com/USER/REPO.png)](https://gemnasium.com/USER/REPO)
6
- [![Coverage Status](https://coveralls.io/repos/USER/REPO/badge.png)](https://coveralls.io/r/USER/REPO)
7
- [![Join the chat at https://gitter.im/USER/REPO](https://badges.gitter.im/USER/REPO.svg)](https://gitter.im/USER/REPO?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8
- [![Support](https://img.shields.io/badge/get-support-blue.svg)](https://boltops.com?utm_source=badge&utm_medium=badge&utm_campaign=cli-template)
3
+ [![Gem Version](https://badge.fury.io/rb/balancer.svg)](https://badge.fury.io/rb/balancer)
4
+ [![CircleCI](https://circleci.com/gh/tongueroo/balancer.svg?style=svg)](https://circleci.com/gh/tongueroo/balancer)[![Support](https://img.shields.io/badge/get-support-blue.svg)](https://boltops.com?utm_source=badge&utm_medium=badge&utm_campaign=balancer)
9
5
 
10
- TODO: Write a gem description
6
+ Tool to create ELB load balancers with a target group and listener. It's performs similar steps to this AWS Tutorial: [Create an Application Load Balancer Using the AWS CLI
7
+ ](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/tutorial-application-load-balancer-cli.html)
11
8
 
12
9
  ## Usage
13
10
 
14
- balancer hello yourname
15
- balancer sub:goodbye yourname
11
+ Quick start to creating and destroying a load balancer.
16
12
 
17
- The CLI tool also detects and tasks in the current folder's Rakefile and delegate to those tasks.
13
+ cd project
14
+ balancer init --vpc-id vpc-123 --subnets subnet-123 subnet-456 --security-groups sg-123
15
+ # edit .balancer/profiles/default.yml to fit your needs
16
+ balancer create my-elb
17
+ balancer destroy my-elb
18
18
 
19
- ## Installation
19
+ ### Profiles
20
+
21
+ Balancer has a concept of profiles. Profiles have preconfigured settings like subnets and vpc_id. The params in the profiles are passed to the ruby [aws-sdk](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ElasticLoadBalancingV2/Client.html) api calls `create_load_balancer`, `create_target_group`, and `create_listener`:
22
+
23
+ ```yaml
24
+ ---
25
+ create_load_balancer:
26
+ subnets: # at least 2 subnets groups required
27
+ - subnet-123
28
+ - subnet-345
29
+ security_groups: # optional thanks to the automatically created security group by balancer
30
+ - sg-123 # additional security groups to use
31
+ create_target_group:
32
+ # vpc_id is required
33
+ vpc_id: vpc-123
34
+ # name: ... # automatically named, matches the load balancer name. override here
35
+ protocol: HTTP # required
36
+ port: 80 # required
37
+ create_listener:
38
+ protocol: HTTP # required
39
+ port: 80 # required, this is is the port that gets open on the security group
40
+ ```
41
+
42
+ ### Security Groups
20
43
 
21
- Add this line to your application's Gemfile:
44
+ Balancer automatically creates a security group with the same name as the elb and opens up the port configured on the listener. To disable this behavior, use the `--no-security-group` optional. If you use this option, you must specify you own security group in the profile file, since at least 1 security group is required. By default, the security group opens up `0.0.0.0/0`. If you want to override this use `--sg-cdir`, example:
22
45
 
23
- gem "balancer"
46
+ balancer create my-elb --sg-cdir 10.0.0.0/16
24
47
 
25
- And then execute:
48
+ When you destroy the ELB like so:
26
49
 
27
- bundle
50
+ balancer destroy my-elb
28
51
 
29
- Or install it yourself as:
52
+ Balancer also attempts to destroy the security group if:
53
+
54
+ * The security group is tagged with the `balancer=my-elb` tag. Balancer automatically adds this tag when creating the ELB.
55
+ * There are no dependencies on the security group. If there are dependencies the ELB is deleted but the security group is left behind for you to clean up.
56
+
57
+
58
+ ## Installation
30
59
 
31
60
  gem install balancer
32
61
 
@@ -19,7 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "activesupport"
22
+ spec.add_dependency "aws-sdk-ec2"
23
+ spec.add_dependency "aws-sdk-elasticloadbalancingv2"
22
24
  spec.add_dependency "colorize"
25
+ spec.add_dependency "memoist"
23
26
  spec.add_dependency "thor"
24
27
 
25
28
  spec.add_development_dependency "bundler"
@@ -1,11 +1,25 @@
1
1
  $:.unshift(File.expand_path("../", __FILE__))
2
2
  require "balancer/version"
3
+ require "colorize"
4
+ require "memoist"
3
5
 
4
6
  module Balancer
7
+ autoload :AwsService, "balancer/aws_service"
5
8
  autoload :Help, "balancer/help"
9
+ autoload :Setting, "balancer/setting"
10
+ autoload :Base, "balancer/base"
11
+ autoload :Profile, "balancer/profile"
12
+ autoload :Init, "balancer/init"
13
+ autoload :Core, "balancer/core"
6
14
  autoload :Command, "balancer/command"
7
15
  autoload :CLI, "balancer/cli"
8
- autoload :Sub, "balancer/sub"
16
+ autoload :Create, "balancer/create"
9
17
  autoload :Completion, "balancer/completion"
10
18
  autoload :Completer, "balancer/completer"
19
+ autoload :Destroy, "balancer/destroy"
20
+ autoload :Param, "balancer/param"
21
+ autoload :OptionTransformer, "balancer/option_transformer"
22
+ autoload :SecurityGroup, "balancer/security_group"
23
+
24
+ extend Core
11
25
  end
@@ -0,0 +1,14 @@
1
+ require 'aws-sdk-ec2'
2
+ require 'aws-sdk-elasticloadbalancingv2'
3
+
4
+ module Balancer
5
+ module AwsService
6
+ def elb
7
+ @elb ||= Aws::ElasticLoadBalancingV2::Client.new
8
+ end
9
+
10
+ def ec2
11
+ @ec2 ||= Aws::EC2::Client.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ module Balancer
2
+ class Base
3
+ def initialize(options={})
4
+ @options = options.clone
5
+ @name = randomize(@options[:name])
6
+ Balancer.validate_in_project!
7
+ end
8
+
9
+ # Appends a short random string at the end of the ec2 instance name.
10
+ # Later we will strip this same random string from the name.
11
+ # Very makes it convenient. We can just type:
12
+ #
13
+ # balancer create server --randomize
14
+ #
15
+ # instead of:
16
+ #
17
+ # balancer create server-123 --profile server
18
+ #
19
+ def randomize(name)
20
+ if @options[:randomize]
21
+ random = (0...3).map { (65 + rand(26)).chr }.join.downcase # Ex: jhx
22
+ [name, random].join('-')
23
+ else
24
+ name
25
+ end
26
+ end
27
+
28
+ # Strip the random string at end of the ec2 instance name
29
+ def derandomize(name)
30
+ if @options[:randomize]
31
+ name.sub(/-(\w{3})$/,'') # strip the random part at the end
32
+ else
33
+ name
34
+ end
35
+ end
36
+ end
37
+ end
@@ -3,17 +3,31 @@ module Balancer
3
3
  class_option :verbose, type: :boolean
4
4
  class_option :noop, type: :boolean
5
5
 
6
- desc "hello NAME", "Say hello to NAME."
7
- long_desc Help.text(:hello)
8
- option :from, desc: "from person"
9
- def hello(name="you")
10
- puts "from: #{options[:from]}" if options[:from]
11
- puts "Hello #{name}"
6
+ desc "create NAME", "Create Load Balancer."
7
+ long_desc Help.text(:create)
8
+ # create_load_balancer options
9
+ option :subnets, type: :array, desc: "Subnets"
10
+ option :security_groups, type: :array, desc: "Security groups"
11
+ # create_target_group options
12
+ option :vpc_id, type: :array, desc: "Vpc id"
13
+ option :target_group_name, desc: "Target group name"
14
+ # security_group options
15
+ option :sg_cidr, default: "0.0.0.0/0", desc: "Security group cidr range"
16
+ def create(name)
17
+ Create.new(options.merge(name: name)).run
12
18
  end
13
19
 
14
- desc "sub SUBCOMMAND", "sub subcommands"
15
- long_desc Help.text(:sub)
16
- subcommand "sub", Sub
20
+ desc "destroy NAME", "Destroy Load Balancer and associated target group."
21
+ long_desc Help.text(:destroy)
22
+ def destroy(name)
23
+ Destroy.new(options.merge(name: name)).run
24
+ end
25
+
26
+ long_desc Help.text(:init)
27
+ Init.cli_options.each do |args|
28
+ option *args
29
+ end
30
+ register(Init, "init", "init", "Sets up balancer for project")
17
31
 
18
32
  desc "completion *PARAMS", "Prints words for auto-completion."
19
33
  long_desc Help.text("completion")
@@ -0,0 +1,26 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Balancer
5
+ module Core
6
+ def root
7
+ path = ENV['BALANCER_ROOT'] || '.'
8
+ Pathname.new(path)
9
+ end
10
+
11
+ def profile
12
+ ENV['BALANCER_PROFILE'] || 'default'
13
+ end
14
+
15
+ def settings
16
+ Setting.new.data
17
+ end
18
+
19
+ def validate_in_project!
20
+ unless File.exist?("#{root}/.balancer")
21
+ puts "Could not find a .balancer folder in the current directory. It does not look like you are running this command within a balancer project. Please confirm that you are in a balancer project and try again.".colorize(:red)
22
+ exit
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,117 @@
1
+ require 'yaml'
2
+
3
+ module Balancer
4
+ class Create
5
+ extend Memoist
6
+ include AwsService
7
+ include SecurityGroup
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ @name = options[:name]
12
+ end
13
+
14
+ # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/tutorial-application-load-balancer-cli.html
15
+ def run
16
+ if ENV['TEST'] # ghetto way to for sanity cli specs
17
+ puts "Creating load balancer"
18
+ return
19
+ end
20
+
21
+ if elb_exists?
22
+ puts "Load balancer #{@name} already exists"
23
+ return
24
+ end
25
+
26
+ @security_group_id = create_security_group
27
+ create_elb
28
+ create_target_group
29
+ create_listener
30
+ end
31
+
32
+ def elb_exists?
33
+ begin
34
+ resp = elb.describe_load_balancers(names: [@name])
35
+ true
36
+ rescue Aws::ElasticLoadBalancingV2::Errors::LoadBalancerNotFound
37
+ false
38
+ end
39
+ end
40
+
41
+ def create_elb
42
+ puts "Creating load balancer with params:"
43
+ params = param.create_load_balancer
44
+ params[:security_groups] ||= []
45
+ params[:security_groups] += [@security_group_id]
46
+ params[:security_groups] = params[:security_groups].uniq
47
+ pretty_display(params)
48
+ aws_cli_command("aws elbv2 create-load-balancer", params)
49
+ return if @options[:noop]
50
+
51
+ begin
52
+ resp = elb.create_load_balancer(params)
53
+ rescue Exception => e
54
+ puts "ERROR: #{e.class}: #{e.message}".colorize(:red)
55
+ exit 1
56
+ end
57
+
58
+ elb = resp.load_balancers.first
59
+ puts "Load balancer created: #{elb.load_balancer_arn}"
60
+ @load_balancer_arn = elb.load_balancer_arn # used later
61
+ puts
62
+ end
63
+
64
+ def create_target_group
65
+ puts "Creating target group with params:"
66
+ params = param.create_target_group
67
+ pretty_display(params)
68
+ aws_cli_command("aws elbv2 create-target-group", params)
69
+
70
+ begin
71
+ resp = elb.create_target_group(params)
72
+ rescue Exception => e
73
+ puts "ERROR: #{e.class}: #{e.message}".colorize(:red)
74
+ exit 1
75
+ end
76
+ target_group = resp.target_groups.first
77
+ puts "Target group created: #{target_group.target_group_arn}"
78
+ @target_group_arn = target_group.target_group_arn # used later
79
+ add_tags(@target_group_arn)
80
+ puts
81
+ end
82
+
83
+ def create_listener
84
+ puts "Creating listener with params:"
85
+ params = param.create_listener
86
+ params.merge!(
87
+ load_balancer_arn: @load_balancer_arn,
88
+ default_actions: [{type: "forward", target_group_arn: @target_group_arn}]
89
+ )
90
+ pretty_display(params)
91
+ aws_cli_command("aws elbv2 create-listener", params)
92
+
93
+ resp = run_with_error_handling do
94
+ elb.create_listener(params)
95
+ end
96
+ listener = resp.listeners.first
97
+ puts "Listener created: #{listener.listener_arn}"
98
+ puts
99
+ end
100
+
101
+ def run_with_error_handling
102
+ yield
103
+ rescue Exception => e
104
+ puts "ERROR: #{e.class}: #{e.message}".colorize(:red)
105
+ exit 1
106
+ end
107
+
108
+ def add_tags(arn)
109
+ params = {
110
+ resource_arns: [arn],
111
+ tags: [{ key: "balancer", value: @name }]
112
+ }
113
+ aws_cli_command("aws elbv2 add-tags", params)
114
+ elb.add_tags(params)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,51 @@
1
+ module Balancer
2
+ class Destroy
3
+ extend Memoist
4
+ include AwsService
5
+ include SecurityGroup
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ @name = options[:name]
10
+ end
11
+
12
+ def run
13
+ puts "Destroying ELB and target groups associated with: #{@name}"
14
+ return if @options[:noop]
15
+
16
+ begin
17
+ resp = elb.describe_load_balancers(names: [@name])
18
+ rescue Aws::ElasticLoadBalancingV2::Errors::LoadBalancerNotFound
19
+ puts "Load balancer '#{@name}' not found. Exiting.".colorize(:red)
20
+ exit 1
21
+ end
22
+
23
+ load_balancer = resp.load_balancers.first
24
+
25
+ # Must load resources to be deleted into memory to delete them later since there
26
+ # are dependencies and they won't be available to query after deleting some of the
27
+ # resources.
28
+ resp = elb.describe_listeners(load_balancer_arn: load_balancer.load_balancer_arn)
29
+ listeners = resp.listeners
30
+ resp = elb.describe_target_groups(load_balancer_arn: load_balancer.load_balancer_arn)
31
+ groups = resp.target_groups
32
+
33
+ listeners.each do |listener|
34
+ elb.delete_listener(listener_arn: listener.listener_arn)
35
+ puts "Deleted listener: #{listener.listener_arn}"
36
+ end
37
+
38
+ groups.each do |group|
39
+ elb.delete_target_group(target_group_arn: group.target_group_arn)
40
+ puts "Deleted target group: #{group.target_group_arn}"
41
+ end
42
+
43
+ resp = elb.delete_load_balancer(
44
+ load_balancer_arn: load_balancer.load_balancer_arn,
45
+ )
46
+ puts "Deleted load balancer: #{load_balancer.load_balancer_arn}"
47
+
48
+ destroy_security_group
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ ## Examples
2
+
3
+ balancer create NAME
4
+ balancer create NAME --type application
5
+ balancer create NAME --type network
@@ -0,0 +1,3 @@
1
+ ## Examples
2
+
3
+ balancer destroy NAME
@@ -0,0 +1,4 @@
1
+ ## Examples
2
+
3
+ balancer init
4
+ balancer init --vpc-id vpc-123 --subnets subnet-123 subnet-456 --security-groups sg-123
@@ -0,0 +1,49 @@
1
+ require 'fileutils'
2
+ require 'colorize'
3
+ require 'active_support/core_ext/string'
4
+ require 'thor'
5
+ require 'bundler'
6
+
7
+ module Balancer
8
+ class Init < Thor::Group
9
+ include Thor::Actions
10
+
11
+ # Ugly, but when the class_option is only defined in the Thor::Group class
12
+ # it doesnt show up with cli-template new help :(
13
+ # If anyone knows how to fix this let me know.
14
+ # Also options from the cli can be pass through to here
15
+ def self.cli_options
16
+ [
17
+ [:force, type: :boolean, desc: "Bypass overwrite are you sure prompt for existing files."],
18
+ [:git, type: :boolean, default: true, desc: "Git initialize the project"],
19
+ [:subnets, type: :array, default: ["REPLACE_ME"], desc: "Subnets"],
20
+ [:security_groups, type: :array, default: ["REPLACE_ME"], desc: "Security groups"],
21
+ [:vpc_id, default: "REPLACE_ME", desc: "Vpc id"],
22
+ ]
23
+ end
24
+
25
+ cli_options.each do |args|
26
+ class_option *args
27
+ end
28
+
29
+ def self.source_root
30
+ File.expand_path("../template", File.dirname(__FILE__))
31
+ end
32
+
33
+ def init_project
34
+ puts "Setting up balancer files."
35
+ directory ".", "."
36
+ end
37
+
38
+ def user_message
39
+ puts <<-EOL
40
+ #{"="*64}
41
+ Congrats 🎉 Balancer starter files succesfully created.
42
+
43
+ Check out .balancer/profiles/default.yml make make sure the settings like subnets and vpc_id are okay. Then run `balanace create` to to create an ELB, Target Group and listener. Example:
44
+
45
+ balancer create my-elb
46
+ EOL
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_support/core_ext/string'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Balancer
5
+ class OptionTransformer
6
+ def to_cli(options)
7
+ params = []
8
+ options.each do |k,v|
9
+ case v
10
+ when Symbol, String, Integer
11
+ params << key_to_cli_option(k) + ' ' + v.to_s
12
+ when Array
13
+ values = []
14
+ v.each do |o|
15
+ if o.is_a?(Hash)
16
+ o.each do |x,y|
17
+ values << "#{x.to_s.camelize}=#{y}"
18
+ end
19
+ else # assume string
20
+ values << o
21
+ end
22
+ end
23
+
24
+ list = v.first.is_a?(Hash) ? values.join(',') : values.join(' ')
25
+ params << key_to_cli_option(k) + ' ' + list
26
+ else
27
+ puts "v.class: #{v.class.inspect}"
28
+ raise "the roof"
29
+ end
30
+ end
31
+ params.join(' ')
32
+ end
33
+
34
+ # resource_arns => --resource-arns
35
+ def key_to_cli_option(key)
36
+ '--' + key.to_s.gsub('_','-')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ require 'active_support/core_ext/hash'
2
+
3
+ module Balancer
4
+ class Param
5
+ extend Memoist
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ end
10
+
11
+ def create_load_balancer
12
+ params = settings["create_load_balancer"].deep_symbolize_keys
13
+ params = merge_option(params, :name)
14
+ params = merge_option(params, :subnets)
15
+ params = merge_option(params, :security_groups)
16
+ params[:tags] = [{ key: "balancer", value: "balancer" }]
17
+ params
18
+ end
19
+ memoize :create_load_balancer
20
+
21
+ def create_target_group
22
+ params = settings["create_target_group"].deep_symbolize_keys
23
+ params[:name] ||= @options[:name] if @options[:name] # settings take precedence
24
+ params = merge_option(params, :vpc_id)
25
+ params
26
+ end
27
+ memoize :create_target_group
28
+
29
+ def create_listener
30
+ settings["create_listener"].deep_symbolize_keys
31
+ end
32
+ memoize :create_listener
33
+
34
+ def merge_option(params, option_key)
35
+ params[option_key] = @options[option_key] if @options[option_key]
36
+ params
37
+ end
38
+
39
+ def settings
40
+ @settings ||= Balancer.settings
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ module Balancer
2
+ class Profile < Base
3
+ include Balancer::Template
4
+
5
+ def load
6
+ return @profile_params if @profile_params
7
+
8
+ check!
9
+
10
+ file = profile_file(profile_name)
11
+ @profile_params = load_profile(file)
12
+ end
13
+
14
+ def check!
15
+ file = profile_file(profile_name)
16
+ return if File.exist?(file)
17
+
18
+ puts "Unable to find a #{file.colorize(:green)} profile file."
19
+ puts "Please double check that it exists or that you specified the right profile.".colorize(:red)
20
+ exit 1
21
+ end
22
+
23
+ def load_profile(file)
24
+ return {} unless File.exist?(file)
25
+
26
+ puts "Using profile: #{file}".colorize(:green)
27
+ text = RenderMePretty.result(file, context: context)
28
+ begin
29
+ data = YAML.load(text)
30
+ rescue Psych::SyntaxError => e
31
+ tmp_file = file.sub(".balancer", "tmp")
32
+ FileUtils.mkdir_p(File.dirname(tmp_file))
33
+ IO.write(tmp_file, text)
34
+ puts "There was an error evaluating in your yaml file #{file}".colorize(:red)
35
+ puts "The evaludated yaml file has been saved at #{tmp_file} for debugging."
36
+ puts "ERROR: #{e.message}"
37
+ exit 1
38
+ end
39
+ data ? data : {} # in case the file is empty
40
+ data.has_key?("create_load_balancer") ? data["create_load_balancer"] : data
41
+ end
42
+
43
+ # Determines a valid profile_name. Falls back to default
44
+ def profile_name
45
+ # allow user to specify the path also
46
+ if @options[:profile] && File.exist?(@options[:profile])
47
+ filename_profile = File.basename(@options[:profile], '.yml')
48
+ end
49
+
50
+ name = derandomize(@name)
51
+ if File.exist?(profile_file(name))
52
+ name_profile = name
53
+ end
54
+
55
+ filename_profile ||
56
+ @options[:profile] ||
57
+ name_profile || # conventional profile is the name of the elb
58
+ "default"
59
+ end
60
+
61
+ def profile_file(name)
62
+ "#{Balancer.root}/.balancer/#{name}.yml"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,139 @@
1
+ module Balancer
2
+ module SecurityGroup
3
+ extend Memoist
4
+
5
+ def create_security_group
6
+ sg = find_security_group(@name)
7
+ group_id = sg.group_id if sg
8
+
9
+ unless group_id
10
+ puts "Creating security group #{@name} in vpc #{sg_vpc_id}"
11
+ params = {group_name: @name, description: @name, vpc_id: sg_vpc_id}
12
+ aws_cli_command("aws ec2 create-security-group", params)
13
+ begin
14
+ resp = ec2.create_security_group(params)
15
+ rescue Aws::EC2::Errors::InvalidVpcIDNotFound => e
16
+ puts "ERROR: #{e.class} #{e.message}".colorize(:red)
17
+ exit 1
18
+ end
19
+ group_id = resp.group_id
20
+ puts "Created security group: #{group_id}"
21
+ end
22
+
23
+ authorize_elb_port(group_id)
24
+
25
+ ec2.create_tags(resources: [group_id], tags: [{
26
+ key: "Name",
27
+ value: @name
28
+ },
29
+ key: "balancer",
30
+ value: @name
31
+ ])
32
+
33
+ group_id
34
+ end
35
+
36
+ def authorize_elb_port(group_id)
37
+ resp = ec2.describe_security_groups(group_ids: [group_id])
38
+ sg = resp.security_groups.first
39
+
40
+ already_authorized = sg.ip_permissions.find do |perm|
41
+ perm.from_port == 80 &&
42
+ perm.to_port == 80
43
+ perm.ip_ranges.find { |ip_range| ip_range.cidr_ip == @options[:sg_cidr] }
44
+ end
45
+ if already_authorized
46
+ return
47
+ end
48
+
49
+ listener_port = param.create_listener[:port]
50
+
51
+ # authorize the matching port in the create_listener setting
52
+ params = {group_id: group_id, protocol: "tcp", port: listener_port, cidr: @options[:sg_cidr]}
53
+ puts "Authorizing listening port for security group"
54
+ aws_cli_command("aws ec2 authorize-security-group-ingress", params)
55
+ ec2.authorize_security_group_ingress(
56
+ group_id: params[:group_id],
57
+ ip_permissions: [
58
+ from_port: listener_port,
59
+ to_port: listener_port,
60
+ ip_protocol: "tcp",
61
+ ip_ranges: [
62
+ cidr_ip: @options[:sg_cidr],
63
+ description: "balancer #{@name}"
64
+ ]
65
+ ]
66
+ )
67
+ end
68
+
69
+ def destroy_security_group
70
+ sg = find_security_group(@name)
71
+ return unless sg
72
+
73
+ balancer_tag = sg.tags.find { |t| t.key == "balancer" && t.value == @name }
74
+ unless balancer_tag
75
+ puts "WARN: not destroying the #{@name} security group because it doesn't have a matching balancer tag".colorize(:yellow)
76
+ return
77
+ end
78
+
79
+ puts "Deleting security group #{@name} in vpc #{sg_vpc_id}"
80
+ params = {group_id: sg.group_id}
81
+ aws_cli_command("aws ec2 delete-security-group", params)
82
+
83
+ tries = 0
84
+ begin
85
+ ec2.delete_security_group(params)
86
+ puts "Deleted security group: #{sg.group_id}"
87
+ rescue Aws::EC2::Errors::DependencyViolation => e
88
+ sleep 2**tries
89
+ tries += 1
90
+ if tries <= 4
91
+ # retry because it takes some time for the load balancer to be deleted
92
+ # and that can cause a DependencyViolation exception
93
+ retry
94
+ else
95
+ puts "WARN: #{e.class} #{e.message}".colorize(:yellow)
96
+ puts "Unable to delete the security group because it's still in use by another resource. Leaving the security group."
97
+ end
98
+ end
99
+ end
100
+
101
+ # Use security group that is set in the profile under create_target_group
102
+ def sg_vpc_id
103
+ param.create_target_group[:vpc_id]
104
+ end
105
+ memoize :sg_vpc_id
106
+
107
+ def find_security_group(name)
108
+ resp = ec2.describe_security_groups(filters: [
109
+ {name: "group-name", values: ["my-elb"]},
110
+ {name: "vpc-id", values: [sg_vpc_id]},
111
+ ])
112
+ resp.security_groups.first
113
+ end
114
+ memoize :find_security_group
115
+
116
+ # Few other common methods also included here
117
+
118
+ def param
119
+ Param.new(@options)
120
+ end
121
+ memoize :param
122
+
123
+ def pretty_display(data)
124
+ data = data.deep_stringify_keys
125
+ puts YAML.dump(data)
126
+ end
127
+
128
+ def option_transformer
129
+ Balancer::OptionTransformer.new
130
+ end
131
+ memoize :option_transformer
132
+
133
+ def aws_cli_command(aws_command, params)
134
+ # puts "Equivalent aws cli command:"
135
+ cli_options = option_transformer.to_cli(params)
136
+ puts " #{aws_command} #{cli_options}".colorize(:light_blue)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,47 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Balancer
5
+ class Setting
6
+ extend Memoist
7
+
8
+ def data
9
+ settings_file = Balancer.profile || 'default'
10
+ settings_file += ".yml"
11
+
12
+ profile = yaml_file("#{Balancer.root}/.balancer/profiles/#{settings_file}")
13
+ user = yaml_file("#{home}/.balancer/#{settings_file}")
14
+ default_file = File.expand_path("../default/settings.yml", __FILE__)
15
+ default = yaml_file(default_file)
16
+
17
+ data = merge(default, user, profile)
18
+
19
+ if ENV['DEBUG_SETTINGS']
20
+ puts "settings data:"
21
+ pp data
22
+ end
23
+ data
24
+ end
25
+ memoize :data
26
+
27
+ def merge(*hashes)
28
+ hashes.inject({}) do |result, hash|
29
+ # note: important to compact for keys with nil value
30
+ result.deep_merge(hash.compact)
31
+ end
32
+ end
33
+
34
+ # Any empty file will result in "false". Lets ensure that an empty file
35
+ # loads an empty hash instead.
36
+ def yaml_file(path)
37
+ # puts "yaml_file #{path}"
38
+ return {} unless File.exist?(path)
39
+ YAML.load_file(path) || {}
40
+ end
41
+
42
+ def home
43
+ # hack but fast
44
+ ENV['TEST'] ? "spec/fixtures/home" : ENV['HOME']
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,3 @@
1
1
  module Balancer
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,21 @@
1
+ ---
2
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ElasticLoadBalancingV2/Client.html
3
+ # The keys correspond to methods called in the aws-sdk
4
+ create_load_balancer:
5
+ subnets: # at least 2 subnets required
6
+ <% options[:subnets].each do |subnet| -%>
7
+ - <%= subnet %>
8
+ <% end -%>
9
+ security_groups: # optional thanks to the automatically created security
10
+ <% options[:security_groups].each do |security_group| -%>
11
+ - <%= security_group %>
12
+ <% end -%>
13
+ create_target_group:
14
+ # vpc_id is required
15
+ vpc_id: <%= options[:vpc_id] %>
16
+ # name: ... # automatically named, matches the load balancer name. override here
17
+ protocol: HTTP # required
18
+ port: 80 # required
19
+ create_listener:
20
+ protocol: HTTP # required
21
+ port: 80 # required
@@ -0,0 +1,16 @@
1
+ ---
2
+ create_load_balancer:
3
+ subnets: # required
4
+ - subnet-aaa
5
+ - subnet-bbb
6
+ security_groups: # required
7
+ - sg-123
8
+ create_target_group:
9
+ # vpc_id is required
10
+ vpc_id: vpc-123
11
+ # name: ... # automatically named, matches the load balancer name. override here
12
+ protocol: HTTP # required
13
+ port: 80 # required
14
+ create_listener:
15
+ protocol: HTTP # required
16
+ port: 80 # required
@@ -1,32 +1,23 @@
1
- require "spec_helper"
2
-
3
1
  describe Balancer::CLI do
4
2
  before(:all) do
5
- @args = "--from Tung"
3
+ @args = "--noop"
6
4
  end
7
5
 
8
6
  describe "balancer" do
9
- it "hello" do
10
- out = execute("exe/balancer hello world #{@args}")
11
- expect(out).to include("from: Tung\nHello world")
7
+ it "create" do
8
+ out = execute("exe/balancer create my-elb #{@args}")
9
+ expect(out).to include("Creating load balancer")
12
10
  end
13
11
 
14
- it "goodbye" do
15
- out = execute("exe/balancer sub goodbye world #{@args}")
16
- expect(out).to include("from: Tung\nGoodbye world")
12
+ it "destroy" do
13
+ out = execute("exe/balancer destroy my-elb #{@args}")
14
+ expect(out).to include("Destroying ELB")
17
15
  end
18
16
 
19
17
  commands = {
20
- "hell" => "hello",
21
- "hello" => "name",
22
- "hello -" => "--from",
23
- "hello name" => "--from",
24
- "hello name --" => "--from",
25
- "sub goodb" => "goodbye",
26
- "sub goodbye" => "name",
27
- "sub goodbye name" => "--from",
28
- "sub goodbye name --" => "--from",
29
- "sub goodbye name --from" => "--help",
18
+ "crea" => "create",
19
+ "create" => "name",
20
+ "dest" => "destroy",
30
21
  }
31
22
  commands.each do |command, expected_word|
32
23
  it "completion #{command}" do
@@ -0,0 +1,27 @@
1
+ describe Balancer::OptionTransformer do
2
+ let(:create) { Balancer::OptionTransformer.new }
3
+
4
+ context '#to_cli' do
5
+ it "target group" do
6
+ options = {
7
+ port: 80,
8
+ protocol: "HTTP",
9
+ load_balancer_arn: "load_balancer_arn",
10
+ default_actions: [{type: "forward", target_group_arn: "target_group_arn"}],
11
+ }
12
+ text = create.to_cli(options)
13
+ # puts text
14
+ expect(text).to eq "--port 80 --protocol HTTP --load-balancer-arn load_balancer_arn --default-actions Type=forward,TargetGroupArn=target_group_arn"
15
+ end
16
+
17
+ it "tags" do
18
+ options = {
19
+ resource_arns: %w[arn1 arn2],
20
+ tags: [{ key: "balancer", value: "test-elb" }]
21
+ }
22
+ text = create.to_cli(options)
23
+ # puts text
24
+ expect(text).to eq "--resource-arns arn1 arn2 --tags Key=balancer,Value=test-elb"
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,7 @@
1
1
  ENV["TEST"] = "1"
2
2
 
3
+ ENV["BALANCER_ROOT"] = "spec/fixtures/project"
4
+
3
5
  # CodeClimate test coverage: https://docs.codeclimate.com/docs/configuring-test-coverage
4
6
  # require 'simplecov'
5
7
  # SimpleCov.start
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: balancer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-08 00:00:00.000000000 Z
11
+ date: 2018-06-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-ec2
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-elasticloadbalancingv2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: colorize
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +66,20 @@ dependencies:
38
66
  - - ">="
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: memoist
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
41
83
  - !ruby/object:Gem::Dependency
42
84
  name: thor
43
85
  requirement: !ruby/object:Gem::Requirement
@@ -130,6 +172,8 @@ executables:
130
172
  extensions: []
131
173
  extra_rdoc_files: []
132
174
  files:
175
+ - ".circleci/bin/commit_docs.sh"
176
+ - ".circleci/config.yml"
133
177
  - ".gitignore"
134
178
  - ".rspec"
135
179
  - CHANGELOG.md
@@ -142,19 +186,34 @@ files:
142
186
  - balancer.gemspec
143
187
  - exe/balancer
144
188
  - lib/balancer.rb
189
+ - lib/balancer/aws_service.rb
190
+ - lib/balancer/base.rb
145
191
  - lib/balancer/cli.rb
146
192
  - lib/balancer/command.rb
147
193
  - lib/balancer/completer.rb
148
194
  - lib/balancer/completer/script.rb
149
195
  - lib/balancer/completer/script.sh
196
+ - lib/balancer/core.rb
197
+ - lib/balancer/create.rb
198
+ - lib/balancer/destroy.rb
150
199
  - lib/balancer/help.rb
151
200
  - lib/balancer/help/completion.md
152
201
  - lib/balancer/help/completion_script.md
153
- - lib/balancer/help/hello.md
154
- - lib/balancer/help/sub/goodbye.md
202
+ - lib/balancer/help/create.md
203
+ - lib/balancer/help/destroy.md
204
+ - lib/balancer/help/init.md
205
+ - lib/balancer/init.rb
206
+ - lib/balancer/option_transformer.rb
207
+ - lib/balancer/param.rb
208
+ - lib/balancer/profile.rb
209
+ - lib/balancer/security_group.rb
210
+ - lib/balancer/setting.rb
155
211
  - lib/balancer/sub.rb
156
212
  - lib/balancer/version.rb
213
+ - lib/template/.balancer/profiles/default.yml.tt
214
+ - spec/fixtures/project/.balancer/profiles/default.yml
157
215
  - spec/lib/cli_spec.rb
216
+ - spec/lib/option_transformer_spec.rb
158
217
  - spec/spec_helper.rb
159
218
  homepage: https://github.com/tongueroo/balancer
160
219
  licenses:
@@ -176,10 +235,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
235
  version: '0'
177
236
  requirements: []
178
237
  rubyforge_project:
179
- rubygems_version: 2.7.3
238
+ rubygems_version: 2.7.6
180
239
  signing_key:
181
240
  specification_version: 4
182
241
  summary: Balancer tool
183
242
  test_files:
243
+ - spec/fixtures/project/.balancer/profiles/default.yml
184
244
  - spec/lib/cli_spec.rb
245
+ - spec/lib/option_transformer_spec.rb
185
246
  - spec/spec_helper.rb
@@ -1,5 +0,0 @@
1
- Examples:
2
-
3
- balancer hello
4
- balancer hello NAME
5
- balancer hello NAME --from me
@@ -1,5 +0,0 @@
1
- Examples:
2
-
3
- balancer sub:goodbye
4
- balancer sub:goodbye NAME
5
- balancer sub:goodbye NAME --from me