From 3cca4185be3fd55cc580bdf7c0f85562a997371b Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 5 Nov 2018 01:25:11 +0100 Subject: [PATCH] docker_container: simplify minimal required version per option handling (#47711) * Store parsed docker-py / docker API versions in client. * Began refactoring 'minimal required version' for docker_container options. * Removing some fake defaults. * Added changelog. * Improve tests (check older docker versions). * Fix comparison. The breaking point is not docker-py 2.0.0, but 1.10.0. (Verified by testing with these versions.) * Move docker-py/API version detection to setup_docker. * Add YAML document starter. * docker_network requirement for docker-py was bumped to 1.10.0 in #47492. --- ...ocker_container-minimal-version-checks.yml | 2 + lib/ansible/module_utils/docker_common.py | 11 +- .../modules/cloud/docker/docker_container.py | 320 +++++++++--------- .../targets/docker_config/tasks/main.yml | 8 +- .../tasks/test_docker_config.yml | 1 + .../targets/docker_container/tasks/main.yml | 6 +- .../docker_container/tasks/tests/options.yml | 274 +++++++++++---- .../targets/setup_docker/tasks/main.yml | 18 + 8 files changed, 410 insertions(+), 230 deletions(-) create mode 100644 changelogs/fragments/47711-docker_container-minimal-version-checks.yml diff --git a/changelogs/fragments/47711-docker_container-minimal-version-checks.yml b/changelogs/fragments/47711-docker_container-minimal-version-checks.yml new file mode 100644 index 0000000000..9f55b6efdd --- /dev/null +++ b/changelogs/fragments/47711-docker_container-minimal-version-checks.yml @@ -0,0 +1,2 @@ +bugfixes: +- "docker_container - refactored minimal docker-py/API version handling, and fixing such handling of some options." diff --git a/lib/ansible/module_utils/docker_common.py b/lib/ansible/module_utils/docker_common.py index 3743dd271c..4a36686a63 100644 --- a/lib/ansible/module_utils/docker_common.py +++ b/lib/ansible/module_utils/docker_common.py @@ -188,6 +188,8 @@ class AnsibleDockerClient(Client): NEEDS_DOCKER_PY2 = (LooseVersion(min_docker_version) >= LooseVersion('2.0.0')) + self.docker_py_version = LooseVersion(docker_version) + if HAS_DOCKER_MODELS and HAS_DOCKER_SSLADAPTER: self.fail("Cannot have both the docker-py and docker python modules installed together as they use the same namespace and " "cause a corrupt installation. Please uninstall both packages, and re-install only the docker-py or docker python " @@ -201,7 +203,7 @@ class AnsibleDockerClient(Client): msg = "Failed to import docker or docker-py - %s. Try `pip install docker` or `pip install docker-py` (Python 2.6)" self.fail(msg % HAS_DOCKER_ERROR) - if LooseVersion(docker_version) < LooseVersion(min_docker_version): + if self.docker_py_version < LooseVersion(min_docker_version): if NEEDS_DOCKER_PY2: if docker_version < LooseVersion('2.0'): msg = "Error: docker-py version is %s, while this module requires docker %s. Try `pip uninstall docker-py` and then `pip install docker`" @@ -226,9 +228,10 @@ class AnsibleDockerClient(Client): self.fail("Error connecting: %s" % exc) if min_docker_api_version is not None: - docker_api_version = self.version()['ApiVersion'] - if LooseVersion(docker_api_version) < LooseVersion(min_docker_api_version): - self.fail('docker API version is %s. Minimum version required is %s.' % (docker_api_version, min_docker_api_version)) + self.docker_api_version_str = self.version()['ApiVersion'] + self.docker_api_version = LooseVersion(self.docker_api_version_str) + if self.docker_api_version < LooseVersion(min_docker_api_version): + self.fail('docker API version is %s. Minimum version required is %s.' % (self.docker_api_version_str, min_docker_api_version)) def log(self, msg, pretty_print=False): pass diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py index d9ea24c4fb..65aa9da104 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -76,11 +76,9 @@ options: cpu_period: description: - Limit CPU CFS (Completely Fair Scheduler) period - default: 0 cpu_quota: description: - Limit CPU CFS (Completely Fair Scheduler) quota - default: 0 cpuset_cpus: description: - CPUs in which to allow execution C(1,3) or C(1-3). @@ -243,7 +241,6 @@ options: Unit can be C(B) (byte), C(K) (kibibyte, 1024B), C(M) (mebibyte), C(G) (gibibyte), C(T) (tebibyte), or C(P) (pebibyte). Minimum is C(4M)." - Omitting the unit defaults to bytes. - default: 0 labels: description: - Dictionary of key value pairs. @@ -278,14 +275,12 @@ options: Unit can be C(B) (byte), C(K) (kibibyte, 1024B), C(M) (mebibyte), C(G) (gibibyte), C(T) (tebibyte), or C(P) (pebibyte)." - Omitting the unit defaults to bytes. - default: 0 memory_swap: description: - "Total memory limit (memory + swap, format: C([])). Number is a positive integer. Unit can be C(B) (byte), C(K) (kibibyte, 1024B), C(M) (mebibyte), C(G) (gibibyte), C(T) (tebibyte), or C(P) (pebibyte)." - Omitting the unit defaults to bytes. - default: 0 memory_swappiness: description: - Tune a container's memory swappiness behavior. Accepts an integer between 0 and 100. @@ -318,11 +313,9 @@ options: description: - Whether or not to disable OOM Killer for the container. type: bool - default: 'no' oom_score_adj: description: - An integer value containing the score given to the container in order to tune OOM killer preferences. - default: 0 version_added: "2.2" output_logs: description: @@ -405,7 +398,6 @@ options: restart_retries: description: - Use with restart policy to control maximum number of restart attempts. - default: 0 runtime: description: - Runtime to use for the container. @@ -797,7 +789,7 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import human_to_bytes from ansible.module_utils.docker_common import ( - HAS_DOCKER_PY_2, HAS_DOCKER_PY_3, AnsibleDockerClient, + AnsibleDockerClient, DockerBaseClass, sanitize_result, is_image_name_id, compare_generic, ) @@ -805,11 +797,11 @@ from ansible.module_utils.six import string_types try: from docker import utils - if HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3: + from ansible.module_utils.docker_common import docker_version + if LooseVersion(docker_version) >= LooseVersion('1.10.0'): from docker.types import Ulimit, LogConfig else: from docker.utils.types import Ulimit, LogConfig - from ansible.module_utils.docker_common import docker_version except Exception as dummy: # missing docker-py handled in ansible.module_utils.docker pass @@ -1058,28 +1050,23 @@ class TaskParameters(DockerBaseClass): ''' update_parameters = dict( + blkio_weight='blkio_weight', cpu_period='cpu_period', cpu_quota='cpu_quota', cpu_shares='cpu_shares', cpuset_cpus='cpuset_cpus', + cpuset_mems='cpuset_mems', mem_limit='memory', mem_reservation='memory_reservation', memswap_limit='memory_swap', kernel_memory='kernel_memory', ) - if self.client.HAS_BLKIO_WEIGHT_OPT: - # blkio_weight is only supported in docker>=1.9 - update_parameters['blkio_weight'] = 'blkio_weight' - - if self.client.HAS_CPUSET_MEMS_OPT: - # cpuset_mems is only supported in docker>=2.3 - update_parameters['cpuset_mems'] = 'cpuset_mems' - result = dict() for key, value in update_parameters.items(): if getattr(self, value, None) is not None: - result[key] = getattr(self, value) + if self.client.option_minimal_versions[value]['supported']: + result[key] = getattr(self, value) return result @property @@ -1103,18 +1090,15 @@ class TaskParameters(DockerBaseClass): labels='labels', stop_signal='stop_signal', working_dir='working_dir', + stop_timeout='stop_timeout', + healthcheck='healthcheck', ) - if not HAS_DOCKER_PY_3: + if self.client.docker_py_version < LooseVersion('3.0'): + # cpu_shares and volume_driver moved to create_host_config in > 3 create_params['cpu_shares'] = 'cpu_shares' create_params['volume_driver'] = 'volume_driver' - if self.client.HAS_STOP_TIMEOUT_OPT: - create_params['stop_timeout'] = 'stop_timeout' - - if self.client.HAS_HEALTHCHECK_OPT: - create_params['healthcheck'] = 'healthcheck' - result = dict( host_config=self._host_config(), volumes=self._get_mounts(), @@ -1122,7 +1106,8 @@ class TaskParameters(DockerBaseClass): for key, value in create_params.items(): if getattr(self, value, None) is not None: - result[key] = getattr(self, value) + if self.client.option_minimal_versions[value]['supported']: + result[key] = getattr(self, value) return result def _expand_host_paths(self): @@ -1205,41 +1190,31 @@ class TaskParameters(DockerBaseClass): devices='devices', pid_mode='pid_mode', tmpfs='tmpfs', + init='init', + uts_mode='uts', + runtime='runtime', + auto_remove='auto_remove', + device_read_bps='device_read_bps', + device_write_bps='device_write_bps', + device_read_iops='device_read_iops', + device_write_iops='device_write_iops', ) - if self.client.HAS_AUTO_REMOVE_OPT: - # auto_remove is only supported in docker>=2 - host_config_params['auto_remove'] = 'auto_remove' - - if self.client.HAS_BLKIO_WEIGHT_OPT: - # blkio_weight is only supported in docker>=1.9 + if self.client.docker_py_version >= LooseVersion('1.9') and self.client.docker_api_version >= LooseVersion('1.22'): + # blkio_weight can always be updated, but can only be set on creation + # when docker-py and docker API are new enough host_config_params['blkio_weight'] = 'blkio_weight' - if HAS_DOCKER_PY_3: + if self.client.docker_py_version >= LooseVersion('3.0'): # cpu_shares and volume_driver moved to create_host_config in > 3 host_config_params['cpu_shares'] = 'cpu_shares' host_config_params['volume_driver'] = 'volume_driver' - if self.client.HAS_INIT_OPT: - host_config_params['init'] = 'init' - - if self.client.HAS_UTS_MODE_OPT: - host_config_params['uts_mode'] = 'uts' - - if self.client.HAS_RUNTIME_OPT: - host_config_params['runtime'] = 'runtime' - - if self.client.HAS_DEVICE_RW_LIMIT_OPT: - # device_read/write_bps/iops are only supported in docker>=1.9 and docker-api>=1.22 - host_config_params['device_read_bps'] = 'device_read_bps' - host_config_params['device_write_bps'] = 'device_write_bps' - host_config_params['device_read_iops'] = 'device_read_iops' - host_config_params['device_write_iops'] = 'device_write_iops' - params = dict() for key, value in host_config_params.items(): if getattr(self, value, None) is not None: - params[key] = getattr(self, value) + if self.client.option_minimal_versions[value]['supported']: + params[key] = getattr(self, value) if self.restart_policy: params['restart_policy'] = dict(Name=self.restart_policy, @@ -1733,6 +1708,7 @@ class Container(DockerBaseClass): uts=host_config.get('UTSMode'), expected_volumes=config.get('Volumes'), expected_binds=host_config.get('Binds'), + volume_driver=host_config.get('VolumeDriver'), volumes_from=host_config.get('VolumesFrom'), working_dir=config.get('WorkingDir'), publish_all_ports=host_config.get('PublishAllPorts'), @@ -1743,23 +1719,42 @@ class Container(DockerBaseClass): device_read_iops=host_config.get('BlkioDeviceReadIOps'), device_write_iops=host_config.get('BlkioDeviceWriteIOps'), ) + # Options which don't make sense without their accompanying option if self.parameters.restart_policy: config_mapping['restart_retries'] = restart_policy.get('MaximumRetryCount') if self.parameters.log_driver: config_mapping['log_driver'] = log_config.get('Type') config_mapping['log_options'] = log_config.get('Config') - if self.parameters.client.HAS_AUTO_REMOVE_OPT: - # auto_remove is only supported in docker>=2 + if self.parameters.client.option_minimal_versions['auto_remove']['supported']: + # auto_remove is only supported in docker>=2; unfortunately it has a default + # value, that's why we have to jump through the hoops here config_mapping['auto_remove'] = host_config.get('AutoRemove') - if self.parameters.client.HAS_STOP_TIMEOUT_OPT: - # stop_timeout is only supported in docker>=2.1 + if self.parameters.client.option_minimal_versions['stop_timeout']['supported']: + # stop_timeout is only supported in docker>=2.1. Note that stop_timeout + # has a hybrid role, in that it used to be something only used for stopping + # containers, and is now also used as a container property. That's why + # it needs special handling here. config_mapping['stop_timeout'] = config.get('StopTimeout') - if HAS_DOCKER_PY_3: - # volume_driver moved to create_host_config in > 3 - config_mapping['volume_driver'] = host_config.get('VolumeDriver') + if self.parameters.client.docker_api_version < LooseVersion('1.22'): + # For docker API < 1.22, update_container() is not supported. Thus + # we need to handle all limits which are usually handled by + # update_container() as configuration changes which require a container + # restart. + config_mapping.update(dict( + blkio_weight=host_config.get('BlkioWeight'), + cpu_period=host_config.get('CpuPeriod'), + cpu_quota=host_config.get('CpuQuota'), + cpu_shares=host_config.get('CpuShares'), + cpuset_cpus=host_config.get('CpusetCpus'), + cpuset_mems=host_config.get('CpusetMems'), + kernel_memory=host_config.get("KernelMemory"), + memory=host_config.get('Memory'), + memory_reservation=host_config.get('MemoryReservation'), + memory_swap=host_config.get('MemorySwap'), + )) differences = [] for key, value in config_mapping.items(): @@ -1786,33 +1781,25 @@ class Container(DockerBaseClass): ''' if not self.container.get('HostConfig'): self.fail("limits_differ_from_container: Error parsing container properties. HostConfig missing.") + if self.parameters.client.docker_api_version < LooseVersion('1.22'): + # update_container() call not supported + return False, [] host_config = self.container['HostConfig'] config_mapping = dict( + blkio_weight=host_config.get('BlkioWeight'), cpu_period=host_config.get('CpuPeriod'), cpu_quota=host_config.get('CpuQuota'), + cpu_shares=host_config.get('CpuShares'), cpuset_cpus=host_config.get('CpusetCpus'), + cpuset_mems=host_config.get('CpusetMems'), kernel_memory=host_config.get("KernelMemory"), memory=host_config.get('Memory'), memory_reservation=host_config.get('MemoryReservation'), memory_swap=host_config.get('MemorySwap'), - oom_score_adj=host_config.get('OomScoreAdj'), - oom_killer=host_config.get('OomKillDisable'), ) - if self.parameters.client.HAS_BLKIO_WEIGHT_OPT: - # blkio_weight is only supported in docker>=1.9 - config_mapping['blkio_weight'] = host_config.get('BlkioWeight') - - if self.parameters.client.HAS_CPUSET_MEMS_OPT: - # cpuset_mems is only supported in docker>=2.3 - config_mapping['cpuset_mems'] = host_config.get('CpusetMems') - - if HAS_DOCKER_PY_3: - # cpu_shares moved to create_host_config in > 3 - config_mapping['cpu_shares'] = host_config.get('CpuShares') - differences = [] for key, value in config_mapping.items(): if getattr(self.parameters, key, None): @@ -2148,7 +2135,7 @@ class ContainerManager(DockerBaseClass): client.module.warn('log_options is ignored when log_driver is not specified') if client.module.params.get('healthcheck') and not client.module.params.get('healthcheck').get('test'): client.module.warn('healthcheck is ignored when test is not specified') - if client.module.params.get('restart_retries') and not client.module.params.get('restart_policy'): + if client.module.params.get('restart_retries') is not None and not client.module.params.get('restart_policy'): client.module.warn('restart_retries is ignored when restart_policy is not specified') self.client = client @@ -2389,7 +2376,7 @@ class ContainerManager(DockerBaseClass): self.fail("Error starting container %s: %s" % (container_id, str(exc))) if not self.parameters.detach: - if HAS_DOCKER_PY_3: + if self.client.docker_py_version >= LooseVersion('3.0'): status = self.client.wait(container_id)['StatusCode'] else: status = self.client.wait(container_id) @@ -2480,6 +2467,14 @@ class ContainerManager(DockerBaseClass): class AnsibleDockerClientContainer(AnsibleDockerClient): + # A list of module options which are not docker container properties + __NON_CONTAINER_PROPERTY_OPTIONS = ( + 'docker_host', 'tls_hostname', 'api_version', 'timeout', 'cacert_path', 'cert_path', + 'key_path', 'ssl_version', 'tls', 'tls_verify', 'debug', 'env_file', 'force_kill', + 'keep_volumes', 'ignore_image', 'name', 'pull', 'purge_networks', 'recreate', + 'restart', 'state', 'trust_image_content', 'networks', 'cleanup', 'kill_signal', + 'output_logs', 'paused' + ) def _parse_comparisons(self): comparisons = {} @@ -2508,10 +2503,7 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): for alias in data.get('aliases', []): all_options.add(alias) # Ignore options which aren't used as container properties - if option in ('docker_host', 'tls_hostname', 'api_version', 'timeout', 'cacert_path', 'cert_path', - 'key_path', 'ssl_version', 'tls', 'tls_verify', 'debug', 'env_file', 'force_kill', - 'keep_volumes', 'ignore_image', 'name', 'pull', 'purge_networks', 'recreate', - 'restart', 'state', 'trust_image_content', 'networks'): + if option in self.__NON_CONTAINER_PROPERTY_OPTIONS: continue # Determine option type if option in explicit_types: @@ -2581,37 +2573,102 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): self.module.warn('The ignore_image option has been overridden by the comparisons option!') self.comparisons = comparisons - def __init__(self, **kwargs): - super(AnsibleDockerClientContainer, self).__init__(**kwargs) + def _get_minimal_versions(self): + # Helper function to detect whether any specified network uses ipv4_address or ipv6_address + def detect_ipvX_address_usage(): + for network in self.module.params.get("networks") or []: + if 'ipv4_address' in network or 'ipv6_address' in network: + return True + return False - docker_api_version = self.version()['ApiVersion'] - init_supported = LooseVersion(docker_api_version) >= LooseVersion('1.25') - if self.module.params.get("init") and not init_supported: - self.fail('docker API version is %s. Minimum version required is 1.25 to set init option.' % (docker_api_version,)) + self.option_minimal_versions = dict( + # internal options + log_config=dict(), + publish_all_ports=dict(), + ports=dict(), + volume_binds=dict(), + name=dict(), + ) + for option, data in self.module.argument_spec.items(): + if option in self.__NON_CONTAINER_PROPERTY_OPTIONS: + continue + self.option_minimal_versions[option] = dict() + self.option_minimal_versions.update(dict( + device_read_bps=dict(docker_py_version='1.9.0', docker_api_version='1.22'), + device_read_iops=dict(docker_py_version='1.9.0', docker_api_version='1.22'), + device_write_bps=dict(docker_py_version='1.9.0', docker_api_version='1.22'), + device_write_iops=dict(docker_py_version='1.9.0', docker_api_version='1.22'), + dns_opts=dict(docker_api_version='1.21', docker_py_version='1.10.0'), + ipc_mode=dict(docker_api_version='1.25'), + mac_address=dict(docker_api_version='1.25'), + oom_killer=dict(docker_py_version='2.0.0'), + oom_score_adj=dict(docker_api_version='1.22', docker_py_version='2.0.0'), + shm_size=dict(docker_api_version='1.22'), + stop_signal=dict(docker_api_version='1.21'), + tmpfs=dict(docker_api_version='1.22'), + volume_driver=dict(docker_api_version='1.21'), + memory_reservation=dict(docker_api_version='1.21'), + kernel_memory=dict(docker_api_version='1.21'), + auto_remove=dict(docker_py_version='2.1.0', docker_api_version='1.25'), + healthcheck=dict(docker_py_version='2.0.0', docker_api_version='1.24'), + init=dict(docker_py_version='2.2.0', docker_api_version='1.25'), + runtime=dict(docker_py_version='2.4.0', docker_api_version='1.25'), + sysctls=dict(docker_py_version='1.10.0', docker_api_version='1.24'), + userns_mode=dict(docker_py_version='1.10.0', docker_api_version='1.23'), + uts=dict(docker_py_version='3.5.0', docker_api_version='1.25'), + # specials + ipvX_address_supported=dict(docker_py_version='1.9.0', detect_usage=detect_ipvX_address_usage, + usage_msg='ipv4_address or ipv6_address in networks'), + stop_timeout=dict(), # see below! + )) - init_supported = init_supported and LooseVersion(docker_version) >= LooseVersion('2.2') - if self.module.params.get("init") and not init_supported: - self.fail("docker or docker-py version is %s. Minimum version required is 2.2 to set init option. " - "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,)) + for option, data in self.option_minimal_versions.items(): + # Test whether option is supported, and store result + support_docker_py = True + support_docker_api = True + if 'docker_py_version' in data: + support_docker_py = self.docker_py_version >= LooseVersion(data['docker_py_version']) + if 'docker_api_version' in data: + support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version']) + data['supported'] = support_docker_py and support_docker_api + # Fail if option is not supported but used + if not data['supported']: + # Test whether option is specified + if 'detect_usage' in data: + used = data['detect_usage']() + else: + used = self.module.params.get(option) is not None + if used and 'default' in self.module.argument_spec[option]: + used = self.module.params[option] != self.module.argument_spec[option]['default'] + if used: + # If the option is used, compose error message. + if 'usage_msg' in data: + usg = data['usage_msg'] + else: + usg = 'set %s option' % (option, ) + if not support_docker_api: + msg = 'docker API version is %s. Minimum version required is %s to %s.' + msg = msg % (self.docker_api_version_str, data['docker_api_version'], usg) + elif not support_docker_py: + if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'): + msg = ("docker-py version is %s. Minimum version required is %s to %s. " + "Consider switching to the 'docker' package if you do not require Python 2.6 support.") + elif self.docker_py_version < LooseVersion('2.0.0'): + msg = ("docker-py version is %s. Minimum version required is %s to %s. " + "You have to switch to the Python 'docker' package. First uninstall 'docker-py' before " + "installing 'docker' to avoid a broken installation.") + else: + msg = "docker version is %s. Minimum version required is %s to %s." + msg = msg % (docker_version, data['docker_py_version'], usg) + else: + # should not happen + msg = 'Cannot %s with your configuration.' % (usg, ) + self.fail(msg) - uts_mode_supported = LooseVersion(docker_version) >= LooseVersion('3.5') - if self.module.params.get("uts") is not None and not uts_mode_supported: - self.fail("docker or docker-py version is %s. Minimum version required is 3.5 to set uts option. " - "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,)) - - blkio_weight_supported = LooseVersion(docker_version) >= LooseVersion('1.9') - if self.module.params.get("blkio_weight") is not None and not blkio_weight_supported: - self.fail("docker or docker-py version is %s. Minimum version required is 1.9 to set blkio_weight option.") - - cpuset_mems_supported = LooseVersion(docker_version) >= LooseVersion('2.3') - if self.module.params.get("cpuset_mems") is not None and not cpuset_mems_supported: - self.fail("docker or docker-py version is %s. Minimum version required is 2.3 to set cpuset_mems option. " - "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,)) - - stop_timeout_supported = LooseVersion(docker_api_version) >= LooseVersion('1.25') + stop_timeout_supported = self.docker_api_version >= LooseVersion('1.25') stop_timeout_needed_for_update = self.module.params.get("stop_timeout") is not None and self.module.params.get('state') != 'absent' if stop_timeout_supported: - stop_timeout_supported = LooseVersion(docker_version) >= LooseVersion('2.1') + stop_timeout_supported = self.docker_py_version >= LooseVersion('2.1') if stop_timeout_needed_for_update and not stop_timeout_supported: # We warn (instead of fail) since in older versions, stop_timeout was not used # to update the container's configuration, but only when stopping a container. @@ -2623,55 +2680,12 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): # We warn (instead of fail) since in older versions, stop_timeout was not used # to update the container's configuration, but only when stopping a container. self.module.warn("docker API version is %s. Minimum version required is 1.25 to set or " - "update the container's stop_timeout configuration." % (docker_api_version,)) - - ipvX_address_supported = LooseVersion(docker_version) >= LooseVersion('1.9') - if not ipvX_address_supported: - ipvX_address_used = False - for network in self.module.params.get("networks", []): - if 'ipv4_address' in network or 'ipv6_address' in network: - ipvX_address_used = True - if ipvX_address_used: - self.fail("docker or docker-py version is %s. Minimum version required is 1.9 to use " - "ipv4_address or ipv6_address in networks." % (docker_version,)) - - runtime_supported = LooseVersion(docker_api_version) >= LooseVersion('1.12') - if self.module.params.get("runtime") and not runtime_supported: - self.fail('docker API version is %s. Minimum version required is 1.12 to set runtime option.' % (docker_api_version,)) - - healthcheck_supported = LooseVersion(docker_version) >= LooseVersion('2.0') - if self.module.params.get("healthcheck") and not healthcheck_supported: - self.fail("docker or docker-py version is %s. Minimum version required is 2.0 to set healthcheck option." % (docker_version,)) - - found_device_limit_param = False - for x in ["device_read_bps", "device_write_bps", "device_read_iops", "device_write_iops"]: - if self.module.params.get(x): - found_device_limit_param = True - break - - device_rw_limit_supported = LooseVersion(docker_api_version) >= LooseVersion('1.22') - if found_device_limit_param and not device_rw_limit_supported: - self.fail('docker API version is %s. Minimum version required is 1.22 to set device IO limit options.' % (docker_api_version,)) - - device_rw_limit_supported = device_rw_limit_supported and LooseVersion(docker_version) >= LooseVersion('1.9.0') - if found_device_limit_param and not device_rw_limit_supported: - self.fail("docker or docker-py version is %s. Minimum version required is 1.9 to set device IO limit optons. " - "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,)) - - self.HAS_INIT_OPT = init_supported - self.HAS_UTS_MODE_OPT = uts_mode_supported - self.HAS_BLKIO_WEIGHT_OPT = blkio_weight_supported - self.HAS_CPUSET_MEMS_OPT = cpuset_mems_supported - self.HAS_STOP_TIMEOUT_OPT = stop_timeout_supported - self.HAS_HEALTHCHECK_OPT = healthcheck_supported - self.HAS_DEVICE_RW_LIMIT_OPT = device_rw_limit_supported - - self.HAS_AUTO_REMOVE_OPT = HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3 - self.HAS_RUNTIME_OPT = runtime_supported - - if self.module.params.get('auto_remove') and not self.HAS_AUTO_REMOVE_OPT: - self.fail("'auto_remove' is not compatible with the 'docker-py' Python package. It requires the newer 'docker' Python package.") + "update the container's stop_timeout configuration." % (self.docker_api_version_str,)) + self.option_minimal_versions['stop_timeout']['supported'] = stop_timeout_supported + def __init__(self, **kwargs): + super(AnsibleDockerClientContainer, self).__init__(**kwargs) + self._get_minimal_versions() self._parse_comparisons() diff --git a/test/integration/targets/docker_config/tasks/main.yml b/test/integration/targets/docker_config/tasks/main.yml index e65a75d814..dbbd4349e4 100644 --- a/test/integration/targets/docker_config/tasks/main.yml +++ b/test/integration/targets/docker_config/tasks/main.yml @@ -1,7 +1,3 @@ -- name: Check Docker API version - command: "{{ ansible_python.executable }} -c 'import docker; print(docker.from_env().version()[\"ApiVersion\"])'" - register: docker_api_version - ignore_errors: yes - +--- - include_tasks: test_docker_config.yml - when: docker_api_version.rc == 0 and docker_api_version.stdout is version('1.30', '>=') + when: docker_api_version is version('1.30', '>=') diff --git a/test/integration/targets/docker_config/tasks/test_docker_config.yml b/test/integration/targets/docker_config/tasks/test_docker_config.yml index cf07e51624..d8a484df81 100644 --- a/test/integration/targets/docker_config/tasks/test_docker_config.yml +++ b/test/integration/targets/docker_config/tasks/test_docker_config.yml @@ -1,3 +1,4 @@ +--- - name: Make sure we're not already using Docker swarm docker_swarm: state: absent diff --git a/test/integration/targets/docker_container/tasks/main.yml b/test/integration/targets/docker_container/tasks/main.yml index bb232f176c..3c958b17d6 100644 --- a/test/integration/targets/docker_container/tasks/main.yml +++ b/test/integration/targets/docker_container/tasks/main.yml @@ -1,4 +1,5 @@ --- +# Create random name prefix (for containers, networks, ...) - name: Create random container name prefix set_fact: cname_prefix: "{{ 'ansible-test-%0x' % ((2**32) | random) }}" @@ -8,6 +9,7 @@ - debug: msg: "Using container name prefix {{ cname_prefix }}" +# Run the tests - block: - include_tasks: run-test.yml with_fileglob: @@ -26,6 +28,6 @@ state: absent force: yes with_items: "{{ dnetworks }}" + when: docker_py_version is version('1.10.0', '>=') - # Skip for CentOS 6 - when: ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6 + when: docker_py_version is version('1.8.0', '>=') and docker_api_version is version('1.20', '>=') diff --git a/test/integration/targets/docker_container/tasks/tests/options.yml b/test/integration/targets/docker_container/tasks/tests/options.yml index 9b7ed8bd00..84b2e65cb9 100644 --- a/test/integration/targets/docker_container/tasks/tests/options.yml +++ b/test/integration/targets/docker_container/tasks/tests/options.yml @@ -21,6 +21,7 @@ - "{{ nname_2 }}" loop_control: loop_var: network_name + when: docker_py_version is version('1.10.0', '>=') #################################################################### ## auto_remove ##################################################### @@ -34,6 +35,7 @@ state: started auto_remove: yes register: auto_remove_1 + ignore_errors: yes - name: Give container 1 second to be sure it terminated pause: @@ -44,11 +46,18 @@ name: "{{ cname }}" state: absent register: auto_remove_2 + ignore_errors: yes - assert: that: - auto_remove_1 is changed - auto_remove_2 is not changed + when: docker_py_version is version('2.1.0', '>=') +- assert: + that: + - auto_remove_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 2.1.0') in auto_remove_1.msg" + when: docker_py_version is version('2.1.0', '<') #################################################################### ## blkio_weight #################################################### @@ -509,6 +518,7 @@ auto_remove: yes cleanup: yes register: detach_auto_remove + ignore_errors: yes - name: cleanup (unnecessary) docker_container: @@ -522,8 +532,11 @@ - detach_no_cleanup_cleanup is changed - "'Hello from Docker!' in detach_cleanup.ansible_facts.docker_container.Output" - detach_cleanup_cleanup is not changed +- assert: + that: - "'Cannot retrieve result as auto_remove is enabled' == detach_auto_remove.ansible_facts.docker_container.Output" - detach_auto_remove_cleanup is not changed + when: docker_py_version is version('2.1.0', '>=') #################################################################### ## devices ######################################################### @@ -602,6 +615,7 @@ - path: /dev/urandom rate: 10K register: device_read_bps_1 + ignore_errors: yes - name: device_read_bps (idempotency) docker_container: @@ -615,6 +629,7 @@ - path: /dev/random rate: 20M register: device_read_bps_2 + ignore_errors: yes - name: device_read_bps (lesser entries) docker_container: @@ -626,6 +641,7 @@ - path: /dev/random rate: 20M register: device_read_bps_3 + ignore_errors: yes - name: device_read_bps (changed) docker_container: @@ -640,6 +656,7 @@ rate: 5K stop_timeout: 1 register: device_read_bps_4 + ignore_errors: yes - name: cleanup docker_container: @@ -653,6 +670,12 @@ - device_read_bps_2 is not changed - device_read_bps_3 is not changed - device_read_bps_4 is changed + when: docker_py_version is version('1.9.0', '>=') +- assert: + that: + - device_read_bps_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 1.9.0') in device_read_bps_1.msg" + when: docker_py_version is version('1.9.0', '<') #################################################################### ## device_read_iops ################################################ @@ -670,6 +693,7 @@ - path: /dev/urandom rate: 20 register: device_read_iops_1 + ignore_errors: yes - name: device_read_iops (idempotency) docker_container: @@ -683,6 +707,7 @@ - path: /dev/random rate: 10 register: device_read_iops_2 + ignore_errors: yes - name: device_read_iops (less) docker_container: @@ -694,6 +719,7 @@ - path: /dev/random rate: 10 register: device_read_iops_3 + ignore_errors: yes - name: device_read_iops (changed) docker_container: @@ -708,6 +734,7 @@ rate: 50 stop_timeout: 1 register: device_read_iops_4 + ignore_errors: yes - name: cleanup docker_container: @@ -721,6 +748,12 @@ - device_read_iops_2 is not changed - device_read_iops_3 is not changed - device_read_iops_4 is changed + when: docker_py_version is version('1.9.0', '>=') +- assert: + that: + - device_read_iops_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 1.9.0') in device_read_iops_1.msg" + when: docker_py_version is version('1.9.0', '<') #################################################################### ## device_write_bps and device_write_iops ########################## @@ -739,6 +772,7 @@ - path: /dev/urandom rate: 30 register: device_write_limit_1 + ignore_errors: yes - name: device_write_bps and device_write_iops (idempotency) docker_container: @@ -753,6 +787,7 @@ - path: /dev/urandom rate: 30 register: device_write_limit_2 + ignore_errors: yes - name: device_write_bps device_write_iops (changed) docker_container: @@ -768,6 +803,7 @@ rate: 100 stop_timeout: 1 register: device_write_limit_3 + ignore_errors: yes - name: cleanup docker_container: @@ -780,6 +816,12 @@ - device_write_limit_1 is changed - device_write_limit_2 is not changed - device_write_limit_3 is changed + when: docker_py_version is version('1.9.0', '>=') +- assert: + that: + - device_write_limit_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 1.9.0') in device_write_limit_1.msg" + when: docker_py_version is version('1.9.0', '<') #################################################################### ## dns_opts ######################################################## @@ -795,6 +837,7 @@ - "timeout:10" - rotate register: dns_opts_1 + ignore_errors: yes - name: dns_opts (idempotency) docker_container: @@ -806,6 +849,7 @@ - rotate - "timeout:10" register: dns_opts_2 + ignore_errors: yes - name: dns_opts (less resolv.conf options) docker_container: @@ -816,6 +860,7 @@ dns_opts: - "timeout:10" register: dns_opts_3 + ignore_errors: yes - name: dns_opts (more resolv.conf options) docker_container: @@ -828,6 +873,7 @@ - no-check-names stop_timeout: 1 register: dns_opts_4 + ignore_errors: yes - name: cleanup docker_container: @@ -841,6 +887,12 @@ - dns_opts_2 is not changed - dns_opts_3 is not changed - dns_opts_4 is changed + when: docker_py_version is version('1.10.0', '>=') +- assert: + that: + - dns_opts_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 1.10.0') in dns_opts_1.msg" + when: docker_py_version is version('1.10.0', '<') #################################################################### ## dns_search_domains ############################################## @@ -1391,6 +1443,7 @@ retries: 2 stop_timeout: 1 register: healthcheck_1 + ignore_errors: yes - name: healthcheck (idempotency) docker_container: @@ -1408,6 +1461,7 @@ retries: 2 stop_timeout: 1 register: healthcheck_2 + ignore_errors: yes - name: healthcheck (changed) docker_container: @@ -1425,6 +1479,7 @@ retries: 3 stop_timeout: 1 register: healthcheck_3 + ignore_errors: yes - name: healthcheck (no change) docker_container: @@ -1434,6 +1489,7 @@ state: started stop_timeout: 1 register: healthcheck_4 + ignore_errors: yes - name: healthcheck (disabled) docker_container: @@ -1446,6 +1502,7 @@ - NONE stop_timeout: 1 register: healthcheck_5 + ignore_errors: yes - name: healthcheck (disabled, idempotency) docker_container: @@ -1458,6 +1515,7 @@ - NONE stop_timeout: 1 register: healthcheck_6 + ignore_errors: yes - name: healthcheck (string in healthcheck test, changed) docker_container: @@ -1469,6 +1527,7 @@ test: "sleep 1" stop_timeout: 1 register: healthcheck_7 + ignore_errors: yes - name: healthcheck (string in healthcheck test, idempotency) docker_container: @@ -1480,6 +1539,7 @@ test: "sleep 1" stop_timeout: 1 register: healthcheck_8 + ignore_errors: yes - name: cleanup docker_container: @@ -1497,6 +1557,12 @@ - healthcheck_6 is not changed - healthcheck_7 is changed - healthcheck_8 is not changed + when: docker_py_version is version('2.0.0', '>=') +- assert: + that: + - healthcheck_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 2.0.0') in healthcheck_1.msg" + when: docker_py_version is version('2.0.0', '<') #################################################################### ## hostname ######################################################## @@ -1554,6 +1620,7 @@ init: yes state: started register: init_1 + ignore_errors: yes - name: init (idempotency) docker_container: @@ -1563,6 +1630,7 @@ init: yes state: started register: init_2 + ignore_errors: yes - name: init (change) docker_container: @@ -1573,6 +1641,7 @@ state: started stop_timeout: 1 register: init_3 + ignore_errors: yes - name: cleanup docker_container: @@ -1585,6 +1654,12 @@ - init_1 is changed - init_2 is not changed - init_3 is changed + when: docker_py_version is version('2.2.0', '>=') +- assert: + that: + - init_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 2.2.0') in init_1.msg" + when: docker_py_version is version('2.2.0', '<') #################################################################### ## interactive ##################################################### @@ -2335,77 +2410,80 @@ ## networks, purge_networks ######################################## #################################################################### -- name: networks, purge_networks - docker_container: - image: alpine:3.8 - command: '/bin/sh -c "sleep 10m"' - name: "{{ cname }}" - state: started - purge_networks: yes - networks: - - name: bridge - - name: "{{ nname_1 }}" - register: networks_1 +- block: + - name: networks, purge_networks + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + purge_networks: yes + networks: + - name: bridge + - name: "{{ nname_1 }}" + register: networks_1 -- name: networks, purge_networks (idempotency) - docker_container: - image: alpine:3.8 - command: '/bin/sh -c "sleep 10m"' - name: "{{ cname }}" - state: started - purge_networks: yes - networks: - - name: "{{ nname_1 }}" - - name: bridge - register: networks_2 + - name: networks, purge_networks (idempotency) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + purge_networks: yes + networks: + - name: "{{ nname_1 }}" + - name: bridge + register: networks_2 -- name: networks (less networks) - docker_container: - image: alpine:3.8 - command: '/bin/sh -c "sleep 10m"' - name: "{{ cname }}" - state: started - networks: - - name: bridge - register: networks_3 + - name: networks (less networks) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + networks: + - name: bridge + register: networks_3 -- name: networks, purge_networks (less networks) - docker_container: - image: alpine:3.8 - command: '/bin/sh -c "sleep 10m"' - name: "{{ cname }}" - state: started - purge_networks: yes - networks: - - name: bridge - register: networks_4 + - name: networks, purge_networks (less networks) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + purge_networks: yes + networks: + - name: bridge + register: networks_4 -- name: networks, purge_networks (more networks) - docker_container: - image: alpine:3.8 - command: '/bin/sh -c "sleep 10m"' - name: "{{ cname }}" - state: started - purge_networks: yes - networks: - - name: bridge - - name: "{{ nname_2 }}" - stop_timeout: 1 - register: networks_5 + - name: networks, purge_networks (more networks) + docker_container: + image: alpine:3.8 + command: '/bin/sh -c "sleep 10m"' + name: "{{ cname }}" + state: started + purge_networks: yes + networks: + - name: bridge + - name: "{{ nname_2 }}" + stop_timeout: 1 + register: networks_5 -- name: cleanup - docker_container: - name: "{{ cname }}" - state: absent - stop_timeout: 1 + - name: cleanup + docker_container: + name: "{{ cname }}" + state: absent + stop_timeout: 1 -- assert: - that: - - networks_1 is changed - - networks_2 is not changed - - networks_3 is not changed - - networks_4 is changed - - networks_5 is changed + - assert: + that: + - networks_1 is changed + - networks_2 is not changed + - networks_3 is not changed + - networks_4 is changed + - networks_5 is changed + + when: docker_py_version is version('1.10.0', '>=') #################################################################### ## oom_killer ###################################################### @@ -2419,6 +2497,7 @@ oom_killer: yes state: started register: oom_killer_1 + ignore_errors: yes - name: oom_killer (idempotency) docker_container: @@ -2428,6 +2507,7 @@ oom_killer: yes state: started register: oom_killer_2 + ignore_errors: yes - name: oom_killer (change) docker_container: @@ -2438,6 +2518,7 @@ state: started stop_timeout: 1 register: oom_killer_3 + ignore_errors: yes - name: cleanup docker_container: @@ -2450,6 +2531,12 @@ - oom_killer_1 is changed - oom_killer_2 is not changed - oom_killer_3 is changed + when: docker_py_version is version('2.0.0', '>=') +- assert: + that: + - oom_killer_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 2.0.0') in oom_killer_1.msg" + when: docker_py_version is version('2.0.0', '<') #################################################################### ## oom_score_adj ################################################### @@ -2463,6 +2550,7 @@ oom_score_adj: 5 state: started register: oom_score_adj_1 + ignore_errors: yes - name: oom_score_adj (idempotency) docker_container: @@ -2472,6 +2560,7 @@ oom_score_adj: 5 state: started register: oom_score_adj_2 + ignore_errors: yes - name: oom_score_adj (change) docker_container: @@ -2482,6 +2571,7 @@ state: started stop_timeout: 1 register: oom_score_adj_3 + ignore_errors: yes - name: cleanup docker_container: @@ -2494,6 +2584,12 @@ - oom_score_adj_1 is changed - oom_score_adj_2 is not changed - oom_score_adj_3 is changed + when: docker_py_version is version('2.0.0', '>=') +- assert: + that: + - oom_score_adj_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 2.0.0') in oom_score_adj_1.msg" + when: docker_py_version is version('2.0.0', '<') #################################################################### ## output_logs ##################################################### @@ -2589,6 +2685,8 @@ state: started pid_mode: "container:{{ pid_mode_helper.ansible_facts.docker_container.Id }}" register: pid_mode_1 + ignore_errors: yes + # docker-py < 2.0 does not support "arbitrary" pid_mode values - name: pid_mode (idempotency) docker_container: @@ -2598,6 +2696,8 @@ state: started pid_mode: "container:{{ pid_mode_helper.ansible_facts.docker_container.Id }}" register: pid_mode_2 + ignore_errors: yes + # docker-py < 2.0 does not support "arbitrary" pid_mode values - name: pid_mode (change) docker_container: @@ -2625,6 +2725,13 @@ - pid_mode_1 is changed - pid_mode_2 is not changed - pid_mode_3 is changed + when: docker_py_version is version('2.0.0', '>=') +- assert: + that: + - pid_mode_1 is failed + - pid_mode_2 is failed + - pid_mode_3 is changed + when: docker_py_version is version('2.0.0', '<') #################################################################### ## privileged ###################################################### @@ -2985,6 +3092,7 @@ runtime: runc state: started register: runtime_1 + ignore_errors: yes - name: runtime (idempotency) docker_container: @@ -2994,6 +3102,7 @@ runtime: runc state: started register: runtime_2 + ignore_errors: yes - name: cleanup docker_container: @@ -3005,6 +3114,12 @@ that: - runtime_1 is changed - runtime_2 is not changed + when: docker_py_version is version('2.4.0', '>=') +- assert: + that: + - runtime_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 2.4.0') in runtime_1.msg" + when: docker_py_version is version('2.4.0', '<') #################################################################### ## security_opts ################################################### @@ -3239,6 +3354,7 @@ net.ipv4.icmp_echo_ignore_all: 1 net.ipv4.ip_forward: 1 register: sysctls_1 + ignore_errors: yes - name: sysctls (idempotency) docker_container: @@ -3250,6 +3366,7 @@ net.ipv4.ip_forward: 1 net.ipv4.icmp_echo_ignore_all: 1 register: sysctls_2 + ignore_errors: yes - name: sysctls (less sysctls) docker_container: @@ -3260,6 +3377,7 @@ sysctls: net.ipv4.icmp_echo_ignore_all: 1 register: sysctls_3 + ignore_errors: yes - name: sysctls (more sysctls) docker_container: @@ -3272,6 +3390,7 @@ net.ipv6.conf.default.accept_redirects: 0 stop_timeout: 1 register: sysctls_4 + ignore_errors: yes - name: cleanup docker_container: @@ -3285,6 +3404,12 @@ - sysctls_2 is not changed - sysctls_3 is not changed - sysctls_4 is changed + when: docker_py_version is version('1.10.0', '>=') +- assert: + that: + - sysctls_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 1.10.0') in sysctls_1.msg" + when: docker_py_version is version('1.10.0', '<') #################################################################### ## tmpfs ########################################################### @@ -3514,6 +3639,7 @@ userns_mode: host state: started register: userns_mode_1 + ignore_errors: yes - name: userns_mode (idempotency) docker_container: @@ -3523,6 +3649,7 @@ userns_mode: host state: started register: userns_mode_2 + ignore_errors: yes - name: userns_mode (change) docker_container: @@ -3533,6 +3660,7 @@ state: started stop_timeout: 1 register: userns_mode_3 + ignore_errors: yes - name: cleanup docker_container: @@ -3545,6 +3673,12 @@ - userns_mode_1 is changed - userns_mode_2 is not changed - userns_mode_3 is changed + when: docker_py_version is version('1.10.0', '>=') +- assert: + that: + - userns_mode_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 1.10.0') in userns_mode_1.msg" + when: docker_py_version is version('1.10.0', '<') #################################################################### ## uts ############################################################# @@ -3558,6 +3692,7 @@ uts: host state: started register: uts_1 + ignore_errors: yes - name: uts (idempotency) docker_container: @@ -3567,6 +3702,7 @@ uts: host state: started register: uts_2 + ignore_errors: yes - name: uts (change) docker_container: @@ -3577,6 +3713,7 @@ state: started stop_timeout: 1 register: uts_3 + ignore_errors: yes - name: cleanup docker_container: @@ -3589,6 +3726,12 @@ - uts_1 is changed - uts_2 is not changed - uts_3 is changed + when: docker_py_version is version('3.5.0', '>=') +- assert: + that: + - uts_1 is failed + - "('version is ' ~ docker_py_version ~'. Minimum version required is 3.5.0') in uts_1.msg" + when: docker_py_version is version('3.5.0', '<') #################################################################### ## keep_volumes #################################################### @@ -3836,3 +3979,4 @@ - "{{ nname_2 }}" loop_control: loop_var: network_name + when: docker_py_version is version('1.10.0', '>=') diff --git a/test/integration/targets/setup_docker/tasks/main.yml b/test/integration/targets/setup_docker/tasks/main.yml index 367820288c..4f5a305124 100644 --- a/test/integration/targets/setup_docker/tasks/main.yml +++ b/test/integration/targets/setup_docker/tasks/main.yml @@ -17,3 +17,21 @@ state: present name: 'docker{{ extra_packages }}' extra_args: "-c {{ role_path }}/../../../runner/requirements/constraints.txt" + + # Detect docker API and docker-py versions + - name: Check Docker API version + command: "{{ ansible_python.executable }} -c 'import docker; print(docker.from_env().version()[\"ApiVersion\"])'" + register: docker_api_version_stdout + ignore_errors: yes + + - name: Check docker-py API version + command: "{{ ansible_python.executable }} -c 'import docker; print(docker.__version__)'" + register: docker_py_version_stdout + ignore_errors: yes + + - set_fact: + docker_api_version: "{{ docker_api_version_stdout.stdout or '0.0' }}" + docker_py_version: "{{ docker_py_version_stdout.stdout or '0.0' }}" + + - debug: + msg: "Docker API version: {{ docker_api_version }}; docker-py library version: {{ docker_py_version }}"