From 66b341aa9e7415dbb1b8be5f339e25686ab22334 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 31 Dec 2023 10:41:18 +0100 Subject: [PATCH] Add docker_image_tag module (#730) * Add docker_image_tag module. * Add check mode tests. * Improve and test image ID/digest handling. * Adjust more tests. --- README.md | 1 + meta/runtime.yml | 1 + plugins/modules/docker_image.py | 2 +- plugins/modules/docker_image_push.py | 3 + plugins/modules/docker_image_tag.py | 272 ++++++++++++ .../targets/docker_image_info/tasks/main.yml | 4 +- .../docker_image_load/tasks/tests/basic.yml | 3 +- .../tasks/tests/registry.yml | 13 +- .../tasks/tests/registry.yml | 18 +- .../targets/docker_image_tag/aliases | 6 + .../targets/docker_image_tag/meta/main.yml | 8 + .../targets/docker_image_tag/tasks/main.yml | 406 ++++++++++++++++++ 12 files changed, 715 insertions(+), 22 deletions(-) create mode 100644 plugins/modules/docker_image_tag.py create mode 100644 tests/integration/targets/docker_image_tag/aliases create mode 100644 tests/integration/targets/docker_image_tag/meta/main.yml create mode 100644 tests/integration/targets/docker_image_tag/tasks/main.yml diff --git a/README.md b/README.md index 25ea364d..2a7a87ab 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ If you use the Ansible package and do not update collections independently, use - community.docker.docker_image_load: load Docker images from archives - community.docker.docker_image_pull: pull Docker images from registries - community.docker.docker_image_push: push Docker images to registries + - community.docker.docker_image_tag: tag Docker images with new names and/or tags - community.docker.docker_login: log in and out to/from registries - community.docker.docker_network: manage Docker networks - community.docker.docker_network_info: retrieve information on Docker networks diff --git a/meta/runtime.yml b/meta/runtime.yml index bc5aca47..54522ea6 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -18,6 +18,7 @@ action_groups: - docker_image_load - docker_image_pull - docker_image_push + - docker_image_tag - docker_login - docker_network - docker_network_info diff --git a/plugins/modules/docker_image.py b/plugins/modules/docker_image.py index becb27d5..7ac753a6 100644 --- a/plugins/modules/docker_image.py +++ b/plugins/modules/docker_image.py @@ -254,7 +254,7 @@ seealso: - module: community.docker.docker_image_info - module: community.docker.docker_image_load - module: community.docker.docker_image_pull - - module: community.docker.docker_image_push + - module: community.docker.docker_image_tag ''' EXAMPLES = ''' diff --git a/plugins/modules/docker_image_push.py b/plugins/modules/docker_image_push.py index d2ece0c5..705bf25f 100644 --- a/plugins/modules/docker_image_push.py +++ b/plugins/modules/docker_image_push.py @@ -46,6 +46,9 @@ requirements: author: - Felix Fontein (@felixfontein) + +seealso: + - module: community.docker.docker_image_tag ''' EXAMPLES = ''' diff --git a/plugins/modules/docker_image_tag.py b/plugins/modules/docker_image_tag.py new file mode 100644 index 00000000..49ba0b7b --- /dev/null +++ b/plugins/modules/docker_image_tag.py @@ -0,0 +1,272 @@ +#!/usr/bin/python +# +# Copyright (c) 2023, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: docker_image_tag + +short_description: Tag Docker images with new names and/or tags + +version_added: 3.6.0 + +description: + - This module allows to tag Docker images with new names and/or tags. + +extends_documentation_fragment: + - community.docker.docker.api_documentation + - community.docker.attributes + - community.docker.attributes.actiongroup_docker + +attributes: + check_mode: + support: full + diff_mode: + support: full + +options: + name: + description: + - "Image name. Name format will be one of: C(name), C(repository/name), C(registry_server:port/name). + When pushing or pulling an image the name can optionally include the tag by appending C(:tag_name)." + - Note that image IDs (hashes) can also be used. + type: str + required: true + tag: + description: + - Tag for the image name O(name) that is to be tagged. + - If O(name)'s format is C(name:tag), then the tag value from O(name) will take precedence. + type: str + default: latest + repository: + description: + - List of new image names to tag the image as. + - Expects format C(repository:tag). If no tag is provided, will use the value of the O(tag) parameter if present, or V(latest). + type: list + elements: str + required: true + existing_images: + description: + - Defines the behavior if the image to be tagged already exists and is another image than the one identified by O(name) and O(tag). + - If set to V(keep), the tagged image is kept. + - If set to V(overwrite), the tagged image is overwritten by the specified one. + type: str + choices: + - keep + - overwrite + default: overwrite + +requirements: + - "Docker API >= 1.25" + +author: + - Felix Fontein (@felixfontein) + +seealso: + - module: community.docker.docker_image_push +''' + +EXAMPLES = ''' +- name: Tag Python 3.12 image with two new names + community.docker.docker_image: + name: python:3.12 + repository: + - python-3:3.12 + - local-registry:5000/python-3/3.12:latest +''' + +RETURN = ''' +image: + description: Image inspection results for the affected image. + returned: success + type: dict + sample: {} +tagged_images: + description: + - A list of images that got tagged. + returned: success + type: list + elements: str + sample: + - python-3:3.12 +''' + +import traceback + +from ansible.module_utils.common.text.converters import to_native +from ansible.module_utils.common.text.formatters import human_to_bytes + +from ansible_collections.community.docker.plugins.module_utils.common_api import ( + AnsibleDockerClient, + RequestException, +) + +from ansible_collections.community.docker.plugins.module_utils.util import ( + DockerBaseClass, + is_image_name_id, + is_valid_tag, +) + +from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException +from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import ( + parse_repository_tag, +) + + +def convert_to_bytes(value, module, name, unlimited_value=None): + if value is None: + return value + try: + if unlimited_value is not None and value in ('unlimited', str(unlimited_value)): + return unlimited_value + return human_to_bytes(value) + except ValueError as exc: + module.fail_json(msg='Failed to convert %s to bytes: %s' % (name, to_native(exc))) + + +def image_info(name, tag, image): + result = dict(name=name, tag=tag) + if image: + result['id'] = image['Id'] + else: + result['exists'] = False + return result + + +class ImageTagger(DockerBaseClass): + def __init__(self, client): + super(ImageTagger, self).__init__() + + self.client = client + parameters = self.client.module.params + self.check_mode = self.client.check_mode + + self.name = parameters['name'] + self.tag = parameters['tag'] + if not is_valid_tag(self.tag, allow_empty=True): + self.fail('"{0}" is not a valid docker tag'.format(self.tag)) + + # If name contains a tag, it takes precedence over tag parameter. + if not is_image_name_id(self.name): + repo, repo_tag = parse_repository_tag(self.name) + if repo_tag: + self.name = repo + self.tag = repo_tag + + self.keep_existing_images = parameters['existing_images'] == 'keep' + + # Make sure names in repository are valid images, and add tag if needed + self.repositories = [] + for i, repository in enumerate(parameters['repository']): + if is_image_name_id(repository): + self.fail("repository[%d] must not be an image ID; got: %s" % (i + 1, repository)) + repo, repo_tag = parse_repository_tag(repository) + if not repo_tag: + repo_tag = parameters['tag'] + elif not is_valid_tag(repo_tag, allow_empty=False): + self.fail("repository[%d] must not have a digest; got: %s" % (i + 1, repository)) + self.repositories.append((repo, repo_tag)) + + def fail(self, msg): + self.client.fail(msg) + + def tag_image(self, image, name, tag): + tagged_image = self.client.find_image(name=name, tag=tag) + if tagged_image: + # Idempotency checks + if tagged_image['Id'] == image['Id']: + return ( + False, + "target image already exists (%s) and is as expected" % tagged_image['Id'], + tagged_image, + ) + if self.keep_existing_images: + return ( + False, + "target image already exists (%s) and is not as expected, but kept" % tagged_image['Id'], + tagged_image, + ) + msg = "target image existed (%s) and was not as expected" % tagged_image['Id'] + else: + msg = "target image did not exist" + + if not self.check_mode: + try: + params = { + 'tag': tag, + 'repo': name, + 'force': True, + } + res = self.client._post(self.client._url('/images/{0}/tag', image['Id']), params=params) + self.client._raise_for_status(res) + if res.status_code != 201: + raise Exception("Tag operation failed.") + except Exception as exc: + self.fail("Error: failed to tag image as %s:%s - %s" % (name, tag, to_native(exc))) + + return True, msg, tagged_image + + def tag_images(self): + if is_image_name_id(self.name): + image = self.client.find_image_by_id(self.name, accept_missing_image=False) + else: + image = self.client.find_image(name=self.name, tag=self.tag) + if not image: + self.fail("Cannot find image %s:%s" % (self.name, self.tag)) + + before = [] + after = [] + tagged_images = [] + results = dict( + changed=False, + actions=[], + image=image, + tagged_images=tagged_images, + diff=dict(before=dict(images=before), after=dict(images=after)), + ) + for repository, tag in self.repositories: + tagged, msg, old_image = self.tag_image(image, repository, tag) + before.append(image_info(repository, tag, old_image)) + after.append(image_info(repository, tag, image if tagged else old_image)) + if tagged: + results['changed'] = True + results['actions'].append('Tagged image %s as %s:%s: %s' % (image['Id'], repository, tag, msg)) + tagged_images.append('%s:%s' % (repository, tag)) + else: + results['actions'].append('Not tagged image %s as %s:%s: %s' % (image['Id'], repository, tag, msg)) + + return results + + +def main(): + argument_spec = dict( + name=dict(type='str', required=True), + tag=dict(type='str', default='latest'), + repository=dict(type='list', elements='str', required=True), + existing_images=dict(type='str', choices=['keep', 'overwrite'], default='overwrite'), + ) + + client = AnsibleDockerClient( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + results = ImageTagger(client).tag_images() + client.module.exit_json(**results) + except DockerException as e: + client.fail('An unexpected Docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) + except RequestException as e: + client.fail( + 'An unexpected requests error occurred when trying to talk to the Docker daemon: {0}'.format(to_native(e)), + exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/docker_image_info/tasks/main.yml b/tests/integration/targets/docker_image_info/tasks/main.yml index 5bd053ac..1e18d8c5 100644 --- a/tests/integration/targets/docker_image_info/tasks/main.yml +++ b/tests/integration/targets/docker_image_info/tasks/main.yml @@ -24,10 +24,8 @@ - "result.images|length == 0" - name: Make sure images are there - docker_image: + docker_image_pull: name: "{{ item }}" - source: pull - state: present loop: - "{{ docker_test_image_hello_world }}" - "{{ docker_test_image_alpine }}" diff --git a/tests/integration/targets/docker_image_load/tasks/tests/basic.yml b/tests/integration/targets/docker_image_load/tasks/tests/basic.yml index 8d9de994..6b8ef710 100644 --- a/tests/integration/targets/docker_image_load/tasks/tests/basic.yml +++ b/tests/integration/targets/docker_image_load/tasks/tests/basic.yml @@ -10,9 +10,8 @@ - "{{ docker_test_image_alpine }}" - name: Make sure images are there - docker_image: + docker_image_pull: name: "{{ item }}" - source: pull register: images loop: "{{ image_names }}" diff --git a/tests/integration/targets/docker_image_pull/tasks/tests/registry.yml b/tests/integration/targets/docker_image_pull/tasks/tests/registry.yml index 76ca57b9..c7197c42 100644 --- a/tests/integration/targets/docker_image_pull/tasks/tests/registry.yml +++ b/tests/integration/targets/docker_image_pull/tasks/tests/registry.yml @@ -29,12 +29,15 @@ name: "{{ docker_test_image_hello_world }}" diff: true - - name: Push image to test registry - docker_image: + - name: Tag image + docker_image_tag: name: "{{ docker_test_image_hello_world }}" - repository: "{{ hello_world_image_base }}:latest" - push: true - source: local + repository: + - "{{ hello_world_image_base }}:latest" + + - name: Push image to test registry + docker_image_push: + name: "{{ hello_world_image_base }}:latest" - name: Get facts of local image docker_image_info: diff --git a/tests/integration/targets/docker_image_push/tasks/tests/registry.yml b/tests/integration/targets/docker_image_push/tasks/tests/registry.yml index 01834dcb..ca18d196 100644 --- a/tests/integration/targets/docker_image_push/tasks/tests/registry.yml +++ b/tests/integration/targets/docker_image_push/tasks/tests/registry.yml @@ -25,14 +25,11 @@ inames: "{{ inames + [image_name_base ~ ':' ~ image_tag, image_name_base2 ~ ':' ~ image_tag] }}" - name: Tag first image - docker_image: + docker_image_tag: name: "{{ docker_test_image_hello_world }}" - repository: "{{ item }}" - source: local - force_tag: true - loop: - - "{{ image_name_base }}:{{ image_tag }}" - - "{{ image_name_base2 }}:{{ image_tag }}" + repository: + - "{{ image_name_base }}:{{ image_tag }}" + - "{{ image_name_base2 }}:{{ image_tag }}" - name: Push first image docker_image_push: @@ -45,11 +42,10 @@ register: push_2 - name: Tag second image - docker_image: + docker_image_tag: name: "{{ docker_test_image_alpine }}" - repository: "{{ image_name_base }}:{{ image_tag }}" - source: local - force_tag: true + repository: + - "{{ image_name_base }}:{{ image_tag }}" - name: Push second image with same name docker_image_push: diff --git a/tests/integration/targets/docker_image_tag/aliases b/tests/integration/targets/docker_image_tag/aliases new file mode 100644 index 00000000..2e1acc0a --- /dev/null +++ b/tests/integration/targets/docker_image_tag/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/4 +destructive diff --git a/tests/integration/targets/docker_image_tag/meta/main.yml b/tests/integration/targets/docker_image_tag/meta/main.yml new file mode 100644 index 00000000..471ddd41 --- /dev/null +++ b/tests/integration/targets/docker_image_tag/meta/main.yml @@ -0,0 +1,8 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_docker + - setup_docker_python_deps diff --git a/tests/integration/targets/docker_image_tag/tasks/main.yml b/tests/integration/targets/docker_image_tag/tasks/main.yml new file mode 100644 index 00000000..8d479ae4 --- /dev/null +++ b/tests/integration/targets/docker_image_tag/tasks/main.yml @@ -0,0 +1,406 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- block: + - name: Pick image prefix + set_fact: + iname_prefix: "{{ 'ansible-docker-test-%0x' % ((2**32) | random) }}" + + - name: Define image names + set_fact: + image_1: "{{ docker_test_image_hello_world }}" + image_2: "{{ docker_test_image_alpine }}" + image_3: "{{ docker_test_image_digest_base }}@sha256:{{ docker_test_image_digest_v1 }}" + image_names: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + - "{{ iname_prefix }}-tagged-2:baz" + + - name: Make sure images we work with are there + docker_image_pull: + name: "{{ item }}" + loop: + - "{{ image_1 }}" + - "{{ image_2 }}" + - "{{ image_3 }}" + register: pulled_images + diff: true + + - name: Remove tagged images + docker_image: + name: "{{ item }}" + source: local + state: absent + loop: "{{ image_names }}" + + - name: Tag image 1 (check mode) + docker_image_tag: + name: "{{ image_1 }}" + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + register: tag_1_check + diff: true + check_mode: true + + - name: Tag image 1 + docker_image_tag: + name: "{{ image_1 }}" + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + register: tag_1 + diff: true + + - name: Fetch image infos + docker_image_info: + name: "{{ image_names }}" + register: info_1 + + - assert: + that: + - tag_1 is changed + - tag_1.diff.before.images | length == 2 + - tag_1.diff.before.images[0] != tag_1.diff.after.images[0] + - tag_1.diff.before.images[1] != tag_1.diff.after.images[1] + - tag_1.diff.before.images[0].exists is false + - tag_1.diff.before.images[1].exists is false + - tag_1.diff.after.images[0].id == pulled_images.results[0].image.Id + - tag_1.diff.after.images[1].id == pulled_images.results[0].image.Id + - info_1.images | length == 2 + - info_1.images[0].Id == pulled_images.results[0].image.Id + - info_1.images[1].Id == pulled_images.results[0].image.Id + - tag_1_check == tag_1 + + - name: Tag image 1 (idempotent, check mode) + docker_image_tag: + name: "{{ image_1 }}" + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + register: tag_2_check + diff: true + check_mode: true + + - name: Tag image 1 (idempotent) + docker_image_tag: + name: "{{ image_1 }}" + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + register: tag_2 + diff: true + + - assert: + that: + - tag_2 is not changed + - tag_2.diff.before == tag_2.diff.after + - tag_2.diff.before.images | length == 2 + - tag_2_check == tag_2 + + - name: Tag image 1 (idempotent, different input format, check mode) + docker_image_tag: + name: "{{ image_1 }}" + tag: foo + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1" + register: tag_3_check + diff: true + check_mode: true + + - name: Tag image 1 (idempotent, different input format) + docker_image_tag: + name: "{{ image_1 }}" + tag: foo + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1" + register: tag_3 + diff: true + + - assert: + that: + - tag_3 is not changed + - tag_3.diff.before == tag_3.diff.after + - tag_3.diff.before.images | length == 2 + - tag_3_check == tag_3 + + - name: Tag image 1 (one more, check mode) + docker_image_tag: + name: "{{ image_1 }}" + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-2:baz" + register: tag_4_check + diff: true + check_mode: true + + - name: Tag image 1 (one more) + docker_image_tag: + name: "{{ image_1 }}" + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-2:baz" + register: tag_4 + diff: true + + - name: Fetch image infos + docker_image_info: + name: "{{ image_names }}" + register: info_4 + + - assert: + that: + - tag_4 is changed + - tag_4.diff.before.images | length == 3 + - tag_4.diff.before.images[0] == tag_4.diff.after.images[0] + - tag_4.diff.before.images[1] == tag_4.diff.after.images[1] + - tag_4.diff.before.images[2] != tag_4.diff.after.images[2] + - tag_4.diff.before.images[2].exists is false + - tag_4.diff.after.images[0].id == pulled_images.results[0].image.Id + - tag_4.diff.after.images[1].id == pulled_images.results[0].image.Id + - tag_4.diff.after.images[2].id == pulled_images.results[0].image.Id + - info_4.images | length == 3 + - info_4.images[0].Id == pulled_images.results[0].image.Id + - info_4.images[1].Id == pulled_images.results[0].image.Id + - info_4.images[2].Id == pulled_images.results[0].image.Id + - tag_4_check == tag_4 + + - name: Tag image 2 (only change missing one, check mode) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: keep + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_5_check + diff: true + check_mode: true + + - name: Tag image 2 (only change missing one) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: keep + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_5 + diff: true + + - name: Fetch image infos + docker_image_info: + name: "{{ image_names }}" + register: info_5 + + - assert: + that: + - tag_5 is changed + - tag_5.diff.before.images | length == 3 + - tag_5.diff.before.images[0] == tag_5.diff.after.images[0] + - tag_5.diff.before.images[1] == tag_5.diff.after.images[1] + - tag_5.diff.before.images[2] != tag_5.diff.after.images[2] + - tag_5.diff.before.images[2].exists is false + - tag_5.diff.after.images[0].id == pulled_images.results[0].image.Id + - tag_5.diff.after.images[1].id == pulled_images.results[0].image.Id + - tag_5.diff.after.images[2].id == pulled_images.results[1].image.Id + - info_5.images | length == 4 + - info_5.images[0].Id == pulled_images.results[0].image.Id + - info_5.images[1].Id == pulled_images.results[0].image.Id + - info_5.images[2].Id == pulled_images.results[1].image.Id + - info_5.images[3].Id == pulled_images.results[0].image.Id + - tag_5_check == tag_5 + + - name: Tag image 2 (idempotent, check mode) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: keep + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_6_check + diff: true + check_mode: true + + - name: Tag image 2 (idempotent) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: keep + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_6 + diff: true + + - assert: + that: + - tag_6 is not changed + - tag_6.diff.before == tag_6.diff.after + - tag_6.diff.before.images | length == 3 + - tag_6_check == tag_6 + + - name: Tag image 2 (only change wrong ones, check mode) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_7_check + diff: true + check_mode: true + + - name: Tag image 2 (only change wrong ones) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_7 + diff: true + + - name: Fetch image infos + docker_image_info: + name: "{{ image_names }}" + register: info_7 + + - assert: + that: + - tag_7 is changed + - tag_7.diff.before.images | length == 3 + - tag_7.diff.before.images[0] != tag_7.diff.after.images[0] + - tag_7.diff.before.images[1] != tag_7.diff.after.images[1] + - tag_7.diff.before.images[2] == tag_7.diff.after.images[2] + - tag_7.diff.before.images[0].id == pulled_images.results[0].image.Id + - tag_7.diff.before.images[1].id == pulled_images.results[0].image.Id + - tag_7.diff.after.images[0].id == pulled_images.results[1].image.Id + - tag_7.diff.after.images[1].id == pulled_images.results[1].image.Id + - tag_7.diff.after.images[2].id == pulled_images.results[1].image.Id + - info_7.images | length == 4 + - info_7.images[0].Id == pulled_images.results[1].image.Id + - info_7.images[1].Id == pulled_images.results[1].image.Id + - info_7.images[2].Id == pulled_images.results[1].image.Id + - info_7.images[3].Id == pulled_images.results[0].image.Id + - tag_7_check == tag_7 + + - name: Tag image 2 (idempotent, check mode) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_8_check + diff: true + check_mode: true + + - name: Tag image 2 (idempotent) + docker_image_tag: + name: "{{ image_2 }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-1:latest" + - "{{ iname_prefix }}-tagged-1:foo" + - "{{ iname_prefix }}-tagged-1:bar" + register: tag_8 + diff: true + + - assert: + that: + - tag_8 is not changed + - tag_8.diff.before == tag_8.diff.after + - tag_8.diff.before.images | length == 3 + - tag_8_check == tag_8 + + - name: Tag image 3 (source image has digest) + docker_image_tag: + name: "{{ image_3 }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-2:baz" + register: tag_9 + diff: true + + - assert: + that: + - tag_9 is changed + - tag_9.diff.before.images | length == 1 + - tag_9.diff.before.images[0].id == pulled_images.results[0].image.Id + - tag_9.diff.after.images[0].id == pulled_images.results[2].image.Id + + - name: Tag image 3 (source image is ID) + docker_image_tag: + name: "{{ pulled_images.results[2].image.Id }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-1:foo" + register: tag_10 + diff: true + + - assert: + that: + - tag_10 is changed + - tag_10.diff.before.images | length == 1 + - tag_10.diff.before.images[0].id == pulled_images.results[1].image.Id + - tag_10.diff.after.images[0].id == pulled_images.results[2].image.Id + + - name: Tag image 3 (fail because of digest) + docker_image_tag: + name: "{{ image_3 }}" + existing_images: overwrite + repository: + - "{{ iname_prefix }}-tagged-2@sha256:{{ docker_test_image_digest_v1 }}" + register: tag_11 + ignore_errors: true + + - assert: + that: + - tag_11 is failed + - >- + tag_11.msg == 'repository[1] must not have a digest; got: ' ~ iname_prefix ~ '-tagged-2@sha256:' ~ docker_test_image_digest_v1 + + - name: Tag image 3 (fail because of image ID) + docker_image_tag: + name: "{{ image_3 }}" + existing_images: overwrite + repository: + - "sha256:{{ docker_test_image_digest_v1 }}" + register: tag_12 + ignore_errors: true + + - assert: + that: + - tag_12 is failed + - >- + tag_12.msg == 'repository[1] must not be an image ID; got: sha256:' ~ docker_test_image_digest_v1 + + always: + - name: Remove tagged images + docker_image: + name: "{{ item }}" + source: local + state: absent + loop: "{{ image_names }}" + + when: docker_api_version is version('1.25', '>=') + +- fail: msg="Too old docker / docker-py version to run docker_image_info tests!" + when: not(docker_api_version is version('1.25', '>=')) and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)