elbas 0.27.0 → 3.0.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.
Files changed (39) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +25 -35
  3. data/elbas.gemspec +15 -12
  4. data/lib/elbas.rb +15 -9
  5. data/lib/elbas/aws/ami.rb +71 -0
  6. data/lib/elbas/aws/autoscale_group.rb +43 -0
  7. data/lib/elbas/aws/base.rb +39 -0
  8. data/lib/elbas/aws/instance.rb +24 -0
  9. data/lib/elbas/aws/instance_collection.rb +36 -0
  10. data/lib/elbas/aws/launch_template.rb +32 -0
  11. data/lib/elbas/aws/snapshot.rb +21 -0
  12. data/lib/elbas/aws/taggable.rb +18 -0
  13. data/lib/elbas/capistrano.rb +14 -12
  14. data/lib/elbas/errors/no_launch_template.rb +6 -0
  15. data/lib/elbas/logger.rb +10 -2
  16. data/lib/elbas/retryable.rb +34 -9
  17. data/lib/elbas/tasks/elbas.rake +19 -10
  18. data/lib/elbas/version.rb +1 -1
  19. data/spec/aws/ami_spec.rb +140 -0
  20. data/spec/aws/autoscale_group_spec.rb +53 -0
  21. data/spec/aws/instance_collection_spec.rb +28 -0
  22. data/spec/aws/instance_spec.rb +30 -0
  23. data/spec/aws/launch_template_spec.rb +52 -0
  24. data/spec/aws/taggable_spec.rb +53 -0
  25. data/spec/spec_helper.rb +14 -24
  26. data/spec/support/stubs/CreateLaunchTemplateVersion.200.xml +16 -0
  27. data/spec/support/stubs/DescribeAutoScalingGroups.200.xml +60 -0
  28. data/spec/support/stubs/DescribeImages.200.xml +36 -1
  29. data/spec/support/stubs/DescribeInstances.200.xml +109 -2
  30. metadata +60 -21
  31. data/lib/elbas/ami.rb +0 -56
  32. data/lib/elbas/aws/autoscaling.rb +0 -21
  33. data/lib/elbas/aws/credentials.rb +0 -20
  34. data/lib/elbas/aws/ec2.rb +0 -13
  35. data/lib/elbas/aws_resource.rb +0 -36
  36. data/lib/elbas/launch_configuration.rb +0 -64
  37. data/lib/elbas/taggable.rb +0 -9
  38. data/spec/ami_spec.rb +0 -10
  39. data/spec/elbas_spec.rb +0 -69
@@ -0,0 +1,18 @@
1
+ module Elbas
2
+ module AWS
3
+ module Taggable
4
+ def tag(key, value)
5
+ @tags ||= {}
6
+
7
+ Elbas::Retryable.times(3).delay(5) do
8
+ aws_counterpart.create_tags tags: [{ key: key, value: value }]
9
+ @tags[key] = value
10
+ end
11
+ end
12
+
13
+ def tags
14
+ @tags || {}
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,26 +1,28 @@
1
- require 'aws-sdk-v1'
2
1
  require 'capistrano/dsl'
3
2
 
4
3
  load File.expand_path("../tasks/elbas.rake", __FILE__)
5
4
 
6
5
  def autoscale(groupname, *args)
7
6
  include Capistrano::DSL
8
- include Elbas::AWS::AutoScaling
7
+ include Elbas::Logger
9
8
 
10
- autoscale_group = autoscaling.groups[groupname]
11
- running_instances = autoscale_group.ec2_instances.filter('instance-state-name', 'running')
9
+ set :aws_autoscale_group_name, groupname
12
10
 
13
- set :aws_autoscale_group, groupname
11
+ asg = Elbas::AWS::AutoscaleGroup.new groupname
12
+ instances = asg.instances.running
14
13
 
15
- running_instances.each do |instance|
16
- hostname = instance.dns_name || instance.private_ip_address
17
- p "ELBAS: Adding server: #{hostname}"
18
- server(hostname, *args)
14
+ instances.each do |instance|
15
+ info "Adding server: #{instance.hostname}"
16
+ server instance.hostname, *args
19
17
  end
20
18
 
