From 39f5716c041ff3fc9b5b5d68049e8ca474453e1d Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Tue, 29 Aug 2017 12:59:50 +1200 Subject: [PATCH] Discover a versioned container image tag This change implements a discover_image_tag method to discover the versioned tag for a container image. The current approach is to expect a label to be set on the image which contains the expected tag value. For example the RDO image building pipeline sets a label rdo_version whose value also matches a tag. This change will be used by the python-tripleoclient change which actually implements the command "openstack overcloud container image tag discover". Change-Id: I27ea031287604d70032fb5392aecbce313d4b096 Partial-Bug: #1708967 --- tripleo_common/image/image_uploader.py | 56 ++++++++++++- .../tests/image/test_image_uploader.py | 84 +++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/tripleo_common/image/image_uploader.py b/tripleo_common/image/image_uploader.py index 5c69186d8..7212c6f6c 100644 --- a/tripleo_common/image/image_uploader.py +++ b/tripleo_common/image/image_uploader.py @@ -15,6 +15,7 @@ import abc +import json import logging import netifaces import six @@ -38,6 +39,11 @@ class ImageUploadManager(BaseImageManager): def __init__(self, config_files, verbose=False, debug=False): super(ImageUploadManager, self).__init__(config_files) + def discover_image_tag(self, image, tag_from_label=None): + uploader = ImageUploader.get_uploader('docker') + return uploader.discover_image_tag( + image, tag_from_label=tag_from_label) + def upload(self): """Start the upload process""" @@ -89,6 +95,11 @@ class ImageUploader(object): """Upload a disk image""" pass + @abc.abstractmethod + def discover_image_tag(self, image, tag_from_label=None): + """Discover a versioned tag for an image""" + pass + class DockerImageUploader(ImageUploader): """Upload images using docker push""" @@ -108,9 +119,7 @@ class DockerImageUploader(ImageUploader): else: repo = image - response = [line for line in dockerc.pull(repo, - tag=tag, stream=True)] - self.logger.debug(response) + self._pull(dockerc, repo, tag=tag) full_image = repo + ':' + tag new_repo = push_destination + '/' + repo.partition('/')[2] @@ -123,3 +132,44 @@ class DockerImageUploader(ImageUploader): self.logger.debug(response) self.logger.info('Completed upload for docker image %s' % image_name) + + def _pull(self, dockerc, image, tag=None): + self.logger.debug('Pulling %s' % image) + for line in dockerc.pull(image, tag=tag, stream=True): + status = json.loads(line) + if 'error' in status: + raise ImageUploaderException('Could not pull image: %s' % + status['error']) + self.logger.debug(status.get('status')) + + def discover_image_tag(self, image, tag_from_label=None): + dockerc = Client(base_url='unix://var/run/docker.sock', version='auto') + + image_name, colon, tag = image.partition(':') + if not tag: + tag = 'latest' + image = '%s:%s' % (image_name, tag) + + self._pull(dockerc, image) + i = dockerc.inspect_image(image) + labels = i['Config']['Labels'] + + label_keys = ', '.join(labels.keys()) + + if not tag_from_label: + raise ImageUploaderException( + 'No label specified. Available labels: %s' % label_keys + ) + + tag_label = labels.get(tag_from_label) + if tag_label is None: + raise ImageUploaderException( + 'Image %s has no label %s. Available labels: %s' % + (image, tag_from_label, label_keys) + ) + + # confirm the tag exists by pulling it, which should be fast + # because that image has just been pulled + versioned_image = '%s:%s' % (image_name, tag_label) + self._pull(dockerc, versioned_image) + return tag_label diff --git a/tripleo_common/tests/image/test_image_uploader.py b/tripleo_common/tests/image/test_image_uploader.py index f6143246f..c33f8ba37 100644 --- a/tripleo_common/tests/image/test_image_uploader.py +++ b/tripleo_common/tests/image/test_image_uploader.py @@ -173,3 +173,87 @@ class TestDockerImageUploader(base.TestCase): self.dockermock.return_value.push( push_destination + '/' + image, tag=expected_tag, stream=True) + + def test_discover_image_tag(self): + image = 'docker.io/tripleoupstream/heat-docker-agents-centos:latest' + vimage = 'docker.io/tripleoupstream/heat-docker-agents-centos:1.2.3' + + dockerc = self.dockermock.return_value + dockerc.pull.return_value = ['{"status": "done"}'] + dockerc.inspect_image.return_value = { + 'Config': {'Labels': {'image-version': '1.2.3'}} + } + result = self.uploader.discover_image_tag(image, 'image-version') + self.assertEqual('1.2.3', result) + + self.dockermock.assert_called_once_with( + base_url='unix://var/run/docker.sock', version='auto') + + dockerc.pull.assert_has_calls([ + mock.call(image, tag=None, stream=True), + mock.call(vimage, tag=None, stream=True), + ]) + + def test_discover_image_tag_no_latest(self): + image = 'docker.io/tripleoupstream/heat-docker-agents-centos' + limage = image + ':latest' + vimage = image + ':1.2.3' + + dockerc = self.dockermock.return_value + dockerc.pull.return_value = ['{"status": "done"}'] + dockerc.inspect_image.return_value = { + 'Config': {'Labels': {'image-version': '1.2.3'}} + } + result = self.uploader.discover_image_tag(image, 'image-version') + self.assertEqual('1.2.3', result) + + dockerc.pull.assert_has_calls([ + mock.call(limage, tag=None, stream=True), + mock.call(vimage, tag=None, stream=True), + ]) + + def test_discover_image_tag_no_tag_from_image(self): + image = 'docker.io/tripleoupstream/heat-docker-agents-centos:latest' + + dockerc = self.dockermock.return_value + dockerc.pull.return_value = ['{"status": "done"}'] + dockerc.inspect_image.return_value = { + 'Config': {'Labels': {'image-version': '1.2.3'}} + } + self.assertRaises(ImageUploaderException, + self.uploader.discover_image_tag, image) + + def test_discover_image_tag_missing_label(self): + image = 'docker.io/tripleoupstream/heat-docker-agents-centos:latest' + + dockerc = self.dockermock.return_value + dockerc.pull.return_value = ['{"status": "done"}'] + dockerc.inspect_image.return_value = { + 'Config': {'Labels': {'image-version': '1.2.3'}} + } + self.assertRaises(ImageUploaderException, + self.uploader.discover_image_tag, image, 'foo') + + def test_discover_image_tag_missing_tag(self): + image = 'docker.io/tripleoupstream/heat-docker-agents-centos:latest' + vimage = 'docker.io/tripleoupstream/heat-docker-agents-centos:1.2.3' + + dockerc = self.dockermock.return_value + dockerc.pull.side_effect = [ + ['{"status": "done"}'], # First pull, :latest + ['{"error": "ouch"}'], # Second pull, :1.2.3 + ] + dockerc.inspect_image.return_value = { + 'Config': {'Labels': {'image-version': '1.2.3'}} + } + self.assertRaises(ImageUploaderException, + self.uploader.discover_image_tag, image, + 'image-version') + + self.dockermock.assert_called_once_with( + base_url='unix://var/run/docker.sock', version='auto') + + dockerc.pull.assert_has_calls([ + mock.call(image, tag=None, stream=True), + mock.call(vimage, tag=None, stream=True), + ])