diff --git a/changelogs/fragments/797-docker_container-pull.yml b/changelogs/fragments/797-docker_container-pull.yml new file mode 100644 index 00000000..4d83139d --- /dev/null +++ b/changelogs/fragments/797-docker_container-pull.yml @@ -0,0 +1,3 @@ +minor_changes: + - "docker_container - the ``pull_check_mode_behavior`` option now allows to control the module's behavior in check mode when ``pull=always`` (https://github.com/ansible-collections/community.docker/issues/792, https://github.com/ansible-collections/community.docker/pull/797)." + - "docker_container - the ``pull`` option now accepts the three values ``never``, ``missing_image`` (default), and ``never``, next to the previously valid values ``true`` (equivalent to ``always``) and ``false`` (equivalent to ``missing_image``). This allows the equivalent to ``--pull=never`` from the Docker command line (https://github.com/ansible-collections/community.docker/issues/783, https://github.com/ansible-collections/community.docker/pull/797)." diff --git a/plugins/module_utils/module_container/module.py b/plugins/module_utils/module_container/module.py index 94dd07de..5d819efa 100644 --- a/plugins/module_utils/module_container/module.py +++ b/plugins/module_utils/module_container/module.py @@ -78,6 +78,11 @@ class ContainerManager(DockerBaseClass): self.param_output_logs = self.module.params['output_logs'] self.param_paused = self.module.params['paused'] self.param_pull = self.module.params['pull'] + if self.param_pull is True: + self.param_pull = 'always' + if self.param_pull is False: + self.param_pull = 'missing' + self.param_pull_check_mode_behavior = self.module.params['pull_check_mode_behavior'] self.param_recreate = self.module.params['recreate'] self.param_removal_wait_timeout = self.module.params['removal_wait_timeout'] self.param_restart = self.module.params['restart'] @@ -444,21 +449,28 @@ class ContainerManager(DockerBaseClass): if not tag: tag = "latest" image = self.engine_driver.inspect_image_by_name(self.client, repository, tag) - if not image or self.param_pull: + if not image and self.param_pull == "never": + self.client.fail("Cannot find image with name %s:%s, and pull=never" % (repository, tag)) + if not image or self.param_pull == "always": if not self.check_mode: self.log("Pull the image.") image, alreadyToLatest = self.engine_driver.pull_image( self.client, repository, tag, platform=self.module.params['platform']) if alreadyToLatest: self.results['changed'] = False + self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag), changed=False)) else: self.results['changed'] = True - self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag))) - elif not image: - # If the image isn't there, claim we'll pull. - # (Implicitly: if the image is there, claim it already was latest.) + self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag), changed=True)) + elif not image or self.param_pull_check_mode_behavior == 'always': + # If the image isn't there, or pull_check_mode_behavior == 'always', claim we'll + # pull. (Implicitly: if the image is there, claim it already was latest unless + # pull_check_mode_behavior == 'always'.) self.results['changed'] = True - self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag))) + action = dict(pulled_image="%s:%s" % (repository, tag)) + if not image: + action['changed'] = True + self.results['actions'].append(action) self.log("image") self.log(image, pretty_print=True) @@ -860,7 +872,8 @@ def run_module(engine_driver): networks_cli_compatible=dict(type='bool', default=True), output_logs=dict(type='bool', default=False), paused=dict(type='bool'), - pull=dict(type='bool', default=False), + pull=dict(type='raw', choices=['never', 'missing', 'always', True, False], default='missing'), + pull_check_mode_behavior=dict(type='str', choices=['image_not_present', 'always'], default='image_not_present'), purge_networks=dict(type='bool', default=False, removed_in_version='4.0.0', removed_from_collection='community.docker'), recreate=dict(type='bool', default=False), removal_wait_timeout=dict(type='float'), diff --git a/plugins/modules/docker_container.py b/plugins/modules/docker_container.py index adf07aaf..d7dbc378 100644 --- a/plugins/modules/docker_container.py +++ b/plugins/modules/docker_container.py @@ -35,7 +35,8 @@ attributes: check_mode: support: partial details: - - When trying to pull an image, the module assumes this is always changed in check mode. + - When trying to pull an image, the module assumes this is never changed in check mode except when the image is not present on the Docker daemon. + - This behavior can be configured with O(pull_check_mode_behavior). diff_mode: support: full @@ -788,12 +789,37 @@ options: - ports pull: description: - - If true, always pull the latest version of an image. Otherwise, will only pull an image - when missing. + - If set to V(never), will never try to pull an image. Will fail if the image is not available + on the Docker daemon. + - If set to V(missing) or V(false), only pull the image if it is not available on the Docker + daemon. This is the default behavior. + - If set to V(always) or V(true), always try to pull the latest version of the image. - "B(Note:) images are only pulled when specified by name. If the image is specified - as a image ID (hash), it cannot be pulled." - type: bool - default: false + as a image ID (hash), it cannot be pulled, and this option is ignored." + - "B(Note:) the values V(never), V(missing), and V(always) are only available since + community.docker 3.8.0. Earlier versions only support V(true) and V(false)." + type: raw + choices: + - never + - missing + - always + - true + - false + default: missing + pull_check_mode_behavior: + description: + - Allows to adjust the behavior when O(pull=always) or O(pull=true) in check mode. + - Since the Docker daemon does not expose any functionality to test whether a pull will result + in a changed image, the module by default acts like O(pull=always) only results in a change when + the image is not present. + - If set to V(image_not_present) (default), only report changes in check mode when the image is not present. + - If set to V(always), always report changes in check mode. + type: str + default: image_not_present + choices: + - image_not_present + - always + version_added: 3.8.0 purge_networks: description: - Remove the container from ALL networks not included in O(networks) parameter. diff --git a/tests/integration/targets/docker_container/tasks/tests/options.yml b/tests/integration/targets/docker_container/tasks/tests/options.yml index b6b35706..845c7897 100644 --- a/tests/integration/targets/docker_container/tasks/tests/options.yml +++ b/tests/integration/targets/docker_container/tasks/tests/options.yml @@ -3642,6 +3642,149 @@ avoid such warnings, please quote the value.' in (log_options_2.warnings | defau ('API version is ' ~ docker_api_version ~ '.') in platform_1.msg and 'Minimum version required is 1.41 ' in platform_1.msg when: docker_api_version is version('1.41', '<') +#################################################################### +## pull / pull_check_mode_behavior ################################# +#################################################################### + +- name: Remove hello-world image + docker_image_remove: + name: "{{ docker_test_image_hello_world }}" + +- name: pull (pull=never) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: never + debug: true + register: pull_1 + ignore_errors: true + +- name: pull (pull=missing, check mode) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: missing + debug: true + register: pull_2 + check_mode: true + ignore_errors: true + +- name: pull (pull=missing) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: missing + debug: true + register: pull_3 + ignore_errors: true + +- name: pull (pull=missing, idempotent, check mode) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: missing + debug: true + register: pull_4 + check_mode: true + ignore_errors: true + +- name: pull (pull=missing, idempotent) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: missing + debug: true + register: pull_5 + ignore_errors: true + +- name: pull (pull=always, check mode, pull_check_mode_behavior=image_not_present) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: always + pull_check_mode_behavior: image_not_present + debug: true + register: pull_6 + check_mode: true + ignore_errors: true + +- name: pull (pull=always, check mode, pull_check_mode_behavior=always) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: always + pull_check_mode_behavior: always + debug: true + register: pull_7 + check_mode: true + ignore_errors: true + +- name: pull (pull=always) + docker_container: + image: "{{ docker_test_image_hello_world }}" + name: "{{ cname }}" + state: present + pull: always + debug: true + register: pull_8 + ignore_errors: true + +- name: cleanup + docker_container: + name: "{{ cname }}" + state: absent + force_kill: true + diff: false + +- assert: + that: + - pull_1 is failed + - pull_1.msg == ("Cannot find image with name " ~ docker_test_image_hello_world ~ ", and pull=never") + - pull_2 is changed + - pulled_image_action not in pull_2.actions + - pulled_image_action_changed in pull_2.actions + - pulled_image_action_unchanged not in pull_2.actions + - pull_3 is changed + - pulled_image_action not in pull_3.actions + - pulled_image_action_changed in pull_3.actions + - pulled_image_action_unchanged not in pull_3.actions + - pull_4 is not changed + - pulled_image_action not in pull_4.actions + - pulled_image_action_changed not in pull_4.actions + - pulled_image_action_unchanged not in pull_4.actions + - pull_5 is not changed + - pulled_image_action not in pull_5.actions + - pulled_image_action_changed not in pull_5.actions + - pulled_image_action_unchanged not in pull_5.actions + - pull_6 is not changed + - pulled_image_action not in pull_6.actions + - pulled_image_action_changed not in pull_6.actions + - pulled_image_action_unchanged not in pull_6.actions + - pull_7 is changed + - pulled_image_action in pull_7.actions + - pulled_image_action_changed not in pull_7.actions + - pulled_image_action_unchanged not in pull_7.actions + - pull_8 is not changed + - pulled_image_action not in pull_8.actions + - pulled_image_action_changed not in pull_8.actions + - pulled_image_action_unchanged in pull_8.actions + vars: + pulled_image_action: + pulled_image: "{{ docker_test_image_hello_world }}" + pulled_image_action_changed: + pulled_image: "{{ docker_test_image_hello_world }}" + changed: true + pulled_image_action_unchanged: + pulled_image: "{{ docker_test_image_hello_world }}" + changed: false + #################################################################### ## privileged ###################################################### ####################################################################