21
- if running_instances.count > 0
22
- after('deploy', 'elbas:scale')
19
+ if instances.any?
20
+ after 'deploy', 'elbas:deploy'
23
21
  else
24
- p "ELBAS: AMI could not be created because no running instances were found. Is your autoscale group name correct?"
22
+ error <<~MESSAGE
23
+ Could not create AMI because no running instances were found in the specified
24
+ AutoScale group. Ensure that the AutoScale group name is correct and that
25
+ there is at least one running instance attached to it.
26
+ MESSAGE
25
27
  end
26
28
  end
@@ -0,0 +1,6 @@
1
+ module Elbas
2
+ module Errors
3
+ class NoLaunchTemplate < StandardError
4
+ end
5
+ end
6
+ end
@@ -1,7 +1,15 @@
1
+ require 'capistrano/doctor/output_helpers'
2
+
1
3
  module Elbas
2
4
  module Logger
5
+ include Capistrano::Doctor::OutputHelpers
6
+
3
7
  def info(message)
4
- p "ELBAS: #{message}"
8
+ $stdout.puts [prefix, message, "\n"].join
9
+ end
10
+
11
+ def prefix
12
+ @prefix ||= color.colorize("\n[ELBAS] ", :cyan)
5
13
  end
6
14
  end
7
- end
15
+ end
@@ -1,16 +1,41 @@
1
1
  module Elbas
2
- module Retryable
3
- def with_retry(max: fetch(:elbas_retry_max, 3), delay: fetch(:elbas_retry_delay, 5))
4
- tries ||= 0
5
- tries += 1
6
- yield
2
+ class Retryable
3
+ include Elbas::Logger
4
+
5
+ def initialize
6
+ @max = 0
7
+ @delay = 0
8
+ end
9
+
10
+ def times(max, &block)
11
+ @max = max
12
+ run block if block_given?
13
+ end
14
+
15
+ def delay(seconds, &block)
16
+ @delay = seconds
17
+ run block if block_given?
18
+ end
19
+
20
+ def run(proc)
21
+ attempts ||= 0
22
+ attempts += 1
23
+ proc.call
7
24
  rescue => e
8
- p "Rescued #{e.message}"
9
- if tries < max
10
- p "Retrying in #{delay} seconds..."
11
- sleep delay
25
+ info "Rescued error in retryable action: #{e.message}"
26
+ if attempts < @max
27
+ info "Retrying in #{@delay} seconds..."
28
+ sleep @delay
12
29
  retry
13
30
  end
14
31
  end
32
+
33
+ def self.times(max, &block)
34
+ new.tap { |r| r.times max }
35
+ end
36
+
37
+ def self.delay(seconds, &block)
38
+ new.tap { |r| r.delay seconds }
39
+ end
15
40
  end
16
41
  end
@@ -1,17 +1,26 @@
1
1
  require 'elbas'
2
+ include Elbas::Logger
2
3
 
3
4
  namespace :elbas do
4
- task :scale do
5
- set :aws_access_key_id, fetch(:aws_access_key_id, ENV['AWS_ACCESS_KEY_ID'])
6
- set :aws_secret_access_key, fetch(:aws_secret_access_key, ENV['AWS_SECRET_ACCESS_KEY'])
5
+ task :deploy do
6
+ asg = Elbas::AWS::AutoscaleGroup.new fetch(:aws_autoscale_group_name)
7
7
 
8
- Elbas::AMI.create do |ami|
9
- p "ELBAS: Created AMI: #{ami.aws_counterpart.id}"
10
- Elbas::LaunchConfiguration.create(ami) do |lc|
11
- p "ELBAS: Created Launch Configuration: #{lc.aws_counterpart.name}"
12
- lc.attach_to_autoscale_group!
13
- end
8
+ info "Creating AMI from a running instance..."
9
+ ami = Elbas::AWS::AMI.create asg.instances.running.sample
10
+ ami.tag 'ELBAS-Deploy-group', asg.name
11
+ ami.tag 'ELBAS-Deploy-id', env.timestamp.to_i.to_s
12
+ info "Created AMI: #{ami.id}"
13
+
14
+ info "Updating launch template with the new AMI..."
15
+ launch_template = asg.launch_template.update ami
16
+ info "Updated launch template, new default version = #{launch_template.version}"
17
+
18
+ info "Cleaning up old AMIs..."
19
+ ami.ancestors.each do |ancestor|
20
+ info "Deleting old AMI: #{ancestor.id}"
21
+ ancestor.delete
14
22
  end
