From a21cad96d5247508a177eb4b32d354ccf85c30cb Mon Sep 17 00:00:00 2001 From: Josh Kline Date: Thu, 30 Apr 2015 15:13:50 -0700 Subject: [PATCH 1/5] Add RSpec with recommended configuration --- .rspec | 2 ++ Gemfile | 4 ++++ spec/spec_helper.rb | 15 +++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 .rspec create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/Gemfile b/Gemfile index dbbbcee..5a0d621 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,7 @@ source 'https://rubygems.org' gem 'aws-sdk', '~> 2.0.22' gem 'uuid', '~> 2.3.7' + +group :test do + gem 'rspec', '~> 3.2' +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..4ce0464 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,15 @@ +# This is the configuration recommended by rspec --init with RSpec 3.2.0 +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + config.filter_run :focus + config.run_all_when_everything_filtered = true + config.disable_monkey_patching! + config.default_formatter = 'doc' if config.files_to_run.one? + config.order = :random + Kernel.srand config.seed +end From 7981937faa69df53a815cfe642a38a5e7fdfa351 Mon Sep 17 00:00:00 2001 From: Josh Kline Date: Thu, 30 Apr 2015 16:27:03 -0700 Subject: [PATCH 2/5] Example listing and filtering EC2 instances Client side filter for EC2 instances based on a Name tag pattern. Use of RSpec to test that code using stubs. RSpec example currently fails, and I do not know why. Failures: 1) InstanceManager#instances returns only EC2 instances with matching Name tag Failure/Error: expect(subject.instances.map(&:id)).to eq ["i-1", "i-2"] expected: ["i-1", "i-2"] got: ["i-1", "i-2", "i-1", "i-2"] (compared using ==) # ./spec/instance_manager_spec.rb:54:in `block (3 levels) in ' --- lib/instance_manager.rb | 42 ++++++++++++++++++++++++++ spec/instance_manager_spec.rb | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 lib/instance_manager.rb create mode 100644 spec/instance_manager_spec.rb diff --git a/lib/instance_manager.rb b/lib/instance_manager.rb new file mode 100644 index 0000000..98f3245 --- /dev/null +++ b/lib/instance_manager.rb @@ -0,0 +1,42 @@ +require "aws-sdk" + +# An example using AWS Ruby SDK v2 to list and filter EC2 instances. +class InstanceManager + # By default manage EC2 instance whose Name tag matches this pattern. + DEFAULT_NAME_RE = /^example-\d+$/ + + attr_reader :ec2_resource + attr_reader :name_re + + # @param instance [Aws::EC2::Instance] + # @return [String] Instance name as judged by its Name tag. + def self.instance_name(instance) + name_tag = instance.tags.find { |t| t.key == "Name" } + name_tag.value if name_tag + end + + # @param ec2_client [Aws::EC2::Client] + # @param name_re [Regexp] Manage EC2 instance whose Name tag matches this + # pattern. + def initialize(ec2_client, name_re = DEFAULT_NAME_RE) + @ec2_resource = Aws::EC2::Resource.new(:client => ec2_client) + @name_re = name_re + end + + # @return [Array] Instance whose Name tag matches the + # given pattern. + def instances + ec2_resource.instances.select { |i| name_matches?(i) } + end + + protected + + # @param instance [Aws::EC2::Instance] + # @return [Boolean] true if the given instance has a Name tag that matches + # the requested pattern. + def name_matches?(instance) + name = self.class.instance_name(instance) + name && name.match(name_re) + end + +end diff --git a/spec/instance_manager_spec.rb b/spec/instance_manager_spec.rb new file mode 100644 index 0000000..5ea8e1e --- /dev/null +++ b/spec/instance_manager_spec.rb @@ -0,0 +1,57 @@ +require "instance_manager" +RSpec.describe InstanceManager do + let(:ec2_client) do + Aws::EC2::Client.new(:region => 'us-east-1', :stub_responses => true) + end + + subject do + described_class.new(ec2_client) + end + + context "#instances" do + before do + ec2_client.stub_responses( + :describe_instances, + { :reservations => [{ :instances => [ + { + :instance_id => "i-1", + :state => {:name => "running" }, + :tags => [ + { :key => "Name", :value => "example-1" }, + { :key => "color", :value => "red" }, + ] + }, + { + :instance_id => "i-2", + :state => {:name => "running" }, + :tags => [ + { :key => "Name", :value => "example-2" }, + { :key => "color", :value => "green" }, + ] + }, + { + :instance_id => "i-3", + :state => {:name => "running" }, + :tags => [ + { :key => "Name", :value => "webapp-3" }, + { :key => "color", :value => "green" }, + ] + }, + { + :instance_id => "i-4", + :state => {:name => "stopped" }, + :tags => [], + }, + ]}]}, + ) + end + + it "returns two matching EC2 instances" do + expect(subject.instances.to_a.size).to eq 2 + end + + it "returns only EC2 instances with matching Name tag" do + expect(subject.instances.map(&:id)).to eq ["i-1", "i-2"] + end + end +end From 8a160c2a2e04846b82eb8e4c4799c7cbe9ef62b5 Mon Sep 17 00:00:00 2001 From: Josh Kline Date: Thu, 30 Apr 2015 17:16:04 -0700 Subject: [PATCH 3/5] Showing incorrect return from stubbed resource A very simple RSpec example showing how stub_responses affects the Aws::EC2::Client interface correctly, but results in doubled resonses from the Aws::EC2::Resource interface. --- spec/ec2_stub_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 spec/ec2_stub_spec.rb diff --git a/spec/ec2_stub_spec.rb b/spec/ec2_stub_spec.rb new file mode 100644 index 0000000..708a855 --- /dev/null +++ b/spec/ec2_stub_spec.rb @@ -0,0 +1,28 @@ +require "aws-sdk" + +RSpec.describe Aws::EC2 do + let(:client) { Aws::EC2::Client.new(:region => 'us-east-1', :stub_responses => true) } + let(:resource) { Aws::EC2::Resource.new(:client => client) } + + context "when describe_instances is stubbed to return a single instance" do + before do + client.stub_responses( + :describe_instances, { + :reservations => [{:instances=>[{:instance_id => "i-1", :state => {:name => "running"}, :tags =>[{:key => "Name", :value => "example-1" }]}]}] + } + ) + + end + context Aws::EC2::Client do + it "#describe_instances to return 1 instance" do + expect(client.describe_instances.reservations.first.instances.size).to eq 1 + end + end + + context Aws::EC2::Resource do + it "#instances to return 1 instance" do + expect(resource.instances.to_a.size).to eq 1 + end + end + end +end From 189dadf4290a57450afabd5a1e031449641c6ca7 Mon Sep 17 00:00:00 2001 From: Trevor Rowe Date: Fri, 1 May 2015 10:50:44 -0700 Subject: [PATCH 4/5] Stubbed nil values for the describe instance calls. The default stub returns non-empty strings for all possible string values. This causes the next token that trigger paging to appear like there is a next response. I updated your stubbs to use nil next tokens to avoid this issue. Please feel free to open this as an issue against aws/aws-sdk-ruby as this is a poor default experience. --- spec/ec2_stub_spec.rb | 1 + spec/instance_manager_spec.rb | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/ec2_stub_spec.rb b/spec/ec2_stub_spec.rb index 708a855..def3f89 100644 --- a/spec/ec2_stub_spec.rb +++ b/spec/ec2_stub_spec.rb @@ -8,6 +8,7 @@ before do client.stub_responses( :describe_instances, { + :next_token => nil, :reservations => [{:instances=>[{:instance_id => "i-1", :state => {:name => "running"}, :tags =>[{:key => "Name", :value => "example-1" }]}]}] } ) diff --git a/spec/instance_manager_spec.rb b/spec/instance_manager_spec.rb index 5ea8e1e..401aa3d 100644 --- a/spec/instance_manager_spec.rb +++ b/spec/instance_manager_spec.rb @@ -12,7 +12,9 @@ before do ec2_client.stub_responses( :describe_instances, - { :reservations => [{ :instances => [ + { + :next_token => nil, + :reservations => [{ :instances => [ { :instance_id => "i-1", :state => {:name => "running" }, From 0096e48ade769a109e7b6ce0080cffb441f508e2 Mon Sep 17 00:00:00 2001 From: Josh Kline Date: Fri, 1 May 2015 19:36:42 -0700 Subject: [PATCH 5/5] Add list_instances.rb sample application And describe the `list_instances.rb`/`InstanceManager` example in the README. --- README.md | 24 +++++++++++++++++++++++- list_instances.rb | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100755 list_instances.rb diff --git a/README.md b/README.md index 7579032..24011f8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ You can also install the SDK directly with: ## Basic Configuration You need to set up your AWS security credentials before the sample code is able -to connect to AWS. You can do this by creating a file named "credentials" at ~/.aws/ +to connect to AWS. You can do this by creating a file named "credentials" at ~/.aws/ (C:\Users\USER_NAME\.aws\ for Windows users) and saving the following lines in the file: [default] @@ -37,6 +37,28 @@ you need to do is run it: The S3 documentation has a good overview of the [restrictions for bucket names](http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) for when you start making your own buckets. +## Running the InstanceManager sample + +This sample application connects to Amazon's [Elastic Compute Cloud (EC2)](http://aws.amazon.com/ec2/), +lists the available instances, filters them based on Name tag, and then prints +useful information about each instance. + +All you need to do is run it: + + ruby list_instances.rb + +The default regular expression will probably not match any instances in your +account. To list all instances run: + + ruby list_instances.rb '.*' + +RSpec tests for `InstanceManager` are included to demonstrate use of +[ClientStubs](http://docs.aws.amazon.com/sdkforruby/api/Aws/ClientStubs.html). + +Execute rspec to run all the tests: + + rspec + ## License This sample application is distributed under the diff --git a/list_instances.rb b/list_instances.rb new file mode 100755 index 0000000..4cd08a0 --- /dev/null +++ b/list_instances.rb @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "instance_manager" + +name_re = Regexp.new(ARGV[0] || InstanceManager::DEFAULT_NAME_RE) +client = Aws::EC2::Client.new(:region => 'us-east-1') +manager = InstanceManager.new(client, name_re) + +puts "Listing instances with Name tag matching #{manager.name_re}" + +manager.instances.each do |instance| + name = InstanceManager.instance_name(instance) + # Stopped instances do not have any private IP address. + private_ip_display = " (#{instance.private_ip_address})" if instance.private_ip_address + puts "#{name} (#{instance.id}) => #{instance.state.name}#{private_ip_display}" +end