rollo 0.3.0 → 0.8.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Toby Clemson
3
+ Copyright (c) 2021 InfraBlocks Maintainers
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/Rakefile CHANGED
@@ -1,24 +1,148 @@
1
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
2
 
3
- RSpec::Core::RakeTask.new(:spec)
3
+ require 'yaml'
4
+ require 'rake_circle_ci'
5
+ require 'rake_github'
6
+ require 'rake_ssh'
7
+ require 'rake_gpg'
8
+ require 'securerandom'
9
+ require 'rspec/core/rake_task'
10
+ require 'rubocop/rake_task'
4
11
 
5
- task :default => :spec
12
+ task default: %i[
13
+ library:fix
14
+ test:unit
15
+ ]
16
+
17
+ namespace :encryption do
18
+ namespace :directory do
19
+ desc 'Ensure CI secrets directory exists.'
20
+ task :ensure do
21
+ FileUtils.mkdir_p('config/secrets/ci')
22
+ end
23
+ end
24
+
25
+ namespace :passphrase do
26
+ desc 'Generate encryption passphrase used by CI.'
27
+ task generate: ['directory:ensure'] do
28
+ File.open('config/secrets/ci/encryption.passphrase', 'w') do |f|
29
+ f.write(SecureRandom.base64(36))
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ namespace :keys do
36
+ namespace :deploy do
37
+ RakeSSH.define_key_tasks(
38
+ path: 'config/secrets/ci/',
39
+ comment: 'maintainers@infrablocks.io'
40
+ )
41
+ end
42
+
43
+ namespace :secrets do
44
+ namespace :gpg do
45
+ RakeGPG.define_generate_key_task(
46
+ output_directory: 'config/secrets/ci',
47
+ name_prefix: 'gpg',
48
+ owner_name: 'InfraBlocks Maintainers',
49
+ owner_email: 'maintainers@infrablocks.io',
50
+ owner_comment: 'rollo CI Key'
51
+ )
52
+ end
53
+
54
+ desc 'Generate key used by CI to access secrets.'
55
+ task generate: [:'gpg:generate']
56
+ end
57
+ end
58
+
59
+ namespace :secrets do
60
+ desc 'Regenerate all generatable secrets.'
61
+ task regenerate: %w[
62
+ encryption:passphrase:generate
63
+ keys:deploy:generate
64
+ keys:secrets:generate
65
+ ]
66
+ end
67
+
68
+ RuboCop::RakeTask.new
69
+
70
+ namespace :library do
71
+ desc 'Run all checks of the library'
72
+ task check: [:rubocop]
73
+
74
+ desc 'Attempt to automatically fix issues with the library'
75
+ task fix: [:'rubocop:auto_correct']
76
+ end
77
+
78
+ namespace :test do
79
+ RSpec::Core::RakeTask.new(:unit)
80
+ end
81
+
82
+ RakeCircleCI.define_project_tasks(
83
+ namespace: :circle_ci,
84
+ project_slug: 'github/infrablocks/rollo'
85
+ ) do |t|
86
+ circle_ci_config =
87
+ YAML.load_file('config/secrets/circle_ci/config.yaml')
88
+
89
+ t.api_token = circle_ci_config['circle_ci_api_token']
90
+ t.environment_variables = {
91
+ ENCRYPTION_PASSPHRASE:
92
+ File.read('config/secrets/ci/encryption.passphrase')
93
+ .chomp
94
+ }
95
+ t.checkout_keys = []
96
+ t.ssh_keys = [
97
+ {
98
+ hostname: 'github.com',
99
+ private_key: File.read('config/secrets/ci/ssh.private')
100
+ }
101
+ ]
102
+ end
103
+
104
+ RakeGithub.define_repository_tasks(
105
+ namespace: :github,
106
+ repository: 'infrablocks/rollo'
107
+ ) do |t|
108
+ github_config =
109
+ YAML.load_file('config/secrets/github/config.yaml')
110
+
111
+ t.access_token = github_config['github_personal_access_token']
112
+ t.deploy_keys = [
113
+ {
114
+ title: 'CircleCI',
115
+ public_key: File.read('config/secrets/ci/ssh.public')
116
+ }
117
+ ]
118
+ end
119
+
120
+ namespace :pipeline do
121
+ desc 'Prepare CircleCI Pipeline'
122
+ task prepare: %i[
123
+ circle_ci:project:follow
124
+ circle_ci:env_vars:ensure
125
+ circle_ci:checkout_keys:ensure
126
+ circle_ci:ssh_keys:ensure
127
+ github:deploy_keys:ensure
128
+ ]
129
+ end
6
130
 
