From 307dc4045ae534f193ad56cdccf4c292e713adcb Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sat, 13 Jan 2024 14:36:26 +0100 Subject: [PATCH] Add docker_compose_v2_pull module (#751) * Add docker_compose_v2_pull module. * Improve and extend parsing of events. * Add ignores. * --policy is only available since Compose 2.22.0. --- plugins/module_utils/compose_v2.py | 231 ++++++++--- plugins/modules/docker_compose_v2_pull.py | 385 ++++++++++++++++++ .../targets/docker_compose_v2_pull/aliases | 6 + .../docker_compose_v2_pull/meta/main.yml | 10 + .../docker_compose_v2_pull/tasks/main.yml | 48 +++ .../docker_compose_v2_pull/tasks/run-test.yml | 7 + .../tasks/tests/pull.yml | 187 +++++++++ tests/sanity/ignore-2.10.txt | 1 + tests/sanity/ignore-2.11.txt | 1 + tests/sanity/ignore-2.12.txt | 1 + tests/sanity/ignore-2.13.txt | 1 + tests/sanity/ignore-2.9.txt | 1 + 12 files changed, 821 insertions(+), 58 deletions(-) create mode 100644 plugins/modules/docker_compose_v2_pull.py create mode 100644 tests/integration/targets/docker_compose_v2_pull/aliases create mode 100644 tests/integration/targets/docker_compose_v2_pull/meta/main.yml create mode 100644 tests/integration/targets/docker_compose_v2_pull/tasks/main.yml create mode 100644 tests/integration/targets/docker_compose_v2_pull/tasks/run-test.yml create mode 100644 tests/integration/targets/docker_compose_v2_pull/tasks/tests/pull.yml diff --git a/plugins/module_utils/compose_v2.py b/plugins/module_utils/compose_v2.py index eba92b27..272a830d 100644 --- a/plugins/module_utils/compose_v2.py +++ b/plugins/module_utils/compose_v2.py @@ -57,11 +57,24 @@ DOCKER_STATUS_ERROR = frozenset(( )) DOCKER_STATUS = frozenset(DOCKER_STATUS_DONE | DOCKER_STATUS_WORKING | DOCKER_STATUS_PULL | DOCKER_STATUS_ERROR) +DOCKER_PULL_PROGRESS_DONE = frozenset(( + 'Already exists', + 'Download complete', + 'Pull complete', +)) +DOCKER_PULL_PROGRESS_WORKING = frozenset(( + 'Pulling fs layer', + 'Downloading', + 'Verifying Checksum', + 'Extracting', +)) + class ResourceType(object): UNKNOWN = "unknown" NETWORK = "network" IMAGE = "image" + IMAGE_LAYER = "image-layer" VOLUME = "volume" CONTAINER = "container" SERVICE = "service" @@ -108,6 +121,18 @@ _RE_PULL_EVENT = re.compile( % '|'.join(re.escape(status) for status in DOCKER_STATUS_PULL) ) +_RE_PULL_PROGRESS = re.compile( + r'^' + r'\s*' + r'(?P\S+)' + r'\s+' + r'(?P%s)' + r'\s*' + r'(?:|\s\[[^]]+\]\s+\S+\s*)' + r'$' + % '|'.join(re.escape(status) for status in sorted(DOCKER_PULL_PROGRESS_DONE | DOCKER_PULL_PROGRESS_WORKING)) +) + _RE_ERROR_EVENT = re.compile( r'^' r'\s*' @@ -119,74 +144,150 @@ _RE_ERROR_EVENT = re.compile( % '|'.join(re.escape(status) for status in DOCKER_STATUS_ERROR) ) +_RE_CONTINUE_EVENT = re.compile( + r'^' + r'\s*' + r'(?P\S+)' + r'\s+' + r'-' + r'\s*' + r'(?P\S(?:|.*\S))' + r'$' +) + +_RE_SKIPPED_EVENT = re.compile( + r'^' + r'\s*' + r'(?P\S+)' + r'\s+' + r'Skipped -' + r'\s*' + r'(?P\S(?:|.*\S))' + r'$' +) + + +def _extract_event(line): + match = _RE_RESOURCE_EVENT.match(line) + if match is not None: + status = match.group('status') + msg = None + if status not in DOCKER_STATUS: + status, msg = msg, status + return Event( + ResourceType.from_docker_compose_event(match.group('resource_type')), + match.group('resource_id'), + status, + msg, + ) + match = _RE_PULL_EVENT.match(line) + if match: + return Event( + ResourceType.SERVICE, + match.group('service'), + match.group('status'), + None, + ) + match = _RE_ERROR_EVENT.match(line) + if match: + return Event( + ResourceType.UNKNOWN, + match.group('resource_id'), + match.group('status'), + None, + ) + match = _RE_PULL_PROGRESS.match(line) + if match: + return Event( + ResourceType.IMAGE_LAYER, + match.group('layer'), + match.group('status'), + None, + ) + match = _RE_SKIPPED_EVENT.match(line) + if match: + return Event( + ResourceType.UNKNOWN, + match.group('resource_id'), + 'Skipped', + match.group('msg'), + ) + return None + + +def _warn_missing_dry_run_prefix(line, warn_missing_dry_run_prefix, warn_function): + if warn_missing_dry_run_prefix and warn_function: + # This could be a bug, a change of docker compose's output format, ... + # Tell the user to report it to us :-) + warn_function( + 'Event line is missing dry-run mode marker: {0!r}. Please report this at ' + 'https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md' + .format(line) + ) + + +def _warn_unparsable_line(line, warn_function): + # This could be a bug, a change of docker compose's output format, ... + # Tell the user to report it to us :-) + if warn_function: + warn_function( + 'Cannot parse event from line: {0!r}. Please report this at ' + 'https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md' + .format(line) + ) + + +def _find_last_event_for(events, resource_id): + for index, event in enumerate(reversed(events)): + if event.resource_id == resource_id: + return len(events) - 1 - index, event + return None + + +def _concat_event_msg(event, append_msg): + return Event( + event.resource_type, + event.resource_id, + event.status, + '\n'.join(msg for msg in [event.msg, append_msg] if msg is not None), + ) + def parse_events(stderr, dry_run=False, warn_function=None): events = [] error_event = None - for line in stderr.splitlines(): + stderr_lines = stderr.splitlines() + if stderr_lines and stderr_lines[-1] == b'': + del stderr_lines[-1] + for line in stderr_lines: line = to_native(line.strip()) if not line: continue + warn_missing_dry_run_prefix = False if dry_run: if line.startswith(_DRY_RUN_MARKER): line = line[len(_DRY_RUN_MARKER):].lstrip() - elif error_event is None and warn_function: - # This could be a bug, a change of docker compose's output format, ... - # Tell the user to report it to us :-) - warn_function( - 'Event line is missing dry-run mode marker: {0!r}. Please report this at ' - 'https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md' - .format(line) - ) - match = _RE_RESOURCE_EVENT.match(line) - if match is not None: - status = match.group('status') - msg = None - if status not in DOCKER_STATUS: - status, msg = msg, status - event = Event( - ResourceType.from_docker_compose_event(match.group('resource_type')), - match.group('resource_id'), - status, - msg, - ) + else: + warn_missing_dry_run_prefix = True + event = _extract_event(line) + if event is not None: events.append(event) - if status in DOCKER_STATUS_ERROR: + if event.status in DOCKER_STATUS_ERROR: error_event = event else: error_event = None + _warn_missing_dry_run_prefix(line, warn_missing_dry_run_prefix, warn_function) continue - match = _RE_PULL_EVENT.match(line) + match = _RE_CONTINUE_EVENT.match(line) if match: - events.append( - Event( - ResourceType.SERVICE, - match.group('service'), - match.group('status'), - None, - ) - ) - error_event = None - continue - match = _RE_ERROR_EVENT.match(line) - if match: - error_event = Event( - ResourceType.UNKNOWN, - match.group('resource_id'), - match.group('status'), - None, - ) - events.append(error_event) - continue + # Continuing an existing event + index_event = _find_last_event_for(events, match.group('resource_id')) + if index_event is not None: + index, event = index_event + events[-1] = _concat_event_msg(event, match.group('msg')) if error_event is not None: # Unparsable line that apparently belongs to the previous error event - error_event = Event( - error_event.resource_type, - error_event.resource_id, - error_event.status, - '\n'.join(msg for msg in [error_event.msg, line] if msg is not None), - ) - events[-1] = error_event + events[-1] = _concat_event_msg(error_event, line) continue if line.startswith('Error '): # Error message that is independent of an error event @@ -198,14 +299,18 @@ def parse_events(stderr, dry_run=False, warn_function=None): ) events.append(error_event) continue - # This could be a bug, a change of docker compose's output format, ... - # Tell the user to report it to us :-) - if warn_function: - warn_function( - 'Cannot parse event from line: {0!r}. Please report this at ' - 'https://github.com/ansible-collections/community.docker/issues/new?assignees=&labels=&projects=&template=bug_report.md' - .format(line) + if len(stderr_lines) == 1: + # **Very likely** an error message that is independent of an error event + error_event = Event( + ResourceType.UNKNOWN, + '', + 'Error', + line, ) + events.append(error_event) + continue + _warn_missing_dry_run_prefix(line, warn_missing_dry_run_prefix, warn_function) + _warn_unparsable_line(line, warn_function) return events @@ -218,8 +323,18 @@ def has_changes(events): def extract_actions(events): actions = [] + pull_actions = set() for event in events: - if event.status in DOCKER_STATUS_WORKING: + if event.resource_type == ResourceType.IMAGE_LAYER and event.status in DOCKER_PULL_PROGRESS_WORKING: + pull_id = (event.resource_id, event.status) + if pull_id not in pull_actions: + pull_actions.add(pull_id) + actions.append({ + 'what': event.resource_type, + 'id': event.resource_id, + 'status': event.status, + }) + if event.resource_type != ResourceType.IMAGE_LAYER and event.status in DOCKER_STATUS_WORKING: actions.append({ 'what': event.resource_type, 'id': event.resource_id, diff --git a/plugins/modules/docker_compose_v2_pull.py b/plugins/modules/docker_compose_v2_pull.py new file mode 100644 index 00000000..9abd4b2d --- /dev/null +++ b/plugins/modules/docker_compose_v2_pull.py @@ -0,0 +1,385 @@ +#!/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_compose_v2_pull + +short_description: Pull a Docker compose project + +version_added: 3.6.0 + +description: + - Uses Docker Compose to pull images for a project. + +extends_documentation_fragment: + - community.docker.compose_v2 + - community.docker.docker.cli_documentation + - community.docker.attributes + - community.docker.attributes.actiongroup_docker + +attributes: + check_mode: + support: full + diff_mode: + support: none + +options: + policy: + description: + - Whether to pull images before running. This is used when C(docker compose up) is ran. + - V(always) ensures that the images are always pulled, even when already present on the Docker daemon. + - V(missing) only pulls them when they are not present on the Docker daemon. This is only supported since Docker Compose 2.22.0. + type: str + choices: + - always + - missing + default: always + +requirements: + - "Docker CLI with Docker compose plugin 2.18.0 or later" + +author: + - Felix Fontein (@felixfontein) + +notes: + - |- + The Docker compose CLI plugin has no stable output format (see for example U(https://github.com/docker/compose/issues/10872)), + and for the main operations also no machine friendly output format. The module tries to accomodate this with various + version-dependent behavior adjustments and with testing older and newer versions of the Docker compose CLI plugin. + + Currently the module is tested with multiple plugin versions between 2.18.1 and 2.23.3. The exact list of plugin versions + will change over time. New releases of the Docker compose CLI plugin can break this module at any time. + +seealso: + - module: community.docker.docker_compose +''' + +EXAMPLES = ''' +# Examples use the django example at https://docs.docker.com/compose/django. Follow it to create the +# flask directory + +- name: Run using a project directory + hosts: localhost + gather_facts: false + tasks: + - name: Tear down existing services + community.docker.docker_compose_v2: + project_src: flask + state: absent + + - name: Create and start services + community.docker.docker_compose_v2: + project_src: flask + register: output + + - name: Show results + ansible.builtin.debug: + var: output + + - name: Run `docker-compose up` again + community.docker.docker_compose_v2: + project_src: flask + register: output + + - name: Show results + ansible.builtin.debug: + var: output + + - ansible.builtin.assert: + that: not output.changed + + - name: Stop all services + community.docker.docker_compose_v2: + project_src: flask + state: stopped + register: output + + - name: Show results + ansible.builtin.debug: + var: output + + - name: Verify that web and db services are not running + ansible.builtin.assert: + that: + - "not output.services.web.flask_web_1.state.running" + - "not output.services.db.flask_db_1.state.running" + + - name: Restart services + community.docker.docker_compose_v2: + project_src: flask + state: restarted + register: output + + - name: Show results + ansible.builtin.debug: + var: output + + - name: Verify that web and db services are running + ansible.builtin.assert: + that: + - "output.services.web.flask_web_1.state.running" + - "output.services.db.flask_db_1.state.running" +''' + +RETURN = ''' +containers: + description: + - A list of containers associated to the service. + returned: success + type: list + elements: dict + contains: + Command: + description: + - The container's command. + type: raw + CreatedAt: + description: + - The timestamp when the container was created. + type: str + sample: "2024-01-02 12:20:41 +0100 CET" + ExitCode: + description: + - The container's exit code. + type: int + Health: + description: + - The container's health check. + type: raw + ID: + description: + - The container's ID. + type: str + sample: "44a7d607219a60b7db0a4817fb3205dce46e91df2cb4b78a6100b6e27b0d3135" + Image: + description: + - The container's image. + type: str + Labels: + description: + - Labels for this container. + type: dict + LocalVolumes: + description: + - The local volumes count. + type: str + Mounts: + description: + - Mounts. + type: str + Name: + description: + - The container's primary name. + type: str + Names: + description: + - List of names of the container. + type: list + elements: str + Networks: + description: + - List of networks attached to this container. + type: list + elements: str + Ports: + description: + - List of port assignments as a string. + type: str + Publishers: + description: + - List of port assigments. + type: list + elements: dict + contains: + URL: + description: + - Interface the port is bound to. + type: str + TargetPort: + description: + - The container's port the published port maps to. + type: int + PublishedPort: + description: + - The port that is published. + type: int + Protocol: + description: + - The protocol. + type: str + choices: + - tcp + - udp + RunningFor: + description: + - Amount of time the container runs. + type: str + Service: + description: + - The name of the service. + type: str + Size: + description: + - The container's size. + type: str + sample: "0B" + State: + description: + - The container's state. + type: str + sample: running + Status: + description: + - The container's status. + type: str + sample: Up About a minute +images: + description: + - A list of images associated to the service. + returned: success + type: list + elements: dict + contains: + ID: + description: + - The image's ID. + type: str + sample: sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1 + ContainerName: + description: + - Name of the conainer this image is used by. + type: str + Repository: + description: + - The repository where this image belongs to. + type: str + Tag: + description: + - The tag of the image. + type: str + Size: + description: + - The image's size in bytes. + type: int +actions: + description: + - A list of actions that have been applied. + returned: success + type: list + elements: dict + contains: + what: + description: + - What kind of resource was changed. + type: str + sample: container + choices: + - container + - image + - network + - service + - unknown + - volume + id: + description: + - The ID of the resource that was changed. + type: str + sample: container + status: + description: + - The status change that happened. + type: str + sample: Creating + choices: + - Starting + - Exiting + - Restarting + - Creating + - Stopping + - Killing + - Removing + - Recreating + - Pulling +''' + +import traceback + +from ansible.module_utils.common.text.converters import to_native + +from ansible_collections.community.docker.plugins.module_utils.common_cli import ( + AnsibleModuleDockerClient, + DockerException, +) + +from ansible_collections.community.docker.plugins.module_utils.compose_v2 import ( + BaseComposeManager, + common_compose_argspec, +) + +from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion + + +DOCKER_COMPOSE_MINIMAL_VERSION = '2.18.0' + + +class PullManager(BaseComposeManager): + def __init__(self, client): + super(PullManager, self).__init__(client, min_version=DOCKER_COMPOSE_MINIMAL_VERSION) + parameters = self.client.module.params + + self.policy = parameters['policy'] + + if self.policy != 'always' and self.compose_version < LooseVersion('2.22.0'): + # https://github.com/docker/compose/pull/10981 - 2.22.0 + self.client.fail('A pull policy other than always is only supported since Docker Compose 2.22.0. {0} has version {1}'.format( + self.client.get_cli(), self.compose_version)) + + def get_pull_cmd(self, dry_run, no_start=False): + args = self.get_base_args() + ['pull'] + if self.policy != 'always': + args.extend(['--policy', self.policy]) + if dry_run: + args.append('--dry-run') + args.append('--') + return args + + def run(self): + result = dict() + args = self.get_pull_cmd(self.check_mode) + rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src) + events = self.parse_events(stderr, dry_run=self.check_mode) + self.emit_warnings(events) + self.update_result(result, events, stdout, stderr) + self.update_failed(result, events, args, stdout, stderr, rc) + self.cleanup_result(result) + return result + + +def main(): + argument_spec = dict( + policy=dict(type='str', choices=['always', 'missing'], default='always'), + ) + argument_spec.update(common_compose_argspec()) + + client = AnsibleModuleDockerClient( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + result = PullManager(client).run() + client.module.exit_json(**result) + except DockerException as e: + client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/docker_compose_v2_pull/aliases b/tests/integration/targets/docker_compose_v2_pull/aliases new file mode 100644 index 00000000..2e1acc0a --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_pull/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_compose_v2_pull/meta/main.yml b/tests/integration/targets/docker_compose_v2_pull/meta/main.yml new file mode 100644 index 00000000..aefcf50f --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_pull/meta/main.yml @@ -0,0 +1,10 @@ +--- +# 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_cli_compose + # The Python dependencies are needed for the other modules + - setup_docker_python_deps + - setup_remote_tmp_dir diff --git a/tests/integration/targets/docker_compose_v2_pull/tasks/main.yml b/tests/integration/targets/docker_compose_v2_pull/tasks/main.yml new file mode 100644 index 00000000..8813f0e7 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_pull/tasks/main.yml @@ -0,0 +1,48 @@ +--- +# 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 # +#################################################################### + +# Create random name prefix (for services, ...) +- name: Create random container name prefix + set_fact: + name_prefix: "{{ 'ansible-docker-test-%0x' % ((2**32) | random) }}" + cnames: [] + dnetworks: [] + +- debug: + msg: "Using name prefix {{ name_prefix }}" + +# Run the tests +- block: + - command: docker compose --help + + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + loop_control: + loop_var: test_name + + always: + - name: "Make sure all containers are removed" + docker_container: + name: "{{ item }}" + state: absent + force_kill: true + with_items: "{{ cnames }}" + diff: false + + - name: "Make sure all networks are removed" + docker_network: + name: "{{ item }}" + state: absent + force: true + with_items: "{{ dnetworks }}" + diff: false + + when: docker_has_compose and docker_compose_version is version('2.18.0', '>=') diff --git a/tests/integration/targets/docker_compose_v2_pull/tasks/run-test.yml b/tests/integration/targets/docker_compose_v2_pull/tasks/run-test.yml new file mode 100644 index 00000000..72a58962 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_pull/tasks/run-test.yml @@ -0,0 +1,7 @@ +--- +# 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 + +- name: "Loading tasks from {{ test_name }}" + include_tasks: "{{ test_name }}" diff --git a/tests/integration/targets/docker_compose_v2_pull/tasks/tests/pull.yml b/tests/integration/targets/docker_compose_v2_pull/tasks/tests/pull.yml new file mode 100644 index 00000000..8d7ae469 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_pull/tasks/tests/pull.yml @@ -0,0 +1,187 @@ +--- +# 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 + +- vars: + pname: "{{ name_prefix }}-pull" + cname: "{{ name_prefix }}-cont" + non_existing_image: does-not-exist:latest + project_src: "{{ remote_tmp_dir }}/{{ pname }}" + test_service_non_existing: | + version: '3' + services: + {{ cname }}: + image: {{ non_existing_image }} + test_service_alpine: | + version: '3' + services: + {{ cname }}: + image: {{ docker_test_image_alpine }} + command: /bin/sh -c 'sleep 10m' + stop_grace_period: 1s + + block: + - name: Registering container name + set_fact: + cnames: "{{ cnames + [pname ~ '-' ~ cname ~ '-1'] }}" + dnetworks: "{{ dnetworks + [pname ~ '_default'] }}" + + - name: Create project directory + file: + path: '{{ project_src }}' + state: directory + + - name: Make sure images are not around + docker_image_remove: + name: '{{ item }}' + loop: + - '{{ non_existing_image }}' + - '{{ docker_test_image_alpine }}' + +#################################################################### +## Missing image ################################################### +#################################################################### + + - name: Template project file with non-existing image + copy: + dest: '{{ project_src }}/docker-compose.yml' + content: '{{ test_service_non_existing }}' + + - name: Pull (check) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + check_mode: true + register: pull_1_check + ignore_errors: true + + - name: Pull + docker_compose_v2_pull: + project_src: '{{ project_src }}' + register: pull_1 + ignore_errors: true + + - assert: + that: + - pull_1_check is failed or pull_1_check is changed + - pull_1_check is changed or pull_1_check.msg.startswith('Error when processing ') + - pull_1 is failed + - pull_1.msg.startswith('Error when processing ') + +#################################################################### +## Regular image ################################################### +#################################################################### + + - name: Template project file with Alpine image + copy: + dest: '{{ project_src }}/docker-compose.yml' + content: '{{ test_service_alpine }}' + + - when: docker_compose_version is version('2.22.0', '>=') + block: + - name: Pull with policy=missing (check) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: missing + check_mode: true + register: pull_1_check + + - name: Pull with policy=missing + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: missing + register: pull_1 + + - name: Pull with policy=missing (idempotent, check) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: missing + check_mode: true + register: pull_2_check + + - name: Pull with policy=missing (idempotent) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: missing + register: pull_2 + + - name: Pull with policy=always (check) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: always + check_mode: true + register: pull_3_check + + - name: Pull with policy=always + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: always + register: pull_3 + + - assert: + that: + - pull_1_check is changed + - pull_1_check.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_1_check.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_1_check.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + - pull_1 is changed + - pull_1.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_1.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_1.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + - pull_2_check is not changed + - pull_2 is not changed + - pull_3_check is changed + - pull_3_check.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_3_check.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_3_check.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + - pull_3 is changed + - pull_3.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_3.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_3.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + + - when: docker_compose_version is version('2.22.0', '<') + block: + - name: Pull with policy=always (check) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: always + check_mode: true + register: pull_1_check + + - name: Pull with policy=always + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: always + register: pull_1 + + - name: Pull with policy=always (again, check) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: always + check_mode: true + register: pull_2_check + + - name: Pull with policy=always (again) + docker_compose_v2_pull: + project_src: '{{ project_src }}' + policy: always + register: pull_2 + + - assert: + that: + - pull_1_check is changed + - pull_1_check.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_1_check.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_1_check.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + - pull_1 is changed + - pull_1.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_1.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_1.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + - pull_2_check is changed + - pull_2_check.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_2_check.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_2_check.actions | selectattr('status', 'eq', 'Recreating') | length == 0 + - pull_2 is changed + - pull_2.actions | selectattr('status', 'eq', 'Pulling') | first + - pull_2.actions | selectattr('status', 'eq', 'Creating') | length == 0 + - pull_2.actions | selectattr('status', 'eq', 'Recreating') | length == 0 diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index e5d7ab53..2bc38ac2 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -8,5 +8,6 @@ plugins/modules/current_container_facts.py validate-modules:return-syntax-error plugins/module_utils/module_container/module.py compile-2.6!skip # Uses Python 2.7+ syntax plugins/module_utils/module_container/module.py import-2.6!skip # Uses Python 2.7+ syntax plugins/modules/docker_compose_v2.py validate-modules:return-syntax-error +plugins/modules/docker_compose_v2_pull.py validate-modules:return-syntax-error plugins/modules/docker_container.py import-2.6!skip # Import uses Python 2.7+ syntax plugins/modules/docker_container_copy_into.py validate-modules:undocumented-parameter # _max_file_size_for_diff is used by the action plugin diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index e5d7ab53..2bc38ac2 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -8,5 +8,6 @@ plugins/modules/current_container_facts.py validate-modules:return-syntax-error plugins/module_utils/module_container/module.py compile-2.6!skip # Uses Python 2.7+ syntax plugins/module_utils/module_container/module.py import-2.6!skip # Uses Python 2.7+ syntax plugins/modules/docker_compose_v2.py validate-modules:return-syntax-error +plugins/modules/docker_compose_v2_pull.py validate-modules:return-syntax-error plugins/modules/docker_container.py import-2.6!skip # Import uses Python 2.7+ syntax plugins/modules/docker_container_copy_into.py validate-modules:undocumented-parameter # _max_file_size_for_diff is used by the action plugin diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 91497724..f3c4575f 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -1,4 +1,5 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen plugins/modules/current_container_facts.py validate-modules:return-syntax-error plugins/modules/docker_compose_v2.py validate-modules:return-syntax-error +plugins/modules/docker_compose_v2_pull.py validate-modules:return-syntax-error plugins/modules/docker_container_copy_into.py validate-modules:undocumented-parameter # _max_file_size_for_diff is used by the action plugin diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt index c0d5c549..c1e354d2 100644 --- a/tests/sanity/ignore-2.13.txt +++ b/tests/sanity/ignore-2.13.txt @@ -1,3 +1,4 @@ .azure-pipelines/scripts/publish-codecov.py replace-urlopen plugins/modules/docker_compose_v2.py validate-modules:return-syntax-error +plugins/modules/docker_compose_v2_pull.py validate-modules:return-syntax-error plugins/modules/docker_container_copy_into.py validate-modules:undocumented-parameter # _max_file_size_for_diff is used by the action plugin diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 8e4b495d..d3b3bd88 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -7,5 +7,6 @@ plugins/module_utils/module_container/module.py compile-2.6!skip # Uses Python 2.7+ syntax plugins/module_utils/module_container/module.py import-2.6!skip # Uses Python 2.7+ syntax plugins/modules/docker_compose_v2.py validate-modules:return-syntax-error +plugins/modules/docker_compose_v2_pull.py validate-modules:return-syntax-error plugins/modules/docker_container.py import-2.6!skip # Import uses Python 2.7+ syntax plugins/modules/docker_container_copy_into.py validate-modules:undocumented-parameter # _max_file_size_for_diff is used by the action plugin