docker_swarm_service: Compare image by digest (#51134)

* Compare image by digest

* Add changelog fragment

* Fix version check

* Remove unused import

* Add note about image resolving

* Don’t overwrite image

* Fix documentation error

* Add resolve_image option

* Add version_added

* Remove whitespace

* Remove unused attribute

* Remove unused attribute
This commit is contained in:
Hannes Ljungberg 2019-01-27 17:48:16 +01:00 committed by ansibot
parent 6846152c46
commit 72a44e144a
4 changed files with 100 additions and 7 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- "docker_swarm_service - Resolve image digest from registry to detect and deploy changed images. This behaviour can be turned of by using the new option ``resolve_image: false``"

View file

@ -23,10 +23,18 @@ options:
description: description:
- Service name - Service name
image: image:
type: str
required: true required: true
description: description:
- Service image path and tag. - Service image path and tag.
Maps docker service IMAGE parameter. Maps docker service IMAGE parameter.
resolve_image:
type: bool
required: false
default: true
description:
- If the current image digest should be resolved from registry and updated if changed.
version_added: 2.8
state: state:
required: true required: true
default: present default: present
@ -335,6 +343,9 @@ requirements:
(see L(here,https://github.com/docker/docker-py/issues/1310) for details). (see L(here,https://github.com/docker/docker-py/issues/1310) for details).
Version 2.1.0 or newer is only available with the C(docker) module." Version 2.1.0 or newer is only available with the C(docker) module."
- "Docker API >= 1.24" - "Docker API >= 1.24"
notes:
- "Images will only resolve to the latest digest when using Docker API >= 1.30 and docker-py >= 3.2.0.
When using older versions use C(force_update: true) to trigger the swarm to resolve a new image."
''' '''
RETURN = ''' RETURN = '''
@ -519,9 +530,9 @@ import time
import shlex import shlex
import operator import operator
from ansible.module_utils.docker_common import ( from ansible.module_utils.docker_common import (
DockerBaseClass,
AnsibleDockerClient, AnsibleDockerClient,
DifferenceTracker, DifferenceTracker,
DockerBaseClass,
) )
from ansible.module_utils.basic import human_to_bytes from ansible.module_utils.basic import human_to_bytes
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
@ -530,6 +541,8 @@ from ansible.module_utils._text import to_text
try: try:
from distutils.version import LooseVersion from distutils.version import LooseVersion
from docker import types from docker import types
from docker.utils import parse_repository_tag
from docker.errors import DockerException
except Exception: except Exception:
# missing docker-py handled in ansible.module_utils.docker # missing docker-py handled in ansible.module_utils.docker
pass pass
@ -622,11 +635,11 @@ class DockerService(DockerBaseClass):
'update_order': self.update_order} 'update_order': self.update_order}
@staticmethod @staticmethod
def from_ansible_params(ap, old_service): def from_ansible_params(ap, old_service, image_digest):
s = DockerService() s = DockerService()
s.image = image_digest
s.constraints = ap['constraints'] s.constraints = ap['constraints']
s.placement_preferences = ap['placement_preferences'] s.placement_preferences = ap['placement_preferences']
s.image = ap['image']
s.args = ap['args'] s.args = ap['args']
s.endpoint_mode = ap['endpoint_mode'] s.endpoint_mode = ap['endpoint_mode']
s.dns = ap['dns'] s.dns = ap['dns']
@ -818,8 +831,9 @@ class DockerService(DockerBaseClass):
differences.add('update_max_failure_ratio', parameter=self.update_max_failure_ratio, active=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: 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) differences.add('update_order', parameter=self.update_order, active=os.update_order)
if self.image != os.image.split('@')[0]: has_image_changed, change = self.has_image_changed(os.image)
differences.add('image', parameter=self.image, active=os.image.split('@')[0]) if has_image_changed:
differences.add('image', parameter=self.image, active=change)
if self.user and self.user != os.user: if self.user and self.user != os.user:
differences.add('user', parameter=self.user, active=os.user) differences.add('user', parameter=self.user, active=os.user)
if self.dns != os.dns: if self.dns != os.dns:
@ -857,6 +871,11 @@ class DockerService(DockerBaseClass):
return True return True
return False 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 __str__(self): def __str__(self):
return str({ return str({
'mode': self.mode, 'mode': self.mode,
@ -1172,12 +1191,38 @@ class DockerServiceManager():
def remove_service(self, name): def remove_service(self, name):
self.client.remove_service(name) self.client.remove_service(name)
def get_image_digest(self, name, resolve=True):
if (
not name
or not resolve
or self.client.docker_py_version < LooseVersion('3.2')
or self.client.docker_api_version < LooseVersion('1.30')
):
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 __init__(self, client): def __init__(self, client):
self.client = client self.client = client
self.diff_tracker = DifferenceTracker() self.diff_tracker = DifferenceTracker()
def run(self): def run(self):
module = self.client.module 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:
return module.fail_json(
msg="Error looking for an image named %s: %s" % (image, e))
try: try:
current_service = self.get_service(module.params['name']) current_service = self.get_service(module.params['name'])
except Exception as e: except Exception as e:
@ -1185,7 +1230,11 @@ class DockerServiceManager():
msg="Error looking for service named %s: %s" % msg="Error looking for service named %s: %s" %
(module.params['name'], e)) (module.params['name'], e))
try: try:
new_service = DockerService.from_ansible_params(module.params, current_service) new_service = DockerService.from_ansible_params(
module.params,
current_service,
image_digest
)
except Exception as e: except Exception as e:
return module.fail_json( return module.fail_json(
msg="Error parsing module parameters: %s" % e) msg="Error parsing module parameters: %s" % e)
@ -1291,6 +1340,7 @@ def main():
limit_memory=dict(default=0, type='str'), limit_memory=dict(default=0, type='str'),
reserve_cpu=dict(default=0, type='float'), reserve_cpu=dict(default=0, type='float'),
reserve_memory=dict(default=0, type='str'), reserve_memory=dict(default=0, type='str'),
resolve_image=dict(default=True, type='bool'),
restart_policy_delay=dict(default=0, type='int'), restart_policy_delay=dict(default=0, type='int'),
restart_policy_attempts=dict(default=0, type='int'), restart_policy_attempts=dict(default=0, type='int'),
restart_policy_window=dict(default=0, type='int'), restart_policy_window=dict(default=0, type='int'),

View file

@ -86,10 +86,14 @@
published_port: 60001 published_port: 60001
target_port: 60001 target_port: 60001
- name: fake image key as it is not predictable
set_fact:
ansible_docker_service_output: "{{ output.ansible_docker_service|combine({'image': 'busybox'}) }}"
- name: assert service matches expectations - name: assert service matches expectations
assert: assert:
that: that:
- output.ansible_docker_service == service_expected_output - ansible_docker_service_output == service_expected_output
- name: delete sample service - name: delete sample service
register: output register: output

View file

@ -1269,6 +1269,43 @@
- reserve_memory_2 is not changed - reserve_memory_2 is not changed
- reserve_memory_3 is changed - reserve_memory_3 is changed
###################################################################
# resolve_image ###################################################
###################################################################
- name: resolve_image (false)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
resolve_image: false
register: resolve_image_1
- name: resolve_image (false idempotency)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
resolve_image: false
register: resolve_image_2
- name: resolve_image (change)
docker_swarm_service:
name: "{{ service_name }}"
image: alpine:3.8
resolve_image: true
register: resolve_image_3
- name: cleanup
docker_swarm_service:
name: "{{ service_name }}"
state: absent
diff: no
- assert:
that:
- resolve_image_1 is changed
- resolve_image_2 is not changed
- resolve_image_3 is changed
################################################################### ###################################################################
# restart_policy ################################################## # restart_policy ##################################################
################################################################### ###################################################################