#!/usr/bin/python # # Copyright (c) 2017, Dario Zanzico (git@dariozanzico.com) # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = ''' --- module: docker_swarm_service author: - "Dario Zanzico (@dariko)" - "Jason Witkowski (@jwitko)" - "Hannes Ljungberg (@hannseman)" - "Piotr Wojciechowski (@wojciechowskipiotr)" short_description: docker swarm service description: - Manages docker services via a swarm manager node. - This modules does not support updating services in a stack. extends_documentation_fragment: - community.docker.docker - community.docker.docker.docker_py_2_documentation - community.docker.attributes - community.docker.attributes.actiongroup_docker attributes: check_mode: support: full diff_mode: support: full options: args: description: - List arguments to be passed to the container. - Corresponds to the C(ARG) parameter of C(docker service create). type: list elements: str command: description: - Command to execute when the container starts. - A command may be either a string or a list or a list of strings. - Corresponds to the C(COMMAND) parameter of C(docker service create). type: raw configs: description: - List of dictionaries describing the service configs. - Corresponds to the C(--config) option of C(docker service create). - Requires API version >= 1.30. type: list elements: dict suboptions: config_id: description: - Config's ID. type: str config_name: description: - Config's name as defined at its creation. type: str required: true filename: description: - Name of the file containing the config. Defaults to the O(configs[].config_name) if not specified. type: str uid: description: - UID of the config file's owner. type: str gid: description: - GID of the config file's group. type: str mode: description: - File access mode inside the container. Must be an octal number (like V(0644) or V(0444)). type: int container_labels: description: - Dictionary of key value pairs. - Corresponds to the C(--container-label) option of C(docker service create). type: dict sysctls: description: - Dictionary of key, value pairs. version_added: 3.10.0 type: dict dns: description: - List of custom DNS servers. - Corresponds to the C(--dns) option of C(docker service create). type: list elements: str dns_search: description: - List of custom DNS search domains. - Corresponds to the C(--dns-search) option of C(docker service create). type: list elements: str dns_options: description: - List of custom DNS options. - Corresponds to the C(--dns-option) option of C(docker service create). type: list elements: str endpoint_mode: description: - Service endpoint mode. - Corresponds to the C(--endpoint-mode) option of C(docker service create). type: str choices: - vip - dnsrr env: description: - List or dictionary of the service environment variables. - If passed a list each items need to be in the format of C(KEY=VALUE). - If passed a dictionary values which might be parsed as numbers, booleans or other types by the YAML parser must be quoted (for example V("true")) in order to avoid data loss. - Corresponds to the C(--env) option of C(docker service create). type: raw env_files: description: - List of paths to files, present on the target, containing environment variables C(FOO=BAR). - The order of the list is significant in determining the value assigned to a variable that shows up more than once. - If variable also present in O(env), then O(env) value will override. type: list elements: path force_update: description: - Force update even if no changes require it. - Corresponds to the C(--force) option of C(docker service update). type: bool default: false groups: description: - List of additional group names and/or IDs that the container process will run as. - Corresponds to the C(--group) option of C(docker service update). type: list elements: str healthcheck: description: - Configure a check that is run to determine whether or not containers for this service are "healthy". See the docs for the L(HEALTHCHECK Dockerfile instruction,https://docs.docker.com/engine/reference/builder/#healthcheck) for details on how healthchecks work. - "O(healthcheck.interval), O(healthcheck.timeout), and O(healthcheck.start_period) are specified as durations. They accept duration as a string in a format that look like: V(5h34m56s), V(1m30s), and so on. The supported units are V(us), V(ms), V(s), V(m) and V(h)." type: dict suboptions: test: description: - Command to run to check health. - Must be either a string or a list. If it is a list, the first item must be one of V(NONE), V(CMD) or V(CMD-SHELL). type: raw interval: description: - Time between running the check. type: str timeout: description: - Maximum time to allow one check to run. type: str retries: description: - Consecutive failures needed to report unhealthy. It accept integer value. type: int start_period: description: - Start period for the container to initialize before starting health-retries countdown. type: str hostname: description: - Container hostname. - Corresponds to the C(--hostname) option of C(docker service create). type: str hosts: description: - Dict of host-to-IP mappings, where each host name is a key in the dictionary. Each host name will be added to the container's /etc/hosts file. - Corresponds to the C(--host) option of C(docker service create). type: dict image: description: - Service image path and tag. - Corresponds to the C(IMAGE) parameter of C(docker service create). type: str init: description: - Use an init inside each service container to forward signals and reap processes. - Corresponds to the C(--init) option of C(docker service create). - Requires API version >= 1.37. type: bool labels: description: - Dictionary of key value pairs. - Corresponds to the C(--label) option of C(docker service create). type: dict limits: description: - Configures service resource limits. suboptions: cpus: description: - Service CPU limit. V(0) equals no limit. - Corresponds to the C(--limit-cpu) option of C(docker service create). type: float memory: description: - "Service memory limit in format C([]). Number is a positive integer. Unit can be V(B) (byte), V(K) (kibibyte, 1024B), V(M) (mebibyte), V(G) (gibibyte), V(T) (tebibyte), or V(P) (pebibyte)." - V(0) equals no limit. - Omitting the unit defaults to bytes. - Corresponds to the C(--limit-memory) option of C(docker service create). type: str type: dict logging: description: - "Logging configuration for the service." suboptions: driver: description: - Configure the logging driver for a service. - Corresponds to the C(--log-driver) option of C(docker service create). type: str options: description: - Options for service logging driver. - Corresponds to the C(--log-opt) option of C(docker service create). type: dict type: dict mode: description: - Service replication mode. - Service will be removed and recreated when changed. - Corresponds to the C(--mode) option of C(docker service create). type: str default: replicated choices: - replicated - global mounts: description: - List of dictionaries describing the service mounts. - Corresponds to the C(--mount) option of C(docker service create). type: list elements: dict suboptions: source: description: - Mount source (for example a volume name or a host path). - Must be specified if O(mounts[].type) is not V(tmpfs). type: str target: description: - Container path. type: str required: true type: description: - The mount type. - Note that V(npipe) is only supported by Docker for Windows. Also note that V(npipe) was added in Ansible 2.9. type: str default: bind choices: - bind - volume - tmpfs - npipe readonly: description: - Whether the mount should be read-only. type: bool labels: description: - Volume labels to apply. type: dict propagation: description: - The propagation mode to use. - Can only be used when O(mounts[].type=bind). type: str choices: - shared - slave - private - rshared - rslave - rprivate no_copy: description: - Disable copying of data from a container when a volume is created. - Can only be used when O(mounts[].type=volume). type: bool driver_config: description: - Volume driver configuration. - Can only be used when O(mounts[].type=volume). suboptions: name: description: - Name of the volume-driver plugin to use for the volume. type: str options: description: - Options as key-value pairs to pass to the driver for this volume. type: dict type: dict tmpfs_size: description: - "Size of the tmpfs mount in format C([]). Number is a positive integer. Unit can be V(B) (byte), V(K) (kibibyte, 1024B), V(M) (mebibyte), V(G) (gibibyte), V(T) (tebibyte), or V(P) (pebibyte)." - Can only be used when O(mounts[].type=tmpfs). type: str tmpfs_mode: description: - File mode of the tmpfs in octal. - Can only be used when O(mounts[].type=tmpfs). type: int name: description: - Service name. - Corresponds to the C(--name) option of C(docker service create). type: str required: true networks: description: - List of the service networks names or dictionaries. - When passed dictionaries valid sub-options are C(name), which is required, and C(aliases) and C(options). - Prior to API version 1.29, updating and removing networks is not supported. If changes are made the service will then be removed and recreated. - Corresponds to the C(--network) option of C(docker service create). type: list elements: raw placement: description: - Configures service placement preferences and constraints. suboptions: constraints: description: - List of the service constraints. - Corresponds to the C(--constraint) option of C(docker service create). type: list elements: str preferences: description: - List of the placement preferences as key value pairs. - Corresponds to the C(--placement-pref) option of C(docker service create). - Requires API version >= 1.27. type: list elements: dict replicas_max_per_node: description: - Maximum number of tasks per node. - Corresponds to the C(--replicas_max_per_node) option of C(docker service create). - Requires API version >= 1.40 type: int version_added: 1.3.0 type: dict publish: description: - List of dictionaries describing the service published ports. - Corresponds to the C(--publish) option of C(docker service create). type: list elements: dict suboptions: published_port: description: - The port to make externally available. type: int required: false target_port: description: - The port inside the container to expose. type: int required: true protocol: description: - What protocol to use. type: str default: tcp choices: - tcp - udp mode: description: - What publish mode to use. - Requires API version >= 1.32. type: str choices: - ingress - host read_only: description: - Mount the containers root filesystem as read only. - Corresponds to the C(--read-only) option of C(docker service create). type: bool replicas: description: - Number of containers instantiated in the service. Valid only if O(mode=replicated). - If set to V(-1), and service is not present, service replicas will be set to V(1). - If set to V(-1), and service is present, service replicas will be unchanged. - Corresponds to the C(--replicas) option of C(docker service create). type: int default: -1 reservations: description: - Configures service resource reservations. suboptions: cpus: description: - Service CPU reservation. V(0) equals no reservation. - Corresponds to the C(--reserve-cpu) option of C(docker service create). type: float memory: description: - "Service memory reservation in format C([]). Number is a positive integer. Unit can be V(B) (byte), V(K) (kibibyte, 1024B), V(M) (mebibyte), V(G) (gibibyte), V(T) (tebibyte), or V(P) (pebibyte)." - V(0) equals no reservation. - Omitting the unit defaults to bytes. - Corresponds to the C(--reserve-memory) option of C(docker service create). type: str type: dict resolve_image: description: - If the current image digest should be resolved from registry and updated if changed. - Requires API version >= 1.30. type: bool default: false restart_config: description: - Configures if and how to restart containers when they exit. suboptions: condition: description: - Restart condition of the service. - Corresponds to the C(--restart-condition) option of C(docker service create). type: str choices: - none - on-failure - any delay: description: - Delay between restarts. - "Accepts a a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--restart-delay) option of C(docker service create). type: str max_attempts: description: - Maximum number of service restarts. - Corresponds to the C(--restart-condition) option of C(docker service create). type: int window: description: - Restart policy evaluation window. - "Accepts a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--restart-window) option of C(docker service create). type: str type: dict rollback_config: description: - Configures how the service should be rolled back in case of a failing update. suboptions: parallelism: description: - The number of containers to rollback at a time. If set to 0, all containers rollback simultaneously. - Corresponds to the C(--rollback-parallelism) option of C(docker service create). - Requires API version >= 1.28. type: int delay: description: - Delay between task rollbacks. - "Accepts a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--rollback-delay) option of C(docker service create). - Requires API version >= 1.28. type: str failure_action: description: - Action to take in case of rollback failure. - Corresponds to the C(--rollback-failure-action) option of C(docker service create). - Requires API version >= 1.28. type: str choices: - continue - pause monitor: description: - Duration after each task rollback to monitor for failure. - "Accepts a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--rollback-monitor) option of C(docker service create). - Requires API version >= 1.28. type: str max_failure_ratio: description: - Fraction of tasks that may fail during a rollback. - Corresponds to the C(--rollback-max-failure-ratio) option of C(docker service create). - Requires API version >= 1.28. type: float order: description: - Specifies the order of operations during rollbacks. - Corresponds to the C(--rollback-order) option of C(docker service create). - Requires API version >= 1.29. type: str type: dict secrets: description: - List of dictionaries describing the service secrets. - Corresponds to the C(--secret) option of C(docker service create). type: list elements: dict suboptions: secret_id: description: - Secret's ID. type: str secret_name: description: - Secret's name as defined at its creation. type: str required: true filename: description: - Name of the file containing the secret. Defaults to the O(secrets[].secret_name) if not specified. - Corresponds to the C(target) key of C(docker service create --secret). type: str uid: description: - UID of the secret file's owner. type: str gid: description: - GID of the secret file's group. type: str mode: description: - File access mode inside the container. Must be an octal number (like V(0644) or V(0444)). type: int state: description: - V(absent) - A service matching the specified name will be removed and have its tasks stopped. - V(present) - Asserts the existence of a service matching the name and provided configuration parameters. Unspecified configuration parameters will be set to docker defaults. type: str default: present choices: - present - absent stop_grace_period: description: - Time to wait before force killing a container. - "Accepts a duration as a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--stop-grace-period) option of C(docker service create). type: str stop_signal: description: - Override default signal used to stop the container. - Corresponds to the C(--stop-signal) option of C(docker service create). type: str tty: description: - Allocate a pseudo-TTY. - Corresponds to the C(--tty) option of C(docker service create). type: bool update_config: description: - Configures how the service should be updated. Useful for configuring rolling updates. suboptions: parallelism: description: - Rolling update parallelism. - Corresponds to the C(--update-parallelism) option of C(docker service create). type: int delay: description: - Rolling update delay. - "Accepts a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--update-delay) option of C(docker service create). type: str failure_action: description: - Action to take in case of container failure. - Corresponds to the C(--update-failure-action) option of C(docker service create). - Usage of V(rollback) requires API version >= 1.29. type: str choices: - continue - pause - rollback monitor: description: - Time to monitor updated tasks for failures. - "Accepts a string in a format that look like: V(5h34m56s), V(1m30s) etc. The supported units are V(us), V(ms), V(s), V(m) and V(h)." - Corresponds to the C(--update-monitor) option of C(docker service create). type: str max_failure_ratio: description: - Fraction of tasks that may fail during an update before the failure action is invoked. - Corresponds to the C(--update-max-failure-ratio) option of C(docker service create). type: float order: description: - Specifies the order of operations when rolling out an updated task. - Corresponds to the C(--update-order) option of C(docker service create). - Requires API version >= 1.29. type: str type: dict user: description: - Sets the username or UID used for the specified command. - Before Ansible 2.8, the default value for this option was V(root). - The default has been removed so that the user defined in the image is used if no user is specified here. - Corresponds to the C(--user) option of C(docker service create). type: str working_dir: description: - Path to the working directory. - Corresponds to the C(--workdir) option of C(docker service create). type: str cap_add: description: - List of capabilities to add to the container. - Requires API version >= 1.41. type: list elements: str version_added: 2.2.0 cap_drop: description: - List of capabilities to drop from the container. - Requires API version >= 1.41. type: list elements: str version_added: 2.2.0 requirements: - "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.0.2" - "Docker API >= 1.25" notes: - "Images will only resolve to the latest digest when using Docker API >= 1.30 and Docker SDK for Python >= 3.2.0. When using older versions use O(force_update=true) to trigger the swarm to resolve a new image." ''' RETURN = ''' swarm_service: returned: always type: dict description: - Dictionary of variables representing the current state of the service. Matches the module parameters format. - Note that facts are not part of registered vars but accessible directly. - Note that before Ansible 2.7.9, the return variable was documented as C(ansible_swarm_service), while the module actually returned a variable called C(ansible_docker_service). The variable was renamed to RV(swarm_service) in both code and documentation for Ansible 2.7.9 and Ansible 2.8.0. In Ansible 2.7.x, the old name C(ansible_docker_service) can still be used. sample: '{ "args": [ "3600" ], "cap_add": null, "cap_drop": [ "ALL" ], "command": [ "sleep" ], "configs": null, "constraints": [ "node.role == manager", "engine.labels.operatingsystem == ubuntu 14.04" ], "container_labels": null, "sysctls": null, "dns": null, "dns_options": null, "dns_search": null, "endpoint_mode": null, "env": [ "ENVVAR1=envvar1", "ENVVAR2=envvar2" ], "force_update": null, "groups": null, "healthcheck": { "interval": 90000000000, "retries": 3, "start_period": 30000000000, "test": [ "CMD", "curl", "--fail", "http://nginx.host.com" ], "timeout": 10000000000 }, "healthcheck_disabled": false, "hostname": null, "hosts": null, "image": "alpine:latest@sha256:b3dbf31b77fd99d9c08f780ce6f5282aba076d70a513a8be859d8d3a4d0c92b8", "labels": { "com.example.department": "Finance", "com.example.description": "Accounting webapp" }, "limit_cpu": 0.5, "limit_memory": 52428800, "log_driver": "fluentd", "log_driver_options": { "fluentd-address": "127.0.0.1:24224", "fluentd-async-connect": "true", "tag": "myservice" }, "mode": "replicated", "mounts": [ { "readonly": false, "source": "/tmp/", "target": "/remote_tmp/", "type": "bind", "labels": null, "propagation": null, "no_copy": null, "driver_config": null, "tmpfs_size": null, "tmpfs_mode": null } ], "networks": null, "placement_preferences": [ { "spread": "node.labels.mylabel" } ], "publish": null, "read_only": null, "replicas": 1, "replicas_max_per_node": 1, "reserve_cpu": 0.25, "reserve_memory": 20971520, "restart_policy": "on-failure", "restart_policy_attempts": 3, "restart_policy_delay": 5000000000, "restart_policy_window": 120000000000, "secrets": null, "stop_grace_period": null, "stop_signal": null, "tty": null, "update_delay": 10000000000, "update_failure_action": null, "update_max_failure_ratio": null, "update_monitor": null, "update_order": "stop-first", "update_parallelism": 2, "user": null, "working_dir": null }' changes: returned: always description: - List of changed service attributes if a service has been altered, [] otherwise. type: list elements: str sample: ['container_labels', 'replicas'] rebuilt: returned: always description: - True if the service has been recreated (removed and created) type: bool sample: true ''' EXAMPLES = ''' - name: Set command and arguments community.docker.docker_swarm_service: name: myservice image: alpine command: sleep args: - "3600" - name: Set a bind mount community.docker.docker_swarm_service: name: myservice image: alpine mounts: - source: /tmp/ target: /remote_tmp/ type: bind - name: Set service labels community.docker.docker_swarm_service: name: myservice image: alpine labels: com.example.description: "Accounting webapp" com.example.department: "Finance" - name: Set environment variables community.docker.docker_swarm_service: name: myservice image: alpine env: ENVVAR1: envvar1 ENVVAR2: envvar2 env_files: - envs/common.env - envs/apps/web.env - name: Set fluentd logging community.docker.docker_swarm_service: name: myservice image: alpine logging: driver: fluentd options: fluentd-address: "127.0.0.1:24224" fluentd-async-connect: "true" tag: myservice - name: Set restart policies community.docker.docker_swarm_service: name: myservice image: alpine restart_config: condition: on-failure delay: 5s max_attempts: 3 window: 120s - name: Set update config community.docker.docker_swarm_service: name: myservice image: alpine update_config: parallelism: 2 delay: 10s order: stop-first - name: Set rollback config community.docker.docker_swarm_service: name: myservice image: alpine update_config: failure_action: rollback rollback_config: parallelism: 2 delay: 10s order: stop-first - name: Set placement preferences community.docker.docker_swarm_service: name: myservice image: alpine:edge placement: preferences: - spread: node.labels.mylabel constraints: - node.role == manager - engine.labels.operatingsystem == ubuntu 14.04 replicas_max_per_node: 2 - name: Set configs community.docker.docker_swarm_service: name: myservice image: alpine:edge configs: - config_name: myconfig_name filename: "/tmp/config.txt" - name: Set networks community.docker.docker_swarm_service: name: myservice image: alpine:edge networks: - mynetwork - name: Set networks as a dictionary community.docker.docker_swarm_service: name: myservice image: alpine:edge networks: - name: "mynetwork" aliases: - "mynetwork_alias" options: foo: bar - name: Set secrets community.docker.docker_swarm_service: name: myservice image: alpine:edge secrets: - secret_name: mysecret_name filename: "/run/secrets/secret.txt" - name: Start service with healthcheck community.docker.docker_swarm_service: name: myservice image: nginx:1.13 healthcheck: # Check if nginx server is healthy by curl'ing the server. # If this fails or timeouts, the healthcheck fails. test: ["CMD", "curl", "--fail", "http://nginx.host.com"] interval: 1m30s timeout: 10s retries: 3 start_period: 30s - name: Configure service resources community.docker.docker_swarm_service: name: myservice image: alpine:edge reservations: cpus: 0.25 memory: 20M limits: cpus: 0.50 memory: 50M - name: Remove service community.docker.docker_swarm_service: name: myservice state: absent ''' import shlex import time import traceback from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion from ansible_collections.community.docker.plugins.module_utils.common import ( AnsibleDockerClient, RequestException, ) from ansible_collections.community.docker.plugins.module_utils.util import ( DifferenceTracker, DockerBaseClass, convert_duration_to_nanosecond, parse_healthcheck, clean_dict_booleans_for_docker_api, ) from ansible.module_utils.basic import human_to_bytes from ansible.module_utils.six import string_types from ansible.module_utils.common.text.converters import to_text, to_native try: from docker import types from docker.utils import ( parse_repository_tag, parse_env_file, format_environment, ) from docker.errors import ( APIError, DockerException, NotFound, ) except ImportError: # missing Docker SDK for Python handled in ansible.module_utils.docker.common pass def get_docker_environment(env, env_files): """ Will return a list of "KEY=VALUE" items. Supplied env variable can be either a list or a dictionary. If environment files are combined with explicit environment variables, the explicit environment variables take precedence. """ env_dict = {} if env_files: for env_file in env_files: parsed_env_file = parse_env_file(env_file) for name, value in parsed_env_file.items(): env_dict[name] = str(value) if env is not None and isinstance(env, string_types): env = env.split(',') if env is not None and isinstance(env, dict): for name, value in env.items(): if not isinstance(value, string_types): raise ValueError( 'Non-string value found for env option. ' 'Ambiguous env options must be wrapped in quotes to avoid YAML parsing. Key: %s' % name ) env_dict[name] = str(value) elif env is not None and isinstance(env, list): for item in env: try: name, value = item.split('=', 1) except ValueError: raise ValueError('Invalid environment variable found in list, needs to be in format KEY=VALUE.') env_dict[name] = value elif env is not None: raise ValueError( 'Invalid type for env %s (%s). Only list or dict allowed.' % (env, type(env)) ) env_list = format_environment(env_dict) if not env_list: if env is not None or env_files is not None: return [] else: return None return sorted(env_list) def get_docker_networks(networks, network_ids): """ Validate a list of network names or a list of network dictionaries. Network names will be resolved to ids by using the network_ids mapping. """ if networks is None: return None parsed_networks = [] for network in networks: if isinstance(network, string_types): parsed_network = {'name': network} elif isinstance(network, dict): if 'name' not in network: raise TypeError( '"name" is required when networks are passed as dictionaries.' ) name = network.pop('name') parsed_network = {'name': name} aliases = network.pop('aliases', None) if aliases is not None: if not isinstance(aliases, list): raise TypeError('"aliases" network option is only allowed as a list') if not all( isinstance(alias, string_types) for alias in aliases ): raise TypeError('Only strings are allowed as network aliases.') parsed_network['aliases'] = aliases options = network.pop('options', None) if options is not None: if not isinstance(options, dict): raise TypeError('Only dict is allowed as network options.') parsed_network['options'] = clean_dict_booleans_for_docker_api(options) # Check if any invalid keys left if network: invalid_keys = ', '.join(network.keys()) raise TypeError( '%s are not valid keys for the networks option' % invalid_keys ) else: raise TypeError( 'Only a list of strings or dictionaries are allowed to be passed as networks.' ) network_name = parsed_network.pop('name') try: parsed_network['id'] = network_ids[network_name] except KeyError as e: raise ValueError('Could not find a network named: %s.' % e) parsed_networks.append(parsed_network) return parsed_networks or [] def get_nanoseconds_from_raw_option(name, value): if value is None: return None elif isinstance(value, int): return value elif isinstance(value, string_types): try: return int(value) except ValueError: return convert_duration_to_nanosecond(value) else: raise ValueError( 'Invalid type for %s %s (%s). Only string or int allowed.' % (name, value, type(value)) ) def get_value(key, values, default=None): value = values.get(key) return value if value is not None else default def has_dict_changed(new_dict, old_dict): """ Check if new_dict has differences compared to old_dict while ignoring keys in old_dict which are None in new_dict. """ if new_dict is None: return False if not new_dict and old_dict: return True if not old_dict and new_dict: return True defined_options = dict( (option, value) for option, value in new_dict.items() if value is not None ) for option, value in defined_options.items(): old_value = old_dict.get(option) if not value and not old_value: continue if value != old_value: return True return False def has_list_changed(new_list, old_list, sort_lists=True, sort_key=None): """ Check two lists have differences. Sort lists by default. """ def sort_list(unsorted_list): """ Sort a given list. The list may contain dictionaries, so use the sort key to handle them. """ if unsorted_list and isinstance(unsorted_list[0], dict): if not sort_key: raise Exception( 'A sort key was not specified when sorting list' ) else: return sorted(unsorted_list, key=lambda k: k[sort_key]) # Either the list is empty or does not contain dictionaries try: return sorted(unsorted_list) except TypeError: return unsorted_list if new_list is None: return False old_list = old_list or [] if len(new_list) != len(old_list): return True if sort_lists: zip_data = zip(sort_list(new_list), sort_list(old_list)) else: zip_data = zip(new_list, old_list) for new_item, old_item in zip_data: is_same_type = type(new_item) == type(old_item) # noqa: E721, pylint: disable=unidiomatic-typecheck if not is_same_type: if isinstance(new_item, string_types) and isinstance(old_item, string_types): # Even though the types are different between these items, # they are both strings. Try matching on the same string type. try: new_item_type = type(new_item) old_item_casted = new_item_type(old_item) if new_item != old_item_casted: return True else: continue except UnicodeEncodeError: # Fallback to assuming the strings are different return True else: return True if isinstance(new_item, dict): if has_dict_changed(new_item, old_item): return True elif new_item != old_item: return True return False def have_networks_changed(new_networks, old_networks): """Special case list checking for networks to sort aliases""" if new_networks is None: return False old_networks = old_networks or [] if len(new_networks) != len(old_networks): return True zip_data = zip( sorted(new_networks, key=lambda k: k['id']), sorted(old_networks, key=lambda k: k['id']) ) for new_item, old_item in zip_data: new_item = dict(new_item) old_item = dict(old_item) # Sort the aliases if 'aliases' in new_item: new_item['aliases'] = sorted(new_item['aliases'] or []) if 'aliases' in old_item: old_item['aliases'] = sorted(old_item['aliases'] or []) if has_dict_changed(new_item, old_item): return True return False class DockerService(DockerBaseClass): def __init__(self, docker_api_version, docker_py_version): super(DockerService, self).__init__() self.image = "" self.command = None self.args = None self.endpoint_mode = None self.dns = None self.healthcheck = None self.healthcheck_disabled = None self.hostname = None self.hosts = None self.tty = None self.dns_search = None self.dns_options = None self.env = None self.force_update = None self.groups = None self.log_driver = None self.log_driver_options = None self.labels = None self.container_labels = None self.sysctls = None self.limit_cpu = None self.limit_memory = None self.reserve_cpu = None self.reserve_memory = None self.mode = "replicated" self.user = None self.mounts = None self.configs = None self.secrets = None self.constraints = None self.replicas_max_per_node = None self.networks = None self.stop_grace_period = None self.stop_signal = None self.publish = None self.placement_preferences = None self.replicas = -1 self.service_id = False self.service_version = False self.read_only = None self.restart_policy = None self.restart_policy_attempts = None self.restart_policy_delay = None self.restart_policy_window = None self.rollback_config = None self.update_delay = None self.update_parallelism = None self.update_failure_action = None self.update_monitor = None self.update_max_failure_ratio = None self.update_order = None self.working_dir = None self.init = None self.cap_add = None self.cap_drop = None self.docker_api_version = docker_api_version self.docker_py_version = docker_py_version def get_facts(self): return { 'image': self.image, 'mounts': self.mounts, 'configs': self.configs, 'networks': self.networks, 'command': self.command, 'args': self.args, 'tty': self.tty, 'dns': self.dns, 'dns_search': self.dns_search, 'dns_options': self.dns_options, 'healthcheck': self.healthcheck, 'healthcheck_disabled': self.healthcheck_disabled, 'hostname': self.hostname, 'hosts': self.hosts, 'env': self.env, 'force_update': self.force_update, 'groups': self.groups, 'log_driver': self.log_driver, 'log_driver_options': self.log_driver_options, 'publish': self.publish, 'constraints': self.constraints, 'replicas_max_per_node': self.replicas_max_per_node, 'placement_preferences': self.placement_preferences, 'labels': self.labels, 'container_labels': self.container_labels, 'sysctls': self.sysctls, 'mode': self.mode, 'replicas': self.replicas, 'endpoint_mode': self.endpoint_mode, 'restart_policy': self.restart_policy, 'secrets': self.secrets, 'stop_grace_period': self.stop_grace_period, 'stop_signal': self.stop_signal, 'limit_cpu': self.limit_cpu, 'limit_memory': self.limit_memory, 'read_only': self.read_only, 'reserve_cpu': self.reserve_cpu, 'reserve_memory': self.reserve_memory, 'restart_policy_delay': self.restart_policy_delay, 'restart_policy_attempts': self.restart_policy_attempts, 'restart_policy_window': self.restart_policy_window, 'rollback_config': self.rollback_config, 'update_delay': self.update_delay, 'update_parallelism': self.update_parallelism, 'update_failure_action': self.update_failure_action, 'update_monitor': self.update_monitor, 'update_max_failure_ratio': self.update_max_failure_ratio, 'update_order': self.update_order, 'user': self.user, 'working_dir': self.working_dir, 'init': self.init, 'cap_add': self.cap_add, 'cap_drop': self.cap_drop, } @property def can_update_networks(self): # Before Docker API 1.29 adding/removing networks was not supported return ( self.docker_api_version >= LooseVersion('1.29') and self.docker_py_version >= LooseVersion('2.7') ) @property def can_use_task_template_networks(self): # In Docker API 1.25 attaching networks to TaskTemplate is preferred over Spec return self.docker_py_version >= LooseVersion('2.7') @staticmethod def get_restart_config_from_ansible_params(params): restart_config = params['restart_config'] or {} condition = get_value( 'condition', restart_config, ) delay = get_value( 'delay', restart_config, ) delay = get_nanoseconds_from_raw_option( 'restart_policy_delay', delay ) max_attempts = get_value( 'max_attempts', restart_config, ) window = get_value( 'window', restart_config, ) window = get_nanoseconds_from_raw_option( 'restart_policy_window', window ) return { 'restart_policy': condition, 'restart_policy_delay': delay, 'restart_policy_attempts': max_attempts, 'restart_policy_window': window } @staticmethod def get_update_config_from_ansible_params(params): update_config = params['update_config'] or {} parallelism = get_value( 'parallelism', update_config, ) delay = get_value( 'delay', update_config, ) delay = get_nanoseconds_from_raw_option( 'update_delay', delay ) failure_action = get_value( 'failure_action', update_config, ) monitor = get_value( 'monitor', update_config, ) monitor = get_nanoseconds_from_raw_option( 'update_monitor', monitor ) max_failure_ratio = get_value( 'max_failure_ratio', update_config, ) order = get_value( 'order', update_config, ) return { 'update_parallelism': parallelism, 'update_delay': delay, 'update_failure_action': failure_action, 'update_monitor': monitor, 'update_max_failure_ratio': max_failure_ratio, 'update_order': order } @staticmethod def get_rollback_config_from_ansible_params(params): if params['rollback_config'] is None: return None rollback_config = params['rollback_config'] or {} delay = get_nanoseconds_from_raw_option( 'rollback_config.delay', rollback_config.get('delay') ) monitor = get_nanoseconds_from_raw_option( 'rollback_config.monitor', rollback_config.get('monitor') ) return { 'parallelism': rollback_config.get('parallelism'), 'delay': delay, 'failure_action': rollback_config.get('failure_action'), 'monitor': monitor, 'max_failure_ratio': rollback_config.get('max_failure_ratio'), 'order': rollback_config.get('order'), } @staticmethod def get_logging_from_ansible_params(params): logging_config = params['logging'] or {} driver = get_value( 'driver', logging_config, ) options = get_value( 'options', logging_config, ) return { 'log_driver': driver, 'log_driver_options': options, } @staticmethod def get_limits_from_ansible_params(params): limits = params['limits'] or {} cpus = get_value( 'cpus', limits, ) memory = get_value( 'memory', limits, ) if memory is not None: try: memory = human_to_bytes(memory) except ValueError as exc: raise Exception('Failed to convert limit_memory to bytes: %s' % exc) return { 'limit_cpu': cpus, 'limit_memory': memory, } @staticmethod def get_reservations_from_ansible_params(params): reservations = params['reservations'] or {} cpus = get_value( 'cpus', reservations, ) memory = get_value( 'memory', reservations, ) if memory is not None: try: memory = human_to_bytes(memory) except ValueError as exc: raise Exception('Failed to convert reserve_memory to bytes: %s' % exc) return { 'reserve_cpu': cpus, 'reserve_memory': memory, } @staticmethod def get_placement_from_ansible_params(params): placement = params['placement'] or {} constraints = get_value( 'constraints', placement ) preferences = placement.get('preferences') replicas_max_per_node = get_value( 'replicas_max_per_node', placement ) return { 'constraints': constraints, 'placement_preferences': preferences, 'replicas_max_per_node': replicas_max_per_node, } @classmethod def from_ansible_params( cls, ap, old_service, image_digest, secret_ids, config_ids, network_ids, docker_api_version, docker_py_version, ): s = DockerService(docker_api_version, docker_py_version) s.image = image_digest s.args = ap['args'] s.endpoint_mode = ap['endpoint_mode'] s.dns = ap['dns'] s.dns_search = ap['dns_search'] s.dns_options = ap['dns_options'] s.healthcheck, s.healthcheck_disabled = parse_healthcheck(ap['healthcheck']) s.hostname = ap['hostname'] s.hosts = ap['hosts'] s.tty = ap['tty'] s.labels = ap['labels'] s.container_labels = ap['container_labels'] s.sysctls = ap['sysctls'] s.mode = ap['mode'] s.stop_signal = ap['stop_signal'] s.user = ap['user'] s.working_dir = ap['working_dir'] s.read_only = ap['read_only'] s.init = ap['init'] s.cap_add = ap['cap_add'] s.cap_drop = ap['cap_drop'] s.networks = get_docker_networks(ap['networks'], network_ids) s.command = ap['command'] if isinstance(s.command, string_types): s.command = shlex.split(s.command) elif isinstance(s.command, list): invalid_items = [ (index, item) for index, item in enumerate(s.command) if not isinstance(item, string_types) ] if invalid_items: errors = ', '.join( [ '%s (%s) at index %s' % (item, type(item), index) for index, item in invalid_items ] ) raise Exception( 'All items in a command list need to be strings. ' 'Check quoting. Invalid items: %s.' % errors ) s.command = ap['command'] elif s.command is not None: raise ValueError( 'Invalid type for command %s (%s). ' 'Only string or list allowed. Check quoting.' % (s.command, type(s.command)) ) s.env = get_docker_environment(ap['env'], ap['env_files']) s.rollback_config = cls.get_rollback_config_from_ansible_params(ap) update_config = cls.get_update_config_from_ansible_params(ap) for key, value in update_config.items(): setattr(s, key, value) restart_config = cls.get_restart_config_from_ansible_params(ap) for key, value in restart_config.items(): setattr(s, key, value) logging_config = cls.get_logging_from_ansible_params(ap) for key, value in logging_config.items(): setattr(s, key, value) limits = cls.get_limits_from_ansible_params(ap) for key, value in limits.items(): setattr(s, key, value) reservations = cls.get_reservations_from_ansible_params(ap) for key, value in reservations.items(): setattr(s, key, value) placement = cls.get_placement_from_ansible_params(ap) for key, value in placement.items(): setattr(s, key, value) if ap['stop_grace_period'] is not None: s.stop_grace_period = convert_duration_to_nanosecond(ap['stop_grace_period']) if ap['force_update']: s.force_update = int(str(time.time()).replace('.', '')) if ap['groups'] is not None: # In case integers are passed as groups, we need to convert them to # strings as docker internally treats them as strings. s.groups = [str(g) for g in ap['groups']] if ap['replicas'] == -1: if old_service: s.replicas = old_service.replicas else: s.replicas = 1 else: s.replicas = ap['replicas'] if ap['publish'] is not None: s.publish = [] for param_p in ap['publish']: service_p = {} service_p['protocol'] = param_p['protocol'] service_p['mode'] = param_p['mode'] service_p['published_port'] = param_p['published_port'] service_p['target_port'] = param_p['target_port'] s.publish.append(service_p) if ap['mounts'] is not None: s.mounts = [] for param_m in ap['mounts']: service_m = {} service_m['readonly'] = param_m['readonly'] service_m['type'] = param_m['type'] if param_m['source'] is None and param_m['type'] != 'tmpfs': raise ValueError('Source must be specified for mounts which are not of type tmpfs') service_m['source'] = param_m['source'] or '' service_m['target'] = param_m['target'] service_m['labels'] = param_m['labels'] service_m['no_copy'] = param_m['no_copy'] service_m['propagation'] = param_m['propagation'] service_m['driver_config'] = param_m['driver_config'] service_m['tmpfs_mode'] = param_m['tmpfs_mode'] tmpfs_size = param_m['tmpfs_size'] if tmpfs_size is not None: try: tmpfs_size = human_to_bytes(tmpfs_size) except ValueError as exc: raise ValueError( 'Failed to convert tmpfs_size to bytes: %s' % exc ) service_m['tmpfs_size'] = tmpfs_size s.mounts.append(service_m) if ap['configs'] is not None: s.configs = [] for param_m in ap['configs']: service_c = {} config_name = param_m['config_name'] service_c['config_id'] = param_m['config_id'] or config_ids[config_name] service_c['config_name'] = config_name service_c['filename'] = param_m['filename'] or config_name service_c['uid'] = param_m['uid'] service_c['gid'] = param_m['gid'] service_c['mode'] = param_m['mode'] s.configs.append(service_c) if ap['secrets'] is not None: s.secrets = [] for param_m in ap['secrets']: service_s = {} secret_name = param_m['secret_name'] service_s['secret_id'] = param_m['secret_id'] or secret_ids[secret_name] service_s['secret_name'] = secret_name service_s['filename'] = param_m['filename'] or secret_name service_s['uid'] = param_m['uid'] service_s['gid'] = param_m['gid'] service_s['mode'] = param_m['mode'] s.secrets.append(service_s) return s def compare(self, os): differences = DifferenceTracker() needs_rebuild = False force_update = False if self.endpoint_mode is not None and self.endpoint_mode != os.endpoint_mode: differences.add('endpoint_mode', parameter=self.endpoint_mode, active=os.endpoint_mode) if has_list_changed(self.env, os.env): differences.add('env', parameter=self.env, active=os.env) if self.log_driver is not None and self.log_driver != os.log_driver: differences.add('log_driver', parameter=self.log_driver, active=os.log_driver) if self.log_driver_options is not None and self.log_driver_options != (os.log_driver_options or {}): differences.add('log_opt', parameter=self.log_driver_options, active=os.log_driver_options) if self.mode != os.mode: needs_rebuild = True differences.add('mode', parameter=self.mode, active=os.mode) if has_list_changed(self.mounts, os.mounts, sort_key='target'): differences.add('mounts', parameter=self.mounts, active=os.mounts) if has_list_changed(self.configs, os.configs, sort_key='config_name'): differences.add('configs', parameter=self.configs, active=os.configs) if has_list_changed(self.secrets, os.secrets, sort_key='secret_name'): differences.add('secrets', parameter=self.secrets, active=os.secrets) if have_networks_changed(self.networks, os.networks): differences.add('networks', parameter=self.networks, active=os.networks) needs_rebuild = not self.can_update_networks if self.replicas != os.replicas: differences.add('replicas', parameter=self.replicas, active=os.replicas) if has_list_changed(self.command, os.command, sort_lists=False): differences.add('command', parameter=self.command, active=os.command) if has_list_changed(self.args, os.args, sort_lists=False): differences.add('args', parameter=self.args, active=os.args) if has_list_changed(self.constraints, os.constraints): differences.add('constraints', parameter=self.constraints, active=os.constraints) if self.replicas_max_per_node is not None and self.replicas_max_per_node != os.replicas_max_per_node: differences.add('replicas_max_per_node', parameter=self.replicas_max_per_node, active=os.replicas_max_per_node) if has_list_changed(self.placement_preferences, os.placement_preferences, sort_lists=False): differences.add('placement_preferences', parameter=self.placement_preferences, active=os.placement_preferences) if has_list_changed(self.groups, os.groups): differences.add('groups', parameter=self.groups, active=os.groups) if self.labels is not None and self.labels != (os.labels or {}): differences.add('labels', parameter=self.labels, active=os.labels) if self.limit_cpu is not None and self.limit_cpu != os.limit_cpu: differences.add('limit_cpu', parameter=self.limit_cpu, active=os.limit_cpu) if self.limit_memory is not None and self.limit_memory != os.limit_memory: differences.add('limit_memory', parameter=self.limit_memory, active=os.limit_memory) if self.reserve_cpu is not None and self.reserve_cpu != os.reserve_cpu: differences.add('reserve_cpu', parameter=self.reserve_cpu, active=os.reserve_cpu) if self.reserve_memory is not None and self.reserve_memory != os.reserve_memory: differences.add('reserve_memory', parameter=self.reserve_memory, active=os.reserve_memory) if self.container_labels is not None and self.container_labels != (os.container_labels or {}): differences.add('container_labels', parameter=self.container_labels, active=os.container_labels) if self.sysctls is not None and self.sysctls != (os.sysctls or {}): differences.add('sysctls', parameter=self.sysctls, active=os.sysctls) if self.stop_signal is not None and self.stop_signal != os.stop_signal: differences.add('stop_signal', parameter=self.stop_signal, active=os.stop_signal) if self.stop_grace_period is not None and self.stop_grace_period != os.stop_grace_period: differences.add('stop_grace_period', parameter=self.stop_grace_period, active=os.stop_grace_period) if self.has_publish_changed(os.publish): differences.add('publish', parameter=self.publish, active=os.publish) if self.read_only is not None and self.read_only != os.read_only: differences.add('read_only', parameter=self.read_only, active=os.read_only) if self.restart_policy is not None and self.restart_policy != os.restart_policy: differences.add('restart_policy', parameter=self.restart_policy, active=os.restart_policy) if self.restart_policy_attempts is not None and self.restart_policy_attempts != os.restart_policy_attempts: differences.add('restart_policy_attempts', parameter=self.restart_policy_attempts, active=os.restart_policy_attempts) if self.restart_policy_delay is not None and self.restart_policy_delay != os.restart_policy_delay: differences.add('restart_policy_delay', parameter=self.restart_policy_delay, active=os.restart_policy_delay) if self.restart_policy_window is not None and self.restart_policy_window != os.restart_policy_window: differences.add('restart_policy_window', parameter=self.restart_policy_window, active=os.restart_policy_window) if has_dict_changed(self.rollback_config, os.rollback_config): differences.add('rollback_config', parameter=self.rollback_config, active=os.rollback_config) if self.update_delay is not None and self.update_delay != os.update_delay: differences.add('update_delay', parameter=self.update_delay, active=os.update_delay) if self.update_parallelism is not None and self.update_parallelism != os.update_parallelism: differences.add('update_parallelism', parameter=self.update_parallelism, active=os.update_parallelism) if self.update_failure_action is not None and self.update_failure_action != os.update_failure_action: differences.add('update_failure_action', parameter=self.update_failure_action, active=os.update_failure_action) if self.update_monitor is not None and self.update_monitor != os.update_monitor: differences.add('update_monitor', parameter=self.update_monitor, active=os.update_monitor) if self.update_max_failure_ratio is not None and self.update_max_failure_ratio != os.update_max_failure_ratio: differences.add('update_max_failure_ratio', parameter=self.update_max_failure_ratio, active=os.update_max_failure_ratio) if self.update_order is not None and self.update_order != os.update_order: differences.add('update_order', parameter=self.update_order, active=os.update_order) has_image_changed, change = self.has_image_changed(os.image) if has_image_changed: differences.add('image', parameter=self.image, active=change) if self.user and self.user != os.user: differences.add('user', parameter=self.user, active=os.user) if has_list_changed(self.dns, os.dns, sort_lists=False): differences.add('dns', parameter=self.dns, active=os.dns) if has_list_changed(self.dns_search, os.dns_search, sort_lists=False): differences.add('dns_search', parameter=self.dns_search, active=os.dns_search) if has_list_changed(self.dns_options, os.dns_options): differences.add('dns_options', parameter=self.dns_options, active=os.dns_options) if self.has_healthcheck_changed(os): differences.add('healthcheck', parameter=self.healthcheck, active=os.healthcheck) if self.hostname is not None and self.hostname != os.hostname: differences.add('hostname', parameter=self.hostname, active=os.hostname) if self.hosts is not None and self.hosts != (os.hosts or {}): differences.add('hosts', parameter=self.hosts, active=os.hosts) if self.tty is not None and self.tty != os.tty: differences.add('tty', parameter=self.tty, active=os.tty) if self.working_dir is not None and self.working_dir != os.working_dir: differences.add('working_dir', parameter=self.working_dir, active=os.working_dir) if self.force_update: force_update = True if self.init is not None and self.init != os.init: differences.add('init', parameter=self.init, active=os.init) if has_list_changed(self.cap_add, os.cap_add): differences.add('cap_add', parameter=self.cap_add, active=os.cap_add) if has_list_changed(self.cap_drop, os.cap_drop): differences.add('cap_drop', parameter=self.cap_drop, active=os.cap_drop) return not differences.empty or force_update, differences, needs_rebuild, force_update def has_healthcheck_changed(self, old_publish): if self.healthcheck_disabled is False and self.healthcheck is None: return False if self.healthcheck_disabled: if old_publish.healthcheck is None: return False if old_publish.healthcheck.get('test') == ['NONE']: return False return self.healthcheck != old_publish.healthcheck def has_publish_changed(self, old_publish): if self.publish is None: return False old_publish = old_publish or [] if len(self.publish) != len(old_publish): return True def publish_sorter(item): return (item.get('published_port') or 0, item.get('target_port') or 0, item.get('protocol') or '') publish = sorted(self.publish, key=publish_sorter) old_publish = sorted(old_publish, key=publish_sorter) for publish_item, old_publish_item in zip(publish, old_publish): ignored_keys = set() if not publish_item.get('mode'): ignored_keys.add('mode') # Create copies of publish_item dicts where keys specified in ignored_keys are left out filtered_old_publish_item = dict( (k, v) for k, v in old_publish_item.items() if k not in ignored_keys ) filtered_publish_item = dict( (k, v) for k, v in publish_item.items() if k not in ignored_keys ) if filtered_publish_item != filtered_old_publish_item: return True return False def has_image_changed(self, old_image): if '@' not in self.image: old_image = old_image.split('@')[0] return self.image != old_image, old_image def build_container_spec(self): mounts = None if self.mounts is not None: mounts = [] for mount_config in self.mounts: mount_options = { 'target': 'target', 'source': 'source', 'type': 'type', 'readonly': 'read_only', 'propagation': 'propagation', 'labels': 'labels', 'no_copy': 'no_copy', 'driver_config': 'driver_config', 'tmpfs_size': 'tmpfs_size', 'tmpfs_mode': 'tmpfs_mode' } mount_args = {} for option, mount_arg in mount_options.items(): value = mount_config.get(option) if value is not None: mount_args[mount_arg] = value mounts.append(types.Mount(**mount_args)) configs = None if self.configs is not None: configs = [] for config_config in self.configs: config_args = { 'config_id': config_config['config_id'], 'config_name': config_config['config_name'] } filename = config_config.get('filename') if filename: config_args['filename'] = filename uid = config_config.get('uid') if uid: config_args['uid'] = uid gid = config_config.get('gid') if gid: config_args['gid'] = gid mode = config_config.get('mode') if mode: config_args['mode'] = mode configs.append(types.ConfigReference(**config_args)) secrets = None if self.secrets is not None: secrets = [] for secret_config in self.secrets: secret_args = { 'secret_id': secret_config['secret_id'], 'secret_name': secret_config['secret_name'] } filename = secret_config.get('filename') if filename: secret_args['filename'] = filename uid = secret_config.get('uid') if uid: secret_args['uid'] = uid gid = secret_config.get('gid') if gid: secret_args['gid'] = gid mode = secret_config.get('mode') if mode: secret_args['mode'] = mode secrets.append(types.SecretReference(**secret_args)) dns_config_args = {} if self.dns is not None: dns_config_args['nameservers'] = self.dns if self.dns_search is not None: dns_config_args['search'] = self.dns_search if self.dns_options is not None: dns_config_args['options'] = self.dns_options dns_config = types.DNSConfig(**dns_config_args) if dns_config_args else None container_spec_args = {} if self.command is not None: container_spec_args['command'] = self.command if self.args is not None: container_spec_args['args'] = self.args if self.env is not None: container_spec_args['env'] = self.env if self.user is not None: container_spec_args['user'] = self.user if self.container_labels is not None: container_spec_args['labels'] = self.container_labels if self.sysctls is not None: container_spec_args['sysctls'] = self.sysctls if self.healthcheck is not None: container_spec_args['healthcheck'] = types.Healthcheck(**self.healthcheck) elif self.healthcheck_disabled: container_spec_args['healthcheck'] = types.Healthcheck(test=['NONE']) if self.hostname is not None: container_spec_args['hostname'] = self.hostname if self.hosts is not None: container_spec_args['hosts'] = self.hosts if self.read_only is not None: container_spec_args['read_only'] = self.read_only if self.stop_grace_period is not None: container_spec_args['stop_grace_period'] = self.stop_grace_period if self.stop_signal is not None: container_spec_args['stop_signal'] = self.stop_signal if self.tty is not None: container_spec_args['tty'] = self.tty if self.groups is not None: container_spec_args['groups'] = self.groups if self.working_dir is not None: container_spec_args['workdir'] = self.working_dir if secrets is not None: container_spec_args['secrets'] = secrets if mounts is not None: container_spec_args['mounts'] = mounts if dns_config is not None: container_spec_args['dns_config'] = dns_config if configs is not None: container_spec_args['configs'] = configs if self.init is not None: container_spec_args['init'] = self.init if self.cap_add is not None: container_spec_args['cap_add'] = self.cap_add if self.cap_drop is not None: container_spec_args['cap_drop'] = self.cap_drop return types.ContainerSpec(self.image, **container_spec_args) def build_placement(self): placement_args = {} if self.constraints is not None: placement_args['constraints'] = self.constraints if self.replicas_max_per_node is not None: placement_args['maxreplicas'] = self.replicas_max_per_node if self.placement_preferences is not None: placement_args['preferences'] = [ {key.title(): {'SpreadDescriptor': value}} for preference in self.placement_preferences for key, value in preference.items() ] return types.Placement(**placement_args) if placement_args else None def build_update_config(self): update_config_args = {} if self.update_parallelism is not None: update_config_args['parallelism'] = self.update_parallelism if self.update_delay is not None: update_config_args['delay'] = self.update_delay if self.update_failure_action is not None: update_config_args['failure_action'] = self.update_failure_action if self.update_monitor is not None: update_config_args['monitor'] = self.update_monitor if self.update_max_failure_ratio is not None: update_config_args['max_failure_ratio'] = self.update_max_failure_ratio if self.update_order is not None: update_config_args['order'] = self.update_order return types.UpdateConfig(**update_config_args) if update_config_args else None def build_log_driver(self): log_driver_args = {} if self.log_driver is not None: log_driver_args['name'] = self.log_driver if self.log_driver_options is not None: log_driver_args['options'] = self.log_driver_options return types.DriverConfig(**log_driver_args) if log_driver_args else None def build_restart_policy(self): restart_policy_args = {} if self.restart_policy is not None: restart_policy_args['condition'] = self.restart_policy if self.restart_policy_delay is not None: restart_policy_args['delay'] = self.restart_policy_delay if self.restart_policy_attempts is not None: restart_policy_args['max_attempts'] = self.restart_policy_attempts if self.restart_policy_window is not None: restart_policy_args['window'] = self.restart_policy_window return types.RestartPolicy(**restart_policy_args) if restart_policy_args else None def build_rollback_config(self): if self.rollback_config is None: return None rollback_config_options = [ 'parallelism', 'delay', 'failure_action', 'monitor', 'max_failure_ratio', 'order', ] rollback_config_args = {} for option in rollback_config_options: value = self.rollback_config.get(option) if value is not None: rollback_config_args[option] = value return types.RollbackConfig(**rollback_config_args) if rollback_config_args else None def build_resources(self): resources_args = {} if self.limit_cpu is not None: resources_args['cpu_limit'] = int(self.limit_cpu * 1000000000.0) if self.limit_memory is not None: resources_args['mem_limit'] = self.limit_memory if self.reserve_cpu is not None: resources_args['cpu_reservation'] = int(self.reserve_cpu * 1000000000.0) if self.reserve_memory is not None: resources_args['mem_reservation'] = self.reserve_memory return types.Resources(**resources_args) if resources_args else None def build_task_template(self, container_spec, placement=None): log_driver = self.build_log_driver() restart_policy = self.build_restart_policy() resources = self.build_resources() task_template_args = {} if placement is not None: task_template_args['placement'] = placement if log_driver is not None: task_template_args['log_driver'] = log_driver if restart_policy is not None: task_template_args['restart_policy'] = restart_policy if resources is not None: task_template_args['resources'] = resources if self.force_update: task_template_args['force_update'] = self.force_update if self.can_use_task_template_networks: networks = self.build_networks() if networks: task_template_args['networks'] = networks return types.TaskTemplate(container_spec=container_spec, **task_template_args) def build_service_mode(self): if self.mode == 'global': self.replicas = None return types.ServiceMode(self.mode, replicas=self.replicas) def build_networks(self): networks = None if self.networks is not None: networks = [] for network in self.networks: docker_network = {'Target': network['id']} if 'aliases' in network: docker_network['Aliases'] = network['aliases'] if 'options' in network: docker_network['DriverOpts'] = network['options'] networks.append(docker_network) return networks def build_endpoint_spec(self): endpoint_spec_args = {} if self.publish is not None: ports = [] for port in self.publish: port_spec = { 'Protocol': port['protocol'], 'TargetPort': port['target_port'] } if port.get('published_port'): port_spec['PublishedPort'] = port['published_port'] if port.get('mode'): port_spec['PublishMode'] = port['mode'] ports.append(port_spec) endpoint_spec_args['ports'] = ports if self.endpoint_mode is not None: endpoint_spec_args['mode'] = self.endpoint_mode return types.EndpointSpec(**endpoint_spec_args) if endpoint_spec_args else None def build_docker_service(self): container_spec = self.build_container_spec() placement = self.build_placement() task_template = self.build_task_template(container_spec, placement) update_config = self.build_update_config() rollback_config = self.build_rollback_config() service_mode = self.build_service_mode() endpoint_spec = self.build_endpoint_spec() service = {'task_template': task_template, 'mode': service_mode} if update_config: service['update_config'] = update_config if rollback_config: service['rollback_config'] = rollback_config if endpoint_spec: service['endpoint_spec'] = endpoint_spec if self.labels: service['labels'] = self.labels if not self.can_use_task_template_networks: networks = self.build_networks() if networks: service['networks'] = networks return service class DockerServiceManager(object): def __init__(self, client): self.client = client self.retries = 2 self.diff_tracker = None def get_service(self, name): try: raw_data = self.client.inspect_service(name) except NotFound: return None ds = DockerService(self.client.docker_api_version, self.client.docker_py_version) task_template_data = raw_data['Spec']['TaskTemplate'] ds.image = task_template_data['ContainerSpec']['Image'] ds.user = task_template_data['ContainerSpec'].get('User') ds.env = task_template_data['ContainerSpec'].get('Env') ds.command = task_template_data['ContainerSpec'].get('Command') ds.args = task_template_data['ContainerSpec'].get('Args') ds.groups = task_template_data['ContainerSpec'].get('Groups') ds.stop_grace_period = task_template_data['ContainerSpec'].get('StopGracePeriod') ds.stop_signal = task_template_data['ContainerSpec'].get('StopSignal') ds.working_dir = task_template_data['ContainerSpec'].get('Dir') ds.read_only = task_template_data['ContainerSpec'].get('ReadOnly') ds.cap_add = task_template_data['ContainerSpec'].get('CapabilityAdd') ds.cap_drop = task_template_data['ContainerSpec'].get('CapabilityDrop') ds.sysctls = task_template_data['ContainerSpec'].get('Sysctls') healthcheck_data = task_template_data['ContainerSpec'].get('Healthcheck') if healthcheck_data: options = { 'Test': 'test', 'Interval': 'interval', 'Timeout': 'timeout', 'StartPeriod': 'start_period', 'Retries': 'retries' } healthcheck = dict( (options[key], value) for key, value in healthcheck_data.items() if value is not None and key in options ) ds.healthcheck = healthcheck update_config_data = raw_data['Spec'].get('UpdateConfig') if update_config_data: ds.update_delay = update_config_data.get('Delay') ds.update_parallelism = update_config_data.get('Parallelism') ds.update_failure_action = update_config_data.get('FailureAction') ds.update_monitor = update_config_data.get('Monitor') ds.update_max_failure_ratio = update_config_data.get('MaxFailureRatio') ds.update_order = update_config_data.get('Order') rollback_config_data = raw_data['Spec'].get('RollbackConfig') if rollback_config_data: ds.rollback_config = { 'parallelism': rollback_config_data.get('Parallelism'), 'delay': rollback_config_data.get('Delay'), 'failure_action': rollback_config_data.get('FailureAction'), 'monitor': rollback_config_data.get('Monitor'), 'max_failure_ratio': rollback_config_data.get('MaxFailureRatio'), 'order': rollback_config_data.get('Order'), } dns_config = task_template_data['ContainerSpec'].get('DNSConfig') if dns_config: ds.dns = dns_config.get('Nameservers') ds.dns_search = dns_config.get('Search') ds.dns_options = dns_config.get('Options') ds.hostname = task_template_data['ContainerSpec'].get('Hostname') hosts = task_template_data['ContainerSpec'].get('Hosts') if hosts: hosts = [ list(reversed(host.split(":", 1))) if ":" in host else host.split(" ", 1) for host in hosts ] ds.hosts = dict((hostname, ip) for ip, hostname in hosts) ds.tty = task_template_data['ContainerSpec'].get('TTY') placement = task_template_data.get('Placement') if placement: ds.constraints = placement.get('Constraints') ds.replicas_max_per_node = placement.get('MaxReplicas') placement_preferences = [] for preference in placement.get('Preferences', []): placement_preferences.append( dict( (key.lower(), value['SpreadDescriptor']) for key, value in preference.items() ) ) ds.placement_preferences = placement_preferences or None restart_policy_data = task_template_data.get('RestartPolicy') if restart_policy_data: ds.restart_policy = restart_policy_data.get('Condition') ds.restart_policy_delay = restart_policy_data.get('Delay') ds.restart_policy_attempts = restart_policy_data.get('MaxAttempts') ds.restart_policy_window = restart_policy_data.get('Window') raw_data_endpoint_spec = raw_data['Spec'].get('EndpointSpec') if raw_data_endpoint_spec: ds.endpoint_mode = raw_data_endpoint_spec.get('Mode') raw_data_ports = raw_data_endpoint_spec.get('Ports') if raw_data_ports: ds.publish = [] for port in raw_data_ports: ds.publish.append({ 'protocol': port['Protocol'], 'mode': port.get('PublishMode', None), 'published_port': port.get('PublishedPort', None), 'target_port': int(port['TargetPort']) }) raw_data_limits = task_template_data.get('Resources', {}).get('Limits') if raw_data_limits: raw_cpu_limits = raw_data_limits.get('NanoCPUs') if raw_cpu_limits: ds.limit_cpu = float(raw_cpu_limits) / 1000000000 raw_memory_limits = raw_data_limits.get('MemoryBytes') if raw_memory_limits: ds.limit_memory = int(raw_memory_limits) raw_data_reservations = task_template_data.get('Resources', {}).get('Reservations') if raw_data_reservations: raw_cpu_reservations = raw_data_reservations.get('NanoCPUs') if raw_cpu_reservations: ds.reserve_cpu = float(raw_cpu_reservations) / 1000000000 raw_memory_reservations = raw_data_reservations.get('MemoryBytes') if raw_memory_reservations: ds.reserve_memory = int(raw_memory_reservations) ds.labels = raw_data['Spec'].get('Labels') ds.log_driver = task_template_data.get('LogDriver', {}).get('Name') ds.log_driver_options = task_template_data.get('LogDriver', {}).get('Options') ds.container_labels = task_template_data['ContainerSpec'].get('Labels') mode = raw_data['Spec']['Mode'] if 'Replicated' in mode.keys(): ds.mode = to_text('replicated', encoding='utf-8') ds.replicas = mode['Replicated']['Replicas'] elif 'Global' in mode.keys(): ds.mode = 'global' else: raise Exception('Unknown service mode: %s' % mode) raw_data_mounts = task_template_data['ContainerSpec'].get('Mounts') if raw_data_mounts: ds.mounts = [] for mount_data in raw_data_mounts: bind_options = mount_data.get('BindOptions', {}) volume_options = mount_data.get('VolumeOptions', {}) tmpfs_options = mount_data.get('TmpfsOptions', {}) driver_config = volume_options.get('DriverConfig', {}) driver_config = dict( (key.lower(), value) for key, value in driver_config.items() ) or None ds.mounts.append({ 'source': mount_data.get('Source', ''), 'type': mount_data['Type'], 'target': mount_data['Target'], 'readonly': mount_data.get('ReadOnly'), 'propagation': bind_options.get('Propagation'), 'no_copy': volume_options.get('NoCopy'), 'labels': volume_options.get('Labels'), 'driver_config': driver_config, 'tmpfs_mode': tmpfs_options.get('Mode'), 'tmpfs_size': tmpfs_options.get('SizeBytes'), }) raw_data_configs = task_template_data['ContainerSpec'].get('Configs') if raw_data_configs: ds.configs = [] for config_data in raw_data_configs: ds.configs.append({ 'config_id': config_data['ConfigID'], 'config_name': config_data['ConfigName'], 'filename': config_data['File'].get('Name'), 'uid': config_data['File'].get('UID'), 'gid': config_data['File'].get('GID'), 'mode': config_data['File'].get('Mode') }) raw_data_secrets = task_template_data['ContainerSpec'].get('Secrets') if raw_data_secrets: ds.secrets = [] for secret_data in raw_data_secrets: ds.secrets.append({ 'secret_id': secret_data['SecretID'], 'secret_name': secret_data['SecretName'], 'filename': secret_data['File'].get('Name'), 'uid': secret_data['File'].get('UID'), 'gid': secret_data['File'].get('GID'), 'mode': secret_data['File'].get('Mode') }) raw_networks_data = task_template_data.get('Networks', raw_data['Spec'].get('Networks')) if raw_networks_data: ds.networks = [] for network_data in raw_networks_data: network = {'id': network_data['Target']} if 'Aliases' in network_data: network['aliases'] = network_data['Aliases'] if 'DriverOpts' in network_data: network['options'] = network_data['DriverOpts'] ds.networks.append(network) ds.service_version = raw_data['Version']['Index'] ds.service_id = raw_data['ID'] ds.init = task_template_data['ContainerSpec'].get('Init', False) return ds def update_service(self, name, old_service, new_service): service_data = new_service.build_docker_service() result = self.client.update_service( old_service.service_id, old_service.service_version, name=name, **service_data ) # Prior to Docker SDK 4.0.0 no warnings were returned and will thus be ignored. # (see https://github.com/docker/docker-py/pull/2272) self.client.report_warnings(result, ['Warning']) def create_service(self, name, service): service_data = service.build_docker_service() result = self.client.create_service(name=name, **service_data) self.client.report_warnings(result, ['Warning']) def remove_service(self, name): self.client.remove_service(name) def get_image_digest(self, name, resolve=False): if ( not name or not resolve ): return name repo, tag = parse_repository_tag(name) if not tag: tag = 'latest' name = repo + ':' + tag distribution_data = self.client.inspect_distribution(name) digest = distribution_data['Descriptor']['digest'] return '%s@%s' % (name, digest) def get_networks_names_ids(self): return dict( (network['Name'], network['Id']) for network in self.client.networks() ) def get_missing_secret_ids(self): """ Resolve missing secret ids by looking them up by name """ secret_names = [ secret['secret_name'] for secret in self.client.module.params.get('secrets') or [] if secret['secret_id'] is None ] if not secret_names: return {} secrets = self.client.secrets(filters={'name': secret_names}) secrets = dict( (secret['Spec']['Name'], secret['ID']) for secret in secrets if secret['Spec']['Name'] in secret_names ) for secret_name in secret_names: if secret_name not in secrets: self.client.fail( 'Could not find a secret named "%s"' % secret_name ) return secrets def get_missing_config_ids(self): """ Resolve missing config ids by looking them up by name """ config_names = [ config['config_name'] for config in self.client.module.params.get('configs') or [] if config['config_id'] is None ] if not config_names: return {} configs = self.client.configs(filters={'name': config_names}) configs = dict( (config['Spec']['Name'], config['ID']) for config in configs if config['Spec']['Name'] in config_names ) for config_name in config_names: if config_name not in configs: self.client.fail( 'Could not find a config named "%s"' % config_name ) return configs def run(self): self.diff_tracker = DifferenceTracker() module = self.client.module image = module.params['image'] try: image_digest = self.get_image_digest( name=image, resolve=module.params['resolve_image'] ) except DockerException as e: self.client.fail( 'Error looking for an image named %s: %s' % (image, to_native(e)) ) try: current_service = self.get_service(module.params['name']) except Exception as e: self.client.fail( 'Error looking for service named %s: %s' % (module.params['name'], to_native(e)) ) try: secret_ids = self.get_missing_secret_ids() config_ids = self.get_missing_config_ids() network_ids = self.get_networks_names_ids() new_service = DockerService.from_ansible_params( module.params, current_service, image_digest, secret_ids, config_ids, network_ids, self.client.docker_api_version, self.client.docker_py_version ) except Exception as e: return self.client.fail( 'Error parsing module parameters: %s' % to_native(e) ) changed = False msg = 'noop' rebuilt = False differences = DifferenceTracker() facts = {} if current_service: if module.params['state'] == 'absent': if not module.check_mode: self.remove_service(module.params['name']) msg = 'Service removed' changed = True else: changed, differences, need_rebuild, force_update = new_service.compare( current_service ) if changed: self.diff_tracker.merge(differences) if need_rebuild: if not module.check_mode: self.remove_service(module.params['name']) self.create_service( module.params['name'], new_service ) msg = 'Service rebuilt' rebuilt = True else: if not module.check_mode: self.update_service( module.params['name'], current_service, new_service ) msg = 'Service updated' rebuilt = False else: if force_update: if not module.check_mode: self.update_service( module.params['name'], current_service, new_service ) msg = 'Service forcefully updated' rebuilt = False changed = True else: msg = 'Service unchanged' facts = new_service.get_facts() else: if module.params['state'] == 'absent': msg = 'Service absent' else: if not module.check_mode: self.create_service(module.params['name'], new_service) msg = 'Service created' changed = True facts = new_service.get_facts() return msg, changed, rebuilt, differences.get_legacy_docker_diffs(), facts def run_safe(self): while True: try: return self.run() except APIError as e: # Sometimes Version.Index will have changed between an inspect and # update. If this is encountered we'll retry the update. if self.retries > 0 and 'update out of sequence' in str(e.explanation): self.retries -= 1 time.sleep(1) else: raise def _detect_publish_mode_usage(client): for publish_def in client.module.params['publish'] or []: if publish_def.get('mode'): return True return False def _detect_healthcheck_start_period(client): if client.module.params['healthcheck']: return client.module.params['healthcheck']['start_period'] is not None return False def _detect_mount_tmpfs_usage(client): for mount in client.module.params['mounts'] or []: if mount.get('type') == 'tmpfs': return True if mount.get('tmpfs_size') is not None: return True if mount.get('tmpfs_mode') is not None: return True return False def _detect_update_config_failure_action_rollback(client): rollback_config_failure_action = ( (client.module.params['update_config'] or {}).get('failure_action') ) return rollback_config_failure_action == 'rollback' def main(): argument_spec = dict( name=dict(type='str', required=True), image=dict(type='str'), state=dict(type='str', default='present', choices=['present', 'absent']), mounts=dict(type='list', elements='dict', options=dict( source=dict(type='str'), target=dict(type='str', required=True), type=dict( type='str', default='bind', choices=['bind', 'volume', 'tmpfs', 'npipe'], ), readonly=dict(type='bool'), labels=dict(type='dict'), propagation=dict( type='str', choices=[ 'shared', 'slave', 'private', 'rshared', 'rslave', 'rprivate' ] ), no_copy=dict(type='bool'), driver_config=dict(type='dict', options=dict( name=dict(type='str'), options=dict(type='dict') )), tmpfs_size=dict(type='str'), tmpfs_mode=dict(type='int') )), configs=dict(type='list', elements='dict', options=dict( config_id=dict(type='str'), config_name=dict(type='str', required=True), filename=dict(type='str'), uid=dict(type='str'), gid=dict(type='str'), mode=dict(type='int'), )), secrets=dict(type='list', elements='dict', no_log=False, options=dict( secret_id=dict(type='str', no_log=False), secret_name=dict(type='str', required=True, no_log=False), filename=dict(type='str'), uid=dict(type='str'), gid=dict(type='str'), mode=dict(type='int'), )), networks=dict(type='list', elements='raw'), command=dict(type='raw'), args=dict(type='list', elements='str'), env=dict(type='raw'), env_files=dict(type='list', elements='path'), force_update=dict(type='bool', default=False), groups=dict(type='list', elements='str'), logging=dict(type='dict', options=dict( driver=dict(type='str'), options=dict(type='dict'), )), publish=dict(type='list', elements='dict', options=dict( published_port=dict(type='int', required=False), target_port=dict(type='int', required=True), protocol=dict(type='str', default='tcp', choices=['tcp', 'udp']), mode=dict(type='str', choices=['ingress', 'host']), )), placement=dict(type='dict', options=dict( constraints=dict(type='list', elements='str'), preferences=dict(type='list', elements='dict'), replicas_max_per_node=dict(type='int'), )), tty=dict(type='bool'), dns=dict(type='list', elements='str'), dns_search=dict(type='list', elements='str'), dns_options=dict(type='list', elements='str'), healthcheck=dict(type='dict', options=dict( test=dict(type='raw'), interval=dict(type='str'), timeout=dict(type='str'), start_period=dict(type='str'), retries=dict(type='int'), )), hostname=dict(type='str'), hosts=dict(type='dict'), labels=dict(type='dict'), container_labels=dict(type='dict'), sysctls=dict(type='dict'), mode=dict( type='str', default='replicated', choices=['replicated', 'global'] ), replicas=dict(type='int', default=-1), endpoint_mode=dict(type='str', choices=['vip', 'dnsrr']), stop_grace_period=dict(type='str'), stop_signal=dict(type='str'), limits=dict(type='dict', options=dict( cpus=dict(type='float'), memory=dict(type='str'), )), read_only=dict(type='bool'), reservations=dict(type='dict', options=dict( cpus=dict(type='float'), memory=dict(type='str'), )), resolve_image=dict(type='bool', default=False), restart_config=dict(type='dict', options=dict( condition=dict(type='str', choices=['none', 'on-failure', 'any']), delay=dict(type='str'), max_attempts=dict(type='int'), window=dict(type='str'), )), rollback_config=dict(type='dict', options=dict( parallelism=dict(type='int'), delay=dict(type='str'), failure_action=dict( type='str', choices=['continue', 'pause'] ), monitor=dict(type='str'), max_failure_ratio=dict(type='float'), order=dict(type='str'), )), update_config=dict(type='dict', options=dict( parallelism=dict(type='int'), delay=dict(type='str'), failure_action=dict( type='str', choices=['continue', 'pause', 'rollback'] ), monitor=dict(type='str'), max_failure_ratio=dict(type='float'), order=dict(type='str'), )), user=dict(type='str'), working_dir=dict(type='str'), init=dict(type='bool'), cap_add=dict(type='list', elements='str'), cap_drop=dict(type='list', elements='str'), ) option_minimal_versions = dict( dns=dict(docker_py_version='2.6.0'), dns_options=dict(docker_py_version='2.6.0'), dns_search=dict(docker_py_version='2.6.0'), endpoint_mode=dict(docker_py_version='3.0.0'), force_update=dict(docker_py_version='2.1.0'), healthcheck=dict(docker_py_version='2.6.0'), hostname=dict(docker_py_version='2.2.0'), hosts=dict(docker_py_version='2.6.0'), groups=dict(docker_py_version='2.6.0'), tty=dict(docker_py_version='2.4.0'), secrets=dict(docker_py_version='2.4.0'), configs=dict(docker_py_version='2.6.0', docker_api_version='1.30'), stop_signal=dict(docker_py_version='2.6.0', docker_api_version='1.28'), publish=dict(docker_py_version='3.0.0'), read_only=dict(docker_py_version='2.6.0', docker_api_version='1.28'), resolve_image=dict(docker_api_version='1.30', docker_py_version='3.2.0'), rollback_config=dict(docker_py_version='3.5.0', docker_api_version='1.28'), init=dict(docker_py_version='4.0.0', docker_api_version='1.37'), cap_add=dict(docker_py_version='5.0.3', docker_api_version='1.41'), cap_drop=dict(docker_py_version='5.0.3', docker_api_version='1.41'), sysctls=dict(docker_py_version='6.0.0', docker_api_version='1.40'), # specials publish_mode=dict( docker_py_version='3.0.0', detect_usage=_detect_publish_mode_usage, usage_msg='set publish.mode' ), healthcheck_start_period=dict( docker_py_version='2.6.0', docker_api_version='1.29', detect_usage=_detect_healthcheck_start_period, usage_msg='set healthcheck.start_period' ), update_config_max_failure_ratio=dict( docker_py_version='2.1.0', detect_usage=lambda c: (c.module.params['update_config'] or {}).get( 'max_failure_ratio' ) is not None, usage_msg='set update_config.max_failure_ratio' ), update_config_failure_action=dict( docker_py_version='3.5.0', docker_api_version='1.28', detect_usage=_detect_update_config_failure_action_rollback, usage_msg='set update_config.failure_action.rollback' ), update_config_monitor=dict( docker_py_version='2.1.0', detect_usage=lambda c: (c.module.params['update_config'] or {}).get( 'monitor' ) is not None, usage_msg='set update_config.monitor' ), update_config_order=dict( docker_py_version='2.7.0', docker_api_version='1.29', detect_usage=lambda c: (c.module.params['update_config'] or {}).get( 'order' ) is not None, usage_msg='set update_config.order' ), placement_config_preferences=dict( docker_py_version='2.4.0', docker_api_version='1.27', detect_usage=lambda c: (c.module.params['placement'] or {}).get( 'preferences' ) is not None, usage_msg='set placement.preferences' ), placement_config_constraints=dict( docker_py_version='2.4.0', detect_usage=lambda c: (c.module.params['placement'] or {}).get( 'constraints' ) is not None, usage_msg='set placement.constraints' ), placement_config_replicas_max_per_node=dict( docker_py_version='4.4.3', docker_api_version='1.40', detect_usage=lambda c: (c.module.params['placement'] or {}).get( 'replicas_max_per_node' ) is not None, usage_msg='set placement.replicas_max_per_node' ), mounts_tmpfs=dict( docker_py_version='2.6.0', detect_usage=_detect_mount_tmpfs_usage, usage_msg='set mounts.tmpfs' ), rollback_config_order=dict( docker_api_version='1.29', detect_usage=lambda c: (c.module.params['rollback_config'] or {}).get( 'order' ) is not None, usage_msg='set rollback_config.order' ), ) required_if = [ ('state', 'present', ['image']) ] client = AnsibleDockerClient( argument_spec=argument_spec, required_if=required_if, supports_check_mode=True, min_docker_version='2.0.2', option_minimal_versions=option_minimal_versions, ) try: dsm = DockerServiceManager(client) msg, changed, rebuilt, changes, facts = dsm.run_safe() results = dict( msg=msg, changed=changed, rebuilt=rebuilt, changes=changes, swarm_service=facts, ) if client.module._diff: before, after = dsm.diff_tracker.get_before_after() results['diff'] = dict(before=before, after=after) client.module.exit_json(**results) except DockerException as e: client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) except RequestException as e: client.fail( 'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {0}'.format(to_native(e)), exception=traceback.format_exc()) if __name__ == '__main__': main()