launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25538
[Merge] ~pappacena/launchpad:aws-registry-push into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:aws-registry-push into launchpad:master.
Commit message:
Adding OCI registry client for AWS' ECR
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/392639
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:aws-registry-push into launchpad:master.
diff --git a/constraints.txt b/constraints.txt
index 4c33491..116d95e 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -81,7 +81,7 @@ python-gettext==4.0
pytz==2019.3
# Handled in setup-requirements.txt instead.
#setuptools==44.0.0
-six==1.14.0
+six==1.15.0
transaction==3.0.0
# zope.password needs these
@@ -135,7 +135,7 @@ lxml==4.5.0
repoze.sphinx.autointerface==0.8
requests==2.23.0
certifi==2019.11.28
-urllib3==1.25.8
+urllib3==1.25.11
idna==2.9
chardet==3.0.4
sphinxcontrib-applehelp==1.0.2
@@ -179,6 +179,8 @@ billiard==3.5.0.5
bleach==3.1.0
breezy==3.0.1
bson==0.5.9
+boto3==1.16.2
+botocore==1.19.2
# lp:~launchpad/bzr/lp
bzr==2.6.0.lp.4
celery==4.1.1
@@ -204,7 +206,7 @@ fastimport==0.9.8
feedparser==5.2.1
feedvalidator==0.0.0DEV-r1049
FormEncode==1.3.1
-futures==3.2.0
+futures==3.3.0
geoip2==2.9.0
grokcore.component==3.1
gunicorn==19.8.1
@@ -216,6 +218,7 @@ incremental==17.5.0
ipaddress==1.0.18
ipython==0.13.2
iso8601==0.1.12
+jmespath==0.10.0
jsautobuild==0.2
keyring==0.6.2
kombu==4.4.0
@@ -288,6 +291,7 @@ rabbitfixture==0.4.2
requests-file==1.4.3
requests-toolbelt==0.9.1
responses==0.9.0
+s3transfer==0.3.3
scandir==1.7
service-identity==18.1.0
setproctitle==1.1.7
diff --git a/lib/lp/oci/model/ociregistryclient.py b/lib/lp/oci/model/ociregistryclient.py
index 7c67b57..423f64a 100644
--- a/lib/lp/oci/model/ociregistryclient.py
+++ b/lib/lp/oci/model/ociregistryclient.py
@@ -10,11 +10,13 @@ __all__ = [
'OCIRegistryClient'
]
-
+import base64
from functools import partial
import hashlib
from io import BytesIO
import json
+
+
try:
from json.decoder import JSONDecodeError
except ImportError:
@@ -22,6 +24,7 @@ except ImportError:
import logging
import tarfile
+import boto3
from requests.exceptions import (
ConnectionError,
HTTPError,
@@ -30,6 +33,7 @@ from six.moves.urllib.request import (
parse_http_list,
parse_keqv_list,
)
+from six.moves.urllib.parse import urlparse
from tenacity import (
before_log,
retry,
@@ -45,6 +49,7 @@ from lp.oci.interfaces.ociregistryclient import (
MultipleOCIRegistryError,
ManifestUploadFailed,
)
+from lp.services.propertycache import cachedproperty
from lp.services.timeout import urlfetch
@@ -426,6 +431,9 @@ class RegistryHTTPClient:
def getInstance(cls, push_rule):
"""Returns an instance of RegistryHTTPClient adapted to the
given push rule and registry's authentication flow."""
+ registry_domain = urlparse(push_rule.registry_url).netloc
+ if registry_domain.endswith(".amazonaws.com"):
+ return AWSRegistryHTTPClient(push_rule)
try:
proxy_urlfetch("{}/v2/".format(push_rule.registry_url))
# No authorization error? Just return the basic RegistryHTTPClient.
@@ -516,3 +524,38 @@ class BearerTokenRegistryClient(RegistryHTTPClient):
url, auth_retry=False, headers=headers,
*args, **request_kwargs)
raise
+
+
+class AWSRegistryHTTPClient(RegistryHTTPClient):
+
+ def _getRegion(self):
+ """Returns the region from the push URL domain."""
+ domain = urlparse(self.push_rule.registry_url).netloc
+ # The domain format should be something like
+ # 'xxx.dkr.ecr.sa-east-1.amazonaws.com'. 'sa-east-1' is the region.
+ return domain.split(".")[-3]
+
+ @cachedproperty
+ def credentials(self):
+ """Exchange aws_access_key_id and aws_secret_access_key with the
+ authentication token that should be used when talking to ECR."""
+ try:
+ auth = self.push_rule.registry_credentials.getCredentials()
+ username, password = auth['username'], auth.get('password')
+ region = self._getRegion()
+ log.info("Trying to authenticate with AWS in region %s" % region)
+ client = boto3.client('ecr', aws_access_key_id=username,
+ aws_secret_access_key=password,
+ region_name=region)
+ token = client.get_authorization_token()
+ auth_data = token["authorizationData"][0]
+ authorization_token = auth_data['authorizationToken']
+ username, password = base64.b64decode(
+ authorization_token).decode().split(':')
+ return username, password
+ except Exception as e:
+ log.error("Error trying to get authorization token for ECR "
+ "registry: %s(%s)" % (e.__class__, e))
+ raise OCIRegistryAuthenticationError(
+ "It was not possible to get AWS credentials for %s: %s" %
+ (self.push_rule.registry_url, e))
diff --git a/lib/lp/oci/tests/test_ociregistryclient.py b/lib/lp/oci/tests/test_ociregistryclient.py
index 8de88b4..93f33fb 100644
--- a/lib/lp/oci/tests/test_ociregistryclient.py
+++ b/lib/lp/oci/tests/test_ociregistryclient.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
+import base64
from functools import partial
import io
import json
@@ -46,6 +47,7 @@ from lp.oci.interfaces.ociregistryclient import (
)
from lp.oci.model.ocirecipe import OCIRecipeBuildRequest
from lp.oci.model.ociregistryclient import (
+ AWSRegistryHTTPClient,
BearerTokenRegistryClient,
OCIRegistryAuthenticationError,
OCIRegistryClient,
@@ -668,6 +670,70 @@ class TestRegistryHTTPClient(OCIConfigHelperMixin, SpyProxyCallsMixin,
call = responses.calls[0]
self.assertEqual("%s/v2/" % push_rule.registry_url, call.request.url)
+ @responses.activate
+ def test_get_aws_client_instance(self):
+ credentials = self.factory.makeOCIRegistryCredentials(
+ url="https://123456789.dkr.ecr.sa-east-1.amazonaws.com",
+ credentials={
+ 'username': 'aws_access_key_id',
+ 'password': "aws_secret_access_key"})
+ push_rule = removeSecurityProxy(self.factory.makeOCIPushRule(
+ registry_credentials=credentials,
+ image_name="ecr-test"))
+
+ instance = RegistryHTTPClient.getInstance(push_rule)
+ self.assertEqual(AWSRegistryHTTPClient, type(instance))
+ self.assertIsInstance(instance, RegistryHTTPClient)
+
+ @responses.activate
+ def test_aws_credentials(self):
+ boto_patch = self.useFixture(
+ MockPatch('lp.oci.model.ociregistryclient.boto3'))
+ boto = boto_patch.mock
+ get_authorization_token = (
+ boto.client.return_value.get_authorization_token)
+ get_authorization_token.return_value = {
+ "authorizationData": [{
+ "authorizationToken": base64.b64encode(
+ b"the-username:the-token")
+ }]
+ }
+
+ credentials = self.factory.makeOCIRegistryCredentials(
+ url="https://123456789.dkr.ecr.sa-east-1.amazonaws.com",
+ credentials={
+ 'username': 'my_aws_access_key_id',
+ 'password': "my_aws_secret_access_key"})
+ push_rule = removeSecurityProxy(self.factory.makeOCIPushRule(
+ registry_credentials=credentials,
+ image_name="ecr-test"))
+
+ instance = RegistryHTTPClient.getInstance(push_rule)
+ # Check the credentials twice, to make sure they are cached.
+ for _ in range(2):
+ http_user, http_passwd = instance.credentials
+ self.assertEqual("the-username", http_user)
+ self.assertEqual("the-token", http_passwd)
+ self.assertEqual(1, boto.client.call_count)
+ self.assertEqual(mock.call(
+ 'ecr', aws_access_key_id="my_aws_access_key_id",
+ aws_secret_access_key="my_aws_secret_access_key",
+ region_name="sa-east-1"),
+ boto.client.call_args)
+
+ @responses.activate
+ def test_aws_malformed_url_region(self):
+ credentials = self.factory.makeOCIRegistryCredentials(
+ url="https://.amazonaws.com",
+ credentials={'username': 'aa', 'password': "bb"})
+ push_rule = removeSecurityProxy(self.factory.makeOCIPushRule(
+ registry_credentials=credentials,
+ image_name="ecr-test"))
+
+ instance = RegistryHTTPClient.getInstance(push_rule)
+ self.assertRaises(
+ OCIRegistryAuthenticationError, getattr, instance, 'credentials')
+
class TestBearerTokenRegistryClient(OCIConfigHelperMixin,
SpyProxyCallsMixin, TestCaseWithFactory):
diff --git a/setup.py b/setup.py
index 4e76a25..f6d5b6b 100644
--- a/setup.py
+++ b/setup.py
@@ -153,6 +153,7 @@ setup(
'ampoule',
'backports.lzma; python_version < "3.3"',
'beautifulsoup4[lxml]',
+ 'boto3',
'breezy',
# XXX cjwatson 2020-08-07: This should eventually be removed
# entirely, but we need to retain it until codeimport has been