mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
41cfdda6a3
* modules: fix examples to use FQCN * fix * fix * fix
3004 lines
115 KiB
Python
3004 lines
115 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# (c) 2017, Dario Zanzico (git@dariozanzico.com)
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
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)"
|
|
short_description: docker swarm service
|
|
description:
|
|
- Manages docker services via a swarm manager node.
|
|
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: yes
|
|
filename:
|
|
description:
|
|
- Name of the file containing the config. Defaults to the I(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 C(0644) or C(0444)).
|
|
type: int
|
|
constraints:
|
|
description:
|
|
- List of the service constraints.
|
|
- Corresponds to the C(--constraint) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(placement.constraints) instead.
|
|
type: list
|
|
elements: str
|
|
container_labels:
|
|
description:
|
|
- Dictionary of key value pairs.
|
|
- Corresponds to the C(--container-label) option of C(docker service create).
|
|
type: dict
|
|
dns:
|
|
description:
|
|
- List of custom DNS servers.
|
|
- Corresponds to the C(--dns) option of C(docker service create).
|
|
- Requires API version >= 1.25.
|
|
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).
|
|
- Requires API version >= 1.25.
|
|
type: list
|
|
elements: str
|
|
dns_options:
|
|
description:
|
|
- List of custom DNS options.
|
|
- Corresponds to the C(--dns-option) option of C(docker service create).
|
|
- Requires API version >= 1.25.
|
|
type: list
|
|
elements: str
|
|
endpoint_mode:
|
|
description:
|
|
- Service endpoint mode.
|
|
- Corresponds to the C(--endpoint-mode) option of C(docker service create).
|
|
- Requires API version >= 1.25.
|
|
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 (e.g. C("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 I(env), then I(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).
|
|
- Requires API version >= 1.25.
|
|
type: bool
|
|
default: no
|
|
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).
|
|
- Requires API version >= 1.25.
|
|
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.
|
|
- "I(interval), I(timeout) and I(start_period) are specified as durations. They accept duration as a string in a format
|
|
that look like: C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Requires API version >= 1.25.
|
|
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 C(NONE), C(CMD) or C(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).
|
|
- Requires API version >= 1.25.
|
|
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).
|
|
- Requires API version >= 1.25.
|
|
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
|
|
version_added: '0.2.0'
|
|
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. C(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>[<unit>]). 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)."
|
|
- C(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
|
|
limit_cpu:
|
|
description:
|
|
- Service CPU limit. C(0) equals no limit.
|
|
- Corresponds to the C(--limit-cpu) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(limits.cpus) instead.
|
|
type: float
|
|
limit_memory:
|
|
description:
|
|
- "Service memory limit in format C(<number>[<unit>]). 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)."
|
|
- C(0) equals no limit.
|
|
- Omitting the unit defaults to bytes.
|
|
- Corresponds to the C(--limit-memory) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(limits.memory) instead.
|
|
type: str
|
|
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
|
|
log_driver:
|
|
description:
|
|
- Configure the logging driver for a service.
|
|
- Corresponds to the C(--log-driver) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(logging.driver) instead.
|
|
type: str
|
|
log_driver_options:
|
|
description:
|
|
- Options for service logging driver.
|
|
- Corresponds to the C(--log-opt) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(logging.options) instead.
|
|
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 (e.g. a volume name or a host path).
|
|
- Must be specified if I(type) is not C(tmpfs).
|
|
type: str
|
|
target:
|
|
description:
|
|
- Container path.
|
|
type: str
|
|
required: yes
|
|
type:
|
|
description:
|
|
- The mount type.
|
|
- Note that C(npipe) is only supported by Docker for Windows. Also note that C(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 I(type) is C(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 I(type) is C(volume).
|
|
type: bool
|
|
driver_config:
|
|
description:
|
|
- Volume driver configuration.
|
|
- Can only be used when I(type) is C(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>[<unit>]). 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)."
|
|
- Can only be used when I(type) is C(tmpfs).
|
|
type: str
|
|
tmpfs_mode:
|
|
description:
|
|
- File mode of the tmpfs in octal.
|
|
- Can only be used when I(type) is C(tmpfs).
|
|
type: int
|
|
name:
|
|
description:
|
|
- Service name.
|
|
- Corresponds to the C(--name) option of C(docker service create).
|
|
type: str
|
|
required: yes
|
|
networks:
|
|
description:
|
|
- List of the service networks names or dictionaries.
|
|
- When passed dictionaries valid sub-options are I(name), which is required, and
|
|
I(aliases) and I(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
|
|
type: dict
|
|
publish:
|
|
description:
|
|
- List of dictionaries describing the service published ports.
|
|
- Corresponds to the C(--publish) option of C(docker service create).
|
|
- Requires API version >= 1.25.
|
|
type: list
|
|
elements: dict
|
|
suboptions:
|
|
published_port:
|
|
description:
|
|
- The port to make externally available.
|
|
type: int
|
|
required: yes
|
|
target_port:
|
|
description:
|
|
- The port inside the container to expose.
|
|
type: int
|
|
required: yes
|
|
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 I(mode) is C(replicated).
|
|
- If set to C(-1), and service is not present, service replicas will be set to C(1).
|
|
- If set to C(-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. C(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>[<unit>]). 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)."
|
|
- C(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
|
|
reserve_cpu:
|
|
description:
|
|
- Service CPU reservation. C(0) equals no reservation.
|
|
- Corresponds to the C(--reserve-cpu) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(reservations.cpus) instead.
|
|
type: float
|
|
reserve_memory:
|
|
description:
|
|
- "Service memory reservation in format C(<number>[<unit>]). 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)."
|
|
- C(0) equals no reservation.
|
|
- Omitting the unit defaults to bytes.
|
|
- Corresponds to the C(--reserve-memory) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(reservations.memory) instead.
|
|
type: str
|
|
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: no
|
|
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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Corresponds to the C(--restart-window) option of C(docker service create).
|
|
type: str
|
|
type: dict
|
|
restart_policy:
|
|
description:
|
|
- Restart condition of the service.
|
|
- Corresponds to the C(--restart-condition) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(restart_config.condition) instead.
|
|
type: str
|
|
choices:
|
|
- none
|
|
- on-failure
|
|
- any
|
|
restart_policy_attempts:
|
|
description:
|
|
- Maximum number of service restarts.
|
|
- Corresponds to the C(--restart-condition) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(restart_config.max_attempts) instead.
|
|
type: int
|
|
restart_policy_delay:
|
|
description:
|
|
- Delay between restarts.
|
|
- "Accepts a duration as an integer in nanoseconds or as a string in a format that look like:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Corresponds to the C(--restart-delay) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(restart_config.delay) instead.
|
|
type: raw
|
|
restart_policy_window:
|
|
description:
|
|
- Restart policy evaluation window.
|
|
- "Accepts a duration as an integer in nanoseconds or as a string in a format that look like:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Corresponds to the C(--restart-window) option of C(docker service create).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(restart_config.window) instead.
|
|
type: raw
|
|
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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(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).
|
|
- Requires API version >= 1.25.
|
|
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: yes
|
|
filename:
|
|
description:
|
|
- Name of the file containing the secret. Defaults to the I(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 C(0644) or C(0444)).
|
|
type: int
|
|
state:
|
|
description:
|
|
- C(absent) - A service matching the specified name will be removed and have its tasks stopped.
|
|
- C(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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(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).
|
|
- Requires API version >= 1.25.
|
|
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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(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 I(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:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Corresponds to the C(--update-monitor) option of C(docker service create).
|
|
- Requires API version >= 1.25.
|
|
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).
|
|
- Requires API version >= 1.25.
|
|
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
|
|
update_delay:
|
|
description:
|
|
- Rolling update delay.
|
|
- "Accepts a duration as an integer in nanoseconds or as a string in a format that look like:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Corresponds to the C(--update-delay) option of C(docker service create).
|
|
- Before Ansible 2.8, the default value for this option was C(10).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(update_config.delay) instead.
|
|
type: raw
|
|
update_parallelism:
|
|
description:
|
|
- Rolling update parallelism.
|
|
- Corresponds to the C(--update-parallelism) option of C(docker service create).
|
|
- Before Ansible 2.8, the default value for this option was C(1).
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(update_config.parallelism) instead.
|
|
type: int
|
|
update_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 I(rollback) requires API version >= 1.29.
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(update_config.failure_action) instead.
|
|
type: str
|
|
choices:
|
|
- continue
|
|
- pause
|
|
- rollback
|
|
update_monitor:
|
|
description:
|
|
- Time to monitor updated tasks for failures.
|
|
- "Accepts a duration as an integer in nanoseconds or as a string in a format that look like:
|
|
C(5h34m56s), C(1m30s) etc. The supported units are C(us), C(ms), C(s), C(m) and C(h)."
|
|
- Corresponds to the C(--update-monitor) option of C(docker service create).
|
|
- Requires API version >= 1.25.
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(update_config.monitor) instead.
|
|
type: raw
|
|
update_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).
|
|
- Requires API version >= 1.25.
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(update_config.max_failure_ratio) instead.
|
|
type: float
|
|
update_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.
|
|
- Deprecated in 2.8, will be removed in community.general 2.0.0. Use parameter C(update_config.order) instead.
|
|
type: str
|
|
choices:
|
|
- stop-first
|
|
- start-first
|
|
user:
|
|
description:
|
|
- Sets the username or UID used for the specified command.
|
|
- Before Ansible 2.8, the default value for this option was C(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
|
|
extends_documentation_fragment:
|
|
- community.general.docker
|
|
- community.general.docker.docker_py_2_documentation
|
|
|
|
requirements:
|
|
- "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 2.0.2"
|
|
- "Docker API >= 1.24"
|
|
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 C(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 C(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"
|
|
],
|
|
"command": [
|
|
"sleep"
|
|
],
|
|
"configs": null,
|
|
"constraints": [
|
|
"node.role == manager",
|
|
"engine.labels.operatingsystem == ubuntu 14.04"
|
|
],
|
|
"container_labels": 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,
|
|
"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.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine
|
|
command: sleep
|
|
args:
|
|
- "3600"
|
|
|
|
- name: Set a bind mount
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine
|
|
mounts:
|
|
- source: /tmp/
|
|
target: /remote_tmp/
|
|
type: bind
|
|
|
|
- name: Set service labels
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine
|
|
labels:
|
|
com.example.description: "Accounting webapp"
|
|
com.example.department: "Finance"
|
|
|
|
- name: Set environment variables
|
|
community.general.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.general.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.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine
|
|
restart_config:
|
|
condition: on-failure
|
|
delay: 5s
|
|
max_attempts: 3
|
|
window: 120s
|
|
|
|
- name: Set update config
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine
|
|
update_config:
|
|
parallelism: 2
|
|
delay: 10s
|
|
order: stop-first
|
|
|
|
- name: Set rollback config
|
|
community.general.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.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine:edge
|
|
placement:
|
|
preferences:
|
|
- spread: node.labels.mylabel
|
|
constraints:
|
|
- node.role == manager
|
|
- engine.labels.operatingsystem == ubuntu 14.04
|
|
|
|
- name: Set configs
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine:edge
|
|
configs:
|
|
- config_name: myconfig_name
|
|
filename: "/tmp/config.txt"
|
|
|
|
- name: Set networks
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine:edge
|
|
networks:
|
|
- mynetwork
|
|
|
|
- name: Set networks as a dictionary
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine:edge
|
|
networks:
|
|
- name: "mynetwork"
|
|
aliases:
|
|
- "mynetwork_alias"
|
|
options:
|
|
foo: bar
|
|
|
|
- name: Set secrets
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine:edge
|
|
secrets:
|
|
- secret_name: mysecret_name
|
|
filename: "/run/secrets/secret.txt"
|
|
|
|
- name: Start service with healthcheck
|
|
community.general.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.general.docker_swarm_service:
|
|
name: myservice
|
|
image: alpine:edge
|
|
reservations:
|
|
cpus: 0.25
|
|
memory: 20M
|
|
limits:
|
|
cpus: 0.50
|
|
memory: 50M
|
|
|
|
- name: Remove service
|
|
community.general.docker_swarm_service:
|
|
name: myservice
|
|
state: absent
|
|
'''
|
|
|
|
import shlex
|
|
import time
|
|
import operator
|
|
import traceback
|
|
|
|
from distutils.version import LooseVersion
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.docker.common import (
|
|
AnsibleDockerClient,
|
|
DifferenceTracker,
|
|
DockerBaseClass,
|
|
convert_duration_to_nanosecond,
|
|
parse_healthcheck,
|
|
clean_dict_booleans_for_docker_api,
|
|
RequestException,
|
|
)
|
|
|
|
from ansible.module_utils.basic import human_to_bytes
|
|
from ansible.module_utils.six import string_types
|
|
from ansible.module_utils._text import to_text
|
|
|
|
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)
|
|
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.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.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.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,
|
|
'placement_preferences': self.placement_preferences,
|
|
'labels': self.labels,
|
|
'container_labels': self.container_labels,
|
|
'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,
|
|
}
|
|
|
|
@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_api_version >= LooseVersion('1.25') and
|
|
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,
|
|
default=params['restart_policy']
|
|
)
|
|
delay = get_value(
|
|
'delay',
|
|
restart_config,
|
|
default=params['restart_policy_delay']
|
|
)
|
|
delay = get_nanoseconds_from_raw_option(
|
|
'restart_policy_delay',
|
|
delay
|
|
)
|
|
max_attempts = get_value(
|
|
'max_attempts',
|
|
restart_config,
|
|
default=params['restart_policy_attempts']
|
|
)
|
|
window = get_value(
|
|
'window',
|
|
restart_config,
|
|
default=params['restart_policy_window']
|
|
)
|
|
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,
|
|
default=params['update_parallelism']
|
|
)
|
|
delay = get_value(
|
|
'delay',
|
|
update_config,
|
|
default=params['update_delay']
|
|
)
|
|
delay = get_nanoseconds_from_raw_option(
|
|
'update_delay',
|
|
delay
|
|
)
|
|
failure_action = get_value(
|
|
'failure_action',
|
|
update_config,
|
|
default=params['update_failure_action']
|
|
)
|
|
monitor = get_value(
|
|
'monitor',
|
|
update_config,
|
|
default=params['update_monitor']
|
|
)
|
|
monitor = get_nanoseconds_from_raw_option(
|
|
'update_monitor',
|
|
monitor
|
|
)
|
|
max_failure_ratio = get_value(
|
|
'max_failure_ratio',
|
|
update_config,
|
|
default=params['update_max_failure_ratio']
|
|
)
|
|
order = get_value(
|
|
'order',
|
|
update_config,
|
|
default=params['update_order']
|
|
)
|
|
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,
|
|
default=params['log_driver']
|
|
)
|
|
options = get_value(
|
|
'options',
|
|
logging_config,
|
|
default=params['log_driver_options']
|
|
)
|
|
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,
|
|
default=params['limit_cpu']
|
|
)
|
|
memory = get_value(
|
|
'memory',
|
|
limits,
|
|
default=params['limit_memory']
|
|
)
|
|
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,
|
|
default=params['reserve_cpu']
|
|
)
|
|
memory = get_value(
|
|
'memory',
|
|
reservations,
|
|
default=params['reserve_memory']
|
|
)
|
|
|
|
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,
|
|
default=params['constraints']
|
|
)
|
|
|
|
preferences = placement.get('preferences')
|
|
return {
|
|
'constraints': constraints,
|
|
'placement_preferences': preferences,
|
|
}
|
|
|
|
@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.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.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 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.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)
|
|
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
|
|
publish_sorter = operator.itemgetter('published_port', 'target_port', 'protocol')
|
|
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.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
|
|
|
|
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.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'],
|
|
'PublishedPort': port['published_port'],
|
|
'TargetPort': port['target_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')
|
|
|
|
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')
|
|
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': int(port['PublishedPort']),
|
|
'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, 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'], 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' % 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')
|
|
)
|
|
update_failure_action = client.module.params['update_failure_action']
|
|
failure_action = rollback_config_failure_action or update_failure_action
|
|
return 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', options=dict(
|
|
secret_id=dict(type='str'),
|
|
secret_name=dict(type='str', required=True),
|
|
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'),
|
|
)),
|
|
log_driver=dict(type='str', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
log_driver_options=dict(type='dict', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
publish=dict(type='list', elements='dict', options=dict(
|
|
published_port=dict(type='int', required=True),
|
|
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'),
|
|
)),
|
|
constraints=dict(type='list', elements='str', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
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'),
|
|
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'),
|
|
)),
|
|
limit_cpu=dict(type='float', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
limit_memory=dict(type='str', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
read_only=dict(type='bool'),
|
|
reservations=dict(type='dict', options=dict(
|
|
cpus=dict(type='float'),
|
|
memory=dict(type='str'),
|
|
)),
|
|
reserve_cpu=dict(type='float', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
reserve_memory=dict(type='str', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
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'),
|
|
)),
|
|
restart_policy=dict(
|
|
type='str',
|
|
choices=['none', 'on-failure', 'any'],
|
|
removed_in_version='2.0.0',
|
|
removed_from_collection='community.general', # was Ansible 2.12
|
|
),
|
|
restart_policy_delay=dict(type='raw', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
restart_policy_attempts=dict(type='int', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
restart_policy_window=dict(type='raw', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
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'),
|
|
)),
|
|
update_delay=dict(type='raw', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
update_parallelism=dict(type='int', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
update_failure_action=dict(
|
|
type='str',
|
|
choices=['continue', 'pause', 'rollback'],
|
|
removed_in_version='2.0.0',
|
|
removed_from_collection='community.general', # was Ansible 2.12
|
|
),
|
|
update_monitor=dict(type='raw', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
update_max_failure_ratio=dict(type='float', removed_in_version='2.0.0',
|
|
removed_from_collection='community.general'), # was Ansible 2.12
|
|
update_order=dict(
|
|
type='str',
|
|
choices=['stop-first', 'start-first'],
|
|
removed_in_version='2.0.0',
|
|
removed_from_collection='community.general', # was Ansible 2.12
|
|
),
|
|
user=dict(type='str'),
|
|
working_dir=dict(type='str'),
|
|
init=dict(type='bool'),
|
|
)
|
|
|
|
option_minimal_versions = dict(
|
|
constraints=dict(docker_py_version='2.4.0'),
|
|
dns=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
|
|
dns_options=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
|
|
dns_search=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
|
|
endpoint_mode=dict(docker_py_version='3.0.0', docker_api_version='1.25'),
|
|
force_update=dict(docker_py_version='2.1.0', docker_api_version='1.25'),
|
|
healthcheck=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
|
|
hostname=dict(docker_py_version='2.2.0', docker_api_version='1.25'),
|
|
hosts=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
|
|
groups=dict(docker_py_version='2.6.0', docker_api_version='1.25'),
|
|
tty=dict(docker_py_version='2.4.0', docker_api_version='1.25'),
|
|
secrets=dict(docker_py_version='2.4.0', docker_api_version='1.25'),
|
|
configs=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
|
|
update_max_failure_ratio=dict(docker_py_version='2.1.0', docker_api_version='1.25'),
|
|
update_monitor=dict(docker_py_version='2.1.0', docker_api_version='1.25'),
|
|
update_order=dict(docker_py_version='2.7.0', docker_api_version='1.29'),
|
|
stop_signal=dict(docker_py_version='2.6.0', docker_api_version='1.28'),
|
|
publish=dict(docker_py_version='3.0.0', docker_api_version='1.25'),
|
|
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'),
|
|
# specials
|
|
publish_mode=dict(
|
|
docker_py_version='3.0.0',
|
|
docker_api_version='1.25',
|
|
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',
|
|
docker_api_version='1.25',
|
|
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',
|
|
docker_api_version='1.25',
|
|
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'
|
|
),
|
|
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',
|
|
min_docker_api_version='1.24',
|
|
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(e), exception=traceback.format_exc())
|
|
except RequestException as e:
|
|
client.fail('An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(e), exception=traceback.format_exc())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|