docker_container, docker_image_facts: allow to use image IDs (#46324)

* Allow to specify images by hash for docker_container and docker_image_facts.

* flake8

* More sanity checks.

* Added changelog.

* Added test.

* Make compatible with Python < 3.4.

* Remove out-commented imports.
pull/46137/head
Felix Fontein 6 years ago committed by John R Barker
parent 895019c59b
commit a520ca3298

@ -0,0 +1,3 @@
minor_changes:
- "docker_container - Allow to use image ID instead of image name."
- "docker_image_facts - Allow to use image ID instead of image name."

@ -18,9 +18,6 @@
import os import os
import re import re
import json
import sys
import copy
from distutils.version import LooseVersion from distutils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.basic import AnsibleModule, env_fallback
@ -35,22 +32,18 @@ HAS_DOCKER_ERROR = None
try: try:
from requests.exceptions import SSLError from requests.exceptions import SSLError
from docker import __version__ as docker_version from docker import __version__ as docker_version
from docker.errors import APIError, TLSParameterError, NotFound from docker.errors import APIError, TLSParameterError
from docker.tls import TLSConfig from docker.tls import TLSConfig
from docker.constants import DEFAULT_DOCKER_API_VERSION
from docker import auth from docker import auth
if LooseVersion(docker_version) >= LooseVersion('3.0.0'): if LooseVersion(docker_version) >= LooseVersion('3.0.0'):
HAS_DOCKER_PY_3 = True HAS_DOCKER_PY_3 = True
from docker import APIClient as Client from docker import APIClient as Client
from docker.types import Ulimit, LogConfig
elif LooseVersion(docker_version) >= LooseVersion('2.0.0'): elif LooseVersion(docker_version) >= LooseVersion('2.0.0'):
HAS_DOCKER_PY_2 = True HAS_DOCKER_PY_2 = True
from docker import APIClient as Client from docker import APIClient as Client
from docker.types import Ulimit, LogConfig
else: else:
from docker import Client from docker import Client
from docker.utils.types import Ulimit, LogConfig
except ImportError as exc: except ImportError as exc:
HAS_DOCKER_ERROR = str(exc) HAS_DOCKER_ERROR = str(exc)
@ -62,14 +55,14 @@ except ImportError as exc:
# installed, as they utilize the same namespace are are incompatible # installed, as they utilize the same namespace are are incompatible
try: try:
# docker # docker
import docker.models import docker.models # noqa: F401
HAS_DOCKER_MODELS = True HAS_DOCKER_MODELS = True
except ImportError: except ImportError:
HAS_DOCKER_MODELS = False HAS_DOCKER_MODELS = False
try: try:
# docker-py # docker-py
import docker.ssladapter import docker.ssladapter # noqa: F401
HAS_DOCKER_SSLADAPTER = True HAS_DOCKER_SSLADAPTER = True
except ImportError: except ImportError:
HAS_DOCKER_SSLADAPTER = False HAS_DOCKER_SSLADAPTER = False
@ -112,14 +105,21 @@ BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
if not HAS_DOCKER_PY: if not HAS_DOCKER_PY:
# No docker-py. Create a place holder client to allow # No docker-py. Create a place holder client to allow
# instantiation of AnsibleModule and proper error handing # instantiation of AnsibleModule and proper error handing
class Client(object): class Client(object): # noqa: F811
def __init__(self, **kwargs): def __init__(self, **kwargs):
pass pass
class APIError(Exception): class APIError(Exception): # noqa: F811
pass pass
def is_image_name_id(name):
"""Checks whether the given image name is in fact an image ID (hash)."""
if re.match('^sha256:[0-9a-fA-F]{64}$', name):
return True
return False
def sanitize_result(data): def sanitize_result(data):
"""Sanitize data object for return to Ansible. """Sanitize data object for return to Ansible.
@ -428,7 +428,7 @@ class AnsibleDockerClient(Client):
def find_image(self, name, tag): def find_image(self, name, tag):
''' '''
Lookup an image and return the inspection results. Lookup an image (by name and tag) and return the inspection results.
''' '''
if not name: if not name:
return None return None
@ -457,6 +457,20 @@ class AnsibleDockerClient(Client):
self.log("Image %s:%s not found." % (name, tag)) self.log("Image %s:%s not found." % (name, tag))
return None return None
def find_image_by_id(self, id):
'''
Lookup an image (by ID) and return the inspection results.
'''
if not id:
return None
self.log("Find image %s (by ID)" % id)
try:
inspection = self.inspect_image(id)
except Exception as exc:
self.fail("Error inspecting image ID %s - %s" % (id, str(exc)))
return inspection
def _image_lookup(self, name, tag): def _image_lookup(self, name, tag):
''' '''
Including a tag in the name parameter sent to the docker-py images method does not Including a tag in the name parameter sent to the docker-py images method does not

@ -163,7 +163,9 @@ options:
image: image:
description: description:
- Repository path and tag used to create the container. If an image is not found or pull is true, the image - Repository path and tag used to create the container. If an image is not found or pull is true, the image
will be pulled from the registry. If no tag is included, 'latest' will be used. will be pulled from the registry. If no tag is included, C(latest) will be used.
- Can also be an image ID. If this is the case, the image is assumed to be available locally.
The C(pull) option is ignored for this case.
init: init:
description: description:
- Run an init inside the container that forwards signals and reaps processes. - Run an init inside the container that forwards signals and reaps processes.
@ -312,7 +314,10 @@ options:
- ports - ports
pull: pull:
description: description:
- If true, always pull the latest version of an image. Otherwise, will only pull an image when missing. - If true, always pull the latest version of an image. Otherwise, will only pull an image
when missing.
- I(Note) that images are only pulled when specified by name. If the image is specified
as a image ID (hash), it cannot be pulled.
type: bool type: bool
default: 'no' default: 'no'
purge_networks: purge_networks:
@ -693,7 +698,10 @@ import shlex
from distutils.version import LooseVersion from distutils.version import LooseVersion
from ansible.module_utils.basic import human_to_bytes from ansible.module_utils.basic import human_to_bytes
from ansible.module_utils.docker_common import HAS_DOCKER_PY_2, HAS_DOCKER_PY_3, AnsibleDockerClient, DockerBaseClass, sanitize_result from ansible.module_utils.docker_common import (
HAS_DOCKER_PY_2, HAS_DOCKER_PY_3, AnsibleDockerClient,
DockerBaseClass, sanitize_result, is_image_name_id,
)
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
try: try:
@ -979,7 +987,7 @@ class TaskParameters(DockerBaseClass):
for vol in self.volumes: for vol in self.volumes:
if ':' in vol: if ':' in vol:
if len(vol.split(':')) == 3: if len(vol.split(':')) == 3:
host, container, _ = vol.split(':') host, container, dummy = vol.split(':')
result.append(container) result.append(container)
continue continue
if len(vol.split(':')) == 2: if len(vol.split(':')) == 2:
@ -1988,6 +1996,9 @@ class ContainerManager(DockerBaseClass):
if not self.parameters.image: if not self.parameters.image:
self.log('No image specified') self.log('No image specified')
return None return None
if is_image_name_id(self.parameters.image):
image = self.client.find_image_by_id(self.parameters.image)
else:
repository, tag = utils.parse_repository_tag(self.parameters.image) repository, tag = utils.parse_repository_tag(self.parameters.image)
if not tag: if not tag:
tag = "latest" tag = "latest"

@ -58,6 +58,7 @@ options:
description: description:
- "Image name. Name format will be one of: name, repository/name, registry_server:port/name. - "Image name. Name format will be one of: name, repository/name, registry_server:port/name.
When pushing or pulling an image the name can optionally include the tag by appending ':tag_name'." When pushing or pulling an image the name can optionally include the tag by appending ':tag_name'."
- Note that image IDs (hashes) are not supported.
required: true required: true
path: path:
description: description:

@ -26,8 +26,9 @@ description:
options: options:
name: name:
description: description:
- An image name or a list of image names. Name format will be name[:tag] or repository/name[:tag], where tag is - An image name or a list of image names. Name format will be C(name[:tag]) or C(repository/name[:tag]),
optional. If a tag is not provided, 'latest' will be used. where C(tag) is optional. If a tag is not provided, C(latest) will be used. Instead of image names, also
image IDs can be used.
required: true required: true
extends_documentation_fragment: extends_documentation_fragment:
@ -163,7 +164,7 @@ except ImportError:
# missing docker-py handled in ansible.module_utils.docker_common # missing docker-py handled in ansible.module_utils.docker_common
pass pass
from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass from ansible.module_utils.docker_common import AnsibleDockerClient, DockerBaseClass, is_image_name_id
class ImageManager(DockerBaseClass): class ImageManager(DockerBaseClass):
@ -199,6 +200,10 @@ class ImageManager(DockerBaseClass):
names = [names] names = [names]
for name in names: for name in names:
if is_image_name_id(name):
self.log('Fetching image %s (ID)' % (name))
image = self.client.find_image_by_id(name)
else:
repository, tag = utils.parse_repository_tag(name) repository, tag = utils.parse_repository_tag(name)
if not tag: if not tag:
tag = 'latest' tag = 'latest'

@ -0,0 +1,71 @@
---
- name: Registering container name
set_fact:
cname: "{{ cname_prefix ~ '-iid' }}"
- name: Registering container name
set_fact:
cnames: "{{ cnames }} + [cname]"
- name: Pull images
docker_image:
name: "{{ item }}"
pull: true
loop:
- "hello-world:latest"
- "alpine:3.8"
- name: Get image ID of hello-world and alpine images
docker_image_facts:
name:
- "hello-world:latest"
- "alpine:3.8"
register: image_facts
- assert:
that:
- image_facts.images | length == 2
- name: Print image IDs
debug:
msg: "hello-world: {{ image_facts.images[0].Id }}; alpine: {{ image_facts.images[1].Id }}"
- name: Create container with hello-world image via ID
docker_container:
image: "{{ image_facts.images[0].Id }}"
name: "{{ cname }}"
state: present
register: create_1
- name: Create container with hello-world image via ID (idempotent)
docker_container:
image: "{{ image_facts.images[0].Id }}"
name: "{{ cname }}"
state: present
register: create_2
- name: Create container with alpine image via ID
docker_container:
image: "{{ image_facts.images[1].Id }}"
name: "{{ cname }}"
state: present
register: create_3
- name: Create container with alpine image via ID (idempotent)
docker_container:
image: "{{ image_facts.images[1].Id }}"
name: "{{ cname }}"
state: present
register: create_4
- name: Cleanup
docker_container:
name: "{{ cname }}"
state: absent
stop_timeout: 1
- assert:
that:
- create_1 is changed
- create_2 is not changed
- create_3 is changed
- create_4 is not changed

@ -33,7 +33,6 @@ def main():
'lib/ansible/modules/cloud/amazon/route53_zone.py', 'lib/ansible/modules/cloud/amazon/route53_zone.py',
'lib/ansible/modules/cloud/amazon/s3_sync.py', 'lib/ansible/modules/cloud/amazon/s3_sync.py',
'lib/ansible/modules/cloud/azure/azure_rm_loadbalancer.py', 'lib/ansible/modules/cloud/azure/azure_rm_loadbalancer.py',
'lib/ansible/modules/cloud/docker/docker_container.py',
'lib/ansible/modules/cloud/docker/docker_service.py', 'lib/ansible/modules/cloud/docker/docker_service.py',
'lib/ansible/modules/cloud/google/gce.py', 'lib/ansible/modules/cloud/google/gce.py',
'lib/ansible/modules/cloud/google/gce_eip.py', 'lib/ansible/modules/cloud/google/gce_eip.py',

Loading…
Cancel
Save