rollo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Toby Clemson
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Rollo
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to
4
+ be able to package up your Ruby library into a gem. Put your Ruby code in the
5
+ file `lib/rollo`. To experiment with that code, run `bin/console` for an
6
+ interactive prompt.
7
+
8
+ TODO: Delete this and the text above, and describe your gem
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'rollo'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install rollo
25
+
26
+ ## Usage
27
+
28
+ TODO: Write usage instructions here
29
+
30
+ ## Development
31
+
32
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
33
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
34
+ prompt that will allow you to experiment.
35
+
36
+ To install this gem onto your local machine, run `bundle exec rake install`. To
37
+ release a new version, update the version number in `version.rb`, and then run
38
+ `bundle exec rake release`, which will create a git tag for the version, push
39
+ git commits and tags, and push the `.gem` file to
40
+ [rubygems.org](https://rubygems.org).
41
+
42
+ ## Contributing
43
+
44
+ Bug reports and pull requests are welcome on GitHub at
45
+ https://github.com/infrablocks/rollo. This project is intended to be a safe,
46
+ welcoming space for collaboration, and contributors are expected to adhere to
47
+ the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
48
+
49
+ ## License
50
+
51
+ The gem is available as open source under the terms of the
52
+ [MIT License](https://opensource.org/licenses/MIT).
53
+
54
+ ## Code of Conduct
55
+
56
+ Everyone interacting in the Rollo project’s codebases, issue trackers, chat
57
+ rooms and mailing lists is expected to follow the [code of conduct](
58
+ https://github.com/[USERNAME]/rollo/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
6
+
7
+ namespace :version do
8
+ desc "Bump version for specified type (pre, major, minor patch)"
9
+ task :bump, [:type] do |_, args|
10
+ bump_version_for(args.type)
11
+ end
12
+ end
13
+
14
+ desc "Release gem"
15
+ task :release do
16
+ sh "gem release --tag --push"
17
+ end
18
+
19
+ def bump_version_for(version_type)
20
+ sh "gem bump --version #{version_type} " +
21
+ "&& bundle install " +
22
+ "&& export LAST_MESSAGE=\"$(git log -1 --pretty=%B)\" " +
23
+ "&& git commit -a --amend -m \"${LAST_MESSAGE} [ci skip]\""
24
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rollo"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/rollo ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'thor'
3
+ require 'rollo'
4
+
5
+ Rollo::Commands::Main.start
data/go ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+
3
+ [ -n "$GO_DEBUG" ] && set -x
4
+ set -e
5
+
6
+ project_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
7
+
8
+ verbose="no"
9
+ skip_checks="no"
10
+ offline="no"
11
+
12
+ missing_dependency="no"
13
+
14
+ [ -n "$GO_DEBUG" ] && verbose="yes"
15
+ [ -n "$GO_SKIP_CHECKS" ] && skip_checks="yes"
16
+ [ -n "$GO_OFFLINE" ] && offline="yes"
17
+
18
+
19
+ if [[ "$skip_checks" = "no" ]]; then
20
+ echo "Checking for system dependencies."
21
+ ruby_version="$(cat "$project_dir"/.ruby-version)"
22
+ if ! type ruby >/dev/null 2>&1 || ! ruby -v | grep -q "$ruby_version"; then
23
+ echo "This codebase requires Ruby $ruby_version."
24
+ missing_dependency="yes"
25
+ fi
26
+
27
+ if ! type bundler >/dev/null 2>&1; then
28
+ echo "This codebase requires Bundler."
29
+ missing_dependency="yes"
30
+ fi
31
+
32
+ if [[ "$missing_dependency" = "yes" ]]; then
33
+ echo "Please install missing dependencies to continue."
34
+ exit 1
35
+ fi
36
+
37
+ echo "All system dependencies present. Continuing."
38
+ fi
39
+
40
+ if [[ "$offline" = "no" ]]; then
41
+ echo "Installing bundler."
42
+ if [[ "$verbose" = "yes" ]]; then
43
+ gem install --no-document bundler
44
+ else
45
+ gem install --no-document bundler > /dev/null
46
+ fi
47
+
48
+ echo "Installing ruby dependencies."
49
+ if [[ "$verbose" = "yes" ]]; then
50
+ bundle install
51
+ else
52
+ bundle install > /dev/null
53
+ fi
54
+ fi
55
+
56
+ echo "Starting rake."
57
+ if [[ "$verbose" = "yes" ]]; then
58
+ time bundle exec rake --verbose "$@"
59
+ else
60
+ time bundle exec rake "$@"
61
+ fi
@@ -0,0 +1,184 @@
1
+ require 'thor'
2
+ require_relative '../model'
3
+
4
+ module Rollo
5
+ module Commands
6
+ class HostCluster < Thor
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
11
+ desc(
12
+ 'expand REGION ASG_NAME ECS_CLUSTER_NAME',
13
+ '')
14
+ method_option(
15
+ :batch_size,
16
+ aliases: '-b',
17
+ type: :numeric,
18
+ default: 3,
19
+ desc: 'The number of hosts to add at a time.')
20
+ def expand(
21
+ region, asg_name, _,
22
+ host_cluster = nil)
23
+ batch_size = options[:batch_size]
24
+
25
+ host_cluster = host_cluster ||
26
+ Rollo::HostCluster.new(asg_name, region)
27
+
28
+ say("Increasing host cluster desired capacity by #{batch_size}...")
29
+ with_padding do
30
+ host_cluster.increase_capacity_by(batch_size) do |on|
31
+ on.prepare do |current, target|
32
+ say(
33
+ "Changing desired capacity from #{current} to " +
34
+ "#{target}...")
35
+ end
36
+ on.waiting_for_start do |attempt|
37
+ say(
38
+ 'Waiting for capacity change to start ' +
39
+ "(attempt #{attempt})...")
40
+ end
41
+ on.waiting_for_end do |attempt|
42
+ say(
43
+ 'Waiting for capacity change to complete ' +
44
+ "(attempt #{attempt})...")
45
+ end
46
+ on.waiting_for_health do |attempt|
47
+ say("Waiting for a healthy state (attempt #{attempt})")
48
+ end
49
+ end
50
+ end
51
+ say "Host cluster desired capacity increased, continuing..."
52
+ end
53
+
54
+ desc(
55
+ 'contract REGION ASG_NAME ECS_CLUSTER_NAME',
56
+ '')
57
+ method_option(
58
+ :batch_size,
59
+ aliases: '-b',
60
+ type: :numeric,
61
+ default: 3,
62
+ desc: 'The number of hosts to remove at a time.')
63
+ def contract(
64
+ region, asg_name, ecs_cluster_name,
65
+ host_cluster = nil, service_cluster = nil)
66
+ batch_size = options[:batch_size]
67
+
68
+ host_cluster = host_cluster ||
69
+ Rollo::HostCluster.new(asg_name, region)
70
+ service_cluster = service_cluster ||
71
+ Rollo::ServiceCluster.new(ecs_cluster_name, region)
72
+
73
+ say("Decreasing host cluster desired capacity by #{batch_size}...")
74
+ with_padding do
75
+ host_cluster.decrease_capacity_by(batch_size) do |on|
76
+ on.prepare do |current, target|
77
+ say(
78
+ "Changing desired capacity from #{current} to " +
79
+ "#{target}...")
80
+ end
81
+ on.waiting_for_start do |attempt|
82
+ say(
83
+ "Waiting for capacity change to start " +
84
+ "(attempt #{attempt})...")
85
+ end
86
+ on.waiting_for_end do |attempt|
87
+ say(
88
+ "Waiting for capacity change to complete " +
89
+ "(attempt #{attempt})...")
90
+ end
91
+ on.waiting_for_health do |attempt|
92
+ say(
93
+ "Waiting for host cluster to reach healthy state " +
94
+ "(attempt #{attempt})...")
95
+ end
96
+ end
97
+ service_cluster.with_replica_services do |on|
98
+ on.each_service do |service|
99
+ service.wait_for_service_health do |on|
100
+ on.waiting_for_health do |attempt|
101
+ say(
102
+ "Waiting for service #{service.name} to reach a " +
103
+ "steady state (attempt #{attempt})...")
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ say "Host cluster desired capacity decreased, continuing..."
110
+ end
111
+
112
+ desc(
113
+ 'terminate REGION ASG_NAME ECS_CLUSTER_NAME INSTANCE_IDS*',
114
+ '')
115
+ method_option(
116
+ :batch_size,
117
+ aliases: '-b',
118
+ type: :numeric,
119
+ default: 3,
120
+ desc: 'The number of hosts to add at a time.')
121
+ method_option(
122
+ :startup_time,
123
+ aliases: '-t',
124
+ type: :numeric,
125
+ default: 2,
126
+ desc: 'The number of minutes to wait for services to start up.')
127
+ def terminate(
128
+ region, asg_name, ecs_cluster_name, instance_ids,
129
+ host_cluster = nil, service_cluster = nil)
130
+ batch_size = options[:batch_size]
131
+
132
+ service_start_wait_minutes = options[:startup_time]
133
+ service_start_wait_seconds = 60 * service_start_wait_minutes
134
+
135
+ host_cluster = host_cluster ||
136
+ Rollo::HostCluster.new(asg_name, region)
137
+ service_cluster = service_cluster ||
138
+ Rollo::ServiceCluster.new(ecs_cluster_name, region)
139
+
140
+ hosts = host_cluster.hosts.select {|h| instance_ids.include?(h.id) }
141
+ host_batches = hosts.each_slice(batch_size).to_a
142
+
143
+ say(
144
+ 'Terminating old hosts in host cluster in batches of ' +
145
+ "#{batch_size}...")
146
+ with_padding do
147
+ host_batches.each_with_index do |host_batch, index|
148
+ say(
149
+ "Batch #{index + 1} contains hosts: " +
150
+ "\n\t\t[#{host_batch.map(&:id).join(",\n\t\t ")}]\n" +
151
+ 'Terminating...')
152
+ host_batch.each(&:terminate)
153
+ host_cluster.wait_for_capacity_health do |on|
154
+ on.waiting_for_health do |attempt|
155
+ say(
156
+ 'Waiting for host cluster to reach healthy state ' +
157
+ "(attempt #{attempt})")
158
+ end
159
+ end
160
+ service_cluster.with_replica_services do |on|
161
+ on.each_service do |service|
162
+ service.wait_for_service_health do |on|
163
+ on.waiting_for_health do |attempt|
164
+ say(
165
+ "Waiting for service #{service.name} to reach a " +
166
+ "steady state (attempt #{attempt})...")
167
+ end
168
+ end
169
+ end
170
+ end
171
+ say(
172
+ "Waiting #{service_start_wait_minutes} minute(s) for " +
173
+ 'services to finish starting...')
174
+ sleep(service_start_wait_seconds)
175
+ say(
176
+ "Waited #{service_start_wait_minutes} minute(s). " +
177
+ 'Continuing...')
178
+ end
179
+ end
180
+
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,93 @@
1
+ require 'thor'
2
+ require_relative '../model'
3
+ require_relative './host_cluster'
4
+ require_relative './service_cluster'
5
+
6
+ module Rollo
7
+ module Commands
8
+ class Main < Thor
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc('host-cluster', 'manages the host cluster')
14
+ subcommand "host-cluster", HostCluster
15
+
16
+ desc('service-cluster', 'manages the service cluster')
17
+ subcommand "service-cluster", ServiceCluster
18
+
19
+ desc('version', 'prints the version number of rollo')
20
+ def version
21
+ say Rollo::VERSION
22
+ end
23
+
24
+ desc('roll REGION ASG_NAME ECS_CLUSTER_NAME',
25
+ 'rolls all instances in an ECS cluster')
26
+ method_option(
27
+ :batch_size,
28
+ aliases: '-b',
29
+ type: :numeric,
30
+ default: 3,
31
+ desc:
32
+ 'The number of hosts / service instances to add / remove at ' +
33
+ 'a time.')
34
+ def roll(region, asg_name, ecs_cluster_name)
35
+ host_cluster = Rollo::HostCluster.new(asg_name, region)
36
+ service_cluster = Rollo::ServiceCluster.new(ecs_cluster_name, region)
37
+
38
+ initial_hosts = host_cluster.hosts
39
+
40
+ say(
41
+ "Rolling instances in host cluster #{host_cluster.name} for " +
42
+ "service cluster #{service_cluster.name}...")
43
+ with_padding do
44
+ unless host_cluster.has_desired_capacity?
45
+ say('ERROR: Host cluster is not in stable state.')
46
+ say('This may be due to scaling above or below the desired')
47
+ say('capacity or because hosts are not in service or are ')
48
+ say('unhealthy. Cowardly refusing to roll instances.')
49
+ exit 1
50
+ end
51
+
52
+ invoke(
53
+ "host-cluster:expand",
54
+ [
55
+ region, asg_name, ecs_cluster_name,
56
+ host_cluster
57
+ ])
58
+
59
+ invoke(
60
+ "service-cluster:expand",
61
+ [
62
+ region, asg_name, ecs_cluster_name,
63
+ service_cluster
64
+ ])
65
+
66
+ invoke(
67
+ "host-cluster:terminate",
68
+ [
69
+ region, asg_name, ecs_cluster_name, initial_hosts.map(&:id),
70
+ host_cluster, service_cluster
71
+ ])
72
+
73
+ invoke(
74
+ "host-cluster:contract",
75
+ [
76
+ region, asg_name, ecs_cluster_name,
77
+ host_cluster, service_cluster
78
+ ])
79
+
80
+ invoke(
81
+ "service-cluster:contract",
82
+ [
83
+ region, asg_name, ecs_cluster_name,
84
+ service_cluster
85
+ ])
86
+ end
87
+
88
+ say("Instances in host cluster #{host_cluster.name} rolled " +
89
+ "successfully.")
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,124 @@
1
+ require 'thor'
2
+ require_relative '../model'
3
+
4
+ module Rollo
5
+ module Commands
6
+ class ServiceCluster < Thor
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
11
+ desc(
12
+ 'expand REGION ASG_NAME ECS_CLUSTER_NAME',
13
+ '')
14
+ method_option(
15
+ :batch_size,
16
+ aliases: '-b',
17
+ type: :numeric,
18
+ default: 3,
19
+ desc: 'The number of service instances to add at a time.')
20
+ method_option(
21
+ :startup_time,
22
+ aliases: '-t',
23
+ type: :numeric,
24
+ default: 2,
25
+ desc: 'The number of minutes to wait for services to start up.')
26
+
27
+ def expand(
28
+ region, _, ecs_cluster_name,
29
+ service_cluster = nil)
30
+ batch_size = options[:batch_size]
31
+ service_start_wait_minutes = options[:startup_time]
32
+ service_start_wait_seconds = 60 * service_start_wait_minutes
33
+
34
+ service_cluster = service_cluster ||
35
+ Rollo::ServiceCluster.new(ecs_cluster_name, region)
36
+
37
+ say("Increasing service instance counts by #{batch_size}...")
38
+ with_padding do
39
+ service_cluster.with_replica_services do |on|
40
+ on.start do |services|
41
+ say(
42
+ 'Service cluster contains services:' +
43
+ "\n\t\t[#{services.map(&:name).join(",\n\t\t ")}]")
44
+ end
45
+ on.each_service do |service|
46
+ say(
47
+ "Increasing instance count by #{batch_size} " +
48
+ "for #{service.name}")
49
+ with_padding do
50
+ service.increase_instance_count_by(batch_size) do |on|
51
+ on.prepare do |current, target|
52
+ say(
53
+ "Changing instance count from #{current} " +
54
+ "to #{target}...")
55
+ end
56
+ on.waiting_for_health do |attempt|
57
+ say(
58
+ "Waiting for service to reach a steady state " +
59
+ "(attempt #{attempt})...")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ say(
67
+ "Waiting #{service_start_wait_minutes} minute(s) for " +
68
+ 'services to finish starting...')
69
+ with_padding do
70
+ sleep(service_start_wait_seconds)
71
+ say(
72
+ "Waited #{service_start_wait_minutes} minute(s). " +
73
+ 'Continuing...')
74
+ end
75
+ say('Service instance counts increased, continuing...')
76
+ end
77
+
78
+ desc(
79
+ 'contract REGION ASG_NAME ECS_CLUSTER_NAME',
80
+ '')
81
+ method_option(
82
+ :batch_size,
83
+ aliases: '-b',
84
+ type: :numeric,
85
+ default: 3,
86
+ desc: 'The number of service instances to remove at a time.')
87
+
88
+ def contract(
89
+ region, _, ecs_cluster_name,
90
+ service_cluster = nil)
91
+ batch_size = options[:batch_size]
92
+
93
+ service_cluster = service_cluster ||
94
+ Rollo::ServiceCluster.new(ecs_cluster_name, region)
95
+
96
+ say("Decreasing service instance counts by #{batch_size}...")
97
+ with_padding do
98
+ service_cluster.with_replica_services do |on|
99
+ on.each_service do |service|
100
+ say(
101
+ "Decreasing instance count by #{batch_size} " +
102
+ "for #{service.name}")
103
+ with_padding do
104
+ service.decrease_instance_count_by(batch_size) do |on|
105
+ on.prepare do |current, target|
106
+ say(
107
+ "Changing instance count from #{current} " +
108
+ "to #{target}...")
109
+ end
110
+ on.waiting_for_health do |attempt|
111
+ say(
112
+ 'Waiting for service to reach a steady state ' +
113
+ "(attempt #{attempt})...")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ say("Service instance counts decreased, continuing...")
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1 @@
1
+ require_relative 'commands/main'
@@ -0,0 +1,25 @@
1
+ module Rollo
2
+ module Model
3
+ class Host
4
+ def initialize(instance)
5
+ @instance = instance
6
+ end
7
+
8
+ def id
9
+ @instance.id
10
+ end
11
+
12
+ def terminate
13
+ @instance.terminate(should_decrement_desired_capacity: false)
14
+ end
15
+
16
+ def is_in_service?
17
+ @instance.lifecycle_state == 'InService'
18
+ end
19
+
20
+ def is_healthy?
21
+ @instance.health_status == 'Healthy'
22
+ end
23
+ end
24
+ end
25
+ end