Repository: boto/botocore
Pull request #1160 (2017-03-08 00:41:48)

Generalize container credential provider
You can now provide a full url in addition to a relative url to retrieve credentials.
I've added hostname validation to only allow the ECS IP and localhost.
Anything else results in an exception being raised.


cc @kyleknap @JordonPhillips @stealthycoin @dstufft 
cc @jasdel

codecov-io: # [Codecov](https://codecov.io/gh/boto/botocore/pull/1160?src=pr&el=h1) Report
> Merging [#1160](https://codecov.io/gh/boto/botocore/pull/1160?src=pr&el=desc) into [develop](https://codecov.io/gh/boto/botocore/commit/358efbbf9860a35e7dab9407f6f5165d04494cb2?src=pr&el=desc) will **decrease** coverage by `-0.01%`.
> The diff coverage is `97.43%`.


```diff
@@             Coverage Diff             @@
##           develop    #1160      +/-   ##
===========================================
- Coverage    97.95%   97.95%   -0.01%     
===========================================
  Files           45       45              
  Lines         7093     7122      +29     
===========================================
+ Hits          6948     6976      +28     
- Misses         145      146       +1
```


| [Impacted Files](https://codecov.io/gh/boto/botocore/pull/1160?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [botocore/credentials.py](https://codecov.io/gh/boto/botocore/compare/358efbbf9860a35e7dab9407f6f5165d04494cb2...8415138e391f9a1a77262920e05e89784ff090e8?src=pr&el=tree#diff-Ym90b2NvcmUvY3JlZGVudGlhbHMucHk=) | `97.6% <100%> (+0.05%)` | :white_check_mark: |
| [botocore/utils.py](https://codecov.io/gh/boto/botocore/compare/358efbbf9860a35e7dab9407f6f5165d04494cb2...8415138e391f9a1a77262920e05e89784ff090e8?src=pr&el=tree#diff-Ym90b2NvcmUvdXRpbHMucHk=) | `95.74% <94.73%> (-0.06%)` | :x: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/boto/botocore/pull/1160?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/boto/botocore/pull/1160?src=pr&el=footer). Last update [358efbb...8415138](https://codecov.io/gh/boto/botocore/compare/358efbbf9860a35e7dab9407f6f5165d04494cb2...8415138e391f9a1a77262920e05e89784ff090e8?src=pr&el=footer&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Patch:
diff --git a/.changes/1.5.21.json b/.changes/1.5.21.json
deleted file mode 100644
index 69d4bd18c..000000000
--- a/.changes/1.5.21.json
+++ /dev/null
@@ -1,7 +0,0 @@
-[
-  {
-    "category": "``rds``", 
-    "description": "Update rds client to latest version", 
-    "type": "api-change"
-  }
-]
\ No newline at end of file
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dc04fad2f..7e8a01cf9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,12 +2,6 @@
 CHANGELOG
 =========
 
-1.5.21
-======
-
-* api-change:``rds``: Update rds client to latest version
-
-
 1.5.20
 ======
 
diff --git a/botocore/__init__.py b/botocore/__init__.py
index d0d769960..7c3ecc48f 100644
--- a/botocore/__init__.py
+++ b/botocore/__init__.py
@@ -16,7 +16,7 @@ import os
 import re
 import logging
 
-__version__ = '1.5.21'
+__version__ = '1.5.20'
 
 
 class NullHandler(logging.Handler):
diff --git a/botocore/credentials.py b/botocore/credentials.py
index 1572e7016..d5a2da5a2 100644
--- a/botocore/credentials.py
+++ b/botocore/credentials.py
@@ -945,6 +945,8 @@ class ContainerProvider(CredentialProvider):
 
     METHOD = 'container-role'
     ENV_VAR = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'
+    ENV_VAR_FULL = 'AWS_CONTAINER_CREDENTIALS_FULL_URI'
+    ENV_VAR_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN'
 
     def __init__(self, environ=None, fetcher=None):
         if environ is None:
@@ -955,15 +957,18 @@ class ContainerProvider(CredentialProvider):
         self._fetcher = fetcher
 
     def load(self):
-        if self.ENV_VAR not in self._environ:
-            # This cred provider is only triggered if the
-            # self.ENV_VAR is set, which only happens if you opt
-            # into this feature on ECS.
-            return None
-        return self._retrieve_or_fail(self._environ[self.ENV_VAR])
-
-    def _retrieve_or_fail(self, relative_uri):
-        fetcher = self._create_fetcher(relative_uri)
+        # This cred provider is only triggered if the self.ENV_VAR is set,
+        # which only happens if you opt into this feature.
+        if self.ENV_VAR in self._environ or self.ENV_VAR_FULL in self._environ:
+            return self._retrieve_or_fail()
+
+    def _retrieve_or_fail(self):
+        if self._provided_relative_uri():
+            full_uri = self._fetcher.full_url(self._environ[self.ENV_VAR])
+        else:
+            full_uri = self._environ[self.ENV_VAR_FULL]
+        headers = self._build_headers()
+        fetcher = self._create_fetcher(full_uri, headers)
         creds = fetcher()
         return RefreshableCredentials(
             access_key=creds['access_key'],
@@ -974,12 +979,21 @@ class ContainerProvider(CredentialProvider):
             refresh_using=fetcher,
         )
 
-    def _create_fetcher(self, relative_uri):
+    def _build_headers(self):
+        headers = {}
+        auth_token = self._environ.get(self.ENV_VAR_AUTH_TOKEN)
+        if auth_token is not None:
+            return {
+                'Authorization': auth_token
+            }
+
+    def _create_fetcher(self, full_uri, headers):
         def fetch_creds():
             try:
-                response = self._fetcher.retrieve_uri(relative_uri)
+                response = self._fetcher.retrieve_full_uri(
+                    full_uri, headers=headers)
             except MetadataRetrievalError as e:
-                logger.debug("Error retrieving ECS metadata: %s", e,
+                logger.debug("Error retrieving container metadata: %s", e,
                              exc_info=True)
                 raise CredentialRetrievalError(provider=self.METHOD,
                                                error_msg=str(e))
@@ -991,6 +1005,9 @@ class ContainerProvider(CredentialProvider):
             }
         return fetch_creds
 
+    def _provided_relative_uri(self):
+        return self.ENV_VAR in self._environ
+
 
 class CredentialResolver(object):
 
diff --git a/botocore/data/endpoints.json b/botocore/data/endpoints.json
index 2b008cc2e..ecff7d5cf 100644
--- a/botocore/data/endpoints.json
+++ b/botocore/data/endpoints.json
@@ -795,7 +795,6 @@
             "ap-southeast-2": {},
             "eu-central-1": {},
             "eu-west-1": {},
-            "eu-west-2": {},
             "us-east-1": {},
             "us-east-2": {},
             "us-west-2": {}
@@ -978,18 +977,6 @@
             "us-west-2": {}
           }
         },
-        "organizations": {
-          "endpoints": {
-            "aws-global": {
-              "credentialScope": {
-                "region": "us-east-1"
-              },
-              "hostname": "organizations.us-east-1.amazonaws.com"
-            }
-          },
-          "isRegionalized": false,
-          "partitionEndpoint": "aws-global"
-        },
         "pinpoint": {
           "defaults": {
             "credentialScope": {
diff --git a/botocore/data/rds/2014-10-31/service-2.json b/botocore/data/rds/2014-10-31/service-2.json
old mode 100644
new mode 100755
index 6a732fdf0..eac784250
--- a/botocore/data/rds/2014-10-31/service-2.json
+++ b/botocore/data/rds/2014-10-31/service-2.json
@@ -129,7 +129,7 @@
         {"shape":"SnapshotQuotaExceededFault"},
         {"shape":"KMSKeyNotAccessibleFault"}
       ],
-      "documentation":"<p>Copies a snapshot of a DB cluster.</p> <p>To copy a DB cluster snapshot from a shared manual DB cluster snapshot, <code>SourceDBClusterSnapshotIdentifier</code> must be the Amazon Resource Name (ARN) of the shared DB cluster snapshot.</p> <p>You can copy an encrypted DB cluster snapshot from another AWS region. In that case, the region where you call the <code>CopyDBClusterSnapshot</code> action is the destination region for the encrypted DB cluster snapshot to be copied to. To copy an encrypted DB cluster snapshot from another region, you must provide the following values:</p> <ul> <li> <p> <code>KmsKeyId</code> - The AWS Key Management System (KMS) key identifier for the key to use to encrypt the copy of the DB cluster snapshot in the destination region.</p> </li> <li> <p> <code>PreSignedUrl</code> - A URL that contains a Signature Version 4 signed request for the <code>CopyDBClusterSnapshot</code> action to be called in the source region where the DB cluster snapshot will be copied from. The pre-signed URL must be a valid request for the <code>CopyDBClusterSnapshot</code> API action that can be executed in the source region that contains the encrypted DB cluster snapshot to be copied.</p> <p>The pre-signed URL request must contain the following parameter values:</p> <ul> <li> <p> <code>KmsKeyId</code> - The KMS key identifier for the key to use to encrypt the copy of the DB cluster snapshot in the destination region. This is the same identifier for both the <code>CopyDBClusterSnapshot</code> action that is called in the destination region, and the action contained in the pre-signed URL.</p> </li> <li> <p> <code>DestinationRegion</code> - The name of the region that the DB cluster snapshot will be created in.</p> </li> <li> <p> <code>SourceDBClusterSnapshotIdentifier</code> - The DB cluster snapshot identifier for the encrypted DB cluster snapshot to be copied. This identifier must be in the Amazon Resource Name (ARN) format for the source region. For example, if you are copying an encrypted DB cluster snapshot from the us-west-2 region, then your <code>SourceDBClusterSnapshotIdentifier</code> looks like the following example: <code>arn:aws:rds:us-west-2:123456789012:cluster-snapshot:aurora-cluster1-snapshot-20161115</code>.</p> </li> </ul> <p>To learn how to generate a Signature Version 4 signed request, see <a href=\"http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html\"> Authenticating Requests: Using Query Parameters (AWS Signature Version 4)</a> and <a href=\"http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html\"> Signature Version 4 Signing Process</a>.</p> </li> <li> <p> <code>TargetDBClusterSnapshotIdentifier</code> - The identifier for the new copy of the DB cluster snapshot in the destination region.</p> </li> <li> <p> <code>SourceDBClusterSnapshotIdentifier</code> - The DB cluster snapshot identifier for the encrypted DB cluster snapshot to be copied. This identifier must be in the ARN format for the source region and is the same value as the <code>SourceDBClusterSnapshotIdentifier</code> in the pre-signed URL. </p> </li> </ul> <p>To cancel the copy operation once it is in progress, delete the target DB cluster snapshot identified by <code>TargetDBClusterSnapshotIdentifier</code> while that DB cluster snapshot is in \"copying\" status.</p> <p>For more information on copying encrypted DB cluster snapshots from one region to another, see <a href=\"http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CopySnapshot.html#USER_CopyDBClusterSnapshot.CrossRegion\"> Copying a DB Cluster Snapshot in the Same Account, Either in the Same Region or Across Regions</a> in the Amazon RDS User Guide.</p> <p>For more information on Amazon Aurora, see <a href=\"http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Aurora.html\">Aurora on Amazon RDS</a> in the <i>Amazon RDS User Guide.</i> </p>"
+      "documentation":"<p>Creates a snapshot of a DB cluster. For more information on Amazon Aurora, see <a href=\"http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Aurora.html\">Aurora on Amazon RDS</a> in the <i>Amazon RDS User Guide.</i> </p>"
     },
     "CopyDBParameterGroup":{
       "name":"CopyDBParameterGroup",
@@ -215,7 +215,7 @@
         {"shape":"DBInstanceNotFoundFault"},
         {"shape":"DBSubnetGroupDoesNotCoverEnoughAZs"}
       ],
-      "documentation":"<p>Creates a new Amazon Aurora DB cluster.</p> <p>You can use the <code>ReplicationSourceIdentifier</code> parameter to create the DB cluster as a Read Replica of another DB cluster or Amazon RDS MySQL DB instance. For cross-region replication where the DB cluster identified by <code>ReplicationSourceIdentifier</code> is encrypted, you must also specify the <code>PreSignedUrl</code> parameter.</p> <p>For more information on Amazon Aurora, see <a href=\"http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Aurora.html\">Aurora on Amazon RDS</a> in the <i>Amazon RDS User Guide.</i> </p>"
+      "documentation":"<p>Creates a new Amazon Aurora DB cluster.</p> <p>You can use the <code>ReplicationSourceIdentifier</code> parameter to create the DB cluster as a Read Replica of another DB cluster or Amazon RDS MySQL DB instance.</p> <p>For more information on Amazon Aurora, see <a href=\"http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Aurora.html\">Aurora on Amazon RDS</a> in the <i>Amazon RDS User Guide.</i> </p>"
     },
     "CreateDBClusterParameterGroup":{
       "name":"CreateDBClusterParameterGroup",
@@ -1948,15 +1948,6 @@
           "shape":"String",
           "documentation":"<p>The identifier of the new DB cluster snapshot to create from the source DB cluster snapshot. This parameter is not case-sensitive.</p> <p>Constraints:</p> <ul> <li> <p>Must contain from 1 to 63 alphanumeric characters or hyphens.</p> </li> <li> <p>First character must be a letter.</p> </li> <li> <p>Cannot end with a hyphen or contain two consecutive hyphens.</p> </li> </ul> <p>Example: <code>my-cluster-snapshot2</code> </p>"
         },
-        "KmsKeyId":{
-          "shape":"String",
-          "documentation":"<p>The AWS KMS key ID for an encrypted DB cluster snapshot. The KMS key ID is the Amazon Resource Name (ARN), KMS key identifier, or the KMS key alias for the KMS encryption key. </p> <p>If you copy an unencrypted DB cluster snapshot and specify a value for the <code>KmsKeyId</code> parameter, Amazon RDS encrypts the target DB cluster snapshot using the specified KMS encryption key. </p> <p>If you copy an encrypted DB cluster snapshot from your AWS account, you can specify a value for <code>KmsKeyId</code> to encrypt the copy with a new KMS encryption key. If you don't specify a value for <code>KmsKeyId</code>, then the copy of the DB cluster snapshot is encrypted with the same KMS key as the source DB cluster snapshot. </p> <p>If you copy an encrypted DB cluster snapshot that is shared from another AWS account, then you must specify a value for <code>KmsKeyId</code>. </p> <p>To copy an encrypted DB cluster snapshot to another region, you must set <code>KmsKeyId</code> to the KMS key ID you want to use to encrypt the copy of the DB cluster snapshot in the destination region. KMS encryption keys are specific to the region that they are created in, and you cannot use encryption keys from one region in another region.</p>"
-        },
-        "PreSignedUrl":{
-          "shape":"String",
-          "documentation":"<p>The URL that contains a Signature Version 4 signed request for the <code>CopyDBClusterSnapshot</code> API action in the AWS region that contains the source DB cluster snapshot to copy. The <code>PreSignedUrl</code> parameter must be used when copying an encrypted DB cluster snapshot from another AWS region.</p> <p>The pre-signed URL must be a valid request for the <code>CopyDBSClusterSnapshot</code> API action that can be executed in the source region that contains the encrypted DB cluster snapshot to be copied. The pre-signed URL request must contain the following parameter values:</p> <ul> <li> <p> <code>KmsKeyId</code> - The KMS key identifier for the key to use to encrypt the copy of the DB cluster snapshot in the destination region. This is the same identifier for both the <code>CopyDBClusterSnapshot</code> action that is called in the destination region, and the action contained in the pre-signed URL.</p> </li> <li> <p> <code>DestinationRegion</code> - The name of the region that the DB cluster snapshot will be created in.</p> </li> <li> <p> <code>SourceDBClusterSnapshotIdentifier</code> - The DB cluster snapshot identifier for the encrypted DB cluster snapshot to be copied. This identifier must be in the Amazon Resource Name (ARN) format for the source region. For example, if you are copying an encrypted DB cluster snapshot from the us-west-2 region, then your <code>SourceDBClusterSnapshotIdentifier</code> looks like the following example: <code>arn:aws:rds:us-west-2:123456789012:cluster-snapshot:aurora-cluster1-snapshot-20161115</code>.</p> </li> </ul> <p>To learn how to generate a Signature Version 4 signed request, see <a href=\"http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html\"> Authenticating Requests: Using Query Parameters (AWS Signature Version 4)</a> and <a href=\"http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html\"> Signature Version 4 Signing Process</a>.</p>"
-        },
-        "CopyTags":{"shape":"BooleanOptional"},
         "Tags":{"shape":"TagList"}
       },
       "documentation":"<p/>"