7
131
  namespace :version do
8
- desc "Bump version for specified type (pre, major, minor patch)"
132
+ desc 'Bump version for specified type (pre, major, minor, patch)'
9
133
  task :bump, [:type] do |_, args|
10
134
  bump_version_for(args.type)
11
135
  end
12
136
  end
13
137
 
14
- desc "Release gem"
138
+ desc 'Release gem'
15
139
  task :release do
16
- sh "gem release --tag --push"
140
+ sh 'gem release --tag --push'
17
141
  end
18
142
 
19
143
  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]\""
144
+ sh "gem bump --version #{version_type} " \
145
+ '&& bundle install ' \
146
+ '&& export LAST_MESSAGE="$(git log -1 --pretty=%B)" ' \
147
+ '&& git commit -a --amend -m "${LAST_MESSAGE} [ci skip]"'
24
148
  end
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "rollo"
4
+ require 'bundler/setup'
5
+ require 'rollo'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "rollo"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
data/lib/rollo.rb CHANGED
@@ -1,4 +1,6 @@
1
- require_relative "rollo/version"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rollo/version'
2
4
  require_relative 'rollo/commands'
3
5
 
4
6
  module Rollo
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'commands/main'
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative '../model'
5
+
6
+ module Rollo
7
+ module Commands
8
+ # rubocop:disable Metrics/ClassLength
9
+ class Hosts < Thor
10
+ namespace :hosts
11
+
12
+ def self.exit_on_failure?
13
+ true
14
+ end
15
+
16
+ desc(
17
+ 'expand REGION ASG_NAME ECS_CLUSTER_NAME',
18
+ 'Expands the host cluster by one batch.'
19
+ )
20
+ method_option(
21
+ :batch_size,
22
+ aliases: '-b',
23
+ type: :numeric,
24
+ default: 3,
25
+ desc: 'The number of hosts to add at a time.'
26
+ )
27
+ # rubocop:disable Metrics/AbcSize
28
+ # rubocop:disable Metrics/MethodLength
29
+ def expand(
30
+ region, asg_name, _,
31
+ host_cluster = nil
32
+ )
33
+ batch_size = options[:batch_size]
34
+
35
+ host_cluster ||= Rollo::Model::HostCluster.new(asg_name, region)
36
+
37
+ say("Increasing host cluster desired capacity by #{batch_size}...")
38
+ with_padding do
39
+ host_cluster.increase_capacity_by(batch_size) do |on|
40
+ on.prepare do |current, target|
41
+ say(
42
+ "Changing desired capacity from #{current} to " \
43
+ "#{target}..."
44
+ )
45
+ end
46
+ on.waiting_for_start do |attempt|
47
+ say(
48
+ 'Waiting for capacity change to start ' \
49
+ "(attempt #{attempt})..."
50
+ )
51
+ end
52
+ on.waiting_for_end do |attempt|
53
+ say(
54
+ 'Waiting for capacity change to complete ' \
55
+ "(attempt #{attempt})..."
56
+ )
57
+ end
58
+ on.waiting_for_health do |attempt|
59
+ say("Waiting for a healthy state (attempt #{attempt})")
60
+ end
61
+ end
62
+ end
63
+ say 'Host cluster desired capacity increased, continuing...'
64
+ end
65
+
66
+ # rubocop:enable Metrics/MethodLength
67
+ # rubocop:enable Metrics/AbcSize
68
+
69
+ desc(
70
+ 'contract REGION ASG_NAME ECS_CLUSTER_NAME',
71
+ 'Contracts the host cluster by one batch'
72
+ )
73
+ method_option(
74
+ :batch_size,
75
+ aliases: '-b',
76
+ type: :numeric,
77
+ default: 3,
78
+ desc: 'The number of hosts to remove at a time.'
79
+ )
80
+ # rubocop:disable Metrics/AbcSize
81
+ # rubocop:disable Metrics/MethodLength
82
+ def contract(
83
+ region, asg_name, ecs_cluster_name,
84
+ host_cluster = nil, service_cluster = nil
85
+ )
86
+ batch_size = options[:batch_size]
87
+
88
+ host_cluster ||= Rollo::Model::HostCluster.new(asg_name, region)
89
+ service_cluster ||= Rollo::Model::ServiceCluster.new(
90
+ ecs_cluster_name, region
91
+ )
92
+
93
+ say("Decreasing host cluster desired capacity by #{batch_size}...")
94
+ # rubocop:disable Metrics/BlockLength
95
+ with_padding do
96
+ host_cluster.decrease_capacity_by(batch_size) do |on|
97
+ on.prepare do |current, target|
98
+ say(
99
+ "Changing desired capacity from #{current} to " \
100
+ "#{target}..."
101
+ )
102
+ end
103
+ on.waiting_for_start do |attempt|
104
+ say(
105
+ 'Waiting for capacity change to start ' \
106
+ "(attempt #{attempt})..."
107
+ )
108
+ end
109
+ on.waiting_for_end do |attempt|
110
+ say(
111
+ 'Waiting for capacity change to complete ' \
112
+ "(attempt #{attempt})..."
113
+ )
114
+ end
115
+ on.waiting_for_health do |attempt|
116
+ say(
117
+ 'Waiting for host cluster to reach healthy state ' \
118
+ "(attempt #{attempt})..."
119
+ )
120
+ end
121
+ end
122
+ service_cluster.with_replica_services do |services|
123
+ services.each_service do |service|
124
+ service.wait_for_service_health do |on|
125
+ on.waiting_for_health do |attempt|
126
+ say(
127
+ "Waiting for service #{service.name} to reach a " \
128
+ "steady state (attempt #{attempt})..."
129
+ )
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ # rubocop:enable Metrics/BlockLength
136
+ say 'Host cluster desired capacity decreased, continuing...'
137
+ end
138
+
139
+ # rubocop:enable Metrics/MethodLength
140
+ # rubocop:enable Metrics/AbcSize
141
+
142
+ desc(
143
+ 'terminate REGION ASG_NAME ECS_CLUSTER_NAME INSTANCE_IDS*',
144
+ 'Terminates the specified hosts within the cluster.'
145
+ )
146
+ method_option(
147
+ :batch_size,
148
+ aliases: '-b',
149
+ type: :numeric,
150
+ default: 3,
151
+ desc: 'The number of hosts to add at a time.'
152
+ )
153
+ method_option(
154
+ :startup_time,
155
+ aliases: '-t',
156
+ type: :numeric,
157
+ default: 2,
158
+ desc: 'The number of minutes to wait for services to start up.'
159
+ )
160
+ # rubocop:disable Metrics/MethodLength
161
+ # rubocop:disable Metrics/AbcSize
162
+ # rubocop:disable Metrics/ParameterLists
163
+ def terminate(
164
+ region, asg_name, ecs_cluster_name, instance_ids,
165
+ host_cluster = nil, service_cluster = nil
166
+ )
167
+ batch_size = options[:batch_size]
168
+
169
+ service_start_wait_minutes = options[:startup_time]
170
+ service_start_wait_seconds = 60 * service_start_wait_minutes
171
+
172
+ host_cluster ||= Rollo::Model::HostCluster.new(asg_name, region)
173
+ service_cluster ||= Rollo::Model::ServiceCluster.new(ecs_cluster_name,
174
+ region)
175
+
176
+ hosts = host_cluster.hosts.select { |h| instance_ids.include?(h.id) }
177
+ host_batches = hosts.each_slice(batch_size).to_a
178
+
179
+ say(
180
+ 'Terminating old hosts in host cluster in batches of ' \
181
+ "#{batch_size}..."
182
+ )
183
+ # rubocop:disable Metrics/BlockLength
184
+ with_padding do
185
+ host_batches.each_with_index do |host_batch, index|
186
+ say(
187
+ "Batch #{index + 1} contains hosts: " \
188
+ "\n\t\t[#{host_batch.map(&:id).join(",\n\t\t ")}]\n" \
189
+ 'Terminating...'
190
+ )
191
+ host_batch.each(&:terminate)
192
+ host_cluster.wait_for_capacity_health do |on|
193
+ on.waiting_for_health do |attempt|
194
+ say(
195
+ 'Waiting for host cluster to reach healthy state ' \
196
+ "(attempt #{attempt})"
197
+ )
198
+ end
199
+ end
200
+ service_cluster.with_replica_services do |services|
201
+ services.each_service do |service|
202
+ service.wait_for_service_health do |on|
203
+ on.waiting_for_health do |attempt|
204
+ say(
205
+ "Waiting for service #{service.name} to reach a " \
206
+ "steady state (attempt #{attempt})..."
207
+ )
208
+ end
209
+ end
210
+ end
211
+ end
212
+ say(
213
+ "Waiting #{service_start_wait_minutes} minute(s) for " \
214
+ 'services to finish starting...'
215
+ )
216
+ sleep(service_start_wait_seconds)
217
+ say(
218
+ "Waited #{service_start_wait_minutes} minute(s). " \
219
+ 'Continuing...'
220
+ )
221
+ end
222
+ end
223
+ # rubocop:enable Metrics/BlockLength
224
+ end
225
+ # rubocop:enable Metrics/ParameterLists
226
+ # rubocop:enable Metrics/MethodLength
227
+ # rubocop:enable Metrics/AbcSize
228
+ end
229
+
230
+ # rubocop:enable Metrics/ClassLength
231
+ end
232
+ end
@@ -1,48 +1,69 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'thor'
2
4
  require_relative '../model'
