diff --git a/lib/kitchen/driver/ec2.rb b/lib/kitchen/driver/ec2.rb index db2e9327..d55679d1 100644 --- a/lib/kitchen/driver/ec2.rb +++ b/lib/kitchen/driver/ec2.rb @@ -29,8 +29,7 @@ module Driver # # @author Fletcher Nichol class Ec2 < Kitchen::Driver::SSHBase - - extend Fog::AWS::CredentialFetcher::ServiceMethods + include Fog::AWS::CredentialFetcher::ServiceMethods default_config :region, 'us-east-1' default_config :availability_zone, 'us-east-1b' default_config :flavor_id, 'm1.small' @@ -42,13 +41,15 @@ class Ec2 < Kitchen::Driver::SSHBase default_config :iam_profile_name, nil default_config :price, nil default_config :aws_access_key_id do |driver| - ENV['AWS_ACCESS_KEY'] || ENV['AWS_ACCESS_KEY_ID'] || iam_creds[:aws_access_key_id] + ENV['AWS_ACCESS_KEY'] || ENV['AWS_ACCESS_KEY_ID'] || + driver.iam_creds[:aws_access_key_id] end default_config :aws_secret_access_key do |driver| - ENV['AWS_SECRET_KEY'] || ENV['AWS_SECRET_ACCESS_KEY'] || iam_creds[:aws_secret_access_key] + ENV['AWS_SECRET_KEY'] || ENV['AWS_SECRET_ACCESS_KEY'] || + driver.iam_creds[:aws_secret_access_key] end default_config :aws_session_token do |driver| - ENV['AWS_SESSION_TOKEN'] || ENV['AWS_TOKEN'] || iam_creds[:aws_session_token] + driver.default_aws_session_token end default_config :aws_ssh_key_id do |driver| ENV['AWS_SSH_KEY_ID'] @@ -98,10 +99,18 @@ class Ec2 < Kitchen::Driver::SSHBase end end - def self.iam_creds + # First we check the existence of the metadata host. Only fetch_credentials + # if we can find the host. Ping logic taken from + # http://stackoverflow.com/questions/7519159/ruby-ping-pingecho-missing + def iam_creds + require 'net/http' + require 'timeout' @iam_creds ||= begin - fetch_credentials(use_iam_profile:true) - rescue RuntimeError => e + timeout(5) do + Net::HTTP.get(URI.parse('http://169.254.169.254')) + end + fetch_credentials(use_iam_profile: true) + rescue Errno::EHOSTUNREACH, Timeout::Error, NoMethodError, ::StandardError => e debug("fetch_credentials failed with exception #{e.message}:#{e.backtrace.join("\n")}") {} end @@ -167,6 +176,16 @@ def default_public_ip_association !!config[:subnet_id] end + def default_aws_session_token + env = ENV['AWS_SESSION_TOKEN'] || ENV['AWS_TOKEN'] + if config[:aws_secret_access_key] == iam_creds[:aws_secret_access_key] \ + && config[:aws_access_key_id] == iam_creds[:aws_access_key_id] + env || iam_creds[:aws_session_token] + else + env + end + end + private def connection diff --git a/spec/create_spec.rb b/spec/create_spec.rb index fcebf72f..47a38df3 100644 --- a/spec/create_spec.rb +++ b/spec/create_spec.rb @@ -41,14 +41,21 @@ Kitchen::Driver::Ec2.new(config) end + let(:iam_creds) do + { + aws_access_key_id: 'iam_creds_access_key', + aws_secret_access_key: 'iam_creds_secret_access_key', + aws_session_token: 'iam_creds_session_token' + } + end + + context 'Interface is set in config' do before do instance allow(driver).to receive(:create_server).and_return(server) allow(driver).to receive(:wait_for_sshd) end - context 'Interface is set in config' do - it 'derives hostname from DNS when specified in the .kitchen.yml' do config[:interface] = 'dns' driver.create(state) @@ -75,6 +82,11 @@ end context 'Interface is derived automatically' do + before do + instance + allow(driver).to receive(:create_server).and_return(server) + allow(driver).to receive(:wait_for_sshd) + end let(:server) do double(:id => "123", @@ -114,6 +126,11 @@ end context 'user_data implementation is working' do + before do + instance + allow(driver).to receive(:create_server).and_return(server) + allow(driver).to receive(:wait_for_sshd) + end it 'user_data is not defined' do driver.create(state) @@ -128,7 +145,148 @@ end + context 'When #iam_creds returns values' do + let(:config) do + { + aws_ssh_key_id: 'larry', + user_data: nil + } + end + + before do + allow(ENV).to receive(:[]).and_return(nil) + allow(driver).to receive(:iam_creds).and_return(iam_creds) + instance + end + + context 'but they should not be used' do + context 'because :aws_access_key_id is set but not via #iam_creds' do + let(:aws_access_key_id) { 'secret' } + before do + config[:aws_access_key_id] = aws_access_key_id + end + + it 'does not override :aws_access_key_id' do + expect(driver.send(:config)[:aws_access_key_id]).to eq(aws_access_key_id) + end + + it 'does not set :aws_session_token via #iam_creds' do + expect(driver.send(:config)[:aws_session_token]) + .to_not eq(iam_creds[:aws_session_token]) + end + end + + context 'because :aws_secret_access_key is set but not via #iam_creds' do + let(:aws_secret_access_key) { 'moarsecret' } + + before do + config[:aws_secret_access_key] = aws_secret_access_key + end + + it 'does not override :aws_secret_access_key' do + expect(driver.send(:config)[:aws_secret_access_key]).to eq(aws_secret_access_key) + end + + it 'does not set :aws_session_token via #iam_creds' do + expect(driver.send(:config)[:aws_session_token]) + .to_not eq(iam_creds[:aws_session_token]) + end + end + + context 'because :aws_session_token is set but not via #iam_creds' do + let(:aws_session_token) { 'adifferentsessiontoken' } + before do + config[:aws_session_token] = aws_session_token + end + it 'does not override :aws_session_token' do + expect(driver.send(:config)[:aws_session_token]).to eq('adifferentsessiontoken') + end + end + end + + context 'and they should be used' do + let(:config) do + { + aws_ssh_key_id: 'larry', + user_data: nil + } + end + + it 'uses :aws_access_key_id from iam_creds' do + expect(driver.send(:config)[:aws_access_key_id]).to eq(iam_creds[:aws_access_key_id]) + end + + it 'uses :aws_secret_key_id from iam_creds' do + expect(driver.send(:config)[:aws_secret_key_id]).to eq(iam_creds[:aws_secret_key_id]) + end + + it 'uses :aws_session_token from iam_creds' do + expect(driver.send(:config)[:aws_session_token]).to eq(iam_creds[:aws_session_token]) + end + end + end + + describe '#iam_creds' do + context 'when a metadata service is available' do + before do + allow(Net::HTTP).to receive(:get).and_return(true) + end + + context 'and #fetch_credentials returns valid iam credentials' do + it '#iam_creds retuns the iam credentials from fetch_credentials' do + allow(driver).to receive(:fetch_credentials).and_return(iam_creds) + expect(driver.iam_creds).to eq(iam_creds) + end + end + + context 'when #fetch_credentials fails with NoMethodError' do + it 'returns an empty hash' do + allow(driver).to receive(:fetch_credentials).and_raise(NoMethodError) + expect { driver.fetch_credentials }.to raise_error(NoMethodError) + expect(driver.iam_creds).to eq({}) + end + end + + context 'when #fetch_credentials fails with ::StandardError' do + it 'returns an empty hash' do + allow(driver).to receive(:fetch_credentials).and_raise(::StandardError) + expect { driver.fetch_credentials }.to raise_error(::StandardError) + expect(driver.iam_creds).to eq({}) + end + end + + context 'when #fetch_credentials fails with Errno::EHOSTUNREACH' do + it 'returns an empty hash' do + allow(driver).to receive(:fetch_credentials).and_raise(Errno::EHOSTUNREACH) + expect { driver.fetch_credentials }.to raise_error(Errno::EHOSTUNREACH) + expect(driver.iam_creds).to eq({}) + end + end + + context 'when #fetch_credentials fails with Timeout::Error' do + it 'returns an empty hash' do + allow(driver).to receive(:fetch_credentials).and_raise(Timeout::Error) + expect { driver.fetch_credentials }.to raise_error(Timeout::Error) + expect(driver.iam_creds).to eq({}) + end + end + end + + context 'when a metadata service is not available' do + it 'will not call #fetch_credentials' do + allow(Net::HTTP).to receive(:get) + .with(URI.parse('http://169.254.169.254')).and_return(false) + expect(driver).to_not receive(:fetch_credentials) + end + end + end + describe '#block_device_mappings' do + before do + instance + allow(driver).to receive(:create_server).and_return(server) + allow(driver).to receive(:wait_for_sshd) + end let(:connection) { double(Fog::Compute) } let(:image) { double('Image', :root_device_name => 'name') } before do