@@ -2146,11 +2137,7 @@
         },
         "KmsKeyId":{
           "shape":"String",
-          "documentation":"<p>The KMS key identifier for an encrypted DB cluster.</p> <p>The KMS key identifier is the Amazon Resource Name (ARN) for the KMS encryption key. If you are creating a DB cluster with the same AWS account that owns the KMS encryption key used to encrypt the new DB cluster, then you can use the KMS key alias instead of the ARN for the KMS encryption key.</p> <p>If the <code>StorageEncrypted</code> parameter is true, and you do not specify a value for the <code>KmsKeyId</code> parameter, then Amazon RDS will use your default encryption key. AWS KMS creates the default encryption key for your AWS account. Your AWS account has a different default encryption key for each AWS region.</p> <p>If you create a Read Replica of an encrypted DB cluster in another region, you must set <code>KmsKeyId</code> to a KMS key ID that is valid in the destination region. This key is used to encrypt the Read Replica in that region.</p>"
-        },
-        "PreSignedUrl":{
-          "shape":"String",
-          "documentation":"<p>A URL that contains a Signature Version 4 signed request for the <code>CreateDBCluster</code> action to be called in the source region where the DB cluster will be replicated from. You only need to specify <code>PreSignedUrl</code> when you are performing cross-region replication from an encrypted DB cluster.</p> <p>The pre-signed URL must be a valid request for the <code>CreateDBCluster</code> API action that can be executed in the source region that contains the encrypted DB cluster to be copied.</p> <p>The pre-signed URL request must contain the following parameter values:</p> <ul> <li> <p> <code>KmsKeyId</code> - The KMS key identifier for the key to use to encrypt the copy of the DB cluster in the destination region. This should refer to the same KMS key for both the <code>CreateDBCluster</code> action that is called in the destination region, and the action contained in the pre-signed URL.</p> </li> <li> <p> <code>DestinationRegion</code> - The name of the region that Aurora Read Replica will be created in.</p> </li> <li> <p> <code>ReplicationSourceIdentifier</code> - The DB cluster identifier for the encrypted DB cluster to be copied. This identifier must be in the Amazon Resource Name (ARN) format for the source region. For example, if you are copying an encrypted DB cluster from the us-west-2 region, then your <code>ReplicationSourceIdentifier</code> would look like Example: <code>arn:aws:rds:us-west-2:123456789012:cluster:aurora-cluster1</code>.</p> </li> </ul> <p>To learn how to generate a Signature Version 4 signed request, see <a href=\"http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html\"> Authenticating Requests: Using Query Parameters (AWS Signature Version 4)</a> and <a href=\"http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html\"> Signature Version 4 Signing Process</a>.</p>"
+          "documentation":"<p>The KMS key identifier for an encrypted DB cluster.</p> <p>The KMS key identifier is the Amazon Resource Name (ARN) for the KMS encryption key. If you are creating a DB cluster with the same AWS account that owns the KMS encryption key used to encrypt the new DB cluster, then you can use the KMS key alias instead of the ARN for the KM encryption key.</p> <p>If the <code>StorageEncrypted</code> parameter is true, and you do not specify a value for the <code>KmsKeyId</code> parameter, then Amazon RDS will use your default encryption key. AWS KMS creates the default encryption key for your AWS account. Your AWS account has a different default encryption key for each AWS region.</p>"
         }
       },
       "documentation":"<p/>"