3
- require_relative './host_cluster'
4
- require_relative './service_cluster'
5
+ require_relative './hosts'
6
+ require_relative './services'
5
7
 
6
8
  module Rollo
7
9
  module Commands
8
10
  class Main < Thor
11
+ namespace :main
12
+
9
13
  def self.exit_on_failure?
10
14
  true
11
15
  end
12
16
 
13
- desc('host-cluster', 'manages the host cluster')
14
- subcommand "host-cluster", Rollo::Commands::HostCluster
17
+ desc('hosts', 'Manages the host cluster')
18
+ subcommand :hosts, Rollo::Commands::Hosts
15
19
 
16
- desc('service-cluster', 'manages the service cluster')
17
- subcommand "service-cluster", Rollo::Commands::ServiceCluster
20
+ desc('services', 'Manages the service cluster')
21
+ subcommand :services, Rollo::Commands::Services
18
22
 
19
- desc('version', 'prints the version number of rollo')
23
+ desc('version', 'Prints the version number of rollo')
20
24
  def version
21
25
  say Rollo::VERSION
22
26
  end
23
27
 
24
28
  desc('roll REGION ASG_NAME ECS_CLUSTER_NAME',
25
- 'rolls all instances in an ECS cluster')
29
+ 'Rolls all hosts in the cluster')
30
+ method_option(
31
+ :batch_size,
32
+ aliases: '-b',
33
+ type: :numeric,
34
+ default: 3,
35
+ desc:
36
+ 'The number of hosts / service instances to add / remove at ' \
37
+ 'a time.'
38
+ )
26
39
  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.')
