diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ff099be6fc..5ec94d2115a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Feature = Aws::Core - Support EC2 IMDS updates. + 2.11.400 (2019-11-18) ------------------ diff --git a/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index 605313145f5..5c643beb7b2 100644 --- a/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -11,6 +11,15 @@ class InstanceProfileCredentials # @api private class Non200Response < RuntimeError; end + # @api private + class TokenRetrivalError < RuntimeError; end + + # @api private + class TokenExpiredError < RuntimeError; end + + # @api private + class TokenRetrivalUnavailableError < RuntimeError; end + # These are the errors we trap when attempting to talk to the # instance metadata service. Any of these imply the service # is not present, no responding or some other non-recoverable @@ -26,6 +35,14 @@ class Non200Response < RuntimeError; end Non200Response, ] + # Path base for GET request for profile and credentials + # @api private + METADATA_PATH_BASE = '/latest/meta-data/iam/security-credentials/' + + # Path for PUT request for token + # @api private + METADATA_TOKEN_PATH = '/latest/api/token' + # @param [Hash] options # @option options [Integer] :retries (5) Number of times to retry # when retrieving credentials. @@ -40,6 +57,8 @@ class Non200Response < RuntimeError; end # @option options [IO] :http_debug_output (nil) HTTP wire # traces are sent to this object. You can specify something # like $stdout. + # @option options [Integer] :token_ttl (21600) Time-to-Live in seconds for + # EC2 Metadata Token used for fetching Metadata Profile Credentials. def initialize options = {} @retries = options[:retries] || 5 @ip_address = options[:ip_address] || '169.254.169.254' @@ -48,11 +67,13 @@ def initialize options = {} @http_read_timeout = options[:http_read_timeout] || 5 @http_debug_output = options[:http_debug_output] @backoff = backoff(options[:backoff]) + @token_ttl = options[:token_ttl] || 21600 super end - # @return [Integer] The number of times to retry failed attempts to - # fetch credentials from the instance metadata service. Defaults to 0. + # @return [Integer] Number of times to retry when retrieving credentials + # from the instance metadata service. Defaults to 0 when resolving from + # the default credential chain ({Aws::CredentialProviderChain}). attr_reader :retries private @@ -93,9 +114,11 @@ def get_credentials begin retry_errors(NETWORK_ERRORS, max_retries: @retries) do open_connection do |conn| - path = '/latest/meta-data/iam/security-credentials/' - profile_name = http_get(conn, path).lines.first.strip - http_get(conn, path + profile_name) + _token_attempt(conn) + token_value = @token.value if token_set? + profile_name = http_get(conn, METADATA_PATH_BASE, token_value) + .lines.first.strip + http_get(conn, METADATA_PATH_BASE + profile_name, token_value) end end rescue @@ -104,6 +127,28 @@ def get_credentials end end + def token_set? + @token && !@token.expired? + end + + # attempt to fetch token with retries baked in + # would be skipped if token already set + def _token_attempt(conn) + begin + retry_errors(NETWORK_ERRORS, max_retries: @retries) do + unless token_set? + token_value, ttl = http_put(conn, METADATA_TOKEN_PATH, @token_ttl) + @token = Token.new(token_value, ttl) if token_value && ttl + end + end + rescue *NETWORK_ERRORS, TokenRetrivalUnavailableError + # token attempt failed with allowable errors (those indicating + # token retrieval not available on the instance), reset token to + # allow safe failover to non-token mode + @token = nil + end + end + def _metadata_disabled? flag = ENV["AWS_EC2_METADATA_DISABLED"] !flag.nil? && flag.downcase == "true" @@ -118,10 +163,40 @@ def open_connection yield(http).tap { http.finish } end - def http_get(connection, path) - response = connection.request(Net::HTTP::Get.new(path)) - if response.code.to_i == 200 + # GET request fetch profile and credentials + def http_get(connection, path, token=nil) + headers = {"User-Agent" => "aws-sdk-ruby2/#{VERSION}"} + headers["x-aws-ec2-metadata-token"] = token if token + response = connection.request(Net::HTTP::Get.new(path, headers)) + case response.code.to_i + when 200 response.body + when 401 + raise TokenExpiredError + else + raise Non200Response + end + end + + # PUT request fetch token with ttl + def http_put(connection, path, ttl) + headers = { + "User-Agent" => "aws-sdk-ruby2/#{VERSION}", + "x-aws-ec2-metadata-token-ttl-seconds" => ttl.to_s + } + response = connection.request(Net::HTTP::Put.new(path, headers)) + case response.code.to_i + when 200 + [ + response.body, + response.header["x-aws-ec2-metadata-token-ttl-seconds"].to_i + ] + when 400 + raise TokenRetrivalError + when 403 + when 404 + when 405 + raise TokenRetrivalUnavailableError else raise Non200Response end @@ -143,5 +218,24 @@ def retry_errors(error_classes, options = {}, &block) end end + # @api private + # Token used to fetch IMDS profile and credentials + class Token + + def initialize(value, ttl) + @ttl = ttl + @value = value + @created_time = Time.now + end + + # [String] token value + attr_reader :value + + def expired? + Time.now - @created_time > @ttl + end + + end + end end diff --git a/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb b/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb index 13df41213c5..5b0bb0bcf08 100644 --- a/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb +++ b/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb @@ -13,6 +13,14 @@ module Aws '..', 'fixtures', 'credentials', 'mock_shared_config')) } + let(:imds_url) { + 'http://169.254.169.254/latest/meta-data/iam/security-credentials/' + } + + let(:imds_token_url) { + 'http://169.254.169.254/latest/api/token' + } + describe "default behavior" do before(:each) do stub_const('ENV', {}) @@ -56,28 +64,32 @@ module Aws "AR_TOKEN" ) client = Aws::S3::Client.new(profile: "ar_plus_creds", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("AR_AKID") + expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID") end it 'prefers shared credential file static credentials over shared config' do client = Aws::S3::Client.new(profile: "credentials_first", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("ACCESS_KEY_CRD") + expect(client.config.credentials.credentials.access_key_id).to eq("ACCESS_KEY_CRD") end it 'will source static credentials from shared config after shared credentials' do client = Aws::S3::Client.new(profile: "incomplete_cred", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("ACCESS_KEY_SC1") + expect(client.config.credentials.credentials.access_key_id).to eq("ACCESS_KEY_SC1") end it 'attempts to fetch metadata credentials last' do - stub_request( - :get, - "http://169.254.169.254/latest/meta-data/iam/security-credentials/" - ).to_return(:status => 200, :body => "profile-name\n") - stub_request( - :get, - "http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name" - ).to_return(:status => 200, :body => <<-JSON.strip) + stub_request(:put, imds_token_url) + .to_return( + :status => 200, + :body => "my-token\n", + :headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"} + ) + stub_request(:get, imds_url) + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "#{imds_url}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => <<-JSON.strip) { "Code" : "Success", "LastUpdated" : "2013-11-22T20:03:48Z", @@ -89,11 +101,11 @@ module Aws } JSON client = Aws::S3::Client.new(profile: "nonexistant", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("akid-md") + expect(client.config.credentials.credentials.access_key_id).to eq("akid-md") end describe 'Assume Role Resolution' do - it 'will not assume a role without source_profile present' do + it 'will not assume a role without a source present' do expect { Aws::S3::Client.new(profile: "ar_no_src", region: "us-east-1") }.to raise_error(Errors::NoSourceProfileError) @@ -114,7 +126,7 @@ module Aws "AR_TOKEN" ) client = Aws::S3::Client.new(profile: "assumerole_sc", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("AR_AKID") + expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID") end it 'will then try to assume a role from shared config' do @@ -126,7 +138,7 @@ module Aws "AR_TOKEN" ) client = Aws::S3::Client.new(profile: "ar_from_self", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("AR_AKID") + expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID") end it 'will assume a role from config using source credentials in shared credentials' do @@ -138,9 +150,10 @@ module Aws "AR_TOKEN" ) client = Aws::S3::Client.new(profile: "creds_from_sc", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("AR_AKID") + expect(client.config.credentials.credentials.access_key_id).to eq("AR_AKID") end end + end describe "AWS_SDK_CONFIG_OPT_OUT set" do @@ -165,7 +178,7 @@ module Aws profile: "fooprofile", region: "us-east-1" ) - expect(client.config.credentials.access_key_id).to eq("ACCESS_DIRECT") + expect(client.config.credentials.credentials.access_key_id).to eq("ACCESS_DIRECT") end it 'prefers ENV credentials over shared config' do @@ -174,7 +187,7 @@ module Aws "AWS_SECRET_ACCESS_KEY" => "SECRET_ENV_STUB" }) client = Aws::S3::Client.new(profile: "fooprofile", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("AKID_ENV_STUB") + expect(client.config.credentials.credentials.access_key_id).to eq("AKID_ENV_STUB") end it 'will not load credentials from shared config' do @@ -188,14 +201,18 @@ module Aws end it 'attempts to fetch metadata credentials last' do - stub_request( - :get, - "http://169.254.169.254/latest/meta-data/iam/security-credentials/" - ).to_return(:status => 200, :body => "profile-name\n") - stub_request( - :get, - "http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name" - ).to_return(:status => 200, :body => <<-JSON.strip) + stub_request(:put, imds_token_url) + .to_return( + :status => 200, + :body => "my-token\n", + :headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"} + ) + stub_request(:get, imds_url) + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "#{imds_url}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => <<-JSON.strip) { "Code" : "Success", "LastUpdated" : "2013-11-22T20:03:48Z", @@ -207,7 +224,7 @@ module Aws } JSON client = Aws::S3::Client.new(profile: "nonexistant", region: "us-east-1") - expect(client.config.credentials.access_key_id).to eq("akid-md") + expect(client.config.credentials.credentials.access_key_id).to eq("akid-md") end end diff --git a/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb b/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb index 9667543654d..7a97d304fbe 100644 --- a/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb +++ b/aws-sdk-core/spec/aws/instance_profile_credentials_spec.rb @@ -5,7 +5,9 @@ module Aws let(:path) { '/latest/meta-data/iam/security-credentials/' } - describe 'without instance metadata service present' do + let(:token_path) { '/latest/api/token' } + + context 'without instance metadata service present' do [ Errno::EHOSTUNREACH, @@ -14,14 +16,108 @@ module Aws Timeout::Error, ].each do |error_class| it "returns no credentials for #{error_class}" do - stub_request(:get, "http://169.254.169.254#{path}").to_raise(error_class) + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return(:status => 200, :body => "mytoken") + stub_request(:get, "http://169.254.169.254#{path}") + .to_raise(error_class) expect(InstanceProfileCredentials.new(backoff:0).set?).to be(false) end end + it "returns no credentials for 400 when fetching token" do + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return(:status => 400) + stub_request(:get, "http://169.254.169.254#{path}") + .to_return(:status => 200) + expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) + end + + it "returns no credentials for 401 when fetching credentials" do + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return( + :status => 200, + :body => "my-token\n", + :headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"} + ) + stub_request(:get, "http://169.254.169.254#{path}") + .to_return(:status => 401) + expect(InstanceProfileCredentials.new(backoff: 0).set?).to be(false) + end + + end + + context 'can fail over to insecure flow' do + + let(:expiration) { Time.now.utc + 3600 } + + let(:resp) { <<-JSON.strip } +{ + "Code" : "Success", + "LastUpdated" : "2013-11-22T20:03:48Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "akid", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" +} + JSON + + [ + 403, + 404, + 405 + ].each do |error_code| + it "fails over to insecure flow for error code #{error_code}" do + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return(:status => error_code) + .to_return(:status => 400) # will error if retried + stub_request(:get, "http://169.254.169.254#{path}") + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .to_return(:status => 200, :body => resp) + c = InstanceProfileCredentials.new(backoff:0) + expect(c.credentials.access_key_id).to eq('akid') + expect(c.credentials.secret_access_key).to eq('secret') + expect(c.credentials.session_token).to eq('session-token') + end + end + + [ + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED, + SocketError, + Timeout::Error, + ].each do |error_class| + it "fails over to insecure flow for #{error_class}" do + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_raise(error_class) + stub_request(:get, "http://169.254.169.254#{path}") + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .to_return(:status => 200, :body => resp) + c = InstanceProfileCredentials.new(backoff:0) + expect(c.credentials.access_key_id).to eq('akid') + expect(c.credentials.secret_access_key).to eq('secret') + expect(c.credentials.session_token).to eq('session-token') + end + end + + it "fails over when token response incomplete" do + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return(:status => 200) + stub_request(:get, "http://169.254.169.254#{path}") + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .to_return(:status => 200, :body => resp) + c = InstanceProfileCredentials.new(backoff:0) + expect(c.credentials.access_key_id).to eq('akid') + expect(c.credentials.secret_access_key).to eq('secret') + expect(c.credentials.session_token).to eq('session-token') + end + end - describe 'disable flag' do + context 'disable flag' do let(:env) {{}} before(:each) do @@ -38,7 +134,39 @@ module Aws expect(InstanceProfileCredentials.new.set?).to be(false) end - it 'ignores values other than true for the disable flag' do + it 'ignores values other than true for the disable flag secure' do + env["AWS_EC2_METADATA_DISABLED"] = "1" + expiration = Time.now.utc + 3600 + resp = <<-JSON.strip +{ + "Code" : "Success", + "LastUpdated" : "2013-11-22T20:03:48Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "akid", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" +} + JSON + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return( + :status => 200, + :body => "my-token\n", + :headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"} + ) + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => resp) + c = InstanceProfileCredentials.new(backoff:0) + expect(c.credentials.access_key_id).to eq('akid') + expect(c.credentials.secret_access_key).to eq('secret') + expect(c.credentials.session_token).to eq('session-token') + end + + it 'ignores values other than true for the disable flag insecure' do env["AWS_EC2_METADATA_DISABLED"] = "1" expiration = Time.now.utc + 3600 resp = <<-JSON.strip @@ -52,10 +180,12 @@ module Aws "Expiration" : "#{expiration.strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_request(:get, "http://169.254.169.254#{path}"). - to_return(:status => 200, :body => "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => resp) + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return(:status => 404) + stub_request(:get, "http://169.254.169.254#{path}") + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .to_return(:status => 200, :body => resp) c = InstanceProfileCredentials.new(backoff:0) expect(c.credentials.access_key_id).to eq('akid') expect(c.credentials.secret_access_key).to eq('secret') @@ -63,7 +193,7 @@ module Aws end end - describe 'with instance metadata service present' do + context 'with instance metadata service present' do let(:expiration) { Time.now.utc + 3600 } let(:expiration2) { expiration + 3600 } @@ -93,11 +223,19 @@ module Aws JSON before(:each) do - stub_request(:get, "http://169.254.169.254#{path}"). - to_return(:status => 200, :body => "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => resp). - to_return(:status => 200, :body => resp2) + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return( + :status => 200, + :body => "my-token\n", + :headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"} + ) + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => resp) + .to_return(:status => 200, :body => resp2) end it 'populates credentials from the instance profile' do @@ -118,11 +256,13 @@ module Aws end it 'retries if the first load fails' do - stub_request(:get, "http://169.254.169.254#{path}"). - to_return(:status => 500). - to_return(:status => 200, :body => "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => resp2) + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 500) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => resp2) c = InstanceProfileCredentials.new(backoff:0) expect(c.credentials.access_key_id).to eq('akid-2') expect(c.credentials.secret_access_key).to eq('secret-2') @@ -131,14 +271,16 @@ module Aws end it 'retries if get profile response is invalid JSON' do - stub_request(:get, "http://169.254.169.254#{path}"). - to_return(:status => 500). - to_return(:status => 200, :body => "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => ' '). - to_return(:status => 200, :body => ''). - to_return(:status => 200, :body => '{'). - to_return(:status => 200, :body => resp2) + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 500) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => ' ') + .to_return(:status => 200, :body => '') + .to_return(:status => 200, :body => '{') + .to_return(:status => 200, :body => resp2) c = InstanceProfileCredentials.new(backoff:0) expect(c.credentials.access_key_id).to eq('akid-2') expect(c.credentials.secret_access_key).to eq('secret-2') @@ -147,14 +289,16 @@ module Aws end it 'retries invalid JSON exactly 3 times' do - stub_request(:get, "http://169.254.169.254#{path}"). - to_return(:status => 500). - to_return(:status => 200, :body => "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => ''). - to_return(:status => 200, :body => ' '). - to_return(:status => 200, :body => '{'). - to_return(:status => 200, :body => ' ') + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 500) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => '') + .to_return(:status => 200, :body => ' ') + .to_return(:status => 200, :body => '{') + .to_return(:status => 200, :body => ' ') expect { InstanceProfileCredentials.new(backoff:0) }.to raise_error( @@ -164,20 +308,22 @@ module Aws end it 'retries errors parsing expiration time 3 times' do - stub_request(:get, "http://169.254.169.254#{path}"). - to_return(:status => 500). - to_return(:status => 200, :body => "profile-name\n") - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => '{ "Expiration": "Expiration" }'). - to_return(:status => 200, :body => '{ "Expiration": "Expiration" }'). - to_return(:status => 200, :body => '{ "Expiration": "Expiration" }'). - to_return(:status => 200, :body => '{ "Expiration": "Expiration" }') + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 500) + .to_return(:status => 200, :body => "profile-name\n") + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => '{ "Expiration": "Expiration" }') + .to_return(:status => 200, :body => '{ "Expiration": "Expiration" }') + .to_return(:status => 200, :body => '{ "Expiration": "Expiration" }') + .to_return(:status => 200, :body => '{ "Expiration": "Expiration" }') expect { InstanceProfileCredentials.new(backoff:0) }.to raise_error(ArgumentError) end - describe 'auto refreshing' do + context 'auto refreshing' do # expire in 4 minutes let(:expiration) { Time.now.utc + 299 } @@ -192,15 +338,16 @@ module Aws end - describe 'failure cases' do + context 'failure cases' do let(:resp) { '{}' } it 'given an empty response, entry credentials are returned' do # This handles the case when the service response but returns # a JSON document without credentials (error cases) - stub_request(:get, "http://169.254.169.254#{path}profile-name"). - to_return(:status => 200, :body => resp) + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_return(:status => 200, :body => resp) c = InstanceProfileCredentials.new expect(c.set?).to be(false) expect(c.credentials.access_key_id).to be(nil) @@ -215,13 +362,28 @@ module Aws describe '#retries' do + before(:each) do + stub_request(:put, "http://169.254.169.254#{token_path}") + .to_return( + :status => 200, + :body => "my-token\n", + :headers => {"x-aws-ec2-metadata-token-ttl-seconds" => "21600"} + ) + stub_request(:get, "http://169.254.169.254#{path}") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_raise(Errno::ECONNREFUSED) + stub_request(:get, "http://169.254.169.254#{path}profile-name") + .with(:headers => {"x-aws-ec2-metadata-token" => "my-token"}) + .to_raise(Errno::ECONNREFUSED) + end + it 'defaults to 0' do expect(InstanceProfileCredentials.new(backoff:0).retries).to be(5) end it 'keeps trying "retries" times, with exponential backoff' do - expected_request = stub_request(:get, "http://169.254.169.254#{path}"). - to_raise(Errno::ECONNREFUSED) + expected_request = stub_request(:get, "http://169.254.169.254#{path}") + .to_raise(Errno::ECONNREFUSED) expect(Kernel).to receive(:sleep).with(1) expect(Kernel).to receive(:sleep).with(2) expect(Kernel).to receive(:sleep).with(4) @@ -233,5 +395,6 @@ module Aws end end + end end diff --git a/aws-sdk-core/spec/spec_helper.rb b/aws-sdk-core/spec/spec_helper.rb index 630cf7ab4e0..1632727d02b 100644 --- a/aws-sdk-core/spec/spec_helper.rb +++ b/aws-sdk-core/spec/spec_helper.rb @@ -23,8 +23,10 @@ allow(Dir).to receive(:home).and_raise(ArgumentError) # disable instance profile credentials - ec2_md_path = '/latest/meta-data/iam/security-credentials/' - stub_request(:get, "http://169.254.169.254#{ec2_md_path}").to_raise(SocketError) + token_path = '/latest/api/token' + path = '/latest/meta-data/iam/security-credentials/' + stub_request(:get, "http://169.254.169.254#{path}").to_raise(SocketError) + stub_request(:put, "http://169.254.169.254#{token_path}").to_raise(SocketError) Aws.shared_config.fresh end