diff --git a/botocore/data/rds/2014-10-31/service-2.sdk-extras.json b/botocore/data/rds/2014-10-31/service-2.sdk-extras.json
index ab6bfcd3b..fe3a6dabc 100644
--- a/botocore/data/rds/2014-10-31/service-2.sdk-extras.json
+++ b/botocore/data/rds/2014-10-31/service-2.sdk-extras.json
@@ -2,22 +2,6 @@
   "version": 1.0,
   "merge": {
     "shapes": {
-      "CopyDBClusterSnapshotMessage": {
-        "members": {
-          "SourceRegion": {
-            "shape": "String",
-            "documentation": "<p>The ID of the region that contains the snapshot to be copied.</p>"
-          }
-        }
-      },
-      "CreateDBClusterMessage": {
-        "members": {
-          "SourceRegion": {
-            "shape": "String",
-            "documentation": "<p>The ID of the region that contains the source for the db cluster.</p>"
-          }
-        }
-      },
       "CopyDBSnapshotMessage": {
         "members": {
           "SourceRegion": {
diff --git a/botocore/handlers.py b/botocore/handlers.py
index 318dea765..edd240b50 100644
--- a/botocore/handlers.py
+++ b/botocore/handlers.py
@@ -839,10 +839,6 @@ BUILTIN_HANDLERS = [
     ('before-call.glacier.UploadArchive', add_glacier_checksums),
     ('before-call.glacier.UploadMultipartPart', add_glacier_checksums),
     ('before-call.ec2.CopySnapshot', inject_presigned_url_ec2),
-    ('before-call.rds.CopyDBClusterSnapshot',
-     inject_presigned_url_rds),
-    ('before-call.rds.CreateDBCluster',
-     inject_presigned_url_rds),
     ('before-call.rds.CopyDBSnapshot',
      inject_presigned_url_rds),
     ('before-call.rds.CreateDBInstanceReadReplica',
@@ -915,17 +911,6 @@ BUILTIN_HANDLERS = [
      document_base64_encoding('UserData')),
     ('docs.*.autoscaling.CreateLaunchConfiguration.complete-section',
      document_base64_encoding('UserData')),
-
-    # RDS PresignedUrl documentation customizations
-    ('docs.*.rds.CopyDBClusterSnapshot.complete-section',
-     AutoPopulatedParam('PreSignedUrl').document_auto_populated_param),
-    ('docs.*.rds.CreateDBCluster.complete-section',
-     AutoPopulatedParam('PreSignedUrl').document_auto_populated_param),
-    ('docs.*.rds.CopyDBSnapshot.complete-section',
-     AutoPopulatedParam('PreSignedUrl').document_auto_populated_param),
-    ('docs.*.rds.CreateDBInstanceReadReplica.complete-section',
-     AutoPopulatedParam('PreSignedUrl').document_auto_populated_param),
-
     # EC2 CopySnapshot documentation customizations
     ('docs.*.ec2.CopySnapshot.complete-section',
      AutoPopulatedParam('PresignedUrl').document_auto_populated_param),
diff --git a/botocore/utils.py b/botocore/utils.py
index 26e6a877e..735d79d0d 100644
--- a/botocore/utils.py
+++ b/botocore/utils.py
@@ -979,6 +979,7 @@ class ContainerMetadataFetcher(object):
     RETRY_ATTEMPTS = 3
     SLEEP_TIME = 1
     IP_ADDRESS = '169.254.170.2'
+    _ALLOWED_HOSTS = [IP_ADDRESS, 'localhost', '127.0.0.1']
 
     def __init__(self, session=None, sleep=time.sleep):
         if session is None:
@@ -986,6 +987,33 @@ class ContainerMetadataFetcher(object):
         self._session = session
         self._sleep = sleep
 
+    def retrieve_full_uri(self, full_url, headers=None):
+        """Retrieve JSON metadata from container metadata.
+
+        :type full_url: str
+        :param full_url: The full URL of the metadata service.
+            This should include the scheme as well, e.g
+            "http://localhost:123/foo"
+
+        """
+        self._validate_allowed_url(full_url)
+        return self._retrieve_credentials(full_url, headers)
+
+    def _validate_allowed_url(self, full_url):
+        parsed = botocore.compat.urlparse(full_url)
+        is_whitelisted_host = self._check_if_whitelisted_host(
+            parsed.hostname)
+        if not is_whitelisted_host:
+            raise ValueError(
+                "Unsupported host '%s'.  Can only "
+                "retrieve metadata from these hosts: %s" %
+                (parsed.hostname, ', '.join(self._ALLOWED_HOSTS)))
+
+    def _check_if_whitelisted_host(self, host):
+        if host in self._ALLOWED_HOSTS:
+            return True
+        return False
+
     def retrieve_uri(self, relative_uri):
         """Retrieve JSON metadata from ECS metadata.
 
@@ -995,15 +1023,20 @@ class ContainerMetadataFetcher(object):
         :return: The parsed JSON response.
 
         """
-        full_url = self._full_url(relative_uri)
+        full_url = self.full_url(relative_uri)
+        return self._retrieve_credentials(full_url)
+
+    def _retrieve_credentials(self, full_url, extra_headers=None):
         headers = {'Accept': 'application/json'}
+        if extra_headers is not None:
+            headers.update(extra_headers)
         attempts = 0
         while True:
             try:
                 return self._get_response(full_url, headers, self.TIMEOUT_SECONDS)
             except MetadataRetrievalError as e:
                 logger.debug("Received error when attempting to retrieve "
-                             "ECS metadata: %s", e, exc_info=True)
+                             "container metadata: %s", e, exc_info=True)
                 self._sleep(self.SLEEP_TIME)
                 attempts += 1
                 if attempts >= self.RETRY_ATTEMPTS:
@@ -1028,5 +1061,5 @@ class ContainerMetadataFetcher(object):
                          "ECS metadata: %s" % e)
             raise MetadataRetrievalError(error_msg=error_msg)
 
-    def _full_url(self, relative_uri):
+    def full_url(self, relative_uri):
         return 'http://%s%s' % (self.IP_ADDRESS, relative_uri)
diff --git a/docs/source/conf.py b/docs/source/conf.py
index da5a724fa..a6d6e02cb 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -54,7 +54,7 @@ copyright = u'2013, Mitch Garnaat'
 # The short X.Y version.
 version = '1.5.'
 # The full version, including alpha/beta/rc tags.
-release = '1.5.21'
+release = '1.5.20'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.


Test patch:
diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py
index 5c8051030..237ae46f5 100644
--- a/tests/unit/test_credentials.py
+++ b/tests/unit/test_credentials.py
@@ -1153,14 +1153,22 @@ class TestContainerProvider(BaseEnvVar):
         creds = provider.load()
         self.assertIsNone(creds)
 
+    def full_url(self, url):
+        return 'http://%s%s' % (ContainerMetadataFetcher.IP_ADDRESS, url)
+
+    def create_fetcher(self):
+        fetcher = mock.Mock(spec=ContainerMetadataFetcher)
+        fetcher.full_url = self.full_url
+        return fetcher
+
     def test_retrieve_from_provider_if_env_var_present(self):
         environ = {
             'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI': '/latest/credentials?id=foo'
         }
-        fetcher = mock.Mock(spec=ContainerMetadataFetcher)
+        fetcher = self.create_fetcher()
         timeobj = datetime.now(tzlocal())
         timestamp = (timeobj + timedelta(hours=24)).isoformat()
-        fetcher.retrieve_uri.return_value = {
+        fetcher.retrieve_full_uri.return_value = {
             "AccessKeyId" : "access_key",
             "SecretAccessKey" : "secret_key",
             "Token" : "token",
@@ -1169,7 +1177,8 @@ class TestContainerProvider(BaseEnvVar):
         provider = credentials.ContainerProvider(environ, fetcher)
         creds = provider.load()
 
-        fetcher.retrieve_uri.assert_called_with('/latest/credentials?id=foo')
+        fetcher.retrieve_full_uri.assert_called_with(
+            self.full_url('/latest/credentials?id=foo'), headers=None)
         self.assertEqual(creds.access_key, 'access_key')
         self.assertEqual(creds.secret_key, 'secret_key')
         self.assertEqual(creds.token, 'token')
@@ -1183,7 +1192,7 @@ class TestContainerProvider(BaseEnvVar):
         timeobj = datetime.now(tzlocal())
         expired_timestamp = (timeobj - timedelta(hours=23)).isoformat()
         future_timestamp = (timeobj + timedelta(hours=1)).isoformat()
-        fetcher.retrieve_uri.side_effect = [
+        fetcher.retrieve_full_uri.side_effect = [
             {
                 "AccessKeyId" : "access_key_old",
                 "SecretAccessKey" : "secret_key_old",
@@ -1213,7 +1222,7 @@ class TestContainerProvider(BaseEnvVar):
         expired_timestamp = (timeobj - timedelta(hours=23)).isoformat()
         future_timestamp = (timeobj + timedelta(hours=1)).isoformat()
         exception = botocore.exceptions.CredentialRetrievalError
-        fetcher.retrieve_uri.side_effect = exception(provider='ecs-role',
+        fetcher.retrieve_full_uri.side_effect = exception(provider='ecs-role',
                                                      error_msg='fake http error')
         with self.assertRaises(exception):
             provider = credentials.ContainerProvider(environ, fetcher)
@@ -1230,7 +1239,7 @@ class TestContainerProvider(BaseEnvVar):
         expired_timestamp = (timeobj - timedelta(hours=23)).isoformat()
         http_exception = botocore.exceptions.MetadataRetrievalError
         raised_exception = botocore.exceptions.CredentialRetrievalError
-        fetcher.retrieve_uri.side_effect = [
+        fetcher.retrieve_full_uri.side_effect = [
             {
                 "AccessKeyId" : "access_key_old",
                 "SecretAccessKey" : "secret_key_old",
@@ -1245,3 +1254,50 @@ class TestContainerProvider(BaseEnvVar):
         # Second time with a refresh should propagate an error.
         with self.assertRaises(raised_exception):
             frozen_creds = creds.get_frozen_credentials()
+
+    def test_can_use_full_url(self):
+        environ = {
+            'AWS_CONTAINER_CREDENTIALS_FULL_URI': 'http://localhost/foo'
+        }
+        fetcher = self.create_fetcher()
+        timeobj = datetime.now(tzlocal())
+        timestamp = (timeobj + timedelta(hours=24)).isoformat()
+        fetcher.retrieve_full_uri.return_value = {
+            "AccessKeyId" : "access_key",
+            "SecretAccessKey" : "secret_key",
+            "Token" : "token",
+            "Expiration" : timestamp,
+        }
+        provider = credentials.ContainerProvider(environ, fetcher)
+        creds = provider.load()
+
+        fetcher.retrieve_full_uri.assert_called_with('http://localhost/foo',
+                                                     headers=None)
+        self.assertEqual(creds.access_key, 'access_key')
+        self.assertEqual(creds.secret_key, 'secret_key')
+        self.assertEqual(creds.token, 'token')
+        self.assertEqual(creds.method, 'container-role')
+
+    def test_can_pass_basic_auth_token(self):
+        environ = {
+            'AWS_CONTAINER_CREDENTIALS_FULL_URI': 'http://localhost/foo',
+            'AWS_CONTAINER_AUTHORIZATION_TOKEN': 'Basic auth-token',
+        }
+        fetcher = self.create_fetcher()
+        timeobj = datetime.now(tzlocal())
+        timestamp = (timeobj + timedelta(hours=24)).isoformat()
+        fetcher.retrieve_full_uri.return_value = {
+            "AccessKeyId" : "access_key",
+            "SecretAccessKey" : "secret_key",
+            "Token" : "token",
+            "Expiration" : timestamp,
+        }
+        provider = credentials.ContainerProvider(environ, fetcher)
+        creds = provider.load()
+
+        fetcher.retrieve_full_uri.assert_called_with(
+            'http://localhost/foo', headers={'Authorization': 'Basic auth-token'})
+        self.assertEqual(creds.access_key, 'access_key')
+        self.assertEqual(creds.secret_key, 'secret_key')
+        self.assertEqual(creds.token, 'token')
+        self.assertEqual(creds.method, 'container-role')
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 607f29ca4..2c1a23a95 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -1420,6 +1420,41 @@ class TestContainerMetadataFetcher(unittest.TestCase):
             http_responses.append(http_response)
         self.http.get.side_effect = http_responses
 
+    def assert_can_retrieve_metadata_from(self, full_uri):
+        response_body = {'foo': 'bar'}
+        self.set_http_responses_to(response_body)
+        fetcher = self.create_fetcher()
+        response = fetcher.retrieve_full_uri(full_uri)
+        self.assertEqual(response, response_body)
+        self.http.get.assert_called_with(
+            full_uri, headers={'Accept': 'application/json'},
+            timeout=fetcher.TIMEOUT_SECONDS,
+        )
+
+    def assert_host_is_not_allowed(self, full_uri):
+        response_body = {'foo': 'bar'}
+        self.set_http_responses_to(response_body)
+        fetcher = self.create_fetcher()
+        with self.assertRaisesRegexp(ValueError, 'Unsupported host'):
+            fetcher.retrieve_full_uri(full_uri)
+        self.assertFalse(self.http.get.called)
+
+    def test_can_specify_extra_headers_are_merged(self):
+        headers = {
+            # The 'Accept' header will override the
+            # default Accept header of application/json.
+            'Accept': 'application/not-json',
+            'X-Other-Header': 'foo',
+        }
+        self.set_http_responses_to({'foo': 'bar'})
+        fetcher = self.create_fetcher()
+        response = fetcher.retrieve_full_uri(
+            'http://localhost', headers)
+        self.http.get.assert_called_with(
+            'http://localhost', headers=headers,
+            timeout=fetcher.TIMEOUT_SECONDS,
+        )
+
     def test_can_retrieve_uri(self):
         json_body =  {
             "AccessKeyId" : "a",
@@ -1501,6 +1536,40 @@ class TestContainerMetadataFetcher(unittest.TestCase):
         # Should have tried up to RETRY_ATTEMPTS.
         self.assertEqual(self.http.get.call_count, fetcher.RETRY_ATTEMPTS)
 
+    def test_can_retrieve_full_uri_with_fixed_ip(self):
+        self.assert_can_retrieve_metadata_from(
+            'http://%s/foo?id=1' % ContainerMetadataFetcher.IP_ADDRESS)
+
+    def test_localhost_http_is_allowed(self):
+        self.assert_can_retrieve_metadata_from('http://localhost/foo')
+
+    def test_localhost_with_port_http_is_allowed(self):
+        self.assert_can_retrieve_metadata_from('http://localhost:8000/foo')
+
+    def test_localhost_https_is_allowed(self):
+        self.assert_can_retrieve_metadata_from('https://localhost/foo')
+
+    def test_can_use_127_ip_addr(self):
+        self.assert_can_retrieve_metadata_from('https://127.0.0.1/foo')
+
+    def test_can_use_127_ip_addr_with_port(self):
+        self.assert_can_retrieve_metadata_from('https://127.0.0.1:8080/foo')
+
+    def test_link_local_http_is_not_allowed(self):
+        self.assert_host_is_not_allowed('http://169.254.0.1/foo')
+
+    def test_link_local_https_is_not_allowed(self):
+        self.assert_host_is_not_allowed('https://169.254.0.1/foo')
+
+    def test_non_link_local_nonallowed_url(self):
+        self.assert_host_is_not_allowed('http://169.1.2.3/foo')
+
+    def test_error_raised_on_nonallowed_url(self):
+        self.assert_host_is_not_allowed('http://somewhere.com/foo')
+
+    def test_external_host_not_allowed_if_https(self):
+        self.assert_host_is_not_allowed('https://somewhere.com/foo')
+
 
 if __name__ == '__main__':
     unittest.main()