40
+ :maximum_service_instances,
41
+ aliases: '-mx',
42
+ type: :numeric,
43
+ desc: 'The maximum number of service instances to expand to.'
44
+ )
45
+ method_option(
46
+ :minimum_service_instances,
47
+ aliases: '-mn',
48
+ type: :numeric,
49
+ desc: 'The minimum number of service instances to contract to.'
50
+ )
51
+ # rubocop:disable Metrics/MethodLength
52
+ # rubocop:disable Metrics/AbcSize
34
53
  def roll(region, asg_name, ecs_cluster_name)
35
54
  host_cluster = Rollo::Model::HostCluster.new(asg_name, region)
36
55
  service_cluster = Rollo::Model::ServiceCluster
37
- .new(ecs_cluster_name, region)
56
+ .new(ecs_cluster_name, region)
38
57
 
39
58
  initial_hosts = host_cluster.hosts
40
59
 
41
60
  say(
42
- "Rolling instances in host cluster #{host_cluster.name} for " +
43
- "service cluster #{service_cluster.name}...")
61
+ "Rolling instances in host cluster #{host_cluster.name} for " \
62
+ "service cluster #{service_cluster.name}..."
63
+ )
64
+ # rubocop:disable Metrics/BlockLength
44
65
  with_padding do
45
- unless host_cluster.has_desired_capacity?
66
+ unless host_cluster.desired_capacity?
46
67
  say('ERROR: Host cluster is not in stable state.')
