balancer 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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