From fb551bf62c5171e6fa23604a01e74e77a1c744e0 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. (cherry picked from commit 3cca4185be3fd55cc580bdf7c0f85562a997371b) --- ...ocker_container-minimal-version-checks.yml | 2 + lib/ansible/module_utils/docker_common.py | 11 +- .../modules/cloud/docker/docker_container.py | 251 ++++++++++-------- .../targets/docker_container/tasks/main.yml | 6 +- .../docker_container/tasks/tests/options.yml | 235 +++++++++++----- .../targets/setup_docker/tasks/main.yml | 18 ++ 6 files changed, 341 insertions(+), 182 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 00000000000..9f55b6efdd8 --- /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 71819d9800a..32b0b76db6e 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 1015fa3859d..41b6e5ecf8f 100644 --- a/lib/ansible/modules/cloud/docker/docker_container.py +++ b/lib/ansible/modules/cloud/docker/docker_container.py @@ -55,11 +55,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). @@ -172,7 +170,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. @@ -207,14 +204,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. @@ -247,11 +242,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: @@ -327,7 +320,6 @@ options: restart_retries: description: - Use with restart policy to control maximum number of restart attempts. - default: 0 shm_size: description: - "Size of C(/dev/shm) (format: C([])). Number is positive integer. @@ -640,17 +632,17 @@ import shlex 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, DockerBaseClass, sanitize_result +from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, sanitize_result 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: +except Exception as dummy: # missing docker-py handled in ansible.module_utils.docker pass @@ -835,28 +827,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 @@ -882,7 +869,8 @@ class TaskParameters(DockerBaseClass): working_dir='working_dir', ) - 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' @@ -893,7 +881,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): @@ -974,31 +963,26 @@ class TaskParameters(DockerBaseClass): devices='devices', pid_mode='pid_mode', tmpfs='tmpfs', + init='init', + uts_mode='uts', + auto_remove='auto_remove', ) - 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' - 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, @@ -1455,23 +1439,40 @@ 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'), ) + # 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 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(): @@ -1498,33 +1499,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): @@ -1844,7 +1837,7 @@ class ContainerManager(DockerBaseClass): if client.module.params.get('log_options') and not client.module.params.get('log_driver'): client.module.warn('log_options is ignored when log_driver 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 @@ -2068,7 +2061,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) @@ -2159,6 +2152,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', 'stop_timeout', 'trust_image_content', 'networks', 'cleanup', + 'kill_signal', 'output_logs', 'paused' + ) def _setup_comparisons(self): comparisons = {} @@ -2176,10 +2177,7 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): ) for option, data in self.module.argument_spec.items(): # 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', 'stop_timeout', 'trust_image_content', 'networks'): + if option in self.__NON_CONTAINER_PROPERTY_OPTIONS: continue # Determine option type if option in explicit_types: @@ -2208,51 +2206,94 @@ class AnsibleDockerClientContainer(AnsibleDockerClient): comparisons['expected_ports'] = dict(type='dict', comparison=comparisons['published_ports']['comparison'], name='expected_ports') self.comparisons = comparisons - def __init__(self, **kwargs): - super(AnsibleDockerClientContainer, self).__init__(**kwargs) - - 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,)) - - 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,)) - - 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,)) - - 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", []): + 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: - 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,)) - - 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_AUTO_REMOVE_OPT = HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3 - 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.") + return True + return False + + 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( + 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'), + init=dict(docker_py_version='2.2.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'), + )) + + 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) + def __init__(self, **kwargs): + super(AnsibleDockerClientContainer, self).__init__(**kwargs) + self._get_minimal_versions() self._setup_comparisons() diff --git a/test/integration/targets/docker_container/tasks/main.yml b/test/integration/targets/docker_container/tasks/main.yml index bb232f176c4..3c958b17d65 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 623dac374e3..b10c8dab0f1 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 #################################################### @@ -515,6 +524,7 @@ auto_remove: yes cleanup: yes register: detach_auto_remove + ignore_errors: yes - name: cleanup (unnecessary) docker_container: @@ -528,8 +538,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 ######################################################### @@ -606,6 +619,7 @@ - "timeout:10" - rotate register: dns_opts_1 + ignore_errors: yes - name: dns_opts (idempotency) docker_container: @@ -617,6 +631,7 @@ - rotate - "timeout:10" register: dns_opts_2 + ignore_errors: yes - name: dns_opts (less resolv.conf options) docker_container: @@ -627,6 +642,7 @@ dns_opts: - "timeout:10" register: dns_opts_3 + ignore_errors: yes - name: dns_opts (more resolv.conf options) docker_container: @@ -639,6 +655,7 @@ - no-check-names stop_timeout: 1 register: dns_opts_4 + ignore_errors: yes - name: cleanup docker_container: @@ -652,6 +669,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 ############################################## @@ -1238,6 +1261,7 @@ init: yes state: started register: init_1 + ignore_errors: yes - name: init (idempotency) docker_container: @@ -1247,6 +1271,7 @@ init: yes state: started register: init_2 + ignore_errors: yes - name: init (change) docker_container: @@ -1257,6 +1282,7 @@ state: started stop_timeout: 1 register: init_3 + ignore_errors: yes - name: cleanup docker_container: @@ -1269,6 +1295,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 ##################################################### @@ -2018,77 +2050,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 - -- 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, 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: 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 +- 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 (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 (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 + + - 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 ###################################################### @@ -2102,6 +2137,7 @@ oom_killer: yes state: started register: oom_killer_1 + ignore_errors: yes - name: oom_killer (idempotency) docker_container: @@ -2111,6 +2147,7 @@ oom_killer: yes state: started register: oom_killer_2 + ignore_errors: yes - name: oom_killer (change) docker_container: @@ -2121,6 +2158,7 @@ state: started stop_timeout: 1 register: oom_killer_3 + ignore_errors: yes - name: cleanup docker_container: @@ -2133,6 +2171,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 ################################################### @@ -2146,6 +2190,7 @@ oom_score_adj: 5 state: started register: oom_score_adj_1 + ignore_errors: yes - name: oom_score_adj (idempotency) docker_container: @@ -2155,6 +2200,7 @@ oom_score_adj: 5 state: started register: oom_score_adj_2 + ignore_errors: yes - name: oom_score_adj (change) docker_container: @@ -2165,6 +2211,7 @@ state: started stop_timeout: 1 register: oom_score_adj_3 + ignore_errors: yes - name: cleanup docker_container: @@ -2177,6 +2224,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 ##################################################### @@ -2210,6 +2263,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: @@ -2219,6 +2274,8 @@ state: started pid_mode: "container:{{ cname_h1 }}" register: pid_mode_2 + ignore_errors: yes + # docker-py < 2.0 does not support "arbitrary" pid_mode values - name: pid_mode (change) docker_container: @@ -2246,6 +2303,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 ###################################################### @@ -2700,6 +2764,7 @@ net.ipv4.icmp_echo_ignore_all: 1 net.ipv4.ip_forward: 1 register: sysctls_1 + ignore_errors: yes - name: sysctls (idempotency) docker_container: @@ -2711,6 +2776,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: @@ -2721,6 +2787,7 @@ sysctls: net.ipv4.icmp_echo_ignore_all: 1 register: sysctls_3 + ignore_errors: yes - name: sysctls (more sysctls) docker_container: @@ -2733,6 +2800,7 @@ net.ipv6.conf.default.accept_redirects: 0 stop_timeout: 1 register: sysctls_4 + ignore_errors: yes - name: cleanup docker_container: @@ -2746,6 +2814,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', '<') #################################################################### ## timeout ######################################################### @@ -2981,6 +3055,7 @@ userns_mode: host state: started register: userns_mode_1 + ignore_errors: yes - name: userns_mode (idempotency) docker_container: @@ -2990,6 +3065,7 @@ userns_mode: host state: started register: userns_mode_2 + ignore_errors: yes - name: userns_mode (change) docker_container: @@ -3000,6 +3076,7 @@ state: started stop_timeout: 1 register: userns_mode_3 + ignore_errors: yes - name: cleanup docker_container: @@ -3012,6 +3089,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 ############################################################# @@ -3025,6 +3108,7 @@ uts: host state: started register: uts_1 + ignore_errors: yes - name: uts (idempotency) docker_container: @@ -3034,6 +3118,7 @@ uts: host state: started register: uts_2 + ignore_errors: yes - name: uts (change) docker_container: @@ -3044,6 +3129,7 @@ state: started stop_timeout: 1 register: uts_3 + ignore_errors: yes - name: cleanup docker_container: @@ -3056,6 +3142,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 #################################################### @@ -3290,3 +3382,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 367820288c5..4f5a3051247 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 }}"