47
68
  say('This may be due to scaling above or below the desired')
48
69
  say('capacity or because hosts are not in service or are ')
@@ -51,44 +72,53 @@ module Rollo
51
72
  end
52
73
 
53
74
  invoke(
54
- "host-cluster:expand",
55
- [
56
- region, asg_name, ecs_cluster_name,
57
- host_cluster
58
- ])
75
+ 'hosts:expand',
76
+ [
77
+ region, asg_name, ecs_cluster_name,
78
+ host_cluster
79
+ ]
80
+ )
59
81
 
60
82
  invoke(
61
- "service-cluster:expand",
62
- [
63
- region, asg_name, ecs_cluster_name,
64
- service_cluster
65
- ])
83
+ 'services:expand',
84
+ [
85
+ region, asg_name, ecs_cluster_name,
86
+ service_cluster
87
+ ],
88
+ maximum_instances: options[:maximum_service_instances]
89
+ )
66
90
 
67
91
  invoke(
68
- "host-cluster:terminate",
69
- [
70
- region, asg_name, ecs_cluster_name, initial_hosts.map(&:id),
71
- host_cluster, service_cluster
72
- ])
92
+ 'hosts:terminate',
93
+ [
94
+ region, asg_name, ecs_cluster_name, initial_hosts.map(&:id),
95
+ host_cluster, service_cluster
96
+ ]
97
+ )
73
98
 
74
99
  invoke(
75
- "host-cluster:contract",
76
- [
77
- region, asg_name, ecs_cluster_name,
78
- host_cluster, service_cluster
79
- ])
100
+ 'hosts:contract',
101
+ [
102
+ region, asg_name, ecs_cluster_name,
103
+ host_cluster, service_cluster
104
+ ]
105
+ )
80
106
 
81
107
  invoke(
82
- "service-cluster:contract",
83
- [
84
- region, asg_name, ecs_cluster_name,
85
- service_cluster
86
- ])
108
+ 'services:contract',
109
+ [
110
+ region, asg_name, ecs_cluster_name,
111
+ service_cluster
112
+ ],
113
+ minimum_instances: options[:minimum_service_instances]
114
+ )
87
115
  end
88
-
89
- say("Instances in host cluster #{host_cluster.name} rolled " +
90
- "successfully.")
116
+ # rubocop:enable Metrics/BlockLength
117
+ say("Instances in host cluster #{host_cluster.name} rolled " \
118
+ 'successfully.')
91
119
  end
120
+ # rubocop:enable Metrics/MethodLength
121
+ # rubocop:enable Metrics/AbcSize
92
122
  end
93
123
  end
94
- end
124
+ end