mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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.
This commit is contained in:
parent
895019c59b
commit
a520ca3298
7 changed files with 143 additions and 39 deletions
3
changelogs/fragments/docker-image-ids.yaml
Normal file
3
changelogs/fragments/docker-image-ids.yaml
Normal file
|
@ -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,19 +1996,22 @@ 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
|
||||||
repository, tag = utils.parse_repository_tag(self.parameters.image)
|
if is_image_name_id(self.parameters.image):
|
||||||
if not tag:
|
image = self.client.find_image_by_id(self.parameters.image)
|
||||||
tag = "latest"
|
else:
|
||||||
image = self.client.find_image(repository, tag)
|
repository, tag = utils.parse_repository_tag(self.parameters.image)
|
||||||
if not self.check_mode:
|
if not tag:
|
||||||
if not image or self.parameters.pull:
|
tag = "latest"
|
||||||
self.log("Pull the image.")
|
image = self.client.find_image(repository, tag)
|
||||||
image, alreadyToLatest = self.client.pull_image(repository, tag)
|
if not self.check_mode:
|
||||||
if alreadyToLatest:
|
if not image or self.parameters.pull:
|
||||||
self.results['changed'] = False
|
self.log("Pull the image.")
|
||||||
else:
|
image, alreadyToLatest = self.client.pull_image(repository, tag)
|
||||||
self.results['changed'] = True
|
if alreadyToLatest:
|
||||||
self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag)))
|
self.results['changed'] = False
|
||||||
|
else:
|
||||||
|
self.results['changed'] = True
|
||||||
|
self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag)))
|
||||||
self.log("image")
|
self.log("image")
|
||||||
self.log(image, pretty_print=True)
|
self.log(image, pretty_print=True)
|
||||||
return image
|
return image
|
||||||
|
|
|
@ -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,11 +200,15 @@ class ImageManager(DockerBaseClass):
|
||||||
names = [names]
|
names = [names]
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
repository, tag = utils.parse_repository_tag(name)
|
if is_image_name_id(name):
|
||||||
if not tag:
|
self.log('Fetching image %s (ID)' % (name))
|
||||||
tag = 'latest'
|
image = self.client.find_image_by_id(name)
|
||||||
self.log('Fetching image %s:%s' % (repository, tag))
|
else:
|
||||||
image = self.client.find_image(name=repository, tag=tag)
|
repository, tag = utils.parse_repository_tag(name)
|
||||||
|
if not tag:
|
||||||
|
tag = 'latest'
|
||||||
|
self.log('Fetching image %s:%s' % (repository, tag))
|
||||||
|
image = self.client.find_image(name=repository, tag=tag)
|
||||||
if image:
|
if image:
|
||||||
results.append(image)
|
results.append(image)
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -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…
Reference in a new issue