ami_spec 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +85 -0
- data/Rakefile +6 -0
- data/ami_spec.gemspec +27 -0
- data/bin/ami_spec +6 -0
- data/lib/ami_spec/aws_instance.rb +67 -0
- data/lib/ami_spec/aws_instance_options.rb +16 -0
- data/lib/ami_spec/server_spec.rb +42 -0
- data/lib/ami_spec/server_spec_options.rb +14 -0
- data/lib/ami_spec/version.rb +3 -0
- data/lib/ami_spec/wait_for_ssh.rb +27 -0
- data/lib/ami_spec.rb +115 -0
- data/spec/ami_spec_spec.rb +54 -0
- data/spec/aws_instance_spec.rb +87 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/wait_for_ssh_spec.rb +39 -0
- metadata +182 -0
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p551
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) [year] [fullname]
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# AmiSpec
|
2
|
+
|
3
|
+
Acceptance testing your AMIs.
|
4
|
+
|
5
|
+
AmiSpec is a RubyGem used to launch an Amazon Machine Image (AMI) and run ServerSpecs against it. It wraps around the AWS API and ServerSpec to spin up, test and tear down instances.
|
6
|
+
|
7
|
+
## Project Goals
|
8
|
+
|
9
|
+
1. To decouple the building of AMIs from testing them. Other approaches to this problem involve copying ServerSpec tests to an EC2 instance before it's converted to an AMI and running the tests there.
|
10
|
+
The problem with this approach is:
|
11
|
+
|
12
|
+
- It does not test the instance in the state it will be in when it's actually in production.
|
13
|
+
- It does makes it harder to replace the AMI builder software (i.e. [Packer](https://github.com/mitchellh/packer)).
|
14
|
+
- The software required to test the AMI must exist in the AMI.
|
15
|
+
|
16
|
+
2. To run tests as fast as possible; this approach is slightly slower than the alternative listed above (about 1-2 minutes), but should not be onerous.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
System-wide: gem install ami-spec
|
21
|
+
|
22
|
+
With bundler:
|
23
|
+
|
24
|
+
Add `gem 'ami-spec'` to your Gemfile.
|
25
|
+
Run `bundle install`
|
26
|
+
|
27
|
+
## CLI Usage
|
28
|
+
|
29
|
+
```cli
|
30
|
+
$ bundle exec ami_spec --help
|
31
|
+
Options:
|
32
|
+
-r, --role=<s> The role to test, this should map to a directory in the spec folder
|
33
|
+
-a, --ami=<s> The ami ID to run tests against
|
34
|
+
-o, --role-ami-file=<s> A file containing comma separated roles and amis. i.e.
|
35
|
+
web_server,ami-id.
|
36
|
+
-s, --specs=<s> The directory to find ServerSpecs
|
37
|
+
-u, --subnet-id=<s> The subnet to start the instance in
|
38
|
+
-k, --key-name=<s> The SSH key name to assign to instances
|
39
|
+
-e, --key-file=<s> The SSH private key file associated to the key_name
|
40
|
+
-h, --ssh-user=<s> The user to ssh to the instance as
|
41
|
+
-w, --aws-region=<s> The AWS region, defaults to AWS_DEFAULT_REGION environment variable
|
42
|
+
-i, --aws-instance-type=<s> The ec2 instance type, defaults to t2.micro (default: t2.micro)
|
43
|
+
-c, --aws-security-groups=<s+> Security groups to associate to the launched instances. May be specified
|
44
|
+
multiple times
|
45
|
+
-p, --aws-public-ip Launch instances with a public IP
|
46
|
+
-t, --ssh-retries=<i> The number of times we should try sshing to the ec2 instance before
|
47
|
+
giving up. Defaults to 30 (default: 30)
|
48
|
+
-d, --debug Don't terminate instances on exit
|
49
|
+
-l, --help Show this message
|
50
|
+
$ bundle exec ami_spec \
|
51
|
+
--role web_server \
|
52
|
+
--ami ami-12345678 \
|
53
|
+
--subnet-id subnet-abcdefgh \
|
54
|
+
--key-name ec2-key-pair \
|
55
|
+
--key-file ~/.ssh/ec2-key-pair.pem \
|
56
|
+
--ssh-user ubuntu \
|
57
|
+
--specs ./my_project/spec
|
58
|
+
```
|
59
|
+
|
60
|
+
AmiSpec will launch an EC2 instance from the given AMI (`--ami`), in a subnet (`--subnet-id`) with a key-pair (`--key-name`)
|
61
|
+
and try to SSH to it (`--ssh-user` and `--key-file`).
|
62
|
+
When the instances becomes reachable it will run all Specs inside the role spec directory (`--role` i.e. `my_project/spec/web_server`).
|
63
|
+
|
64
|
+
Alternative to the `--ami` and `--role` variables, a file of comma separated roles and AMIs (`ROLE,AMI\n`) can be supplied to `--role-ami-file`.
|
65
|
+
|
66
|
+
## Development Status
|
67
|
+
|
68
|
+
Active and ready for Production
|
69
|
+
|
70
|
+
## Contributing
|
71
|
+
|
72
|
+
For bug fixes, documentation changes, and small features:
|
73
|
+
1. Fork it ( https://github.com/envato/ami-spec/fork )
|
74
|
+
2. Create your feature branch (git checkout -b my-new-feature)
|
75
|
+
3. Commit your changes (git commit -am 'Add some feature')
|
76
|
+
4. Push to the branch (git push origin my-new-feature)
|
77
|
+
5. Create a new Pull Request
|
78
|
+
|
79
|
+
## Maintainers
|
80
|
+
|
81
|
+
Patrick Robinson (@nemski)
|
82
|
+
|
83
|
+
## License
|
84
|
+
|
85
|
+
AmiSpec uses the MIT license. See [LICENSE.txt](./LICENSE.txt)
|
data/Rakefile
ADDED
data/ami_spec.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'ami_spec/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'ami_spec'
|
8
|
+
gem.version = AmiSpec::VERSION
|
9
|
+
gem.authors = ['Patrick Robinson', 'Martin Jagusch']
|
10
|
+
gem.email = []
|
11
|
+
gem.description = 'Acceptance testing your AMIs'
|
12
|
+
gem.summary = gem.description
|
13
|
+
gem.homepage = 'https://github.com/envato/ami-spec'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ['lib']
|
19
|
+
|
20
|
+
gem.add_dependency 'aws-sdk', '~> 2'
|
21
|
+
gem.add_dependency 'rake'
|
22
|
+
gem.add_dependency 'serverspec'
|
23
|
+
gem.add_dependency 'specinfra', '>= 2.45'
|
24
|
+
gem.add_dependency 'trollop'
|
25
|
+
gem.add_dependency 'hashie'
|
26
|
+
gem.add_dependency 'net-ssh', '< 3.0'
|
27
|
+
end
|
data/bin/ami_spec
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module AmiSpec
|
5
|
+
class AwsInstance
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def self.start(args)
|
9
|
+
new(args).tap do |instance|
|
10
|
+
instance.start
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(options)
|
15
|
+
@role = options.fetch(:role)
|
16
|
+
@ami = options.fetch(:ami)
|
17
|
+
@subnet_id = options.fetch(:subnet_id)
|
18
|
+
@key_name = options.fetch(:key_name)
|
19
|
+
@instance_type = options.fetch(:aws_instance_type)
|
20
|
+
@public_ip = options.fetch(:aws_public_ip)
|
21
|
+
@region = options.fetch(:aws_region)
|
22
|
+
@security_group_ids = options.fetch(:aws_security_groups)
|
23
|
+
end
|
24
|
+
|
25
|
+
def_delegators :@instance, :instance_id, :tags, :terminate, :private_ip_address, :public_ip_address
|
26
|
+
|
27
|
+
def start
|
28
|
+
client = Aws::EC2::Client.new(client_options)
|
29
|
+
placeholder_instance = client.run_instances(instances_options).instances.first
|
30
|
+
|
31
|
+
@instance = Aws::EC2::Instance.new(placeholder_instance.instance_id)
|
32
|
+
@instance.wait_until_running
|
33
|
+
tag_instance
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def client_options
|
39
|
+
!@region.nil? ? {region: @region} : {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def instances_options
|
43
|
+
params = {
|
44
|
+
image_id: @ami,
|
45
|
+
min_count: 1,
|
46
|
+
max_count: 1,
|
47
|
+
instance_type: @instance_type,
|
48
|
+
key_name: @key_name,
|
49
|
+
network_interfaces: [{
|
50
|
+
device_index: 0,
|
51
|
+
associate_public_ip_address: @public_ip,
|
52
|
+
subnet_id: @subnet_id,
|
53
|
+
}]
|
54
|
+
}
|
55
|
+
|
56
|
+
unless @security_group_ids.nil?
|
57
|
+
params[:network_interfaces][0][:groups] = @security_group_ids
|
58
|
+
end
|
59
|
+
|
60
|
+
params
|
61
|
+
end
|
62
|
+
|
63
|
+
def tag_instance
|
64
|
+
@instance.create_tags(tags: [{ key: 'AmiSpec', value: @role }])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module AmiSpec
|
4
|
+
class AwsInstanceOptions < Hashie::Dash
|
5
|
+
include Hashie::Extensions::IgnoreUndeclared
|
6
|
+
|
7
|
+
property :ami
|
8
|
+
property :role
|
9
|
+
property :subnet_id
|
10
|
+
property :key_name
|
11
|
+
property :aws_instance_type
|
12
|
+
property :aws_public_ip
|
13
|
+
property :aws_region
|
14
|
+
property :aws_security_groups
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Loading serverspec first causes a weird error - stack level too deep
|
2
|
+
# Requiring rspec first fixes that *shrug*
|
3
|
+
require 'rspec'
|
4
|
+
require 'serverspec'
|
5
|
+
|
6
|
+
module AmiSpec
|
7
|
+
class ServerSpec
|
8
|
+
def initialize(options)
|
9
|
+
instance = options.fetch(:instance)
|
10
|
+
public_ip = options.fetch(:aws_public_ip)
|
11
|
+
|
12
|
+
@debug = options.fetch(:debug)
|
13
|
+
@ip = public_ip ? instance.public_ip_address : instance.private_ip_address
|
14
|
+
@role = instance.tags.find{ |tag| tag.key == 'AmiSpec' }.value
|
15
|
+
@spec = options.fetch(:specs)
|
16
|
+
@user = options.fetch(:ssh_user)
|
17
|
+
@key_file = options.fetch(:key_file)
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
$LOAD_PATH.unshift(@spec) unless $LOAD_PATH.include?(@spec)
|
22
|
+
require File.join(@spec, 'spec_helper')
|
23
|
+
|
24
|
+
set :backend, :ssh
|
25
|
+
set :host, @ip
|
26
|
+
set :ssh_options, :user => @user, :keys => [@key_file], :paranoid => false
|
27
|
+
|
28
|
+
RSpec.configuration.fail_fast = true if @debug
|
29
|
+
|
30
|
+
RSpec::Core::Runner.disable_autorun!
|
31
|
+
result = RSpec::Core::Runner.run(Dir.glob("#{@spec}/#{@role}/*_spec.rb"))
|
32
|
+
|
33
|
+
# We can't use Rspec.clear_examples here because it also clears the shared_examples.
|
34
|
+
# As shared examples are loaded in via the spec_helper, we cannot reload them.
|
35
|
+
RSpec.world.example_groups.clear
|
36
|
+
|
37
|
+
Specinfra::Backend::Ssh.clear
|
38
|
+
|
39
|
+
result.zero?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module AmiSpec
|
4
|
+
class ServerSpecOptions < Hashie::Dash
|
5
|
+
include Hashie::Extensions::IgnoreUndeclared
|
6
|
+
|
7
|
+
property :instance
|
8
|
+
property :aws_public_ip
|
9
|
+
property :debug
|
10
|
+
property :key_file
|
11
|
+
property :specs
|
12
|
+
property :ssh_user
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module AmiSpec
|
4
|
+
class WaitForSSH
|
5
|
+
def self.wait(ip_address, user, key, max_retries)
|
6
|
+
last_error = nil
|
7
|
+
retries = 0
|
8
|
+
|
9
|
+
while retries < max_retries
|
10
|
+
begin
|
11
|
+
Net::SSH.start(ip_address, user, keys: [key], paranoid: false) { |ssh| ssh.exec 'echo boo!' }
|
12
|
+
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, Timeout::Error => error
|
13
|
+
last_error = error
|
14
|
+
sleep 1
|
15
|
+
else
|
16
|
+
break
|
17
|
+
end
|
18
|
+
|
19
|
+
retries = retries + 1
|
20
|
+
end
|
21
|
+
|
22
|
+
if retries > max_retries - 1
|
23
|
+
raise AmiSpec::InstanceConnectionTimeout.new("Timed out waiting for SSH to become available: #{last_error}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/ami_spec.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'ami_spec/aws_instance'
|
2
|
+
require 'ami_spec/aws_instance_options'
|
3
|
+
require 'ami_spec/server_spec'
|
4
|
+
require 'ami_spec/server_spec_options'
|
5
|
+
require 'ami_spec/wait_for_ssh'
|
6
|
+
require 'trollop'
|
7
|
+
|
8
|
+
module AmiSpec
|
9
|
+
class InstanceConnectionTimeout < StandardError; end
|
10
|
+
# == Parameters:
|
11
|
+
# amis::
|
12
|
+
# A hash of roles and amis in the format of:
|
13
|
+
# {role => ami_id}. i.e.
|
14
|
+
# {'web_server' => 'ami-abcd1234'}
|
15
|
+
# specs::
|
16
|
+
# A string of the directory to find ServerSpecs.
|
17
|
+
# There should be a folder in this directory for each role found in ::amis
|
18
|
+
# subnet_id::
|
19
|
+
# The subnet_id to start instances in.
|
20
|
+
# key_name::
|
21
|
+
# The SSH key name to assign to instances. This key name must exist on the executing host for passwordless login.
|
22
|
+
# key_file::
|
23
|
+
# The SSH key file to use to connect to the host.
|
24
|
+
# aws_region::
|
25
|
+
# AWS region to connect to
|
26
|
+
# Defaults to AWS_DEFAULT_REGION
|
27
|
+
# aws_security_group_ids::
|
28
|
+
# AWS Security groups to assign to the instances
|
29
|
+
# Defaults to the default security group for the VPC
|
30
|
+
# aws_instance_type::
|
31
|
+
# AWS ec2 instance type
|
32
|
+
# aws_public_ip::
|
33
|
+
# Should the instances get a public IP address
|
34
|
+
# ssh_user::
|
35
|
+
# The username to SSH to the AMI with.
|
36
|
+
# ssh_retries::
|
37
|
+
# Set the maximum number of ssh retries while waiting for the instance to boot.
|
38
|
+
# debug::
|
39
|
+
# Don't terminate the instances on exit
|
40
|
+
# == Returns:
|
41
|
+
# Boolean - The result of all the server specs.
|
42
|
+
def self.run(options)
|
43
|
+
instances = []
|
44
|
+
options[:amis].each_pair do |role, ami|
|
45
|
+
aws_instance_options = AwsInstanceOptions.new(options.merge(role: role, ami: ami))
|
46
|
+
instances << AwsInstance.start(aws_instance_options)
|
47
|
+
end
|
48
|
+
|
49
|
+
results = []
|
50
|
+
instances.each do |instance|
|
51
|
+
ip_address = options[:aws_public_ip] ? instance.public_ip_address : instance.private_ip_address
|
52
|
+
WaitForSSH.wait(ip_address, options[:ssh_user], options[:key_file], options[:ssh_retries])
|
53
|
+
|
54
|
+
server_spec_options = ServerSpecOptions.new(options.merge(instance: instance))
|
55
|
+
results << ServerSpec.new(server_spec_options).run
|
56
|
+
end
|
57
|
+
|
58
|
+
results.all?
|
59
|
+
ensure
|
60
|
+
stop_instances(instances, options[:debug])
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.stop_instances(instances, debug)
|
64
|
+
instances.each do |instance|
|
65
|
+
begin
|
66
|
+
if debug
|
67
|
+
puts "EC2 instance ##{instance.instance_id} has not been stopped due to debug mode."
|
68
|
+
else
|
69
|
+
instance.terminate
|
70
|
+
end
|
71
|
+
rescue Aws::EC2::Errors::InvalidInstanceIDNotFound
|
72
|
+
puts "Failed to stop EC2 instance ##{instance.instance_id}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private_class_method :stop_instances
|
78
|
+
|
79
|
+
def self.invoke
|
80
|
+
options = Trollop::options do
|
81
|
+
opt :role, "The role to test, this should map to a directory in the spec folder", type: :string
|
82
|
+
opt :ami, "The ami ID to run tests against", type: :string
|
83
|
+
opt :role_ami_file, "A file containing comma separated roles and amis. i.e.\nweb_server,ami-id.",
|
84
|
+
type: :string
|
85
|
+
opt :specs, "The directory to find ServerSpecs", type: :string, required: true
|
86
|
+
opt :subnet_id, "The subnet to start the instance in", type: :string, required: true
|
87
|
+
opt :key_name, "The SSH key name to assign to instances", type: :string, required: true
|
88
|
+
opt :key_file, "The SSH private key file associated to the key_name", type: :string, required: true
|
89
|
+
opt :ssh_user, "The user to ssh to the instance as", type: :string, required: true
|
90
|
+
opt :aws_region, "The AWS region, defaults to AWS_DEFAULT_REGION environment variable", type: :string
|
91
|
+
opt :aws_instance_type, "The ec2 instance type, defaults to t2.micro", type: :string, default: 't2.micro'
|
92
|
+
opt :aws_security_groups, "Security groups to associate to the launched instances. May be specified multiple times",
|
93
|
+
type: :strings, default: nil
|
94
|
+
opt :aws_public_ip, "Launch instances with a public IP"
|
95
|
+
opt :ssh_retries, "The number of times we should try sshing to the ec2 instance before giving up. Defaults to 30",
|
96
|
+
type: :int, default: 30
|
97
|
+
opt :debug, "Don't terminate instances on exit"
|
98
|
+
end
|
99
|
+
|
100
|
+
if options[:role] && options[:ami]
|
101
|
+
options[:amis] = { options[:role] => options[:ami] }
|
102
|
+
options.delete(:role)
|
103
|
+
options.delete(:ami)
|
104
|
+
elsif options[:role_ami_file]
|
105
|
+
file_lines = File.read(options[:role_ami_file]).split("\n")
|
106
|
+
file_array = file_lines.collect { |line| line.split(',') }.flatten
|
107
|
+
options[:amis] = Hash[*file_array]
|
108
|
+
options.delete(:role_ami_file)
|
109
|
+
else
|
110
|
+
fail "You must specify either role and ami or role_ami_file"
|
111
|
+
end
|
112
|
+
|
113
|
+
exit run(options)
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AmiSpec do
|
4
|
+
let(:amis) { {'web_server' => 'ami-1234abcd', 'db_server' => 'ami-1234abcd'} }
|
5
|
+
let(:ec2_double) { instance_double(AmiSpec::AwsInstance) }
|
6
|
+
let(:state) { double(name: 'running') }
|
7
|
+
let(:test_result) { true }
|
8
|
+
let(:server_spec_double) { double(run: test_result) }
|
9
|
+
subject do
|
10
|
+
described_class.run(
|
11
|
+
amis: amis,
|
12
|
+
specs: '/tmp/foobar',
|
13
|
+
subnet_id: 'subnet-1234abcd',
|
14
|
+
key_name: 'key',
|
15
|
+
key_file: 'key.pem',
|
16
|
+
aws_public_ip: false,
|
17
|
+
aws_instance_type: 't2.micro',
|
18
|
+
ssh_user: 'ubuntu',
|
19
|
+
debug: false,
|
20
|
+
ssh_retries: 30,
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#run' do
|
25
|
+
before do
|
26
|
+
allow(AmiSpec::WaitForSSH).to receive(:wait).and_return(true)
|
27
|
+
allow(AmiSpec::AwsInstance).to receive(:start).and_return(ec2_double)
|
28
|
+
allow(AmiSpec::ServerSpec).to receive(:new).and_return(server_spec_double)
|
29
|
+
allow(ec2_double).to receive(:terminate).and_return(true)
|
30
|
+
allow(ec2_double).to receive(:private_ip_address).and_return('127.0.0.1')
|
31
|
+
allow_any_instance_of(Object).to receive(:sleep)
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'successful tests' do
|
35
|
+
it 'calls aws instance for each ami' do
|
36
|
+
expect(AmiSpec::AwsInstance).to receive(:start).with(hash_including(role: 'web_server'))
|
37
|
+
expect(AmiSpec::AwsInstance).to receive(:start).with(hash_including(role: 'db_server'))
|
38
|
+
subject
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns true' do
|
42
|
+
expect(subject).to be_truthy
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'failed tests' do
|
47
|
+
let(:test_result) { false }
|
48
|
+
|
49
|
+
it 'returns false' do
|
50
|
+
expect(subject).to be_falsey
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AmiSpec::AwsInstance do
|
4
|
+
let(:role) { 'web_server' }
|
5
|
+
let(:sec_group_id) { nil }
|
6
|
+
let(:region) { nil }
|
7
|
+
let(:client_double) { instance_double(Aws::EC2::Client) }
|
8
|
+
let(:new_ec2_double) { instance_double(Aws::EC2::Types::Instance) }
|
9
|
+
let(:ec2_double) { instance_double(Aws::EC2::Instance) }
|
10
|
+
subject(:aws_instance) do
|
11
|
+
described_class.new(
|
12
|
+
role: role,
|
13
|
+
ami: 'ami',
|
14
|
+
subnet_id: 'subnet',
|
15
|
+
key_name: 'key',
|
16
|
+
aws_instance_type: 't2.micro',
|
17
|
+
aws_public_ip: false,
|
18
|
+
aws_security_groups: sec_group_id,
|
19
|
+
aws_region: region
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
before do
|
24
|
+
allow(Aws::EC2::Client).to receive(:new).and_return(client_double)
|
25
|
+
allow(client_double).to receive(:run_instances).and_return(double(instances: [new_ec2_double]))
|
26
|
+
allow(ec2_double).to receive(:create_tags).and_return(double)
|
27
|
+
allow(Aws::EC2::Instance).to receive(:new).and_return(ec2_double)
|
28
|
+
allow(new_ec2_double).to receive(:instance_id)
|
29
|
+
allow(ec2_double).to receive(:instance_id)
|
30
|
+
allow(ec2_double).to receive(:wait_until_running)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#start' do
|
34
|
+
subject(:start) { aws_instance.start }
|
35
|
+
context 'without optional values' do
|
36
|
+
it 'does not include the security group' do
|
37
|
+
expect(client_double).to receive(:run_instances).with(
|
38
|
+
hash_excluding(:network_interfaces=>array_including(hash_including(:groups)))
|
39
|
+
)
|
40
|
+
start
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'does include the region' do
|
44
|
+
expect(Aws::EC2::Client).to receive(:new).with(
|
45
|
+
hash_excluding(:region => region)
|
46
|
+
)
|
47
|
+
start
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'with security group' do
|
52
|
+
let(:sec_group_id) { ['1234'] }
|
53
|
+
|
54
|
+
it 'does include security groups' do
|
55
|
+
expect(client_double).to receive(:run_instances).with(
|
56
|
+
hash_including(:network_interfaces=>array_including(hash_including(:groups)))
|
57
|
+
)
|
58
|
+
start
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with region' do
|
63
|
+
let(:region) { 'us-east-1' }
|
64
|
+
|
65
|
+
it 'does include the region' do
|
66
|
+
expect(Aws::EC2::Client).to receive(:new).with(
|
67
|
+
hash_including(:region => region)
|
68
|
+
)
|
69
|
+
start
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'tags the instance with a role' do
|
74
|
+
expect(ec2_double).to receive(:create_tags).with(
|
75
|
+
hash_including(tags: [{ key: 'AmiSpec', value: role}])
|
76
|
+
)
|
77
|
+
start
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'delegates some methods to the instance variable' do
|
81
|
+
expect(ec2_double).to receive(:instance_id)
|
82
|
+
start
|
83
|
+
aws_instance.instance_id
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AmiSpec::WaitForSSH do
|
4
|
+
describe '#wait' do
|
5
|
+
let(:retries) { 30 }
|
6
|
+
subject { described_class.wait('127.0.0.1', 'ubuntu', 'key.pem', 30) }
|
7
|
+
|
8
|
+
before do
|
9
|
+
allow_any_instance_of(Object).to receive(:sleep)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns after one attempt if ssh connection succeeds' do
|
13
|
+
expect(Net::SSH).to receive(:start)
|
14
|
+
|
15
|
+
subject
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'ssh fails' do
|
19
|
+
before do
|
20
|
+
allow(Net::SSH).to receive(:start).and_raise(Errno::ECONNREFUSED, 'ssh failed')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'raises an exception' do
|
24
|
+
expect{subject}.to raise_error(AmiSpec::InstanceConnectionTimeout)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns the last error' do
|
28
|
+
expect(Net::SSH).to receive(:start).and_raise(Errno::ECONNREFUSED, 'some other error')
|
29
|
+
expect{subject}.to raise_error(AmiSpec::InstanceConnectionTimeout, /ssh failed/)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'tries the number of retries specified' do
|
33
|
+
expect(Net::SSH).to receive(:start).exactly(retries).times
|
34
|
+
|
35
|
+
expect{subject}.to raise_error(AmiSpec::InstanceConnectionTimeout)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ami_spec
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Patrick Robinson
|
9
|
+
- Martin Jagusch
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2015-12-18 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: aws-sdk
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '2'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rake
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: serverspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: specinfra
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '2.45'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '2.45'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: trollop
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: hashie
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: net-ssh
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - <
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '3.0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - <
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '3.0'
|
127
|
+
description: Acceptance testing your AMIs
|
128
|
+
email: []
|
129
|
+
executables:
|
130
|
+
- ami_spec
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- .gitignore
|
135
|
+
- .ruby-version
|
136
|
+
- Gemfile
|
137
|
+
- LICENSE.txt
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
140
|
+
- ami_spec.gemspec
|
141
|
+
- bin/ami_spec
|
142
|
+
- lib/ami_spec.rb
|
143
|
+
- lib/ami_spec/aws_instance.rb
|
144
|
+
- lib/ami_spec/aws_instance_options.rb
|
145
|
+
- lib/ami_spec/server_spec.rb
|
146
|
+
- lib/ami_spec/server_spec_options.rb
|
147
|
+
- lib/ami_spec/version.rb
|
148
|
+
- lib/ami_spec/wait_for_ssh.rb
|
149
|
+
- spec/ami_spec_spec.rb
|
150
|
+
- spec/aws_instance_spec.rb
|
151
|
+
- spec/spec_helper.rb
|
152
|
+
- spec/wait_for_ssh_spec.rb
|
153
|
+
homepage: https://github.com/envato/ami-spec
|
154
|
+
licenses: []
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
require_paths:
|
158
|
+
- lib
|
159
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
+
none: false
|
161
|
+
requirements:
|
162
|
+
- - ! '>='
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
|
+
none: false
|
167
|
+
requirements:
|
168
|
+
- - ! '>='
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubyforge_project:
|
173
|
+
rubygems_version: 1.8.23.2
|
174
|
+
signing_key:
|
175
|
+
specification_version: 3
|
176
|
+
summary: Acceptance testing your AMIs
|
177
|
+
test_files:
|
178
|
+
- spec/ami_spec_spec.rb
|
179
|
+
- spec/aws_instance_spec.rb
|
180
|
+
- spec/spec_helper.rb
|
181
|
+
- spec/wait_for_ssh_spec.rb
|
182
|
+
has_rdoc:
|