15
23
 
24
+ info "Deployment complete!"
16
25
  end
17
- end
26
+ end
@@ -1,3 +1,3 @@
1
1
  module Elbas
2
- VERSION = '0.27.0'.freeze
2
+ VERSION = '3.0.0'.freeze
3
3
  end
@@ -0,0 +1,140 @@
1
+ describe Elbas::AWS::AMI do
2
+ subject { Elbas::AWS::AMI.new 'test' }
3
+
4
+ describe '#initialize' do
5
+ it 'sets the id' do
6
+ expect(subject.id).to eq 'test'
7
+ end
8
+
9
+ it 'has an aws-sdk counterpart' do
10
+ expect(subject.aws_counterpart).to be_a_kind_of ::Aws::EC2::Image
11
+ expect(subject.aws_counterpart.id).to eq 'test'
12
+ end
13
+
14
+ context 'with snapshots' do
15
+ subject do
16
+ Elbas::AWS::AMI.new 'test', [
17
+ double(:bdm, ebs: double(:ebs, snapshot_id: 'snap-1'))
18
+ ]
19
+ end
20
+
21
+ it 'sets snapshots to Snapshot objects' do
22
+ expect(subject.snapshots.size).to eq 1
23
+ expect(subject.snapshots[0]).to be_a_kind_of Elbas::AWS::Snapshot
24
+ end
25
+
26
+ it 'sets the ID on the Snapshots' do
27
+ expect(subject.snapshots[0].id).to eq 'snap-1'
28
+ end
29
+ end
30
+ end
31
+
32
+ describe '#deploy_id' do
33
+ it 'returns the ELBAS-Deploy-id tag, if set' do
34
+ webmock :post, /ec2/ => 201, with: Hash[body: /Action=CreateTags/]
35
+ subject.tag 'ELBAS-Deploy-id', 'test'
36
+ expect(subject.deploy_id).to eq 'test'
37
+ end
38
+
39
+ it 'returns nil if the tag was never set' do
40
+ expect(subject.deploy_id).to be_nil
41
+ end
42
+ end
43
+
44
+ describe '#deploy_group' do
45
+ it 'returns the ELBAS-Deploy-group tag, if set' do
46
+ webmock :post, /ec2/ => 201, with: Hash[body: /Action=CreateTags/]
47
+ subject.tag 'ELBAS-Deploy-group', 'test'
48
+ expect(subject.deploy_group).to eq 'test'
49
+ end
50
+
51
+ it 'returns nil if the tag was never set' do
52
+ expect(subject.deploy_group).to be_nil
53
+ end
54
+ end
55
+
56
+ describe '#ancestors' do
57
+ before do
58
+ webmock :post, /ec2/ => 201, with: Hash[body: /Action=CreateTags/]
59
+ subject.tag 'ELBAS-Deploy-group', 'test'
60
+ subject.tag 'ELBAS-Deploy-id', 'test'
61
+
62
+ webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'DescribeImages.200.xml',
63
+ with: Hash[body: /Action=DescribeImages/]
64
+ end
65
+
66
+ it 'includes AMIs from the same deploy group, different deploy ID' do
67
+ expect {
68
+ subject.tag 'ELBAS-Deploy-id', 'not-test'
69
+ }.to change {
70
+ subject.ancestors.size
71
+ }.by 1
72
+ end
73
+ end
74
+
75
+ describe '#delete' do
76
+ before do
77
+ webmock :post, /ec2/ => 201, with: Hash[body: /Action=DeregisterImage/]
78
+ end
79
+
80
+ it 'calls the deregister AMI API' do
81
+ subject.delete
82
+ expect(WebMock)
83
+ .to have_requested(:post, /ec2/)
84
+ .with body: /Action=DeregisterImage&ImageId=test/
85
+ end
86
+
87
+ context 'with snapshots' do
88
+ subject do
89
+ Elbas::AWS::AMI.new 'test', [
90
+ double(:bdm, ebs: double(:ebs, snapshot_id: 'snap-1'))
91
+ ]
92
+ end
93
+
94
+ it 'deletes the AMIs snapshots too' do
95
+ webmock :post, /ec2/ => 201, with: Hash[body: /Action=DeleteSnapshot/]
96
+
97
+ subject.delete
98
+ expect(WebMock)
99
+ .to have_requested(:post, /ec2/)
100
+ .with body: /Action=DeleteSnapshot&SnapshotId=snap-1/
101
+ end
102
+ end
103
+ end
104
+
105
+ describe '.create' do
106
+ subject { described_class }
107
+ let(:instance) { Elbas::AWS::Instance.new 'i-1234567890', nil, nil }
108
+
109
+ before do
110
+ webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'CreateImage.200.xml',
111
+ with: Hash[body: /Action=CreateImage/]
112
+ end
113
+
114
+ it 'calls the API with the instance given' do
115
+ subject.create instance
116
+ expect(WebMock)
117
+ .to have_requested(:post, /ec2/)
118
+ .with body: /Action=CreateImage&InstanceId=i-1234567890&Name=ELBAS-ami-(\d+)/
119
+ end
120
+
121
+ it 'uses the no_reboot option by default' do
122
+ subject.create instance
123
+ expect(WebMock)
124
+ .to have_requested(:post, /ec2/)
125
+ .with(body: /NoReboot=true/)
126
+ end
127
+
128
+ it 'uses no_reboot as false, if given' do
129
+ subject.create instance, no_reboot: false
130
+ expect(WebMock)
131
+ .to have_requested(:post, /ec2/)
132
+ .with(body: /NoReboot=false/)
133
+ end
134
+
135
+ it 'returns the an AMI object with the new id' do
136
+ ami = subject.create instance
137
+ expect(ami.id).to eq 'ami-4fa54026' # from stub
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,53 @@
1
+ describe Elbas::AWS::AutoscaleGroup do
2
+ subject { Elbas::AWS::AutoscaleGroup.new 'test-asg' }
3
+
4
+ before do
5
+ webmock :post, %r{autoscaling.(.*).amazonaws.com\/\z} => 'DescribeAutoScalingGroups.200.xml',
6
+ with: Hash[body: /Action=DescribeAutoScalingGroups/]
7
+
8
+ webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'DescribeInstances.200.xml',
9
+ with: Hash[body: /Action=DescribeInstances/]
10
+ end
11
+
12
+ describe '#initialize' do
13
+ it 'sets the name' do
14
+ expect(subject.name).to eq 'test-asg'
15
+ end
16
+
17
+ it 'has an aws-sdk counterpart' do
18
+ expect(subject.aws_counterpart).to be_a_kind_of ::Aws::AutoScaling::Types::AutoScalingGroup
19
+ expect(subject.aws_counterpart.auto_scaling_group_name).to eq 'test-asg'
20
+ end
21
+ end
22
+
23
+ describe '#instance_ids' do
24
+ it 'returns every instance ID in the ASG' do
25
+ expect(subject.instance_ids).to eq ['i-1234567890', 'i-500']
26
+ end
27
+ end
28
+
29
+ describe '#instances' do
30
+ it 'returns an instance collection with all instances' do
31
+ instances = subject.instances
32
+ expect(instances.count).to eq 2
33
+ end
34
+ end
35
+
36
+ describe '#launch_template' do
37
+ it 'throws an error if there is no launch template' do
38
+ expect { subject.launch_template }.to raise_error(Elbas::Errors::NoLaunchTemplate)
39
+ end
40
+
41
+ it 'returns a LaunchTemplate object with the id/name/version set' do
42
+ allow(subject.aws_counterpart).to receive(:launch_template) do
43
+ double(:lt, launch_template_id: 'test-1', launch_template_name: 'Test', version: '$Latest')
44
+ end
45
+
46
+ expect(subject.launch_template.id).to eq 'test-1'
47
+ expect(subject.launch_template.name).to eq 'Test'
48
+ expect(subject.launch_template.version).to eq '$Latest'
49
+ end
50
+
51
+
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ describe Elbas::AWS::InstanceCollection do
2
+ subject { Elbas::AWS::InstanceCollection.new ['i-1234567890', 'i-500'] }
3
+
4
+ before do
5
+ webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'DescribeInstances.200.xml',
6
+ with: Hash[body: /Action=DescribeInstances/]
7
+ end
8
+
9
+ describe '#instances' do
10
+ it 'returns Instance objects with name/hostname/state' do
11
+ expect(subject.instances[0].id).to eq 'i-1234567890'
12
+ expect(subject.instances[0].hostname).to eq 'ec2-1234567890.amazonaws.com'
13
+ expect(subject.instances[0].state).to eq 16
14
+
15
+ expect(subject.instances[1].id).to eq 'i-500'
16
+ expect(subject.instances[1].hostname).to eq 'ec2-500.amazonaws.com'
17
+ expect(subject.instances[1].state).to eq 32
18
+ end
19
+ end
20
+
21
+ describe '#running' do
22
+ it 'returns only running instances' do
23
+ expect(subject.instances.size).to eq 2
24
+ expect(subject.running.size).to eq 1
25
+ expect(subject.running[0].id).to eq 'i-1234567890'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ describe Elbas::AWS::Instance do
2
+ subject { Elbas::AWS::Instance.new 'i-1234567890', 'ec2-1234567890.amazonaws.com', 16 }
3
+
4
+ describe '#initialize' do
5
+ it 'sets the AWS counterpart' do
6
+ expect(subject.aws_counterpart).to be_a_kind_of ::Aws::EC2::Instance
7
+ expect(subject.aws_counterpart.id).to eq 'i-1234567890'
8
+ end
9
+ end
10
+
11
+ describe '#hostname' do
12
+ it 'returns the public DNS' do
13
+ expect(subject.hostname).to eq 'ec2-1234567890.amazonaws.com'
14
+ end
15
+ end
16
+
17
+ describe '#running?' do
18
+ it 'returns true if the state code is 16 ("running")' do
19
+ expect(subject).to receive(:state) { 16 }
20
+ expect(subject).to be_running
21
+ end
22
+
23
+ it 'returns false for every other state code' do
24
+ [0, 32, 48, 64, 80].each do |code|
25
+ expect(subject).to receive(:state) { code }
26
+ expect(subject).to_not be_running
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ describe Elbas::AWS::LaunchTemplate do
2
+ subject { Elbas::AWS::LaunchTemplate.new 'test-lt', 'test', '1' }
3
+
4
+ before do
5
+ webmock :post, %r{ec2.(.*).amazonaws.com\/\z} => 'CreateLaunchTemplateVersion.200.xml',
6
+ with: Hash[body: /Action=CreateLaunchTemplateVersion/]
7
+ end
8
+
9
+ describe '#initialize' do
10
+ it 'sets the id' do
11
+ expect(subject.id).to eq 'test-lt'
12
+ end
13
+
14
+ it 'sets the name' do
15
+ expect(subject.name).to eq 'test'
16
+ end
17
+
18
+ it 'sets the version' do
19
+ expect(subject.version).to eq '1'
20
+ end
21
+ end
22
+
23
+ describe '#update' do
24
+ it 'hits the CreateLaunchTemplateVersion API' do
25
+ subject.update double(:ami, id: 'ami-123')
26
+ expect(WebMock)
27
+ .to have_requested(:post, /ec2/)
28
+ .with(body: %r{Action=CreateLaunchTemplateVersion})
29
+ end
30
+
31
+ it 'creates a new launch template from the given AMI' do
32
+ subject.update double(:ami, id: 'ami-123')
33
+ expect(WebMock)
34
+ .to have_requested(:post, /ec2/)
35
+ .with(body: %r{LaunchTemplateData.ImageId=ami-123})
36
+ end
37
+
38
+ it 'uses itself as the source' do
39
+ subject.update double(:ami, id: 'ami-123')
40
+ expect(WebMock)
41
+ .to have_requested(:post, /ec2/)
42
+ .with(body: %r{LaunchTemplateId=test-lt&SourceVersion=1})
43
+ end
44
+
45
+ it 'returns a new launch template' do
46
+ launch_template = subject.update double(:ami, id: 'ami-123')
47
+ expect(launch_template.id).to eq 'lt-1234567890'
48
+ expect(launch_template.name).to eq 'elbas-test'
49
+ expect(launch_template.version).to eq 123
50
+ end
51
+ end
52
+ end