mirror of
https://github.com/ansible-collections/community.docker.git
synced 2026-07-30 12:15:45 +00:00
Reformat code with black and isort.
This commit is contained in:
@@ -9,8 +9,9 @@ import base64
|
||||
from ansible import constants as C
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.vars import merge_hash
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._scramble import unscramble
|
||||
from ansible_collections.community.docker.plugins.module_utils._scramble import (
|
||||
unscramble,
|
||||
)
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
@@ -24,15 +25,18 @@ class ActionModule(ActionBase):
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
del tmp # tmp no longer has any effect
|
||||
|
||||
self._task.args['_max_file_size_for_diff'] = C.MAX_FILE_SIZE_FOR_DIFF
|
||||
self._task.args["_max_file_size_for_diff"] = C.MAX_FILE_SIZE_FOR_DIFF
|
||||
|
||||
result = merge_hash(result, self._execute_module(task_vars=task_vars, wrap_async=self._task.async_val))
|
||||
result = merge_hash(
|
||||
result,
|
||||
self._execute_module(task_vars=task_vars, wrap_async=self._task.async_val),
|
||||
)
|
||||
|
||||
if 'diff' in result and result['diff'].get('scrambled_diff'):
|
||||
if "diff" in result and result["diff"].get("scrambled_diff"):
|
||||
# Scrambling is not done for security, but to avoid no_log screwing up the diff
|
||||
diff = result['diff']
|
||||
key = base64.b64decode(diff.pop('scrambled_diff'))
|
||||
for k in ('before', 'after'):
|
||||
diff = result["diff"]
|
||||
key = base64.b64decode(diff.pop("scrambled_diff"))
|
||||
for k in ("before", "after"):
|
||||
if k in diff:
|
||||
diff[k] = unscramble(diff[k], key)
|
||||
|
||||
|
||||
+214
-108
@@ -9,6 +9,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
author:
|
||||
- Lorin Hochestein (!UNKNOWN)
|
||||
@@ -114,26 +115,28 @@ options:
|
||||
import fcntl
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import selectors
|
||||
import subprocess
|
||||
import re
|
||||
from shlex import quote
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound, AnsibleConnectionFailure
|
||||
from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleFileNotFound
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.plugins.connection import ConnectionBase, BUFSIZE
|
||||
from ansible.plugins.connection import BUFSIZE, ConnectionBase
|
||||
from ansible.utils.display import Display
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' Local docker based connections '''
|
||||
"""Local docker based connections"""
|
||||
|
||||
transport = 'community.docker.docker'
|
||||
transport = "community.docker.docker"
|
||||
has_pipelining = True
|
||||
|
||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||
@@ -154,29 +157,31 @@ class Connection(ConnectionBase):
|
||||
|
||||
# Windows uses Powershell modules
|
||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
||||
self.module_implementation_preferences = ('.ps1', '.exe', '')
|
||||
self.module_implementation_preferences = (".ps1", ".exe", "")
|
||||
|
||||
if 'docker_command' in kwargs:
|
||||
self.docker_cmd = kwargs['docker_command']
|
||||
if "docker_command" in kwargs:
|
||||
self.docker_cmd = kwargs["docker_command"]
|
||||
else:
|
||||
try:
|
||||
self.docker_cmd = get_bin_path('docker')
|
||||
self.docker_cmd = get_bin_path("docker")
|
||||
except ValueError:
|
||||
raise AnsibleError("docker command not found in PATH")
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_version(version):
|
||||
version = re.sub('[^0-9a-zA-Z.]', '', version)
|
||||
version = re.sub('^v', '', version)
|
||||
version = re.sub("[^0-9a-zA-Z.]", "", version)
|
||||
version = re.sub("^v", "", version)
|
||||
return version
|
||||
|
||||
def _old_docker_version(self):
|
||||
cmd_args = self._docker_args
|
||||
|
||||
old_version_subcommand = ['version']
|
||||
old_version_subcommand = ["version"]
|
||||
|
||||
old_docker_cmd = [self.docker_cmd] + cmd_args + old_version_subcommand
|
||||
p = subprocess.Popen(old_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
old_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
cmd_output, err = p.communicate()
|
||||
|
||||
return old_docker_cmd, to_native(cmd_output), err, p.returncode
|
||||
@@ -185,10 +190,12 @@ class Connection(ConnectionBase):
|
||||
# no result yet, must be newer Docker version
|
||||
cmd_args = self._docker_args
|
||||
|
||||
new_version_subcommand = ['version', '--format', "'{{.Server.Version}}'"]
|
||||
new_version_subcommand = ["version", "--format", "'{{.Server.Version}}'"]
|
||||
|
||||
new_docker_cmd = [self.docker_cmd] + cmd_args + new_version_subcommand
|
||||
p = subprocess.Popen(new_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
new_docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
cmd_output, err = p.communicate()
|
||||
return new_docker_cmd, to_native(cmd_output), err, p.returncode
|
||||
|
||||
@@ -196,42 +203,49 @@ class Connection(ConnectionBase):
|
||||
|
||||
cmd, cmd_output, err, returncode = self._old_docker_version()
|
||||
if returncode == 0:
|
||||
for line in to_text(cmd_output, errors='surrogate_or_strict').split('\n'):
|
||||
if line.startswith('Server version:'): # old docker versions
|
||||
for line in to_text(cmd_output, errors="surrogate_or_strict").split("\n"):
|
||||
if line.startswith("Server version:"): # old docker versions
|
||||
return self._sanitize_version(line.split()[2])
|
||||
|
||||
cmd, cmd_output, err, returncode = self._new_docker_version()
|
||||
if returncode:
|
||||
raise AnsibleError(f'Docker version check ({to_native(cmd)}) failed: {to_native(err)}')
|
||||
raise AnsibleError(
|
||||
f"Docker version check ({to_native(cmd)}) failed: {to_native(err)}"
|
||||
)
|
||||
|
||||
return self._sanitize_version(to_text(cmd_output, errors='surrogate_or_strict'))
|
||||
return self._sanitize_version(to_text(cmd_output, errors="surrogate_or_strict"))
|
||||
|
||||
def _get_docker_remote_user(self):
|
||||
""" Get the default user configured in the docker container """
|
||||
container = self.get_option('remote_addr')
|
||||
"""Get the default user configured in the docker container"""
|
||||
container = self.get_option("remote_addr")
|
||||
if container in self._container_user_cache:
|
||||
return self._container_user_cache[container]
|
||||
p = subprocess.Popen([self.docker_cmd, 'inspect', '--format', '{{.Config.User}}', container],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
[self.docker_cmd, "inspect", "--format", "{{.Config.User}}", container],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
out, err = p.communicate()
|
||||
out = to_text(out, errors='surrogate_or_strict')
|
||||
out = to_text(out, errors="surrogate_or_strict")
|
||||
|
||||
if p.returncode != 0:
|
||||
display.warning(f'unable to retrieve default user from docker container: {out} {to_text(err)}')
|
||||
display.warning(
|
||||
f"unable to retrieve default user from docker container: {out} {to_text(err)}"
|
||||
)
|
||||
self._container_user_cache[container] = None
|
||||
return None
|
||||
|
||||
# The default exec user is root, unless it was changed in the Dockerfile with USER
|
||||
user = out.strip() or 'root'
|
||||
user = out.strip() or "root"
|
||||
self._container_user_cache[container] = user
|
||||
return user
|
||||
|
||||
def _build_exec_cmd(self, cmd):
|
||||
""" Build the local docker exec command to run cmd on remote_host
|
||||
"""Build the local docker exec command to run cmd on remote_host
|
||||
|
||||
If remote_user is available and is supported by the docker
|
||||
version we are using, it will be provided to docker exec.
|
||||
If remote_user is available and is supported by the docker
|
||||
version we are using, it will be provided to docker exec.
|
||||
"""
|
||||
|
||||
local_cmd = [self.docker_cmd]
|
||||
@@ -239,34 +253,46 @@ class Connection(ConnectionBase):
|
||||
if self._docker_args:
|
||||
local_cmd += self._docker_args
|
||||
|
||||
local_cmd += [b'exec']
|
||||
local_cmd += [b"exec"]
|
||||
|
||||
if self.remote_user is not None:
|
||||
local_cmd += [b'-u', self.remote_user]
|
||||
local_cmd += [b"-u", self.remote_user]
|
||||
|
||||
if self.get_option('extra_env'):
|
||||
for k, v in self.get_option('extra_env').items():
|
||||
for val, what in ((k, 'Key'), (v, 'Value')):
|
||||
if self.get_option("extra_env"):
|
||||
for k, v in self.get_option("extra_env").items():
|
||||
for val, what in ((k, "Key"), (v, "Value")):
|
||||
if not isinstance(val, str):
|
||||
raise AnsibleConnectionFailure(
|
||||
f'Non-string {what.lower()} found for extra_env option. Ambiguous env options must be '
|
||||
f'wrapped in quotes to avoid them being interpreted. {what}: {val!r}'
|
||||
f"Non-string {what.lower()} found for extra_env option. Ambiguous env options must be "
|
||||
f"wrapped in quotes to avoid them being interpreted. {what}: {val!r}"
|
||||
)
|
||||
local_cmd += [b'-e', b"%s=%s" % (to_bytes(k, errors='surrogate_or_strict'), to_bytes(v, errors='surrogate_or_strict'))]
|
||||
local_cmd += [
|
||||
b"-e",
|
||||
b"%s=%s"
|
||||
% (
|
||||
to_bytes(k, errors="surrogate_or_strict"),
|
||||
to_bytes(v, errors="surrogate_or_strict"),
|
||||
),
|
||||
]
|
||||
|
||||
if self.get_option('working_dir') is not None:
|
||||
local_cmd += [b'-w', to_bytes(self.get_option('working_dir'), errors='surrogate_or_strict')]
|
||||
if self.docker_version != 'dev' and LooseVersion(self.docker_version) < LooseVersion('18.06'):
|
||||
if self.get_option("working_dir") is not None:
|
||||
local_cmd += [
|
||||
b"-w",
|
||||
to_bytes(self.get_option("working_dir"), errors="surrogate_or_strict"),
|
||||
]
|
||||
if self.docker_version != "dev" and LooseVersion(
|
||||
self.docker_version
|
||||
) < LooseVersion("18.06"):
|
||||
# https://github.com/docker/cli/pull/732, first appeared in release 18.06.0
|
||||
raise AnsibleConnectionFailure(
|
||||
f'Providing the working directory requires Docker CLI version 18.06 or newer. You have Docker CLI version {self.docker_version}.'
|
||||
f"Providing the working directory requires Docker CLI version 18.06 or newer. You have Docker CLI version {self.docker_version}."
|
||||
)
|
||||
|
||||
if self.get_option('privileged'):
|
||||
local_cmd += [b'--privileged']
|
||||
if self.get_option("privileged"):
|
||||
local_cmd += [b"--privileged"]
|
||||
|
||||
# -i is needed to keep stdin open which allows pipelining to work
|
||||
local_cmd += [b'-i', self.get_option('remote_addr')] + cmd
|
||||
local_cmd += [b"-i", self.get_option("remote_addr")] + cmd
|
||||
|
||||
return local_cmd
|
||||
|
||||
@@ -274,22 +300,23 @@ class Connection(ConnectionBase):
|
||||
# TODO: this is mostly for backwards compatibility, play_context is used as fallback for older versions
|
||||
# docker arguments
|
||||
del self._docker_args[:]
|
||||
extra_args = self.get_option('docker_extra_args') or getattr(self._play_context, 'docker_extra_args', '')
|
||||
extra_args = self.get_option("docker_extra_args") or getattr(
|
||||
self._play_context, "docker_extra_args", ""
|
||||
)
|
||||
if extra_args:
|
||||
self._docker_args += extra_args.split(' ')
|
||||
self._docker_args += extra_args.split(" ")
|
||||
|
||||
def _set_conn_data(self):
|
||||
|
||||
''' initialize for the connection, cannot do only in init since all data is not ready at that point '''
|
||||
"""initialize for the connection, cannot do only in init since all data is not ready at that point"""
|
||||
|
||||
self._set_docker_args()
|
||||
|
||||
self.remote_user = self.get_option('remote_user')
|
||||
self.remote_user = self.get_option("remote_user")
|
||||
if self.remote_user is None and self._play_context.remote_user is not None:
|
||||
self.remote_user = self._play_context.remote_user
|
||||
|
||||
# timeout, use unless default and pc is different, backwards compat
|
||||
self.timeout = self.get_option('container_timeout')
|
||||
self.timeout = self.get_option("container_timeout")
|
||||
if self.timeout == 10 and self.timeout != self._play_context.timeout:
|
||||
self.timeout = self._play_context.timeout
|
||||
|
||||
@@ -300,23 +327,33 @@ class Connection(ConnectionBase):
|
||||
self._set_docker_args()
|
||||
|
||||
self._version = self._get_docker_version()
|
||||
if self._version == 'dev':
|
||||
display.warning('Docker version number is "dev". Will assume latest version.')
|
||||
if self._version != 'dev' and LooseVersion(self._version) < LooseVersion('1.3'):
|
||||
raise AnsibleError('docker connection type requires docker 1.3 or higher')
|
||||
if self._version == "dev":
|
||||
display.warning(
|
||||
'Docker version number is "dev". Will assume latest version.'
|
||||
)
|
||||
if self._version != "dev" and LooseVersion(self._version) < LooseVersion(
|
||||
"1.3"
|
||||
):
|
||||
raise AnsibleError(
|
||||
"docker connection type requires docker 1.3 or higher"
|
||||
)
|
||||
return self._version
|
||||
|
||||
def _get_actual_user(self):
|
||||
if self.remote_user is not None:
|
||||
# An explicit user is provided
|
||||
if self.docker_version == 'dev' or LooseVersion(self.docker_version) >= LooseVersion('1.7'):
|
||||
if self.docker_version == "dev" or LooseVersion(
|
||||
self.docker_version
|
||||
) >= LooseVersion("1.7"):
|
||||
# Support for specifying the exec user was added in docker 1.7
|
||||
return self.remote_user
|
||||
else:
|
||||
self.remote_user = None
|
||||
actual_user = self._get_docker_remote_user()
|
||||
if actual_user != self.get_option('remote_user'):
|
||||
display.warning(f'docker {self.docker_version} does not support remote_user, using container default: {self.actual_user or "?"}')
|
||||
if actual_user != self.get_option("remote_user"):
|
||||
display.warning(
|
||||
f'docker {self.docker_version} does not support remote_user, using container default: {self.actual_user or "?"}'
|
||||
)
|
||||
return actual_user
|
||||
elif self._display.verbosity > 2:
|
||||
# Since we are not setting the actual_user, look it up so we have it for logging later
|
||||
@@ -327,27 +364,30 @@ class Connection(ConnectionBase):
|
||||
return None
|
||||
|
||||
def _connect(self, port=None):
|
||||
""" Connect to the container. Nothing to do """
|
||||
"""Connect to the container. Nothing to do"""
|
||||
super(Connection, self)._connect()
|
||||
if not self._connected:
|
||||
self._set_conn_data()
|
||||
actual_user = self._get_actual_user()
|
||||
display.vvv(f"ESTABLISH DOCKER CONNECTION FOR USER: {actual_user or '?'}", host=self.get_option('remote_addr'))
|
||||
display.vvv(
|
||||
f"ESTABLISH DOCKER CONNECTION FOR USER: {actual_user or '?'}",
|
||||
host=self.get_option("remote_addr"),
|
||||
)
|
||||
self._connected = True
|
||||
|
||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||
""" Run a command on the docker host """
|
||||
"""Run a command on the docker host"""
|
||||
|
||||
self._set_conn_data()
|
||||
|
||||
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
local_cmd = self._build_exec_cmd([self._play_context.executable, '-c', cmd])
|
||||
local_cmd = self._build_exec_cmd([self._play_context.executable, "-c", cmd])
|
||||
|
||||
display.vvv(f"EXEC {to_text(local_cmd)}", host=self.get_option('remote_addr'))
|
||||
display.vvv(f"EXEC {to_text(local_cmd)}", host=self.get_option("remote_addr"))
|
||||
display.debug("opening command with Popen()")
|
||||
|
||||
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
||||
|
||||
p = subprocess.Popen(
|
||||
local_cmd,
|
||||
@@ -358,19 +398,32 @@ class Connection(ConnectionBase):
|
||||
display.debug("done running command with Popen()")
|
||||
|
||||
if self.become and self.become.expect_prompt() and sudoable:
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(
|
||||
p.stdout,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK,
|
||||
)
|
||||
fcntl.fcntl(
|
||||
p.stderr,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK,
|
||||
)
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(p.stdout, selectors.EVENT_READ)
|
||||
selector.register(p.stderr, selectors.EVENT_READ)
|
||||
|
||||
become_output = b''
|
||||
become_output = b""
|
||||
try:
|
||||
while not self.become.check_success(become_output) and not self.become.check_password_prompt(become_output):
|
||||
while not self.become.check_success(
|
||||
become_output
|
||||
) and not self.become.check_password_prompt(become_output):
|
||||
events = selector.select(self.timeout)
|
||||
if not events:
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output))
|
||||
raise AnsibleError(
|
||||
"timeout waiting for privilege escalation password prompt:\n"
|
||||
+ to_native(become_output)
|
||||
)
|
||||
|
||||
for key, event in events:
|
||||
if key.fileobj == p.stdout:
|
||||
@@ -380,16 +433,31 @@ class Connection(ConnectionBase):
|
||||
|
||||
if not chunk:
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('privilege output closed while waiting for password prompt:\n' + to_native(become_output))
|
||||
raise AnsibleError(
|
||||
"privilege output closed while waiting for password prompt:\n"
|
||||
+ to_native(become_output)
|
||||
)
|
||||
become_output += chunk
|
||||
finally:
|
||||
selector.close()
|
||||
|
||||
if not self.become.check_success(become_output):
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
p.stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
become_pass = self.become.get_option(
|
||||
"become_pass", playcontext=self._play_context
|
||||
)
|
||||
p.stdin.write(
|
||||
to_bytes(become_pass, errors="surrogate_or_strict") + b"\n"
|
||||
)
|
||||
fcntl.fcntl(
|
||||
p.stdout,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK,
|
||||
)
|
||||
fcntl.fcntl(
|
||||
p.stderr,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK,
|
||||
)
|
||||
|
||||
display.debug("getting output with communicate()")
|
||||
stdout, stderr = p.communicate(in_data)
|
||||
@@ -399,17 +467,18 @@ class Connection(ConnectionBase):
|
||||
return (p.returncode, stdout, stderr)
|
||||
|
||||
def _prefix_login_path(self, remote_path):
|
||||
''' Make sure that we put files into a standard path
|
||||
"""Make sure that we put files into a standard path
|
||||
|
||||
If a path is relative, then we need to choose where to put it.
|
||||
ssh chooses $HOME but we are not guaranteed that a home dir will
|
||||
exist in any given chroot. So for now we are choosing "/" instead.
|
||||
This also happens to be the former default.
|
||||
If a path is relative, then we need to choose where to put it.
|
||||
ssh chooses $HOME but we are not guaranteed that a home dir will
|
||||
exist in any given chroot. So for now we are choosing "/" instead.
|
||||
This also happens to be the former default.
|
||||
|
||||
Can revisit using $HOME instead if it is a problem
|
||||
'''
|
||||
Can revisit using $HOME instead if it is a problem
|
||||
"""
|
||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
||||
import ntpath
|
||||
|
||||
return ntpath.normpath(remote_path)
|
||||
else:
|
||||
if not remote_path.startswith(os.path.sep):
|
||||
@@ -417,57 +486,79 @@ class Connection(ConnectionBase):
|
||||
return os.path.normpath(remote_path)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
""" Transfer a file from local to docker container """
|
||||
"""Transfer a file from local to docker container"""
|
||||
self._set_conn_data()
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option('remote_addr'))
|
||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option("remote_addr"))
|
||||
|
||||
out_path = self._prefix_login_path(out_path)
|
||||
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
|
||||
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
|
||||
raise AnsibleFileNotFound(
|
||||
f"file or module does not exist: {to_native(in_path)}")
|
||||
f"file or module does not exist: {to_native(in_path)}"
|
||||
)
|
||||
|
||||
out_path = quote(out_path)
|
||||
# Older docker does not have native support for copying files into
|
||||
# running containers, so we use docker exec to implement this
|
||||
# Although docker version 1.8 and later provide support, the
|
||||
# owner and group of the files are always set to root
|
||||
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
|
||||
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
|
||||
if not os.fstat(in_file.fileno()).st_size:
|
||||
count = ' count=0'
|
||||
count = " count=0"
|
||||
else:
|
||||
count = ''
|
||||
args = self._build_exec_cmd([self._play_context.executable, "-c", f"dd of={out_path} bs={BUFSIZE}{count}"])
|
||||
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
|
||||
count = ""
|
||||
args = self._build_exec_cmd(
|
||||
[
|
||||
self._play_context.executable,
|
||||
"-c",
|
||||
f"dd of={out_path} bs={BUFSIZE}{count}",
|
||||
]
|
||||
)
|
||||
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
||||
try:
|
||||
p = subprocess.Popen(args, stdin=in_file, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args, stdin=in_file, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
except OSError:
|
||||
raise AnsibleError("docker connection requires dd command in the container to put files")
|
||||
raise AnsibleError(
|
||||
"docker connection requires dd command in the container to put files"
|
||||
)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError(f"failed to transfer file {to_native(in_path)} to {to_native(out_path)}:\n{to_native(stdout)}\n{to_native(stderr)}")
|
||||
raise AnsibleError(
|
||||
f"failed to transfer file {to_native(in_path)} to {to_native(out_path)}:\n{to_native(stdout)}\n{to_native(stderr)}"
|
||||
)
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
""" Fetch a file from container to local. """
|
||||
"""Fetch a file from container to local."""
|
||||
self._set_conn_data()
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.get_option('remote_addr'))
|
||||
display.vvv(
|
||||
f"FETCH {in_path} TO {out_path}", host=self.get_option("remote_addr")
|
||||
)
|
||||
|
||||
in_path = self._prefix_login_path(in_path)
|
||||
# out_path is the final file path, but docker takes a directory, not a
|
||||
# file path
|
||||
out_dir = os.path.dirname(out_path)
|
||||
|
||||
args = [self.docker_cmd, "cp", f"{self.get_option('remote_addr')}:{in_path}", out_dir]
|
||||
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
|
||||
args = [
|
||||
self.docker_cmd,
|
||||
"cp",
|
||||
f"{self.get_option('remote_addr')}:{in_path}",
|
||||
out_dir,
|
||||
]
|
||||
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
||||
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
p.communicate()
|
||||
|
||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
||||
import ntpath
|
||||
|
||||
actual_out_path = ntpath.join(out_dir, ntpath.basename(in_path))
|
||||
else:
|
||||
actual_out_path = os.path.join(out_dir, os.path.basename(in_path))
|
||||
@@ -475,25 +566,40 @@ class Connection(ConnectionBase):
|
||||
if p.returncode != 0:
|
||||
# Older docker does not have native support for fetching files command `cp`
|
||||
# If `cp` fails, try to use `dd` instead
|
||||
args = self._build_exec_cmd([self._play_context.executable, "-c", f"dd if={in_path} bs={BUFSIZE}"])
|
||||
args = [to_bytes(i, errors='surrogate_or_strict') for i in args]
|
||||
with open(to_bytes(actual_out_path, errors='surrogate_or_strict'), 'wb') as out_file:
|
||||
args = self._build_exec_cmd(
|
||||
[self._play_context.executable, "-c", f"dd if={in_path} bs={BUFSIZE}"]
|
||||
)
|
||||
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
||||
with open(
|
||||
to_bytes(actual_out_path, errors="surrogate_or_strict"), "wb"
|
||||
) as out_file:
|
||||
try:
|
||||
p = subprocess.Popen(args, stdin=subprocess.PIPE,
|
||||
stdout=out_file, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen(
|
||||
args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=out_file,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except OSError:
|
||||
raise AnsibleError("docker connection requires dd command in the container to put files")
|
||||
raise AnsibleError(
|
||||
"docker connection requires dd command in the container to put files"
|
||||
)
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
raise AnsibleError(f"failed to fetch file {in_path} to {out_path}:\n{stdout}\n{stderr}")
|
||||
raise AnsibleError(
|
||||
f"failed to fetch file {in_path} to {out_path}:\n{stdout}\n{stderr}"
|
||||
)
|
||||
|
||||
# Rename if needed
|
||||
if actual_out_path != out_path:
|
||||
os.rename(to_bytes(actual_out_path, errors='strict'), to_bytes(out_path, errors='strict'))
|
||||
os.rename(
|
||||
to_bytes(actual_out_path, errors="strict"),
|
||||
to_bytes(out_path, errors="strict"),
|
||||
)
|
||||
|
||||
def close(self):
|
||||
""" Terminate the connection. Nothing to do for Docker"""
|
||||
"""Terminate the connection. Nothing to do for Docker"""
|
||||
super(Connection, self).close()
|
||||
self._connected = False
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
author:
|
||||
- Felix Fontein (@felixfontein)
|
||||
@@ -107,11 +108,15 @@ options:
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from ansible.errors import AnsibleFileNotFound, AnsibleConnectionFailure
|
||||
from ansible.errors import AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.plugins.connection import ConnectionBase
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
RequestException,
|
||||
)
|
||||
@@ -121,17 +126,16 @@ from ansible_collections.community.docker.plugins.module_utils.copy import (
|
||||
fetch_file,
|
||||
put_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.socket_handler import (
|
||||
DockerSocketHandler,
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.socket_handler import (
|
||||
DockerSocketHandler,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
MIN_DOCKER_API = None
|
||||
|
||||
@@ -140,23 +144,29 @@ display = Display()
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
''' Local docker based connections '''
|
||||
"""Local docker based connections"""
|
||||
|
||||
transport = 'community.docker.docker_api'
|
||||
transport = "community.docker.docker_api"
|
||||
has_pipelining = True
|
||||
|
||||
def _call_client(self, callable, not_found_can_be_resource=False):
|
||||
remote_addr = self.get_option('remote_addr')
|
||||
remote_addr = self.get_option("remote_addr")
|
||||
try:
|
||||
return callable()
|
||||
except NotFound as e:
|
||||
if not_found_can_be_resource:
|
||||
raise AnsibleConnectionFailure(f'Could not find container "{remote_addr}" or resource in it ({e})')
|
||||
raise AnsibleConnectionFailure(
|
||||
f'Could not find container "{remote_addr}" or resource in it ({e})'
|
||||
)
|
||||
else:
|
||||
raise AnsibleConnectionFailure(f'Could not find container "{remote_addr}" ({e})')
|
||||
raise AnsibleConnectionFailure(
|
||||
f'Could not find container "{remote_addr}" ({e})'
|
||||
)
|
||||
except APIError as e:
|
||||
if e.response is not None and e.response.status_code == 409:
|
||||
raise AnsibleConnectionFailure(f'The container "{remote_addr}" has been paused ({e})')
|
||||
raise AnsibleConnectionFailure(
|
||||
f'The container "{remote_addr}" has been paused ({e})'
|
||||
)
|
||||
self.client.fail(
|
||||
f'An unexpected Docker error occurred for container "{remote_addr}": {e}'
|
||||
)
|
||||
@@ -177,18 +187,23 @@ class Connection(ConnectionBase):
|
||||
|
||||
# Windows uses Powershell modules
|
||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
||||
self.module_implementation_preferences = ('.ps1', '.exe', '')
|
||||
self.module_implementation_preferences = (".ps1", ".exe", "")
|
||||
|
||||
self.actual_user = None
|
||||
|
||||
def _connect(self, port=None):
|
||||
""" Connect to the container. Nothing to do """
|
||||
"""Connect to the container. Nothing to do"""
|
||||
super(Connection, self)._connect()
|
||||
if not self._connected:
|
||||
self.actual_user = self.get_option('remote_user')
|
||||
display.vvv(f"ESTABLISH DOCKER CONNECTION FOR USER: {self.actual_user or '?'}", host=self.get_option('remote_addr'))
|
||||
self.actual_user = self.get_option("remote_user")
|
||||
display.vvv(
|
||||
f"ESTABLISH DOCKER CONNECTION FOR USER: {self.actual_user or '?'}",
|
||||
host=self.get_option("remote_addr"),
|
||||
)
|
||||
if self.client is None:
|
||||
self.client = AnsibleDockerClient(self, min_docker_api_version=MIN_DOCKER_API)
|
||||
self.client = AnsibleDockerClient(
|
||||
self, min_docker_api_version=MIN_DOCKER_API
|
||||
)
|
||||
self._connected = True
|
||||
|
||||
if self.actual_user is None and display.verbosity > 2:
|
||||
@@ -196,95 +211,125 @@ class Connection(ConnectionBase):
|
||||
# Only do this if display verbosity is high enough that we'll need the value
|
||||
# This saves overhead from calling into docker when we do not need to
|
||||
display.vvv("Trying to determine actual user")
|
||||
result = self._call_client(lambda: self.client.get_json('/containers/{0}/json', self.get_option('remote_addr')))
|
||||
if result.get('Config'):
|
||||
self.actual_user = result['Config'].get('User')
|
||||
result = self._call_client(
|
||||
lambda: self.client.get_json(
|
||||
"/containers/{0}/json", self.get_option("remote_addr")
|
||||
)
|
||||
)
|
||||
if result.get("Config"):
|
||||
self.actual_user = result["Config"].get("User")
|
||||
if self.actual_user is not None:
|
||||
display.vvv(f"Actual user is '{self.actual_user}'")
|
||||
|
||||
def exec_command(self, cmd, in_data=None, sudoable=False):
|
||||
""" Run a command on the docker host """
|
||||
"""Run a command on the docker host"""
|
||||
|
||||
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||
|
||||
command = [self._play_context.executable, '-c', to_text(cmd)]
|
||||
command = [self._play_context.executable, "-c", to_text(cmd)]
|
||||
|
||||
do_become = self.become and self.become.expect_prompt() and sudoable
|
||||
|
||||
stdin_part = f', with stdin ({len(in_data)} bytes)' if in_data is not None else ''
|
||||
become_part = ', with become prompt' if do_become else ''
|
||||
stdin_part = (
|
||||
f", with stdin ({len(in_data)} bytes)" if in_data is not None else ""
|
||||
)
|
||||
become_part = ", with become prompt" if do_become else ""
|
||||
display.vvv(
|
||||
f"EXEC {to_text(command)}{stdin_part}{become_part}",
|
||||
host=self.get_option('remote_addr')
|
||||
host=self.get_option("remote_addr"),
|
||||
)
|
||||
|
||||
need_stdin = True if (in_data is not None) or do_become else False
|
||||
|
||||
data = {
|
||||
'Container': self.get_option('remote_addr'),
|
||||
'User': self.get_option('remote_user') or '',
|
||||
'Privileged': self.get_option('privileged'),
|
||||
'Tty': False,
|
||||
'AttachStdin': need_stdin,
|
||||
'AttachStdout': True,
|
||||
'AttachStderr': True,
|
||||
'Cmd': command,
|
||||
"Container": self.get_option("remote_addr"),
|
||||
"User": self.get_option("remote_user") or "",
|
||||
"Privileged": self.get_option("privileged"),
|
||||
"Tty": False,
|
||||
"AttachStdin": need_stdin,
|
||||
"AttachStdout": True,
|
||||
"AttachStderr": True,
|
||||
"Cmd": command,
|
||||
}
|
||||
|
||||
if 'detachKeys' in self.client._general_configs:
|
||||
data['detachKeys'] = self.client._general_configs['detachKeys']
|
||||
if "detachKeys" in self.client._general_configs:
|
||||
data["detachKeys"] = self.client._general_configs["detachKeys"]
|
||||
|
||||
if self.get_option('extra_env'):
|
||||
data['Env'] = []
|
||||
for k, v in self.get_option('extra_env').items():
|
||||
for val, what in ((k, 'Key'), (v, 'Value')):
|
||||
if self.get_option("extra_env"):
|
||||
data["Env"] = []
|
||||
for k, v in self.get_option("extra_env").items():
|
||||
for val, what in ((k, "Key"), (v, "Value")):
|
||||
if not isinstance(val, str):
|
||||
raise AnsibleConnectionFailure(
|
||||
f'Non-string {what.lower()} found for extra_env option. Ambiguous env options must be '
|
||||
f'wrapped in quotes to avoid them being interpreted. {what}: {val!r}'
|
||||
f"Non-string {what.lower()} found for extra_env option. Ambiguous env options must be "
|
||||
f"wrapped in quotes to avoid them being interpreted. {what}: {val!r}"
|
||||
)
|
||||
kk = to_text(k, errors='surrogate_or_strict')
|
||||
vv = to_text(v, errors='surrogate_or_strict')
|
||||
data['Env'].append(f'{kk}={vv}')
|
||||
kk = to_text(k, errors="surrogate_or_strict")
|
||||
vv = to_text(v, errors="surrogate_or_strict")
|
||||
data["Env"].append(f"{kk}={vv}")
|
||||
|
||||
if self.get_option('working_dir') is not None:
|
||||
data['WorkingDir'] = self.get_option('working_dir')
|
||||
if self.client.docker_api_version < LooseVersion('1.35'):
|
||||
if self.get_option("working_dir") is not None:
|
||||
data["WorkingDir"] = self.get_option("working_dir")
|
||||
if self.client.docker_api_version < LooseVersion("1.35"):
|
||||
raise AnsibleConnectionFailure(
|
||||
'Providing the working directory requires Docker API version 1.35 or newer.'
|
||||
f' The Docker daemon the connection is using has API version {self.client.docker_api_version_str}.'
|
||||
"Providing the working directory requires Docker API version 1.35 or newer."
|
||||
f" The Docker daemon the connection is using has API version {self.client.docker_api_version_str}."
|
||||
)
|
||||
|
||||
exec_data = self._call_client(lambda: self.client.post_json_to_json('/containers/{0}/exec', self.get_option('remote_addr'), data=data))
|
||||
exec_id = exec_data['Id']
|
||||
exec_data = self._call_client(
|
||||
lambda: self.client.post_json_to_json(
|
||||
"/containers/{0}/exec", self.get_option("remote_addr"), data=data
|
||||
)
|
||||
)
|
||||
exec_id = exec_data["Id"]
|
||||
|
||||
data = {
|
||||
'Tty': False,
|
||||
'Detach': False
|
||||
}
|
||||
data = {"Tty": False, "Detach": False}
|
||||
if need_stdin:
|
||||
exec_socket = self._call_client(lambda: self.client.post_json_to_stream_socket('/exec/{0}/start', exec_id, data=data))
|
||||
exec_socket = self._call_client(
|
||||
lambda: self.client.post_json_to_stream_socket(
|
||||
"/exec/{0}/start", exec_id, data=data
|
||||
)
|
||||
)
|
||||
try:
|
||||
with DockerSocketHandler(display, exec_socket, container=self.get_option('remote_addr')) as exec_socket_handler:
|
||||
with DockerSocketHandler(
|
||||
display, exec_socket, container=self.get_option("remote_addr")
|
||||
) as exec_socket_handler:
|
||||
if do_become:
|
||||
become_output = [b'']
|
||||
become_output = [b""]
|
||||
|
||||
def append_become_output(stream_id, data):
|
||||
become_output[0] += data
|
||||
|
||||
exec_socket_handler.set_block_done_callback(append_become_output)
|
||||
exec_socket_handler.set_block_done_callback(
|
||||
append_become_output
|
||||
)
|
||||
|
||||
while not self.become.check_success(become_output[0]) and not self.become.check_password_prompt(become_output[0]):
|
||||
if not exec_socket_handler.select(self.get_option('container_timeout')):
|
||||
while not self.become.check_success(
|
||||
become_output[0]
|
||||
) and not self.become.check_password_prompt(become_output[0]):
|
||||
if not exec_socket_handler.select(
|
||||
self.get_option("container_timeout")
|
||||
):
|
||||
stdout, stderr = exec_socket_handler.consume()
|
||||
raise AnsibleConnectionFailure('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output[0]))
|
||||
raise AnsibleConnectionFailure(
|
||||
"timeout waiting for privilege escalation password prompt:\n"
|
||||
+ to_native(become_output[0])
|
||||
)
|
||||
|
||||
if exec_socket_handler.is_eof():
|
||||
raise AnsibleConnectionFailure('privilege output closed while waiting for password prompt:\n' + to_native(become_output[0]))
|
||||
raise AnsibleConnectionFailure(
|
||||
"privilege output closed while waiting for password prompt:\n"
|
||||
+ to_native(become_output[0])
|
||||
)
|
||||
|
||||
if not self.become.check_success(become_output[0]):
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
exec_socket_handler.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
become_pass = self.become.get_option(
|
||||
"become_pass", playcontext=self._play_context
|
||||
)
|
||||
exec_socket_handler.write(
|
||||
to_bytes(become_pass, errors="surrogate_or_strict")
|
||||
+ b"\n"
|
||||
)
|
||||
|
||||
if in_data is not None:
|
||||
exec_socket_handler.write(in_data)
|
||||
@@ -293,25 +338,36 @@ class Connection(ConnectionBase):
|
||||
finally:
|
||||
exec_socket.close()
|
||||
else:
|
||||
stdout, stderr = self._call_client(lambda: self.client.post_json_to_stream(
|
||||
'/exec/{0}/start', exec_id, stream=False, demux=True, tty=False, data=data))
|
||||
stdout, stderr = self._call_client(
|
||||
lambda: self.client.post_json_to_stream(
|
||||
"/exec/{0}/start",
|
||||
exec_id,
|
||||
stream=False,
|
||||
demux=True,
|
||||
tty=False,
|
||||
data=data,
|
||||
)
|
||||
)
|
||||
|
||||
result = self._call_client(lambda: self.client.get_json('/exec/{0}/json', exec_id))
|
||||
result = self._call_client(
|
||||
lambda: self.client.get_json("/exec/{0}/json", exec_id)
|
||||
)
|
||||
|
||||
return result.get('ExitCode') or 0, stdout or b'', stderr or b''
|
||||
return result.get("ExitCode") or 0, stdout or b"", stderr or b""
|
||||
|
||||
def _prefix_login_path(self, remote_path):
|
||||
''' Make sure that we put files into a standard path
|
||||
"""Make sure that we put files into a standard path
|
||||
|
||||
If a path is relative, then we need to choose where to put it.
|
||||
ssh chooses $HOME but we are not guaranteed that a home dir will
|
||||
exist in any given chroot. So for now we are choosing "/" instead.
|
||||
This also happens to be the former default.
|
||||
If a path is relative, then we need to choose where to put it.
|
||||
ssh chooses $HOME but we are not guaranteed that a home dir will
|
||||
exist in any given chroot. So for now we are choosing "/" instead.
|
||||
This also happens to be the former default.
|
||||
|
||||
Can revisit using $HOME instead if it is a problem
|
||||
'''
|
||||
Can revisit using $HOME instead if it is a problem
|
||||
"""
|
||||
if getattr(self._shell, "_IS_WINDOWS", False):
|
||||
import ntpath
|
||||
|
||||
return ntpath.normpath(remote_path)
|
||||
else:
|
||||
if not remote_path.startswith(os.path.sep):
|
||||
@@ -319,21 +375,21 @@ class Connection(ConnectionBase):
|
||||
return os.path.normpath(remote_path)
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
""" Transfer a file from local to docker container """
|
||||
"""Transfer a file from local to docker container"""
|
||||
super(Connection, self).put_file(in_path, out_path)
|
||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option('remote_addr'))
|
||||
display.vvv(f"PUT {in_path} TO {out_path}", host=self.get_option("remote_addr"))
|
||||
|
||||
out_path = self._prefix_login_path(out_path)
|
||||
|
||||
if self.actual_user not in self.ids:
|
||||
dummy, ids, dummy = self.exec_command(b'id -u && id -g')
|
||||
remote_addr = self.get_option('remote_addr')
|
||||
dummy, ids, dummy = self.exec_command(b"id -u && id -g")
|
||||
remote_addr = self.get_option("remote_addr")
|
||||
try:
|
||||
user_id, group_id = ids.splitlines()
|
||||
self.ids[self.actual_user] = int(user_id), int(group_id)
|
||||
display.vvvv(
|
||||
f'PUT: Determined uid={user_id} and gid={group_id} for user "{self.actual_user}"',
|
||||
host=remote_addr
|
||||
host=remote_addr,
|
||||
)
|
||||
except Exception as e:
|
||||
raise AnsibleConnectionFailure(
|
||||
@@ -345,7 +401,7 @@ class Connection(ConnectionBase):
|
||||
self._call_client(
|
||||
lambda: put_file(
|
||||
self.client,
|
||||
container=self.get_option('remote_addr'),
|
||||
container=self.get_option("remote_addr"),
|
||||
in_path=in_path,
|
||||
out_path=out_path,
|
||||
user_id=user_id,
|
||||
@@ -361,9 +417,11 @@ class Connection(ConnectionBase):
|
||||
raise AnsibleConnectionFailure(to_native(exc))
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
""" Fetch a file from container to local. """
|
||||
"""Fetch a file from container to local."""
|
||||
super(Connection, self).fetch_file(in_path, out_path)
|
||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self.get_option('remote_addr'))
|
||||
display.vvv(
|
||||
f"FETCH {in_path} TO {out_path}", host=self.get_option("remote_addr")
|
||||
)
|
||||
|
||||
in_path = self._prefix_login_path(in_path)
|
||||
|
||||
@@ -371,11 +429,13 @@ class Connection(ConnectionBase):
|
||||
self._call_client(
|
||||
lambda: fetch_file(
|
||||
self.client,
|
||||
container=self.get_option('remote_addr'),
|
||||
container=self.get_option("remote_addr"),
|
||||
in_path=in_path,
|
||||
out_path=out_path,
|
||||
follow_links=True,
|
||||
log=lambda msg: display.vvvv(msg, host=self.get_option('remote_addr')),
|
||||
log=lambda msg: display.vvvv(
|
||||
msg, host=self.get_option("remote_addr")
|
||||
),
|
||||
),
|
||||
not_found_can_be_resource=True,
|
||||
)
|
||||
@@ -385,7 +445,7 @@ class Connection(ConnectionBase):
|
||||
raise AnsibleConnectionFailure(to_native(exc))
|
||||
|
||||
def close(self):
|
||||
""" Terminate the connection. Nothing to do for Docker"""
|
||||
"""Terminate the connection. Nothing to do for Docker"""
|
||||
super(Connection, self).close()
|
||||
self._connected = False
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: nsenter
|
||||
short_description: execute on host running controller container
|
||||
@@ -39,11 +40,11 @@ notes:
|
||||
PID namespace (C(--pid host))."
|
||||
"""
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import pty
|
||||
import selectors
|
||||
import subprocess
|
||||
import fcntl
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
@@ -57,10 +58,9 @@ display = Display()
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
'''Connections to a container host using nsenter
|
||||
'''
|
||||
"""Connections to a container host using nsenter"""
|
||||
|
||||
transport = 'community.docker.nsenter'
|
||||
transport = "community.docker.nsenter"
|
||||
has_pipelining = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -89,9 +89,11 @@ class Connection(ConnectionBase):
|
||||
|
||||
executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else None
|
||||
|
||||
if not os.path.exists(to_bytes(executable, errors='surrogate_or_strict')):
|
||||
raise AnsibleError(f"failed to find the executable specified {executable}."
|
||||
" Please verify if the executable exists and re-try.")
|
||||
if not os.path.exists(to_bytes(executable, errors="surrogate_or_strict")):
|
||||
raise AnsibleError(
|
||||
f"failed to find the executable specified {executable}."
|
||||
" Please verify if the executable exists and re-try."
|
||||
)
|
||||
|
||||
# Rewrite the provided command to prefix it with nsenter
|
||||
nsenter_cmd_parts = [
|
||||
@@ -148,19 +150,32 @@ class Connection(ConnectionBase):
|
||||
display.debug("done running command with Popen()")
|
||||
|
||||
if self.become and self.become.expect_prompt() and sudoable:
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(
|
||||
p.stdout,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) | os.O_NONBLOCK,
|
||||
)
|
||||
fcntl.fcntl(
|
||||
p.stderr,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stderr, fcntl.F_GETFL) | os.O_NONBLOCK,
|
||||
)
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(p.stdout, selectors.EVENT_READ)
|
||||
selector.register(p.stderr, selectors.EVENT_READ)
|
||||
|
||||
become_output = b''
|
||||
become_output = b""
|
||||
try:
|
||||
while not self.become.check_success(become_output) and not self.become.check_password_prompt(become_output):
|
||||
while not self.become.check_success(
|
||||
become_output
|
||||
) and not self.become.check_password_prompt(become_output):
|
||||
events = selector.select(self._play_context.timeout)
|
||||
if not events:
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output))
|
||||
raise AnsibleError(
|
||||
"timeout waiting for privilege escalation password prompt:\n"
|
||||
+ to_native(become_output)
|
||||
)
|
||||
|
||||
for key, event in events:
|
||||
if key.fileobj == p.stdout:
|
||||
@@ -170,20 +185,38 @@ class Connection(ConnectionBase):
|
||||
|
||||
if not chunk:
|
||||
stdout, stderr = p.communicate()
|
||||
raise AnsibleError('privilege output closed while waiting for password prompt:\n' + to_native(become_output))
|
||||
raise AnsibleError(
|
||||
"privilege output closed while waiting for password prompt:\n"
|
||||
+ to_native(become_output)
|
||||
)
|
||||
become_output += chunk
|
||||
finally:
|
||||
selector.close()
|
||||
|
||||
if not self.become.check_success(become_output):
|
||||
become_pass = self.become.get_option('become_pass', playcontext=self._play_context)
|
||||
become_pass = self.become.get_option(
|
||||
"become_pass", playcontext=self._play_context
|
||||
)
|
||||
if master is None:
|
||||
p.stdin.write(to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
p.stdin.write(
|
||||
to_bytes(become_pass, errors="surrogate_or_strict") + b"\n"
|
||||
)
|
||||
else:
|
||||
os.write(master, to_bytes(become_pass, errors='surrogate_or_strict') + b'\n')
|
||||
os.write(
|
||||
master,
|
||||
to_bytes(become_pass, errors="surrogate_or_strict") + b"\n",
|
||||
)
|
||||
|
||||
fcntl.fcntl(p.stdout, fcntl.F_SETFL, fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(p.stderr, fcntl.F_SETFL, fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(
|
||||
p.stdout,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stdout, fcntl.F_GETFL) & ~os.O_NONBLOCK,
|
||||
)
|
||||
fcntl.fcntl(
|
||||
p.stderr,
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(p.stderr, fcntl.F_GETFL) & ~os.O_NONBLOCK,
|
||||
)
|
||||
|
||||
display.debug("getting output with communicate()")
|
||||
stdout, stderr = p.communicate(in_data)
|
||||
@@ -220,14 +253,18 @@ class Connection(ConnectionBase):
|
||||
|
||||
try:
|
||||
rc, out, err = self.exec_command(cmd=["cat", in_path])
|
||||
display.vvv(f"FETCH {in_path} TO {out_path}", host=self._play_context.remote_addr)
|
||||
display.vvv(
|
||||
f"FETCH {in_path} TO {out_path}", host=self._play_context.remote_addr
|
||||
)
|
||||
if rc != 0:
|
||||
raise AnsibleError(f"failed to transfer file to {in_path}: {err}")
|
||||
with open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb') as out_file:
|
||||
with open(
|
||||
to_bytes(out_path, errors="surrogate_or_strict"), "wb"
|
||||
) as out_file:
|
||||
out_file.write(out)
|
||||
except IOError as e:
|
||||
raise AnsibleError(f"failed to transfer file to {to_native(out_path)}: {e}")
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
"""terminate the connection; nothing to do here"""
|
||||
self._connected = False
|
||||
|
||||
@@ -34,7 +34,7 @@ attributes:
|
||||
"""
|
||||
|
||||
# Should be used together with the standard fragment
|
||||
INFO_MODULE = r'''
|
||||
INFO_MODULE = r"""
|
||||
options: {}
|
||||
attributes:
|
||||
check_mode:
|
||||
@@ -45,9 +45,9 @@ attributes:
|
||||
support: N/A
|
||||
details:
|
||||
- This action does not modify state.
|
||||
'''
|
||||
"""
|
||||
|
||||
ACTIONGROUP_DOCKER = r'''
|
||||
ACTIONGROUP_DOCKER = r"""
|
||||
options: {}
|
||||
attributes:
|
||||
action_group:
|
||||
@@ -56,7 +56,7 @@ attributes:
|
||||
membership:
|
||||
- community.docker.docker
|
||||
- docker
|
||||
'''
|
||||
"""
|
||||
|
||||
CONN = r"""
|
||||
options: {}
|
||||
@@ -77,7 +77,7 @@ attributes:
|
||||
"""
|
||||
|
||||
# Should be used together with the standard fragment and the FACTS fragment
|
||||
FACTS_MODULE = r'''
|
||||
FACTS_MODULE = r"""
|
||||
options: {}
|
||||
attributes:
|
||||
check_mode:
|
||||
@@ -90,7 +90,7 @@ attributes:
|
||||
- This action does not modify state.
|
||||
facts:
|
||||
support: full
|
||||
'''
|
||||
"""
|
||||
|
||||
FILES = r"""
|
||||
options: {}
|
||||
|
||||
@@ -74,8 +74,8 @@ notes:
|
||||
"""
|
||||
|
||||
# The following needs to be kept in sync with the compose_v2 module utils
|
||||
MINIMUM_VERSION = r'''
|
||||
MINIMUM_VERSION = r"""
|
||||
options: {}
|
||||
requirements:
|
||||
- "Docker CLI with Docker compose plugin 2.18.0 or later"
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -123,7 +123,7 @@ notes:
|
||||
|
||||
# For plugins: allow to define common options with Ansible variables
|
||||
|
||||
VAR_NAMES = r'''
|
||||
VAR_NAMES = r"""
|
||||
options:
|
||||
docker_host:
|
||||
vars:
|
||||
@@ -154,11 +154,11 @@ options:
|
||||
validate_certs:
|
||||
vars:
|
||||
- name: ansible_docker_validate_certs
|
||||
'''
|
||||
"""
|
||||
|
||||
# Additional, more specific stuff for minimal Docker SDK for Python version < 2.0
|
||||
|
||||
DOCKER_PY_1_DOCUMENTATION = r'''
|
||||
DOCKER_PY_1_DOCUMENTATION = r"""
|
||||
options: {}
|
||||
notes:
|
||||
- This module uses the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to
|
||||
@@ -170,12 +170,12 @@ requirements:
|
||||
modules should *not* be installed at the same time. Also note that when both modules are
|
||||
installed and one of them is uninstalled, the other might no longer function and a reinstall
|
||||
of it is required."
|
||||
'''
|
||||
"""
|
||||
|
||||
# Additional, more specific stuff for minimal Docker SDK for Python version >= 2.0.
|
||||
# Note that Docker SDK for Python >= 2.0 requires Python 2.7 or newer.
|
||||
|
||||
DOCKER_PY_2_DOCUMENTATION = r'''
|
||||
DOCKER_PY_2_DOCUMENTATION = r"""
|
||||
options: {}
|
||||
notes:
|
||||
- This module uses the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to
|
||||
@@ -186,10 +186,10 @@ requirements:
|
||||
Python module has been superseded by L(docker,https://pypi.org/project/docker/)
|
||||
(see L(here,https://github.com/docker/docker-py/issues/1310) for details).
|
||||
This module does *not* work with docker-py."
|
||||
'''
|
||||
"""
|
||||
|
||||
# Docker doc fragment when using the vendored API access code
|
||||
API_DOCUMENTATION = r'''
|
||||
API_DOCUMENTATION = r"""
|
||||
options:
|
||||
docker_host:
|
||||
description:
|
||||
@@ -302,10 +302,10 @@ requirements:
|
||||
- pywin32 (when using named pipes on Windows 32)
|
||||
- paramiko (when using SSH with O(use_ssh_client=false))
|
||||
- pyOpenSSL (when using TLS)
|
||||
'''
|
||||
"""
|
||||
|
||||
# Docker doc fragment when using the Docker CLI
|
||||
CLI_DOCUMENTATION = r'''
|
||||
CLI_DOCUMENTATION = r"""
|
||||
options:
|
||||
docker_cli:
|
||||
description:
|
||||
@@ -403,4 +403,4 @@ notes:
|
||||
U(https://docs.docker.com/machine/reference/env/) for more details.
|
||||
- This module does B(not) use the L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) to
|
||||
communicate with the Docker daemon. It directly calls the Docker CLI program.
|
||||
'''
|
||||
"""
|
||||
|
||||
@@ -107,7 +107,7 @@ options:
|
||||
version_added: 3.5.0
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
EXAMPLES = """
|
||||
---
|
||||
# Minimal example using local Docker daemon
|
||||
plugin: community.docker.docker_containers
|
||||
@@ -167,13 +167,16 @@ filters:
|
||||
inventory_hostname.startswith("a")
|
||||
# Exclude all containers that did not match any of the above filters
|
||||
- exclude: true
|
||||
'''
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
RequestException,
|
||||
)
|
||||
@@ -183,64 +186,66 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
|
||||
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host
|
||||
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
|
||||
filter_host,
|
||||
parse_filters,
|
||||
)
|
||||
|
||||
|
||||
MIN_DOCKER_API = None
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
''' Host inventory parser for ansible using Docker daemon as source. '''
|
||||
"""Host inventory parser for ansible using Docker daemon as source."""
|
||||
|
||||
NAME = 'community.docker.docker_containers'
|
||||
NAME = "community.docker.docker_containers"
|
||||
|
||||
def _slugify(self, value):
|
||||
slug = re.sub(r'[^\w-]', '_', value).lower().lstrip('_')
|
||||
return f'docker_{slug}'
|
||||
slug = re.sub(r"[^\w-]", "_", value).lower().lstrip("_")
|
||||
return f"docker_{slug}"
|
||||
|
||||
def _populate(self, client):
|
||||
strict = self.get_option('strict')
|
||||
strict = self.get_option("strict")
|
||||
|
||||
ssh_port = self.get_option('private_ssh_port')
|
||||
default_ip = self.get_option('default_ip')
|
||||
hostname = self.get_option('docker_host')
|
||||
verbose_output = self.get_option('verbose_output')
|
||||
connection_type = self.get_option('connection_type')
|
||||
add_legacy_groups = self.get_option('add_legacy_groups')
|
||||
ssh_port = self.get_option("private_ssh_port")
|
||||
default_ip = self.get_option("default_ip")
|
||||
hostname = self.get_option("docker_host")
|
||||
verbose_output = self.get_option("verbose_output")
|
||||
connection_type = self.get_option("connection_type")
|
||||
add_legacy_groups = self.get_option("add_legacy_groups")
|
||||
|
||||
try:
|
||||
params = {
|
||||
'limit': -1,
|
||||
'all': 1,
|
||||
'size': 0,
|
||||
'trunc_cmd': 0,
|
||||
'since': None,
|
||||
'before': None,
|
||||
"limit": -1,
|
||||
"all": 1,
|
||||
"size": 0,
|
||||
"trunc_cmd": 0,
|
||||
"since": None,
|
||||
"before": None,
|
||||
}
|
||||
containers = client.get_json('/containers/json', params=params)
|
||||
containers = client.get_json("/containers/json", params=params)
|
||||
except APIError as exc:
|
||||
raise AnsibleError(f"Error listing containers: {exc}")
|
||||
|
||||
if add_legacy_groups:
|
||||
self.inventory.add_group('running')
|
||||
self.inventory.add_group('stopped')
|
||||
self.inventory.add_group("running")
|
||||
self.inventory.add_group("stopped")
|
||||
|
||||
extra_facts = {}
|
||||
if self.get_option('configure_docker_daemon'):
|
||||
if self.get_option("configure_docker_daemon"):
|
||||
for option_name, var_name in DOCKER_COMMON_ARGS_VARS.items():
|
||||
value = self.get_option(option_name)
|
||||
if value is not None:
|
||||
extra_facts[var_name] = value
|
||||
|
||||
filters = parse_filters(self.get_option('filters'))
|
||||
filters = parse_filters(self.get_option("filters"))
|
||||
for container in containers:
|
||||
id = container.get('Id')
|
||||
id = container.get("Id")
|
||||
short_id = id[:13]
|
||||
|
||||
try:
|
||||
name = container.get('Names', list())[0].lstrip('/')
|
||||
name = container.get("Names", list())[0].lstrip("/")
|
||||
full_name = name
|
||||
except IndexError:
|
||||
name = short_id
|
||||
@@ -253,66 +258,72 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
full_facts = dict()
|
||||
|
||||
try:
|
||||
inspect = client.get_json('/containers/{0}/json', id)
|
||||
inspect = client.get_json("/containers/{0}/json", id)
|
||||
except APIError as exc:
|
||||
raise AnsibleError(f"Error inspecting container {name} - {exc}")
|
||||
|
||||
state = inspect.get('State') or dict()
|
||||
config = inspect.get('Config') or dict()
|
||||
labels = config.get('Labels') or dict()
|
||||
state = inspect.get("State") or dict()
|
||||
config = inspect.get("Config") or dict()
|
||||
labels = config.get("Labels") or dict()
|
||||
|
||||
running = state.get('Running')
|
||||
running = state.get("Running")
|
||||
|
||||
groups = []
|
||||
|
||||
# Add container to groups
|
||||
image_name = config.get('Image')
|
||||
image_name = config.get("Image")
|
||||
if image_name and add_legacy_groups:
|
||||
groups.append(f'image_{image_name}')
|
||||
groups.append(f"image_{image_name}")
|
||||
|
||||
stack_name = labels.get('com.docker.stack.namespace')
|
||||
stack_name = labels.get("com.docker.stack.namespace")
|
||||
if stack_name:
|
||||
full_facts['docker_stack'] = stack_name
|
||||
full_facts["docker_stack"] = stack_name
|
||||
if add_legacy_groups:
|
||||
groups.append(f'stack_{stack_name}')
|
||||
groups.append(f"stack_{stack_name}")
|
||||
|
||||
service_name = labels.get('com.docker.swarm.service.name')
|
||||
service_name = labels.get("com.docker.swarm.service.name")
|
||||
if service_name:
|
||||
full_facts['docker_service'] = service_name
|
||||
full_facts["docker_service"] = service_name
|
||||
if add_legacy_groups:
|
||||
groups.append(f'service_{service_name}')
|
||||
groups.append(f"service_{service_name}")
|
||||
|
||||
ansible_connection = None
|
||||
if connection_type == 'ssh':
|
||||
if connection_type == "ssh":
|
||||
# Figure out ssh IP and Port
|
||||
try:
|
||||
# Lookup the public facing port Nat'ed to ssh port.
|
||||
network_settings = inspect.get('NetworkSettings') or {}
|
||||
port_settings = network_settings.get('Ports') or {}
|
||||
port = port_settings.get(f'{ssh_port}/tcp')[0]
|
||||
network_settings = inspect.get("NetworkSettings") or {}
|
||||
port_settings = network_settings.get("Ports") or {}
|
||||
port = port_settings.get(f"{ssh_port}/tcp")[0]
|
||||
except (IndexError, AttributeError, TypeError):
|
||||
port = dict()
|
||||
|
||||
try:
|
||||
ip = default_ip if port['HostIp'] == '0.0.0.0' else port['HostIp']
|
||||
ip = default_ip if port["HostIp"] == "0.0.0.0" else port["HostIp"]
|
||||
except KeyError:
|
||||
ip = ''
|
||||
ip = ""
|
||||
|
||||
facts.update(dict(
|
||||
ansible_ssh_host=ip,
|
||||
ansible_ssh_port=port.get('HostPort', 0),
|
||||
))
|
||||
elif connection_type == 'docker-cli':
|
||||
facts.update(dict(
|
||||
ansible_host=full_name,
|
||||
))
|
||||
ansible_connection = 'community.docker.docker'
|
||||
elif connection_type == 'docker-api':
|
||||
facts.update(dict(
|
||||
ansible_host=full_name,
|
||||
))
|
||||
facts.update(
|
||||
dict(
|
||||
ansible_ssh_host=ip,
|
||||
ansible_ssh_port=port.get("HostPort", 0),
|
||||
)
|
||||
)
|
||||
elif connection_type == "docker-cli":
|
||||
facts.update(
|
||||
dict(
|
||||
ansible_host=full_name,
|
||||
)
|
||||
)
|
||||
ansible_connection = "community.docker.docker"
|
||||
elif connection_type == "docker-api":
|
||||
facts.update(
|
||||
dict(
|
||||
ansible_host=full_name,
|
||||
)
|
||||
)
|
||||
facts.update(extra_facts)
|
||||
ansible_connection = 'community.docker.docker_api'
|
||||
ansible_connection = "community.docker.docker_api"
|
||||
|
||||
full_facts.update(facts)
|
||||
for key, value in inspect.items():
|
||||
@@ -323,8 +334,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
|
||||
if ansible_connection:
|
||||
for d in (facts, full_facts):
|
||||
if 'ansible_connection' not in d:
|
||||
d['ansible_connection'] = ansible_connection
|
||||
if "ansible_connection" not in d:
|
||||
d["ansible_connection"] = ansible_connection
|
||||
|
||||
if not filter_host(self, name, full_facts, filters):
|
||||
continue
|
||||
@@ -342,11 +353,17 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
|
||||
# Use constructed if applicable
|
||||
# Composed variables
|
||||
self._set_composite_vars(self.get_option('compose'), full_facts, name, strict=strict)
|
||||
self._set_composite_vars(
|
||||
self.get_option("compose"), full_facts, name, strict=strict
|
||||
)
|
||||
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), full_facts, name, strict=strict)
|
||||
self._add_host_to_composed_groups(
|
||||
self.get_option("groups"), full_facts, name, strict=strict
|
||||
)
|
||||
# Create groups based on variable values and add the corresponding hosts to it
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), full_facts, name, strict=strict)
|
||||
self._add_host_to_keyed_groups(
|
||||
self.get_option("keyed_groups"), full_facts, name, strict=strict
|
||||
)
|
||||
|
||||
# We need to do this last since we also add a group called `name`.
|
||||
# When we do this before a set_variable() call, the variables are assigned
|
||||
@@ -362,15 +379,15 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
self.inventory.add_host(name, group=hostname)
|
||||
|
||||
if running is True:
|
||||
self.inventory.add_host(name, group='running')
|
||||
self.inventory.add_host(name, group="running")
|
||||
else:
|
||||
self.inventory.add_host(name, group='stopped')
|
||||
self.inventory.add_host(name, group="stopped")
|
||||
|
||||
def verify_file(self, path):
|
||||
"""Return the possibly of a file being consumable by this plugin."""
|
||||
return (
|
||||
super(InventoryModule, self).verify_file(path) and
|
||||
path.endswith(('docker.yaml', 'docker.yml')))
|
||||
return super(InventoryModule, self).verify_file(path) and path.endswith(
|
||||
("docker.yaml", "docker.yml")
|
||||
)
|
||||
|
||||
def _create_client(self):
|
||||
return AnsibleDockerClient(self, min_docker_api_version=MIN_DOCKER_API)
|
||||
@@ -382,10 +399,8 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
try:
|
||||
self._populate(client)
|
||||
except DockerException as e:
|
||||
raise AnsibleError(
|
||||
f'An unexpected Docker error occurred: {e}'
|
||||
)
|
||||
raise AnsibleError(f"An unexpected Docker error occurred: {e}")
|
||||
except RequestException as e:
|
||||
raise AnsibleError(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}'
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: docker_machine
|
||||
author: Ximon Eighteen (@ximon18)
|
||||
@@ -59,7 +60,7 @@ options:
|
||||
version_added: 3.5.0
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
EXAMPLES = """
|
||||
---
|
||||
# Minimal example
|
||||
plugin: community.docker.docker_machine
|
||||
@@ -96,57 +97,63 @@ keyed_groups:
|
||||
plugin: community.docker.docker_machine
|
||||
compose:
|
||||
ansible_ssh_common_args: '"-o StrictHostKeyChecking=accept-new"'
|
||||
'''
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
|
||||
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, Constructable
|
||||
from ansible.utils.display import Display
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
|
||||
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
|
||||
filter_host,
|
||||
parse_filters,
|
||||
)
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
''' Host inventory parser for ansible using Docker machine as source. '''
|
||||
"""Host inventory parser for ansible using Docker machine as source."""
|
||||
|
||||
NAME = 'community.docker.docker_machine'
|
||||
NAME = "community.docker.docker_machine"
|
||||
|
||||
DOCKER_MACHINE_PATH = None
|
||||
|
||||
def _run_command(self, args):
|
||||
if not self.DOCKER_MACHINE_PATH:
|
||||
try:
|
||||
self.DOCKER_MACHINE_PATH = get_bin_path('docker-machine')
|
||||
self.DOCKER_MACHINE_PATH = get_bin_path("docker-machine")
|
||||
except ValueError as e:
|
||||
raise AnsibleError(to_native(e))
|
||||
|
||||
command = [self.DOCKER_MACHINE_PATH]
|
||||
command.extend(args)
|
||||
display.debug(f'Executing command {command}')
|
||||
display.debug(f"Executing command {command}")
|
||||
try:
|
||||
result = subprocess.check_output(command)
|
||||
except subprocess.CalledProcessError as e:
|
||||
display.warning(f'Exception {type(e).__name__} caught while executing command {command}, this was the original exception: {e}')
|
||||
display.warning(
|
||||
f"Exception {type(e).__name__} caught while executing command {command}, this was the original exception: {e}"
|
||||
)
|
||||
raise e
|
||||
|
||||
return to_text(result).strip()
|
||||
|
||||
def _get_docker_daemon_variables(self, machine_name):
|
||||
'''
|
||||
"""
|
||||
Capture settings from Docker Machine that would be needed to connect to the remote Docker daemon installed on
|
||||
the Docker Machine remote host. Note: passing '--shell=sh' is a workaround for 'Error: Unknown shell'.
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
env_lines = self._run_command(['env', '--shell=sh', machine_name]).splitlines()
|
||||
env_lines = self._run_command(
|
||||
["env", "--shell=sh", machine_name]
|
||||
).splitlines()
|
||||
except subprocess.CalledProcessError:
|
||||
# This can happen when the machine is created but provisioning is incomplete
|
||||
return []
|
||||
@@ -174,9 +181,9 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
def _get_machine_names(self):
|
||||
# Filter out machines that are not in the Running state as we probably cannot do anything useful actions
|
||||
# with them.
|
||||
ls_command = ['ls', '-q']
|
||||
if self.get_option('running_required'):
|
||||
ls_command.extend(['--filter', 'state=Running'])
|
||||
ls_command = ["ls", "-q"]
|
||||
if self.get_option("running_required"):
|
||||
ls_command.extend(["--filter", "state=Running"])
|
||||
|
||||
try:
|
||||
ls_lines = self._run_command(ls_command)
|
||||
@@ -187,7 +194,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
def _inspect_docker_machine_host(self, node):
|
||||
try:
|
||||
inspect_lines = self._run_command(['inspect', node])
|
||||
inspect_lines = self._run_command(["inspect", node])
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
@@ -195,7 +202,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
def _ip_addr_docker_machine_host(self, node):
|
||||
try:
|
||||
ip_addr = self._run_command(['ip', node])
|
||||
ip_addr = self._run_command(["ip", node])
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
@@ -203,19 +210,21 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
def _should_skip_host(self, machine_name, env_var_tuples, daemon_env):
|
||||
if not env_var_tuples:
|
||||
warning_prefix = f'Unable to fetch Docker daemon env vars from Docker Machine for host {machine_name}'
|
||||
if daemon_env in ('require', 'require-silently'):
|
||||
if daemon_env == 'require':
|
||||
display.warning(f'{warning_prefix}: host will be skipped')
|
||||
warning_prefix = f"Unable to fetch Docker daemon env vars from Docker Machine for host {machine_name}"
|
||||
if daemon_env in ("require", "require-silently"):
|
||||
if daemon_env == "require":
|
||||
display.warning(f"{warning_prefix}: host will be skipped")
|
||||
return True
|
||||
else: # 'optional', 'optional-silently'
|
||||
if daemon_env == 'optional':
|
||||
display.warning(f'{warning_prefix}: host will lack dm_DOCKER_xxx variables')
|
||||
if daemon_env == "optional":
|
||||
display.warning(
|
||||
f"{warning_prefix}: host will lack dm_DOCKER_xxx variables"
|
||||
)
|
||||
return False
|
||||
|
||||
def _populate(self):
|
||||
daemon_env = self.get_option('daemon_env')
|
||||
filters = parse_filters(self.get_option('filters'))
|
||||
daemon_env = self.get_option("daemon_env")
|
||||
filters = parse_filters(self.get_option("filters"))
|
||||
try:
|
||||
for node in self._get_machine_names():
|
||||
node_attrs = self._inspect_docker_machine_host(node)
|
||||
@@ -224,13 +233,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
|
||||
unsafe_node_attrs = make_unsafe(node_attrs)
|
||||
|
||||
machine_name = unsafe_node_attrs['Driver']['MachineName']
|
||||
machine_name = unsafe_node_attrs["Driver"]["MachineName"]
|
||||
if not filter_host(self, machine_name, unsafe_node_attrs, filters):
|
||||
continue
|
||||
|
||||
# query `docker-machine env` to obtain remote Docker daemon connection settings in the form of commands
|
||||
# that could be used to set environment variables to influence a local Docker client:
|
||||
if daemon_env == 'skip':
|
||||
if daemon_env == "skip":
|
||||
env_var_tuples = []
|
||||
else:
|
||||
env_var_tuples = self._get_docker_daemon_variables(machine_name)
|
||||
@@ -243,49 +252,82 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
# check for valid ip address from inspect output, else explicitly use ip command to find host ip address
|
||||
# this works around an issue seen with Google Compute Platform where the IP address was not available
|
||||
# via the 'inspect' subcommand but was via the 'ip' subcomannd.
|
||||
if unsafe_node_attrs['Driver']['IPAddress']:
|
||||
ip_addr = unsafe_node_attrs['Driver']['IPAddress']
|
||||
if unsafe_node_attrs["Driver"]["IPAddress"]:
|
||||
ip_addr = unsafe_node_attrs["Driver"]["IPAddress"]
|
||||
else:
|
||||
ip_addr = self._ip_addr_docker_machine_host(node)
|
||||
|
||||
# set standard Ansible remote host connection settings to details captured from `docker-machine`
|
||||
# see: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html
|
||||
self.inventory.set_variable(machine_name, 'ansible_host', make_unsafe(ip_addr))
|
||||
self.inventory.set_variable(machine_name, 'ansible_port', unsafe_node_attrs['Driver']['SSHPort'])
|
||||
self.inventory.set_variable(machine_name, 'ansible_user', unsafe_node_attrs['Driver']['SSHUser'])
|
||||
self.inventory.set_variable(machine_name, 'ansible_ssh_private_key_file', unsafe_node_attrs['Driver']['SSHKeyPath'])
|
||||
self.inventory.set_variable(
|
||||
machine_name, "ansible_host", make_unsafe(ip_addr)
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
machine_name, "ansible_port", unsafe_node_attrs["Driver"]["SSHPort"]
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
machine_name, "ansible_user", unsafe_node_attrs["Driver"]["SSHUser"]
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
machine_name,
|
||||
"ansible_ssh_private_key_file",
|
||||
unsafe_node_attrs["Driver"]["SSHKeyPath"],
|
||||
)
|
||||
|
||||
# set variables based on Docker Machine tags
|
||||
tags = unsafe_node_attrs['Driver'].get('Tags') or ''
|
||||
self.inventory.set_variable(machine_name, 'dm_tags', make_unsafe(tags))
|
||||
tags = unsafe_node_attrs["Driver"].get("Tags") or ""
|
||||
self.inventory.set_variable(machine_name, "dm_tags", make_unsafe(tags))
|
||||
|
||||
# set variables based on Docker Machine env variables
|
||||
for kv in env_var_tuples:
|
||||
self.inventory.set_variable(machine_name, f'dm_{kv[0]}', make_unsafe(kv[1]))
|
||||
self.inventory.set_variable(
|
||||
machine_name, f"dm_{kv[0]}", make_unsafe(kv[1])
|
||||
)
|
||||
|
||||
if self.get_option('verbose_output'):
|
||||
self.inventory.set_variable(machine_name, 'docker_machine_node_attributes', unsafe_node_attrs)
|
||||
if self.get_option("verbose_output"):
|
||||
self.inventory.set_variable(
|
||||
machine_name,
|
||||
"docker_machine_node_attributes",
|
||||
unsafe_node_attrs,
|
||||
)
|
||||
|
||||
# Use constructed if applicable
|
||||
strict = self.get_option('strict')
|
||||
strict = self.get_option("strict")
|
||||
|
||||
# Composed variables
|
||||
self._set_composite_vars(self.get_option('compose'), unsafe_node_attrs, machine_name, strict=strict)
|
||||
self._set_composite_vars(
|
||||
self.get_option("compose"),
|
||||
unsafe_node_attrs,
|
||||
machine_name,
|
||||
strict=strict,
|
||||
)
|
||||
|
||||
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), unsafe_node_attrs, machine_name, strict=strict)
|
||||
self._add_host_to_composed_groups(
|
||||
self.get_option("groups"),
|
||||
unsafe_node_attrs,
|
||||
machine_name,
|
||||
strict=strict,
|
||||
)
|
||||
|
||||
# Create groups based on variable values and add the corresponding hosts to it
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), unsafe_node_attrs, machine_name, strict=strict)
|
||||
self._add_host_to_keyed_groups(
|
||||
self.get_option("keyed_groups"),
|
||||
unsafe_node_attrs,
|
||||
machine_name,
|
||||
strict=strict,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise AnsibleError(f'Unable to fetch hosts from Docker Machine, this was the original exception: {e}') from e
|
||||
raise AnsibleError(
|
||||
f"Unable to fetch hosts from Docker Machine, this was the original exception: {e}"
|
||||
) from e
|
||||
|
||||
def verify_file(self, path):
|
||||
"""Return the possibility of a file being consumable by this plugin."""
|
||||
return (
|
||||
super(InventoryModule, self).verify_file(path) and
|
||||
path.endswith(('docker_machine.yaml', 'docker_machine.yml')))
|
||||
return super(InventoryModule, self).verify_file(path) and path.endswith(
|
||||
("docker_machine.yaml", "docker_machine.yml")
|
||||
)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
super(InventoryModule, self).parse(inventory, loader, path, cache)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
name: docker_swarm
|
||||
author:
|
||||
@@ -103,7 +104,7 @@ options:
|
||||
version_added: 3.5.0
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
EXAMPLES = """
|
||||
---
|
||||
# Minimal example using local docker
|
||||
plugin: community.docker.docker_swarm
|
||||
@@ -146,126 +147,176 @@ keyed_groups:
|
||||
# hint: labels containing special characters will be converted to safe names
|
||||
- key: 'Spec.Labels'
|
||||
prefix: label
|
||||
'''
|
||||
"""
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import get_connect_params
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import update_tls_hostname
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible.parsing.utils.addresses import parse_address
|
||||
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
get_connect_params,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
update_tls_hostname,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.unsafe import make_unsafe
|
||||
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import parse_filters, filter_host
|
||||
from ansible_collections.community.library_inventory_filtering_v1.plugins.plugin_utils.inventory_filter import (
|
||||
filter_host,
|
||||
parse_filters,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
import docker
|
||||
|
||||
HAS_DOCKER = True
|
||||
except ImportError:
|
||||
HAS_DOCKER = False
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
''' Host inventory parser for ansible using Docker swarm as source. '''
|
||||
"""Host inventory parser for ansible using Docker swarm as source."""
|
||||
|
||||
NAME = 'community.docker.docker_swarm'
|
||||
NAME = "community.docker.docker_swarm"
|
||||
|
||||
def _fail(self, msg):
|
||||
raise AnsibleError(msg)
|
||||
|
||||
def _populate(self):
|
||||
raw_params = dict(
|
||||
docker_host=self.get_option('docker_host'),
|
||||
tls=self.get_option('tls'),
|
||||
tls_verify=self.get_option('validate_certs'),
|
||||
key_path=self.get_option('client_key'),
|
||||
cacert_path=self.get_option('ca_path'),
|
||||
cert_path=self.get_option('client_cert'),
|
||||
tls_hostname=self.get_option('tls_hostname'),
|
||||
api_version=self.get_option('api_version'),
|
||||
timeout=self.get_option('timeout'),
|
||||
use_ssh_client=self.get_option('use_ssh_client'),
|
||||
docker_host=self.get_option("docker_host"),
|
||||
tls=self.get_option("tls"),
|
||||
tls_verify=self.get_option("validate_certs"),
|
||||
key_path=self.get_option("client_key"),
|
||||
cacert_path=self.get_option("ca_path"),
|
||||
cert_path=self.get_option("client_cert"),
|
||||
tls_hostname=self.get_option("tls_hostname"),
|
||||
api_version=self.get_option("api_version"),
|
||||
timeout=self.get_option("timeout"),
|
||||
use_ssh_client=self.get_option("use_ssh_client"),
|
||||
debug=None,
|
||||
)
|
||||
update_tls_hostname(raw_params)
|
||||
connect_params = get_connect_params(raw_params, fail_function=self._fail)
|
||||
self.client = docker.DockerClient(**connect_params)
|
||||
self.inventory.add_group('all')
|
||||
self.inventory.add_group('manager')
|
||||
self.inventory.add_group('worker')
|
||||
self.inventory.add_group('leader')
|
||||
self.inventory.add_group('nonleaders')
|
||||
self.inventory.add_group("all")
|
||||
self.inventory.add_group("manager")
|
||||
self.inventory.add_group("worker")
|
||||
self.inventory.add_group("leader")
|
||||
self.inventory.add_group("nonleaders")
|
||||
|
||||
filters = parse_filters(self.get_option('filters'))
|
||||
filters = parse_filters(self.get_option("filters"))
|
||||
|
||||
if self.get_option('include_host_uri'):
|
||||
if self.get_option('include_host_uri_port'):
|
||||
host_uri_port = str(self.get_option('include_host_uri_port'))
|
||||
elif self.get_option('tls') or self.get_option('validate_certs'):
|
||||
host_uri_port = '2376'
|
||||
if self.get_option("include_host_uri"):
|
||||
if self.get_option("include_host_uri_port"):
|
||||
host_uri_port = str(self.get_option("include_host_uri_port"))
|
||||
elif self.get_option("tls") or self.get_option("validate_certs"):
|
||||
host_uri_port = "2376"
|
||||
else:
|
||||
host_uri_port = '2375'
|
||||
host_uri_port = "2375"
|
||||
|
||||
try:
|
||||
self.nodes = self.client.nodes.list()
|
||||
for node in self.nodes:
|
||||
node_attrs = self.client.nodes.get(node.id).attrs
|
||||
unsafe_node_attrs = make_unsafe(node_attrs)
|
||||
if not filter_host(self, unsafe_node_attrs['ID'], unsafe_node_attrs, filters):
|
||||
if not filter_host(
|
||||
self, unsafe_node_attrs["ID"], unsafe_node_attrs, filters
|
||||
):
|
||||
continue
|
||||
self.inventory.add_host(unsafe_node_attrs['ID'])
|
||||
self.inventory.add_host(unsafe_node_attrs['ID'], group=unsafe_node_attrs['Spec']['Role'])
|
||||
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host',
|
||||
unsafe_node_attrs['Status']['Addr'])
|
||||
if self.get_option('include_host_uri'):
|
||||
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host_uri',
|
||||
make_unsafe('tcp://' + unsafe_node_attrs['Status']['Addr'] + ':' + host_uri_port))
|
||||
if self.get_option('verbose_output'):
|
||||
self.inventory.set_variable(unsafe_node_attrs['ID'], 'docker_swarm_node_attributes', unsafe_node_attrs)
|
||||
if 'ManagerStatus' in unsafe_node_attrs:
|
||||
if unsafe_node_attrs['ManagerStatus'].get('Leader'):
|
||||
self.inventory.add_host(unsafe_node_attrs["ID"])
|
||||
self.inventory.add_host(
|
||||
unsafe_node_attrs["ID"], group=unsafe_node_attrs["Spec"]["Role"]
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
unsafe_node_attrs["ID"],
|
||||
"ansible_host",
|
||||
unsafe_node_attrs["Status"]["Addr"],
|
||||
)
|
||||
if self.get_option("include_host_uri"):
|
||||
self.inventory.set_variable(
|
||||
unsafe_node_attrs["ID"],
|
||||
"ansible_host_uri",
|
||||
make_unsafe(
|
||||
"tcp://"
|
||||
+ unsafe_node_attrs["Status"]["Addr"]
|
||||
+ ":"
|
||||
+ host_uri_port
|
||||
),
|
||||
)
|
||||
if self.get_option("verbose_output"):
|
||||
self.inventory.set_variable(
|
||||
unsafe_node_attrs["ID"],
|
||||
"docker_swarm_node_attributes",
|
||||
unsafe_node_attrs,
|
||||
)
|
||||
if "ManagerStatus" in unsafe_node_attrs:
|
||||
if unsafe_node_attrs["ManagerStatus"].get("Leader"):
|
||||
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
|
||||
# Check moby/moby#35437 for details
|
||||
swarm_leader_ip = parse_address(node_attrs['ManagerStatus']['Addr'])[0] or \
|
||||
unsafe_node_attrs['Status']['Addr']
|
||||
if self.get_option('include_host_uri'):
|
||||
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host_uri',
|
||||
make_unsafe('tcp://' + swarm_leader_ip + ':' + host_uri_port))
|
||||
self.inventory.set_variable(unsafe_node_attrs['ID'], 'ansible_host', make_unsafe(swarm_leader_ip))
|
||||
self.inventory.add_host(unsafe_node_attrs['ID'], group='leader')
|
||||
swarm_leader_ip = (
|
||||
parse_address(node_attrs["ManagerStatus"]["Addr"])[0]
|
||||
or unsafe_node_attrs["Status"]["Addr"]
|
||||
)
|
||||
if self.get_option("include_host_uri"):
|
||||
self.inventory.set_variable(
|
||||
unsafe_node_attrs["ID"],
|
||||
"ansible_host_uri",
|
||||
make_unsafe(
|
||||
"tcp://" + swarm_leader_ip + ":" + host_uri_port
|
||||
),
|
||||
)
|
||||
self.inventory.set_variable(
|
||||
unsafe_node_attrs["ID"],
|
||||
"ansible_host",
|
||||
make_unsafe(swarm_leader_ip),
|
||||
)
|
||||
self.inventory.add_host(unsafe_node_attrs["ID"], group="leader")
|
||||
else:
|
||||
self.inventory.add_host(unsafe_node_attrs['ID'], group='nonleaders')
|
||||
self.inventory.add_host(
|
||||
unsafe_node_attrs["ID"], group="nonleaders"
|
||||
)
|
||||
else:
|
||||
self.inventory.add_host(unsafe_node_attrs['ID'], group='nonleaders')
|
||||
self.inventory.add_host(unsafe_node_attrs["ID"], group="nonleaders")
|
||||
# Use constructed if applicable
|
||||
strict = self.get_option('strict')
|
||||
strict = self.get_option("strict")
|
||||
# Composed variables
|
||||
self._set_composite_vars(self.get_option('compose'),
|
||||
unsafe_node_attrs,
|
||||
unsafe_node_attrs['ID'],
|
||||
strict=strict)
|
||||
self._set_composite_vars(
|
||||
self.get_option("compose"),
|
||||
unsafe_node_attrs,
|
||||
unsafe_node_attrs["ID"],
|
||||
strict=strict,
|
||||
)
|
||||
# Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
|
||||
self._add_host_to_composed_groups(self.get_option('groups'),
|
||||
unsafe_node_attrs,
|
||||
unsafe_node_attrs['ID'],
|
||||
strict=strict)
|
||||
self._add_host_to_composed_groups(
|
||||
self.get_option("groups"),
|
||||
unsafe_node_attrs,
|
||||
unsafe_node_attrs["ID"],
|
||||
strict=strict,
|
||||
)
|
||||
# Create groups based on variable values and add the corresponding hosts to it
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'),
|
||||
unsafe_node_attrs,
|
||||
unsafe_node_attrs['ID'],
|
||||
strict=strict)
|
||||
self._add_host_to_keyed_groups(
|
||||
self.get_option("keyed_groups"),
|
||||
unsafe_node_attrs,
|
||||
unsafe_node_attrs["ID"],
|
||||
strict=strict,
|
||||
)
|
||||
except Exception as e:
|
||||
raise AnsibleError(f'Unable to fetch hosts from Docker swarm API, this was the original exception: {e}')
|
||||
raise AnsibleError(
|
||||
f"Unable to fetch hosts from Docker swarm API, this was the original exception: {e}"
|
||||
)
|
||||
|
||||
def verify_file(self, path):
|
||||
"""Return the possibly of a file being consumable by this plugin."""
|
||||
return (
|
||||
super(InventoryModule, self).verify_file(path) and
|
||||
path.endswith(('docker_swarm.yaml', 'docker_swarm.yml')))
|
||||
return super(InventoryModule, self).verify_file(path) and path.endswith(
|
||||
("docker_swarm.yaml", "docker_swarm.yml")
|
||||
)
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
if not HAS_DOCKER:
|
||||
raise AnsibleError('The Docker swarm dynamic inventory plugin requires the Docker SDK for Python: '
|
||||
'https://github.com/docker/docker-py.')
|
||||
raise AnsibleError(
|
||||
"The Docker swarm dynamic inventory plugin requires the Docker SDK for Python: "
|
||||
"https://github.com/docker/docker-py."
|
||||
)
|
||||
super(InventoryModule, self).parse(inventory, loader, path, cache)
|
||||
self._read_config_data(path)
|
||||
self._populate()
|
||||
|
||||
@@ -18,8 +18,13 @@ URLLIB3_IMPORT_ERROR = None
|
||||
|
||||
try:
|
||||
from requests import Session # noqa: F401, pylint: disable=unused-import
|
||||
from requests.adapters import HTTPAdapter # noqa: F401, pylint: disable=unused-import
|
||||
from requests.exceptions import HTTPError, InvalidSchema # noqa: F401, pylint: disable=unused-import
|
||||
from requests.adapters import ( # noqa: F401, pylint: disable=unused-import
|
||||
HTTPAdapter,
|
||||
)
|
||||
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
|
||||
HTTPError,
|
||||
InvalidSchema,
|
||||
)
|
||||
except ImportError:
|
||||
REQUESTS_IMPORT_ERROR = traceback.format_exc()
|
||||
|
||||
@@ -38,11 +43,15 @@ except ImportError:
|
||||
|
||||
try:
|
||||
from requests.packages import urllib3 # pylint: disable=unused-import
|
||||
from requests.packages.urllib3 import connection as urllib3_connection # pylint: disable=unused-import
|
||||
from requests.packages.urllib3 import (
|
||||
connection as urllib3_connection, # pylint: disable=unused-import
|
||||
)
|
||||
except ImportError:
|
||||
try:
|
||||
import urllib3 # pylint: disable=unused-import
|
||||
from urllib3 import connection as urllib3_connection # pylint: disable=unused-import
|
||||
from urllib3 import (
|
||||
connection as urllib3_connection, # pylint: disable=unused-import
|
||||
)
|
||||
except ImportError:
|
||||
URLLIB3_IMPORT_ERROR = traceback.format_exc()
|
||||
|
||||
@@ -77,11 +86,11 @@ def fail_on_missing_imports():
|
||||
from .errors import MissingRequirementException
|
||||
|
||||
raise MissingRequirementException(
|
||||
'You have to install requests',
|
||||
'requests', REQUESTS_IMPORT_ERROR)
|
||||
"You have to install requests", "requests", REQUESTS_IMPORT_ERROR
|
||||
)
|
||||
if URLLIB3_IMPORT_ERROR is not None:
|
||||
from .errors import MissingRequirementException
|
||||
|
||||
raise MissingRequirementException(
|
||||
'You have to install urllib3',
|
||||
'urllib3', URLLIB3_IMPORT_ERROR)
|
||||
"You have to install urllib3", "urllib3", URLLIB3_IMPORT_ERROR
|
||||
)
|
||||
|
||||
@@ -16,37 +16,45 @@ from functools import partial
|
||||
from urllib.parse import quote
|
||||
|
||||
from .. import auth
|
||||
from .._import_helper import fail_on_missing_imports
|
||||
from .._import_helper import HTTPError as _HTTPError
|
||||
from .._import_helper import InvalidSchema as _InvalidSchema
|
||||
from .._import_helper import Session as _Session
|
||||
from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH,
|
||||
DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS,
|
||||
DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM,
|
||||
MINIMUM_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES,
|
||||
DEFAULT_DATA_CHUNK_SIZE)
|
||||
from ..errors import (DockerException, InvalidVersion, TLSParameterError, MissingRequirementException,
|
||||
create_api_error_from_http_exception)
|
||||
from .._import_helper import fail_on_missing_imports
|
||||
from ..constants import (
|
||||
DEFAULT_DATA_CHUNK_SIZE,
|
||||
DEFAULT_MAX_POOL_SIZE,
|
||||
DEFAULT_NUM_POOLS,
|
||||
DEFAULT_NUM_POOLS_SSH,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
DEFAULT_USER_AGENT,
|
||||
IS_WINDOWS_PLATFORM,
|
||||
MINIMUM_DOCKER_API_VERSION,
|
||||
STREAM_HEADER_SIZE_BYTES,
|
||||
)
|
||||
from ..errors import (
|
||||
DockerException,
|
||||
InvalidVersion,
|
||||
MissingRequirementException,
|
||||
TLSParameterError,
|
||||
create_api_error_from_http_exception,
|
||||
)
|
||||
from ..tls import TLSConfig
|
||||
from ..transport.npipeconn import NpipeHTTPAdapter
|
||||
from ..transport.npipesocket import PYWIN32_IMPORT_ERROR
|
||||
from ..transport.unixconn import UnixHTTPAdapter
|
||||
from ..transport.sshconn import SSHHTTPAdapter, PARAMIKO_IMPORT_ERROR
|
||||
from ..transport.sshconn import PARAMIKO_IMPORT_ERROR, SSHHTTPAdapter
|
||||
from ..transport.ssladapter import SSLHTTPAdapter
|
||||
from ..utils import config, utils, json_stream
|
||||
from ..transport.unixconn import UnixHTTPAdapter
|
||||
from ..utils import config, json_stream, utils
|
||||
from ..utils.decorators import check_resource, update_headers
|
||||
from ..utils.proxy import ProxyConfig
|
||||
from ..utils.socket import consume_socket_output, demux_adaptor, frames_iter
|
||||
|
||||
from .daemon import DaemonApiMixin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIClient(
|
||||
_Session,
|
||||
DaemonApiMixin):
|
||||
class APIClient(_Session, DaemonApiMixin):
|
||||
"""
|
||||
A low-level client for the Docker Engine API.
|
||||
|
||||
@@ -85,139 +93,155 @@ class APIClient(
|
||||
to save in the pool.
|
||||
"""
|
||||
|
||||
__attrs__ = _Session.__attrs__ + ['_auth_configs',
|
||||
'_general_configs',
|
||||
'_version',
|
||||
'base_url',
|
||||
'timeout']
|
||||
__attrs__ = _Session.__attrs__ + [
|
||||
"_auth_configs",
|
||||
"_general_configs",
|
||||
"_version",
|
||||
"base_url",
|
||||
"timeout",
|
||||
]
|
||||
|
||||
def __init__(self, base_url=None, version=None,
|
||||
timeout=DEFAULT_TIMEOUT_SECONDS, tls=False,
|
||||
user_agent=DEFAULT_USER_AGENT, num_pools=None,
|
||||
credstore_env=None, use_ssh_client=False,
|
||||
max_pool_size=DEFAULT_MAX_POOL_SIZE):
|
||||
def __init__(
|
||||
self,
|
||||
base_url=None,
|
||||
version=None,
|
||||
timeout=DEFAULT_TIMEOUT_SECONDS,
|
||||
tls=False,
|
||||
user_agent=DEFAULT_USER_AGENT,
|
||||
num_pools=None,
|
||||
credstore_env=None,
|
||||
use_ssh_client=False,
|
||||
max_pool_size=DEFAULT_MAX_POOL_SIZE,
|
||||
):
|
||||
super(APIClient, self).__init__()
|
||||
|
||||
fail_on_missing_imports()
|
||||
|
||||
if tls and not base_url:
|
||||
raise TLSParameterError(
|
||||
'If using TLS, the base_url argument must be provided.'
|
||||
"If using TLS, the base_url argument must be provided."
|
||||
)
|
||||
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self.headers['User-Agent'] = user_agent
|
||||
self.headers["User-Agent"] = user_agent
|
||||
|
||||
self._general_configs = config.load_general_config()
|
||||
|
||||
proxy_config = self._general_configs.get('proxies', {})
|
||||
proxy_config = self._general_configs.get("proxies", {})
|
||||
try:
|
||||
proxies = proxy_config[base_url]
|
||||
except KeyError:
|
||||
proxies = proxy_config.get('default', {})
|
||||
proxies = proxy_config.get("default", {})
|
||||
|
||||
self._proxy_configs = ProxyConfig.from_dict(proxies)
|
||||
|
||||
self._auth_configs = auth.load_config(
|
||||
config_dict=self._general_configs, credstore_env=credstore_env,
|
||||
config_dict=self._general_configs,
|
||||
credstore_env=credstore_env,
|
||||
)
|
||||
self.credstore_env = credstore_env
|
||||
|
||||
base_url = utils.parse_host(
|
||||
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
|
||||
)
|
||||
base_url = utils.parse_host(base_url, IS_WINDOWS_PLATFORM, tls=bool(tls))
|
||||
# SSH has a different default for num_pools to all other adapters
|
||||
num_pools = num_pools or DEFAULT_NUM_POOLS_SSH if \
|
||||
base_url.startswith('ssh://') else DEFAULT_NUM_POOLS
|
||||
num_pools = (
|
||||
num_pools or DEFAULT_NUM_POOLS_SSH
|
||||
if base_url.startswith("ssh://")
|
||||
else DEFAULT_NUM_POOLS
|
||||
)
|
||||
|
||||
if base_url.startswith('http+unix://'):
|
||||
if base_url.startswith("http+unix://"):
|
||||
self._custom_adapter = UnixHTTPAdapter(
|
||||
base_url, timeout, pool_connections=num_pools,
|
||||
max_pool_size=max_pool_size
|
||||
base_url,
|
||||
timeout,
|
||||
pool_connections=num_pools,
|
||||
max_pool_size=max_pool_size,
|
||||
)
|
||||
self.mount('http+docker://', self._custom_adapter)
|
||||
self._unmount('http://', 'https://')
|
||||
self.mount("http+docker://", self._custom_adapter)
|
||||
self._unmount("http://", "https://")
|
||||
# host part of URL should be unused, but is resolved by requests
|
||||
# module in proxy_bypass_macosx_sysconf()
|
||||
self.base_url = 'http+docker://localhost'
|
||||
elif base_url.startswith('npipe://'):
|
||||
self.base_url = "http+docker://localhost"
|
||||
elif base_url.startswith("npipe://"):
|
||||
if not IS_WINDOWS_PLATFORM:
|
||||
raise DockerException(
|
||||
'The npipe:// protocol is only supported on Windows'
|
||||
"The npipe:// protocol is only supported on Windows"
|
||||
)
|
||||
if PYWIN32_IMPORT_ERROR is not None:
|
||||
raise MissingRequirementException(
|
||||
'Install pypiwin32 package to enable npipe:// support',
|
||||
'pywin32',
|
||||
PYWIN32_IMPORT_ERROR)
|
||||
"Install pypiwin32 package to enable npipe:// support",
|
||||
"pywin32",
|
||||
PYWIN32_IMPORT_ERROR,
|
||||
)
|
||||
self._custom_adapter = NpipeHTTPAdapter(
|
||||
base_url, timeout, pool_connections=num_pools,
|
||||
max_pool_size=max_pool_size
|
||||
base_url,
|
||||
timeout,
|
||||
pool_connections=num_pools,
|
||||
max_pool_size=max_pool_size,
|
||||
)
|
||||
self.mount('http+docker://', self._custom_adapter)
|
||||
self.base_url = 'http+docker://localnpipe'
|
||||
elif base_url.startswith('ssh://'):
|
||||
self.mount("http+docker://", self._custom_adapter)
|
||||
self.base_url = "http+docker://localnpipe"
|
||||
elif base_url.startswith("ssh://"):
|
||||
if PARAMIKO_IMPORT_ERROR is not None and not use_ssh_client:
|
||||
raise MissingRequirementException(
|
||||
'Install paramiko package to enable ssh:// support',
|
||||
'paramiko',
|
||||
PARAMIKO_IMPORT_ERROR)
|
||||
"Install paramiko package to enable ssh:// support",
|
||||
"paramiko",
|
||||
PARAMIKO_IMPORT_ERROR,
|
||||
)
|
||||
self._custom_adapter = SSHHTTPAdapter(
|
||||
base_url, timeout, pool_connections=num_pools,
|
||||
max_pool_size=max_pool_size, shell_out=use_ssh_client
|
||||
base_url,
|
||||
timeout,
|
||||
pool_connections=num_pools,
|
||||
max_pool_size=max_pool_size,
|
||||
shell_out=use_ssh_client,
|
||||
)
|
||||
self.mount('http+docker://ssh', self._custom_adapter)
|
||||
self._unmount('http://', 'https://')
|
||||
self.base_url = 'http+docker://ssh'
|
||||
self.mount("http+docker://ssh", self._custom_adapter)
|
||||
self._unmount("http://", "https://")
|
||||
self.base_url = "http+docker://ssh"
|
||||
else:
|
||||
# Use SSLAdapter for the ability to specify SSL version
|
||||
if isinstance(tls, TLSConfig):
|
||||
tls.configure_client(self)
|
||||
elif tls:
|
||||
self._custom_adapter = SSLHTTPAdapter(
|
||||
pool_connections=num_pools)
|
||||
self.mount('https://', self._custom_adapter)
|
||||
self._custom_adapter = SSLHTTPAdapter(pool_connections=num_pools)
|
||||
self.mount("https://", self._custom_adapter)
|
||||
self.base_url = base_url
|
||||
|
||||
# version detection needs to be after unix adapter mounting
|
||||
if version is None or (isinstance(version, str) and version.lower() == 'auto'):
|
||||
if version is None or (isinstance(version, str) and version.lower() == "auto"):
|
||||
self._version = self._retrieve_server_version()
|
||||
else:
|
||||
self._version = version
|
||||
if not isinstance(self._version, str):
|
||||
raise DockerException(
|
||||
f'Version parameter must be a string or None. Found {type(version).__name__}'
|
||||
f"Version parameter must be a string or None. Found {type(version).__name__}"
|
||||
)
|
||||
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
|
||||
raise InvalidVersion(
|
||||
f'API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library.'
|
||||
f"API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library."
|
||||
)
|
||||
|
||||
def _retrieve_server_version(self):
|
||||
try:
|
||||
version_result = self.version(api_version=False)
|
||||
except Exception as e:
|
||||
raise DockerException(
|
||||
f'Error while fetching server API version: {e}'
|
||||
)
|
||||
raise DockerException(f"Error while fetching server API version: {e}")
|
||||
|
||||
try:
|
||||
return version_result["ApiVersion"]
|
||||
except KeyError:
|
||||
raise DockerException(
|
||||
'Invalid response from docker daemon: key "ApiVersion"'
|
||||
' is missing.'
|
||||
'Invalid response from docker daemon: key "ApiVersion"' " is missing."
|
||||
)
|
||||
except Exception as e:
|
||||
raise DockerException(
|
||||
f'Error while fetching server API version: {e}. Response seems to be broken.'
|
||||
f"Error while fetching server API version: {e}. Response seems to be broken."
|
||||
)
|
||||
|
||||
def _set_request_timeout(self, kwargs):
|
||||
"""Prepare the kwargs for an HTTP request by inserting the timeout
|
||||
parameter, if not already present."""
|
||||
kwargs.setdefault('timeout', self.timeout)
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
return kwargs
|
||||
|
||||
@update_headers
|
||||
@@ -244,16 +268,16 @@ class APIClient(
|
||||
for arg in args:
|
||||
if not isinstance(arg, str):
|
||||
raise ValueError(
|
||||
f'Expected a string but found {arg} ({type(arg)}) instead'
|
||||
f"Expected a string but found {arg} ({type(arg)}) instead"
|
||||
)
|
||||
|
||||
quote_f = partial(quote, safe="/:")
|
||||
args = map(quote_f, args)
|
||||
|
||||
if kwargs.get('versioned_api', True):
|
||||
return f'{self.base_url}/v{self._version}{pathfmt.format(*args)}'
|
||||
if kwargs.get("versioned_api", True):
|
||||
return f"{self.base_url}/v{self._version}{pathfmt.format(*args)}"
|
||||
else:
|
||||
return f'{self.base_url}{pathfmt.format(*args)}'
|
||||
return f"{self.base_url}{pathfmt.format(*args)}"
|
||||
|
||||
def _raise_for_status(self, response):
|
||||
"""Raises stored :class:`APIError`, if one occurred."""
|
||||
@@ -264,7 +288,7 @@ class APIClient(
|
||||
|
||||
def _result(self, response, json=False, binary=False):
|
||||
if json and binary:
|
||||
raise AssertionError('json and binary must not be both True')
|
||||
raise AssertionError("json and binary must not be both True")
|
||||
self._raise_for_status(response)
|
||||
|
||||
if json:
|
||||
@@ -284,23 +308,19 @@ class APIClient(
|
||||
elif data is not None:
|
||||
data2 = data
|
||||
|
||||
if 'headers' not in kwargs:
|
||||
kwargs['headers'] = {}
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
if "headers" not in kwargs:
|
||||
kwargs["headers"] = {}
|
||||
kwargs["headers"]["Content-Type"] = "application/json"
|
||||
return self._post(url, data=json.dumps(data2), **kwargs)
|
||||
|
||||
def _attach_params(self, override=None):
|
||||
return override or {
|
||||
'stdout': 1,
|
||||
'stderr': 1,
|
||||
'stream': 1
|
||||
}
|
||||
return override or {"stdout": 1, "stderr": 1, "stream": 1}
|
||||
|
||||
def _get_raw_response_socket(self, response):
|
||||
self._raise_for_status(response)
|
||||
if self.base_url == "http+docker://localnpipe":
|
||||
sock = response.raw._fp.fp.raw.sock
|
||||
elif self.base_url.startswith('http+docker://ssh'):
|
||||
elif self.base_url.startswith("http+docker://ssh"):
|
||||
sock = response.raw._fp.fp.channel
|
||||
else:
|
||||
sock = response.raw._fp.fp.raw
|
||||
@@ -348,8 +368,8 @@ class APIClient(
|
||||
while True:
|
||||
if buf_length - walker < STREAM_HEADER_SIZE_BYTES:
|
||||
break
|
||||
header = buf[walker:walker + STREAM_HEADER_SIZE_BYTES]
|
||||
dummy, length = struct.unpack_from('>BxxxL', header)
|
||||
header = buf[walker : walker + STREAM_HEADER_SIZE_BYTES]
|
||||
dummy, length = struct.unpack_from(">BxxxL", header)
|
||||
start = walker + STREAM_HEADER_SIZE_BYTES
|
||||
end = start + length
|
||||
walker = end
|
||||
@@ -368,7 +388,7 @@ class APIClient(
|
||||
header = response.raw.read(STREAM_HEADER_SIZE_BYTES)
|
||||
if not header:
|
||||
break
|
||||
dummy, length = struct.unpack('>BxxxL', header)
|
||||
dummy, length = struct.unpack(">BxxxL", header)
|
||||
if not length:
|
||||
continue
|
||||
data = response.raw.read(length)
|
||||
@@ -377,7 +397,7 @@ class APIClient(
|
||||
yield data
|
||||
|
||||
def _stream_raw_result(self, response, chunk_size=1, decode=True):
|
||||
''' Stream result for TTY-enabled container and raw binary data'''
|
||||
"""Stream result for TTY-enabled container and raw binary data"""
|
||||
self._raise_for_status(response)
|
||||
|
||||
# Disable timeout on the underlying socket to prevent
|
||||
@@ -413,7 +433,7 @@ class APIClient(
|
||||
response.close()
|
||||
|
||||
def _disable_socket_timeout(self, socket):
|
||||
""" Depending on the combination of python version and whether we are
|
||||
"""Depending on the combination of python version and whether we are
|
||||
connecting over http or https, we might need to access _sock, which
|
||||
may or may not exist; or we may need to just settimeout on socket
|
||||
itself, which also may or may not have settimeout on it. To avoid
|
||||
@@ -423,15 +443,15 @@ class APIClient(
|
||||
you run the risk of changing a socket that was non-blocking to
|
||||
blocking, for example when using gevent.
|
||||
"""
|
||||
sockets = [socket, getattr(socket, '_sock', None)]
|
||||
sockets = [socket, getattr(socket, "_sock", None)]
|
||||
|
||||
for s in sockets:
|
||||
if not hasattr(s, 'settimeout'):
|
||||
if not hasattr(s, "settimeout"):
|
||||
continue
|
||||
|
||||
timeout = -1
|
||||
|
||||
if hasattr(s, 'gettimeout'):
|
||||
if hasattr(s, "gettimeout"):
|
||||
timeout = s.gettimeout()
|
||||
|
||||
# Do not change the timeout if it is already disabled.
|
||||
@@ -440,10 +460,10 @@ class APIClient(
|
||||
|
||||
s.settimeout(None)
|
||||
|
||||
@check_resource('container')
|
||||
@check_resource("container")
|
||||
def _check_is_tty(self, container):
|
||||
cont = self.inspect_container(container)
|
||||
return cont['Config']['Tty']
|
||||
return cont["Config"]["Tty"]
|
||||
|
||||
def _get_result(self, container, stream, res):
|
||||
return self._get_result_tty(stream, res, self._check_is_tty(container))
|
||||
@@ -452,17 +472,18 @@ class APIClient(
|
||||
# We should also use raw streaming (without keep-alive)
|
||||
# if we are dealing with a tty-enabled container.
|
||||
if is_tty:
|
||||
return self._stream_raw_result(res) if stream else \
|
||||
self._result(res, binary=True)
|
||||
return (
|
||||
self._stream_raw_result(res)
|
||||
if stream
|
||||
else self._result(res, binary=True)
|
||||
)
|
||||
|
||||
self._raise_for_status(res)
|
||||
sep = b''
|
||||
sep = b""
|
||||
if stream:
|
||||
return self._multiplexed_response_stream_helper(res)
|
||||
else:
|
||||
return sep.join(
|
||||
list(self._multiplexed_buffer_helper(res))
|
||||
)
|
||||
return sep.join(list(self._multiplexed_buffer_helper(res)))
|
||||
|
||||
def _unmount(self, *args):
|
||||
for proto in args:
|
||||
@@ -498,15 +519,13 @@ class APIClient(
|
||||
)
|
||||
|
||||
def _set_auth_headers(self, headers):
|
||||
log.debug('Looking for auth config')
|
||||
log.debug("Looking for auth config")
|
||||
|
||||
# If we do not have any auth data so far, try reloading the config
|
||||
# file one more time in case anything showed up in there.
|
||||
if not self._auth_configs or self._auth_configs.is_empty:
|
||||
log.debug("No auth config in memory - loading from filesystem")
|
||||
self._auth_configs = auth.load_config(
|
||||
credstore_env=self.credstore_env
|
||||
)
|
||||
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
|
||||
|
||||
# Send the full auth configuration (if any exists), since the build
|
||||
# could use any (or all) of the registries.
|
||||
@@ -514,87 +533,134 @@ class APIClient(
|
||||
auth_data = self._auth_configs.get_all_credentials()
|
||||
|
||||
# See https://github.com/docker/docker-py/issues/1683
|
||||
if (auth.INDEX_URL not in auth_data and
|
||||
auth.INDEX_NAME in auth_data):
|
||||
if auth.INDEX_URL not in auth_data and auth.INDEX_NAME in auth_data:
|
||||
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
|
||||
|
||||
log.debug(
|
||||
'Sending auth config (%s)',
|
||||
', '.join(repr(k) for k in auth_data.keys())
|
||||
"Sending auth config (%s)", ", ".join(repr(k) for k in auth_data.keys())
|
||||
)
|
||||
|
||||
if auth_data:
|
||||
headers['X-Registry-Config'] = auth.encode_header(
|
||||
auth_data
|
||||
)
|
||||
headers["X-Registry-Config"] = auth.encode_header(auth_data)
|
||||
else:
|
||||
log.debug('No auth config found')
|
||||
log.debug("No auth config found")
|
||||
|
||||
def get_binary(self, pathfmt, *args, **kwargs):
|
||||
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs), binary=True)
|
||||
return self._result(
|
||||
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
binary=True,
|
||||
)
|
||||
|
||||
def get_json(self, pathfmt, *args, **kwargs):
|
||||
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
|
||||
return self._result(
|
||||
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
json=True,
|
||||
)
|
||||
|
||||
def get_text(self, pathfmt, *args, **kwargs):
|
||||
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs))
|
||||
return self._result(
|
||||
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs)
|
||||
)
|
||||
|
||||
def get_raw_stream(self, pathfmt, *args, **kwargs):
|
||||
chunk_size = kwargs.pop('chunk_size', DEFAULT_DATA_CHUNK_SIZE)
|
||||
res = self._get(self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs)
|
||||
chunk_size = kwargs.pop("chunk_size", DEFAULT_DATA_CHUNK_SIZE)
|
||||
res = self._get(
|
||||
self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs
|
||||
)
|
||||
self._raise_for_status(res)
|
||||
return self._stream_raw_result(res, chunk_size, False)
|
||||
|
||||
def delete_call(self, pathfmt, *args, **kwargs):
|
||||
self._raise_for_status(self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs))
|
||||
self._raise_for_status(
|
||||
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs)
|
||||
)
|
||||
|
||||
def delete_json(self, pathfmt, *args, **kwargs):
|
||||
return self._result(self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
|
||||
return self._result(
|
||||
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
json=True,
|
||||
)
|
||||
|
||||
def post_call(self, pathfmt, *args, **kwargs):
|
||||
self._raise_for_status(self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs))
|
||||
self._raise_for_status(
|
||||
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs)
|
||||
)
|
||||
|
||||
def post_json(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop('data', None)
|
||||
self._raise_for_status(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs))
|
||||
data = kwargs.pop("data", None)
|
||||
self._raise_for_status(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def post_json_to_binary(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop('data', None)
|
||||
return self._result(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs), binary=True)
|
||||
data = kwargs.pop("data", None)
|
||||
return self._result(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
),
|
||||
binary=True,
|
||||
)
|
||||
|
||||
def post_json_to_json(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop('data', None)
|
||||
return self._result(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs), json=True)
|
||||
data = kwargs.pop("data", None)
|
||||
return self._result(
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
|
||||
),
|
||||
json=True,
|
||||
)
|
||||
|
||||
def post_json_to_text(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop('data', None)
|
||||
data = kwargs.pop("data", None)
|
||||
|
||||
def post_json_to_stream_socket(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop('data', None)
|
||||
headers = (kwargs.pop('headers', None) or {}).copy()
|
||||
headers.update({
|
||||
'Connection': 'Upgrade',
|
||||
'Upgrade': 'tcp',
|
||||
})
|
||||
data = kwargs.pop("data", None)
|
||||
headers = (kwargs.pop("headers", None) or {}).copy()
|
||||
headers.update(
|
||||
{
|
||||
"Connection": "Upgrade",
|
||||
"Upgrade": "tcp",
|
||||
}
|
||||
)
|
||||
return self._get_raw_response_socket(
|
||||
self._post_json(self._url(pathfmt, *args, versioned_api=True), data, headers=headers, stream=True, **kwargs))
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True),
|
||||
data,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
def post_json_to_stream(self, pathfmt, *args, **kwargs):
|
||||
data = kwargs.pop('data', None)
|
||||
headers = (kwargs.pop('headers', None) or {}).copy()
|
||||
headers.update({
|
||||
'Connection': 'Upgrade',
|
||||
'Upgrade': 'tcp',
|
||||
})
|
||||
stream = kwargs.pop('stream', False)
|
||||
demux = kwargs.pop('demux', False)
|
||||
tty = kwargs.pop('tty', False)
|
||||
data = kwargs.pop("data", None)
|
||||
headers = (kwargs.pop("headers", None) or {}).copy()
|
||||
headers.update(
|
||||
{
|
||||
"Connection": "Upgrade",
|
||||
"Upgrade": "tcp",
|
||||
}
|
||||
)
|
||||
stream = kwargs.pop("stream", False)
|
||||
demux = kwargs.pop("demux", False)
|
||||
tty = kwargs.pop("tty", False)
|
||||
return self._read_from_socket(
|
||||
self._post_json(self._url(pathfmt, *args, versioned_api=True), data, headers=headers, stream=True, **kwargs),
|
||||
self._post_json(
|
||||
self._url(pathfmt, *args, versioned_api=True),
|
||||
data,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
**kwargs,
|
||||
),
|
||||
stream,
|
||||
tty=tty,
|
||||
demux=demux
|
||||
demux=demux,
|
||||
)
|
||||
|
||||
def post_to_json(self, pathfmt, *args, **kwargs):
|
||||
return self._result(self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
|
||||
return self._result(
|
||||
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs),
|
||||
json=True,
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from ..utils.decorators import minimum_version
|
||||
|
||||
|
||||
class DaemonApiMixin(object):
|
||||
@minimum_version('1.25')
|
||||
@minimum_version("1.25")
|
||||
def df(self):
|
||||
"""
|
||||
Get data usage information.
|
||||
@@ -29,7 +29,7 @@ class DaemonApiMixin(object):
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
url = self._url('/system/df')
|
||||
url = self._url("/system/df")
|
||||
return self._result(self._get(url), True)
|
||||
|
||||
def info(self):
|
||||
@@ -46,8 +46,15 @@ class DaemonApiMixin(object):
|
||||
"""
|
||||
return self._result(self._get(self._url("/info")), True)
|
||||
|
||||
def login(self, username, password=None, email=None, registry=None,
|
||||
reauth=False, dockercfg_path=None):
|
||||
def login(
|
||||
self,
|
||||
username,
|
||||
password=None,
|
||||
email=None,
|
||||
registry=None,
|
||||
reauth=False,
|
||||
dockercfg_path=None,
|
||||
):
|
||||
"""
|
||||
Authenticate with a registry. Similar to the ``docker login`` command.
|
||||
|
||||
@@ -80,25 +87,22 @@ class DaemonApiMixin(object):
|
||||
dockercfg_path, credstore_env=self.credstore_env
|
||||
)
|
||||
elif not self._auth_configs or self._auth_configs.is_empty:
|
||||
self._auth_configs = auth.load_config(
|
||||
credstore_env=self.credstore_env
|
||||
)
|
||||
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
|
||||
|
||||
authcfg = self._auth_configs.resolve_authconfig(registry)
|
||||
# If we found an existing auth config for this registry and username
|
||||
# combination, we can return it immediately unless reauth is requested.
|
||||
if authcfg and authcfg.get('username', None) == username \
|
||||
and not reauth:
|
||||
if authcfg and authcfg.get("username", None) == username and not reauth:
|
||||
return authcfg
|
||||
|
||||
req_data = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': email,
|
||||
'serveraddress': registry,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": email,
|
||||
"serveraddress": registry,
|
||||
}
|
||||
|
||||
response = self._post_json(self._url('/auth'), data=req_data)
|
||||
response = self._post_json(self._url("/auth"), data=req_data)
|
||||
if response.status_code == 200:
|
||||
self._auth_configs.add_auth(registry or auth.INDEX_NAME, req_data)
|
||||
return self._result(response, json=True)
|
||||
@@ -115,7 +119,7 @@ class DaemonApiMixin(object):
|
||||
:py:class:`docker.errors.APIError`
|
||||
If the server returns an error.
|
||||
"""
|
||||
return self._result(self._get(self._url('/_ping'))) == 'OK'
|
||||
return self._result(self._get(self._url("/_ping"))) == "OK"
|
||||
|
||||
def version(self, api_version=True):
|
||||
"""
|
||||
|
||||
@@ -14,44 +14,43 @@ import json
|
||||
import logging
|
||||
|
||||
from . import errors
|
||||
from .credentials.errors import CredentialsNotFound, StoreError
|
||||
from .credentials.store import Store
|
||||
from .credentials.errors import StoreError, CredentialsNotFound
|
||||
from .utils import config
|
||||
|
||||
INDEX_NAME = 'docker.io'
|
||||
INDEX_URL = f'https://index.{INDEX_NAME}/v1/'
|
||||
TOKEN_USERNAME = '<token>'
|
||||
|
||||
INDEX_NAME = "docker.io"
|
||||
INDEX_URL = f"https://index.{INDEX_NAME}/v1/"
|
||||
TOKEN_USERNAME = "<token>"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resolve_repository_name(repo_name):
|
||||
if '://' in repo_name:
|
||||
if "://" in repo_name:
|
||||
raise errors.InvalidRepository(
|
||||
f'Repository name cannot contain a scheme ({repo_name})'
|
||||
f"Repository name cannot contain a scheme ({repo_name})"
|
||||
)
|
||||
|
||||
index_name, remote_name = split_repo_name(repo_name)
|
||||
if index_name[0] == '-' or index_name[-1] == '-':
|
||||
if index_name[0] == "-" or index_name[-1] == "-":
|
||||
raise errors.InvalidRepository(
|
||||
f'Invalid index name ({index_name}). Cannot begin or end with a hyphen.'
|
||||
f"Invalid index name ({index_name}). Cannot begin or end with a hyphen."
|
||||
)
|
||||
return resolve_index_name(index_name), remote_name
|
||||
|
||||
|
||||
def resolve_index_name(index_name):
|
||||
index_name = convert_to_hostname(index_name)
|
||||
if index_name == 'index.' + INDEX_NAME:
|
||||
if index_name == "index." + INDEX_NAME:
|
||||
index_name = INDEX_NAME
|
||||
return index_name
|
||||
|
||||
|
||||
def get_config_header(client, registry):
|
||||
log.debug('Looking for auth config')
|
||||
log.debug("Looking for auth config")
|
||||
if not client._auth_configs or client._auth_configs.is_empty:
|
||||
log.debug(
|
||||
"No auth config in memory - loading from filesystem"
|
||||
)
|
||||
log.debug("No auth config in memory - loading from filesystem")
|
||||
client._auth_configs = load_config(credstore_env=client.credstore_env)
|
||||
authcfg = resolve_authconfig(
|
||||
client._auth_configs, registry, credstore_env=client.credstore_env
|
||||
@@ -60,18 +59,18 @@ def get_config_header(client, registry):
|
||||
# specific registry as we can have a readonly pull. Just
|
||||
# put the header if we can.
|
||||
if authcfg:
|
||||
log.debug('Found auth config')
|
||||
log.debug("Found auth config")
|
||||
# auth_config needs to be a dict in the format used by
|
||||
# auth.py username , password, serveraddress, email
|
||||
return encode_header(authcfg)
|
||||
log.debug('No auth config found')
|
||||
log.debug("No auth config found")
|
||||
return None
|
||||
|
||||
|
||||
def split_repo_name(repo_name):
|
||||
parts = repo_name.split('/', 1)
|
||||
parts = repo_name.split("/", 1)
|
||||
if len(parts) == 1 or (
|
||||
'.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost'
|
||||
"." not in parts[0] and ":" not in parts[0] and parts[0] != "localhost"
|
||||
):
|
||||
# This is a docker index repo (ex: username/foobar or ubuntu)
|
||||
return INDEX_NAME, repo_name
|
||||
@@ -86,8 +85,8 @@ def get_credential_store(authconfig, registry):
|
||||
|
||||
class AuthConfig(dict):
|
||||
def __init__(self, dct, credstore_env=None):
|
||||
if 'auths' not in dct:
|
||||
dct['auths'] = {}
|
||||
if "auths" not in dct:
|
||||
dct["auths"] = {}
|
||||
self.update(dct)
|
||||
self._credstore_env = credstore_env
|
||||
self._stores = {}
|
||||
@@ -109,39 +108,42 @@ class AuthConfig(dict):
|
||||
conf = {}
|
||||
for registry, entry in entries.items():
|
||||
if not isinstance(entry, dict):
|
||||
log.debug('Config entry for key %s is not auth config', registry)
|
||||
log.debug("Config entry for key %s is not auth config", registry)
|
||||
# We sometimes fall back to parsing the whole config as if it
|
||||
# was the auth config by itself, for legacy purposes. In that
|
||||
# case, we fail silently and return an empty conf if any of the
|
||||
# keys is not formatted properly.
|
||||
if raise_on_error:
|
||||
raise errors.InvalidConfigFile(
|
||||
f'Invalid configuration for registry {registry}'
|
||||
f"Invalid configuration for registry {registry}"
|
||||
)
|
||||
return {}
|
||||
if 'identitytoken' in entry:
|
||||
log.debug('Found an IdentityToken entry for registry %s', registry)
|
||||
conf[registry] = {
|
||||
'IdentityToken': entry['identitytoken']
|
||||
}
|
||||
if "identitytoken" in entry:
|
||||
log.debug("Found an IdentityToken entry for registry %s", registry)
|
||||
conf[registry] = {"IdentityToken": entry["identitytoken"]}
|
||||
continue # Other values are irrelevant if we have a token
|
||||
|
||||
if 'auth' not in entry:
|
||||
if "auth" not in entry:
|
||||
# Starting with engine v1.11 (API 1.23), an empty dictionary is
|
||||
# a valid value in the auths config.
|
||||
# https://github.com/docker/compose/issues/3265
|
||||
log.debug('Auth data for %s is absent. Client might be using a credentials store instead.', registry)
|
||||
log.debug(
|
||||
"Auth data for %s is absent. Client might be using a credentials store instead.",
|
||||
registry,
|
||||
)
|
||||
conf[registry] = {}
|
||||
continue
|
||||
|
||||
username, password = decode_auth(entry['auth'])
|
||||
log.debug('Found entry (registry=%s, username=%s)', repr(registry), repr(username))
|
||||
username, password = decode_auth(entry["auth"])
|
||||
log.debug(
|
||||
"Found entry (registry=%s, username=%s)", repr(registry), repr(username)
|
||||
)
|
||||
|
||||
conf[registry] = {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': entry.get('email'),
|
||||
'serveraddress': registry,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": entry.get("email"),
|
||||
"serveraddress": registry,
|
||||
}
|
||||
return conf
|
||||
|
||||
@@ -171,19 +173,17 @@ class AuthConfig(dict):
|
||||
return cls(_load_legacy_config(config_file), credstore_env)
|
||||
|
||||
res = {}
|
||||
if config_dict.get('auths'):
|
||||
if config_dict.get("auths"):
|
||||
log.debug("Found 'auths' section")
|
||||
res.update({
|
||||
'auths': cls.parse_auth(
|
||||
config_dict.pop('auths'), raise_on_error=True
|
||||
)
|
||||
})
|
||||
if config_dict.get('credsStore'):
|
||||
res.update(
|
||||
{"auths": cls.parse_auth(config_dict.pop("auths"), raise_on_error=True)}
|
||||
)
|
||||
if config_dict.get("credsStore"):
|
||||
log.debug("Found 'credsStore' section")
|
||||
res.update({'credsStore': config_dict.pop('credsStore')})
|
||||
if config_dict.get('credHelpers'):
|
||||
res.update({"credsStore": config_dict.pop("credsStore")})
|
||||
if config_dict.get("credHelpers"):
|
||||
log.debug("Found 'credHelpers' section")
|
||||
res.update({'credHelpers': config_dict.pop('credHelpers')})
|
||||
res.update({"credHelpers": config_dict.pop("credHelpers")})
|
||||
if res:
|
||||
return cls(res, credstore_env)
|
||||
|
||||
@@ -191,25 +191,23 @@ class AuthConfig(dict):
|
||||
"Could not find auth-related section ; attempting to interpret "
|
||||
"as auth-only file"
|
||||
)
|
||||
return cls({'auths': cls.parse_auth(config_dict)}, credstore_env)
|
||||
return cls({"auths": cls.parse_auth(config_dict)}, credstore_env)
|
||||
|
||||
@property
|
||||
def auths(self):
|
||||
return self.get('auths', {})
|
||||
return self.get("auths", {})
|
||||
|
||||
@property
|
||||
def creds_store(self):
|
||||
return self.get('credsStore', None)
|
||||
return self.get("credsStore", None)
|
||||
|
||||
@property
|
||||
def cred_helpers(self):
|
||||
return self.get('credHelpers', {})
|
||||
return self.get("credHelpers", {})
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return (
|
||||
not self.auths and not self.creds_store and not self.cred_helpers
|
||||
)
|
||||
return not self.auths and not self.creds_store and not self.cred_helpers
|
||||
|
||||
def resolve_authconfig(self, registry=None):
|
||||
"""
|
||||
@@ -226,7 +224,7 @@ class AuthConfig(dict):
|
||||
cfg = self._resolve_authconfig_credstore(registry, store_name)
|
||||
if cfg is not None:
|
||||
return cfg
|
||||
log.debug('No entry in credstore - fetching from auth dict')
|
||||
log.debug("No entry in credstore - fetching from auth dict")
|
||||
|
||||
# Default to the public index server
|
||||
registry = resolve_index_name(registry) if registry else INDEX_NAME
|
||||
@@ -254,29 +252,27 @@ class AuthConfig(dict):
|
||||
try:
|
||||
data = store.get(registry)
|
||||
res = {
|
||||
'ServerAddress': registry,
|
||||
"ServerAddress": registry,
|
||||
}
|
||||
if data['Username'] == TOKEN_USERNAME:
|
||||
res['IdentityToken'] = data['Secret']
|
||||
if data["Username"] == TOKEN_USERNAME:
|
||||
res["IdentityToken"] = data["Secret"]
|
||||
else:
|
||||
res.update({
|
||||
'Username': data['Username'],
|
||||
'Password': data['Secret'],
|
||||
})
|
||||
res.update(
|
||||
{
|
||||
"Username": data["Username"],
|
||||
"Password": data["Secret"],
|
||||
}
|
||||
)
|
||||
return res
|
||||
except CredentialsNotFound:
|
||||
log.debug('No entry found')
|
||||
log.debug("No entry found")
|
||||
return None
|
||||
except StoreError as e:
|
||||
raise errors.DockerException(
|
||||
f'Credentials store error: {e}'
|
||||
)
|
||||
raise errors.DockerException(f"Credentials store error: {e}")
|
||||
|
||||
def _get_store_instance(self, name):
|
||||
if name not in self._stores:
|
||||
self._stores[name] = Store(
|
||||
name, environment=self._credstore_env
|
||||
)
|
||||
self._stores[name] = Store(name, environment=self._credstore_env)
|
||||
return self._stores[name]
|
||||
|
||||
def get_credential_store(self, registry):
|
||||
@@ -291,22 +287,18 @@ class AuthConfig(dict):
|
||||
# Retrieve all credentials from the default store
|
||||
store = self._get_store_instance(self.creds_store)
|
||||
for k in store.list().keys():
|
||||
auth_data[k] = self._resolve_authconfig_credstore(
|
||||
k, self.creds_store
|
||||
)
|
||||
auth_data[k] = self._resolve_authconfig_credstore(k, self.creds_store)
|
||||
auth_data[convert_to_hostname(k)] = auth_data[k]
|
||||
|
||||
# credHelpers entries take priority over all others
|
||||
for reg, store_name in self.cred_helpers.items():
|
||||
auth_data[reg] = self._resolve_authconfig_credstore(
|
||||
reg, store_name
|
||||
)
|
||||
auth_data[reg] = self._resolve_authconfig_credstore(reg, store_name)
|
||||
auth_data[convert_to_hostname(reg)] = auth_data[reg]
|
||||
|
||||
return auth_data
|
||||
|
||||
def add_auth(self, reg, data):
|
||||
self['auths'][reg] = data
|
||||
self["auths"][reg] = data
|
||||
|
||||
|
||||
def resolve_authconfig(authconfig, registry=None, credstore_env=None):
|
||||
@@ -316,19 +308,19 @@ def resolve_authconfig(authconfig, registry=None, credstore_env=None):
|
||||
|
||||
|
||||
def convert_to_hostname(url):
|
||||
return url.replace('http://', '').replace('https://', '').split('/', 1)[0]
|
||||
return url.replace("http://", "").replace("https://", "").split("/", 1)[0]
|
||||
|
||||
|
||||
def decode_auth(auth):
|
||||
if isinstance(auth, str):
|
||||
auth = auth.encode('ascii')
|
||||
auth = auth.encode("ascii")
|
||||
s = base64.b64decode(auth)
|
||||
login, pwd = s.split(b':', 1)
|
||||
return login.decode('utf8'), pwd.decode('utf8')
|
||||
login, pwd = s.split(b":", 1)
|
||||
return login.decode("utf8"), pwd.decode("utf8")
|
||||
|
||||
|
||||
def encode_header(auth):
|
||||
auth_json = json.dumps(auth).encode('ascii')
|
||||
auth_json = json.dumps(auth).encode("ascii")
|
||||
return base64.urlsafe_b64encode(auth_json)
|
||||
|
||||
|
||||
@@ -358,22 +350,22 @@ def _load_legacy_config(config_file):
|
||||
data = []
|
||||
with open(config_file) as f:
|
||||
for line in f.readlines():
|
||||
data.append(line.strip().split(' = ')[1])
|
||||
data.append(line.strip().split(" = ")[1])
|
||||
if len(data) < 2:
|
||||
# Not enough data
|
||||
raise errors.InvalidConfigFile(
|
||||
'Invalid or empty configuration file!'
|
||||
)
|
||||
raise errors.InvalidConfigFile("Invalid or empty configuration file!")
|
||||
|
||||
username, password = decode_auth(data[0])
|
||||
return {'auths': {
|
||||
INDEX_NAME: {
|
||||
'username': username,
|
||||
'password': password,
|
||||
'email': data[1],
|
||||
'serveraddress': INDEX_URL,
|
||||
return {
|
||||
"auths": {
|
||||
INDEX_NAME: {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"email": data[1],
|
||||
"serveraddress": INDEX_URL,
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
except Exception as e:
|
||||
log.debug(e)
|
||||
pass
|
||||
|
||||
@@ -11,26 +11,20 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
MINIMUM_DOCKER_API_VERSION = '1.21'
|
||||
|
||||
MINIMUM_DOCKER_API_VERSION = "1.21"
|
||||
DEFAULT_TIMEOUT_SECONDS = 60
|
||||
STREAM_HEADER_SIZE_BYTES = 8
|
||||
CONTAINER_LIMITS_KEYS = [
|
||||
'memory', 'memswap', 'cpushares', 'cpusetcpus'
|
||||
]
|
||||
CONTAINER_LIMITS_KEYS = ["memory", "memswap", "cpushares", "cpusetcpus"]
|
||||
|
||||
DEFAULT_HTTP_HOST = "127.0.0.1"
|
||||
DEFAULT_UNIX_SOCKET = "http+unix:///var/run/docker.sock"
|
||||
DEFAULT_NPIPE = 'npipe:////./pipe/docker_engine'
|
||||
DEFAULT_NPIPE = "npipe:////./pipe/docker_engine"
|
||||
|
||||
BYTE_UNITS = {
|
||||
'b': 1,
|
||||
'k': 1024,
|
||||
'm': 1024 * 1024,
|
||||
'g': 1024 * 1024 * 1024
|
||||
}
|
||||
BYTE_UNITS = {"b": 1, "k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}
|
||||
|
||||
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
|
||||
WINDOWS_LONGPATH_PREFIX = '\\\\?\\'
|
||||
IS_WINDOWS_PLATFORM = sys.platform == "win32"
|
||||
WINDOWS_LONGPATH_PREFIX = "\\\\?\\"
|
||||
|
||||
DEFAULT_USER_AGENT = "ansible-community.docker"
|
||||
DEFAULT_NUM_POOLS = 25
|
||||
|
||||
@@ -13,7 +13,6 @@ import json
|
||||
import os
|
||||
|
||||
from .. import errors
|
||||
|
||||
from .config import (
|
||||
METAFILE,
|
||||
get_current_context_name,
|
||||
@@ -25,9 +24,11 @@ from .context import Context
|
||||
|
||||
def create_default_context():
|
||||
host = None
|
||||
if os.environ.get('DOCKER_HOST'):
|
||||
host = os.environ.get('DOCKER_HOST')
|
||||
return Context("default", "swarm", host, description="Current DOCKER_HOST based configuration")
|
||||
if os.environ.get("DOCKER_HOST"):
|
||||
host = os.environ.get("DOCKER_HOST")
|
||||
return Context(
|
||||
"default", "swarm", host, description="Current DOCKER_HOST based configuration"
|
||||
)
|
||||
|
||||
|
||||
class ContextAPI(object):
|
||||
@@ -35,6 +36,7 @@ class ContextAPI(object):
|
||||
Contains methods for context management:
|
||||
create, list, remove, get, inspect.
|
||||
"""
|
||||
|
||||
DEFAULT_CONTEXT = None
|
||||
|
||||
@classmethod
|
||||
@@ -47,8 +49,14 @@ class ContextAPI(object):
|
||||
|
||||
@classmethod
|
||||
def create_context(
|
||||
cls, name, orchestrator=None, host=None, tls_cfg=None,
|
||||
default_namespace=None, skip_tls_verify=False):
|
||||
cls,
|
||||
name,
|
||||
orchestrator=None,
|
||||
host=None,
|
||||
tls_cfg=None,
|
||||
default_namespace=None,
|
||||
skip_tls_verify=False,
|
||||
):
|
||||
"""Creates a new context.
|
||||
Returns:
|
||||
(Context): a Context object.
|
||||
@@ -79,8 +87,7 @@ class ContextAPI(object):
|
||||
if not name:
|
||||
raise errors.MissingContextParameter("name")
|
||||
if name == "default":
|
||||
raise errors.ContextException(
|
||||
'"default" is a reserved context name')
|
||||
raise errors.ContextException('"default" is a reserved context name')
|
||||
ctx = Context.load_context(name)
|
||||
if ctx:
|
||||
raise errors.ContextAlreadyExists(name)
|
||||
@@ -89,9 +96,12 @@ class ContextAPI(object):
|
||||
endpoint = orchestrator
|
||||
ctx = Context(name, orchestrator)
|
||||
ctx.set_endpoint(
|
||||
endpoint, host, tls_cfg,
|
||||
endpoint,
|
||||
host,
|
||||
tls_cfg,
|
||||
skip_tls_verify=skip_tls_verify,
|
||||
def_namespace=default_namespace)
|
||||
def_namespace=default_namespace,
|
||||
)
|
||||
ctx.save()
|
||||
return ctx
|
||||
|
||||
@@ -173,8 +183,7 @@ class ContextAPI(object):
|
||||
|
||||
err = write_context_name_to_docker_config(name)
|
||||
if err:
|
||||
raise errors.ContextException(
|
||||
f'Failed to set current context: {err}')
|
||||
raise errors.ContextException(f"Failed to set current context: {err}")
|
||||
|
||||
@classmethod
|
||||
def remove_context(cls, name):
|
||||
@@ -200,8 +209,7 @@ class ContextAPI(object):
|
||||
if not name:
|
||||
raise errors.MissingContextParameter("name")
|
||||
if name == "default":
|
||||
raise errors.ContextException(
|
||||
'context "default" cannot be removed')
|
||||
raise errors.ContextException('context "default" cannot be removed')
|
||||
ctx = Context.load_context(name)
|
||||
if not ctx:
|
||||
raise errors.ContextNotFound(name)
|
||||
|
||||
@@ -17,19 +17,23 @@ from ..constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM
|
||||
from ..utils.config import find_config_file, get_default_config_file
|
||||
from ..utils.utils import parse_host
|
||||
|
||||
|
||||
METAFILE = "meta.json"
|
||||
|
||||
|
||||
def get_current_context_name_with_source():
|
||||
if os.environ.get('DOCKER_HOST'):
|
||||
if os.environ.get("DOCKER_HOST"):
|
||||
return "default", "DOCKER_HOST environment variable set"
|
||||
if os.environ.get('DOCKER_CONTEXT'):
|
||||
return os.environ['DOCKER_CONTEXT'], "DOCKER_CONTEXT environment variable set"
|
||||
if os.environ.get("DOCKER_CONTEXT"):
|
||||
return os.environ["DOCKER_CONTEXT"], "DOCKER_CONTEXT environment variable set"
|
||||
docker_cfg_path = find_config_file()
|
||||
if docker_cfg_path:
|
||||
try:
|
||||
with open(docker_cfg_path) as f:
|
||||
return json.load(f).get("currentContext", "default"), f"configuration file {docker_cfg_path}"
|
||||
return (
|
||||
json.load(f).get("currentContext", "default"),
|
||||
f"configuration file {docker_cfg_path}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return "default", "fallback value"
|
||||
@@ -40,7 +44,7 @@ def get_current_context_name():
|
||||
|
||||
|
||||
def write_context_name_to_docker_config(name=None):
|
||||
if name == 'default':
|
||||
if name == "default":
|
||||
name = None
|
||||
docker_cfg_path = find_config_file()
|
||||
config = {}
|
||||
@@ -67,7 +71,7 @@ def write_context_name_to_docker_config(name=None):
|
||||
|
||||
|
||||
def get_context_id(name):
|
||||
return hashlib.sha256(name.encode('utf-8')).hexdigest()
|
||||
return hashlib.sha256(name.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def get_context_dir():
|
||||
|
||||
@@ -15,7 +15,6 @@ from shutil import copyfile, rmtree
|
||||
|
||||
from ..errors import ContextException
|
||||
from ..tls import TLSConfig
|
||||
|
||||
from .config import (
|
||||
get_context_host,
|
||||
get_meta_dir,
|
||||
@@ -30,8 +29,16 @@ IN_MEMORY = "IN MEMORY"
|
||||
class Context(object):
|
||||
"""A context."""
|
||||
|
||||
def __init__(self, name, orchestrator=None, host=None, endpoints=None,
|
||||
skip_tls_verify=False, tls=False, description=None):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
orchestrator=None,
|
||||
host=None,
|
||||
endpoints=None,
|
||||
skip_tls_verify=False,
|
||||
tls=False,
|
||||
description=None,
|
||||
):
|
||||
if not name:
|
||||
raise Exception("Name not provided")
|
||||
self.name = name
|
||||
@@ -45,9 +52,11 @@ class Context(object):
|
||||
|
||||
if not endpoints:
|
||||
# set default docker endpoint if no endpoint is set
|
||||
default_endpoint = "docker" if (
|
||||
not orchestrator or orchestrator == "swarm"
|
||||
) else orchestrator
|
||||
default_endpoint = (
|
||||
"docker"
|
||||
if (not orchestrator or orchestrator == "swarm")
|
||||
else orchestrator
|
||||
)
|
||||
|
||||
self.endpoints = {
|
||||
default_endpoint: {
|
||||
@@ -69,17 +78,24 @@ class Context(object):
|
||||
if k != "docker":
|
||||
continue
|
||||
|
||||
self.endpoints[k]["Host"] = v.get("Host", get_context_host(
|
||||
host, skip_tls_verify or tls))
|
||||
self.endpoints[k]["SkipTLSVerify"] = bool(v.get(
|
||||
"SkipTLSVerify", skip_tls_verify))
|
||||
self.endpoints[k]["Host"] = v.get(
|
||||
"Host", get_context_host(host, skip_tls_verify or tls)
|
||||
)
|
||||
self.endpoints[k]["SkipTLSVerify"] = bool(
|
||||
v.get("SkipTLSVerify", skip_tls_verify)
|
||||
)
|
||||
|
||||
def set_endpoint(
|
||||
self, name="docker", host=None, tls_cfg=None,
|
||||
skip_tls_verify=False, def_namespace=None):
|
||||
self,
|
||||
name="docker",
|
||||
host=None,
|
||||
tls_cfg=None,
|
||||
skip_tls_verify=False,
|
||||
def_namespace=None,
|
||||
):
|
||||
self.endpoints[name] = {
|
||||
"Host": get_context_host(host, not skip_tls_verify or tls_cfg is not None),
|
||||
"SkipTLSVerify": skip_tls_verify
|
||||
"SkipTLSVerify": skip_tls_verify,
|
||||
}
|
||||
if def_namespace:
|
||||
self.endpoints[name]["DefaultNamespace"] = def_namespace
|
||||
@@ -98,7 +114,8 @@ class Context(object):
|
||||
meta["Name"],
|
||||
orchestrator=meta["Metadata"].get("StackOrchestrator", None),
|
||||
endpoints=meta.get("Endpoints", None),
|
||||
description=meta["Metadata"].get('Description'))
|
||||
description=meta["Metadata"].get("Description"),
|
||||
)
|
||||
instance.context_type = meta["Metadata"].get("Type", None)
|
||||
instance._load_certs()
|
||||
instance.meta_path = get_meta_dir(name)
|
||||
@@ -127,9 +144,11 @@ class Context(object):
|
||||
if k != "docker":
|
||||
continue
|
||||
metadata["Endpoints"][k]["Host"] = v.get(
|
||||
"Host", get_context_host(None, False))
|
||||
"Host", get_context_host(None, False)
|
||||
)
|
||||
metadata["Endpoints"][k]["SkipTLSVerify"] = bool(
|
||||
v.get("SkipTLSVerify", True))
|
||||
v.get("SkipTLSVerify", True)
|
||||
)
|
||||
|
||||
return metadata
|
||||
|
||||
@@ -152,10 +171,14 @@ class Context(object):
|
||||
if all([cert, key]) or ca_cert:
|
||||
verify = None
|
||||
if endpoint == "docker" and not self.endpoints["docker"].get(
|
||||
"SkipTLSVerify", False):
|
||||
"SkipTLSVerify", False
|
||||
):
|
||||
verify = True
|
||||
certs[endpoint] = TLSConfig(
|
||||
client_cert=(cert, key) if cert and key else None, ca_cert=ca_cert, verify=verify)
|
||||
client_cert=(cert, key) if cert and key else None,
|
||||
ca_cert=ca_cert,
|
||||
verify=verify,
|
||||
)
|
||||
self.tls_cfg = certs
|
||||
self.tls_path = tls_dir
|
||||
|
||||
@@ -173,15 +196,20 @@ class Context(object):
|
||||
|
||||
ca_file = tls.ca_cert
|
||||
if ca_file:
|
||||
copyfile(ca_file, os.path.join(
|
||||
tls_dir, endpoint, os.path.basename(ca_file)))
|
||||
copyfile(
|
||||
ca_file, os.path.join(tls_dir, endpoint, os.path.basename(ca_file))
|
||||
)
|
||||
|
||||
if tls.cert:
|
||||
cert_file, key_file = tls.cert
|
||||
copyfile(cert_file, os.path.join(
|
||||
tls_dir, endpoint, os.path.basename(cert_file)))
|
||||
copyfile(key_file, os.path.join(
|
||||
tls_dir, endpoint, os.path.basename(key_file)))
|
||||
copyfile(
|
||||
cert_file,
|
||||
os.path.join(tls_dir, endpoint, os.path.basename(cert_file)),
|
||||
)
|
||||
copyfile(
|
||||
key_file,
|
||||
os.path.join(tls_dir, endpoint, os.path.basename(key_file)),
|
||||
)
|
||||
|
||||
self.meta_path = get_meta_dir(self.name)
|
||||
self.tls_path = get_tls_dir(self.name)
|
||||
@@ -230,11 +258,7 @@ class Context(object):
|
||||
meta = {}
|
||||
if self.orchestrator:
|
||||
meta = {"StackOrchestrator": self.orchestrator}
|
||||
return {
|
||||
"Name": self.name,
|
||||
"Metadata": meta,
|
||||
"Endpoints": self.endpoints
|
||||
}
|
||||
return {"Name": self.name, "Metadata": meta, "Endpoints": self.endpoints}
|
||||
|
||||
@property
|
||||
def TLSConfig(self):
|
||||
@@ -250,16 +274,9 @@ class Context(object):
|
||||
certs = {}
|
||||
for endpoint, tls in self.tls_cfg.items():
|
||||
cert, key = tls.cert
|
||||
certs[endpoint] = list(
|
||||
map(os.path.basename, [tls.ca_cert, cert, key]))
|
||||
return {
|
||||
"TLSMaterial": certs
|
||||
}
|
||||
certs[endpoint] = list(map(os.path.basename, [tls.ca_cert, cert, key]))
|
||||
return {"TLSMaterial": certs}
|
||||
|
||||
@property
|
||||
def Storage(self):
|
||||
return {
|
||||
"Storage": {
|
||||
"MetadataPath": self.meta_path,
|
||||
"TLSPath": self.tls_path
|
||||
}}
|
||||
return {"Storage": {"MetadataPath": self.meta_path, "TLSPath": self.tls_path}}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
PROGRAM_PREFIX = 'docker-credential-'
|
||||
DEFAULT_LINUX_STORE = 'secretservice'
|
||||
DEFAULT_OSX_STORE = 'osxkeychain'
|
||||
DEFAULT_WIN32_STORE = 'wincred'
|
||||
|
||||
PROGRAM_PREFIX = "docker-credential-"
|
||||
DEFAULT_LINUX_STORE = "secretservice"
|
||||
DEFAULT_OSX_STORE = "osxkeychain"
|
||||
DEFAULT_WIN32_STORE = "wincred"
|
||||
|
||||
@@ -23,11 +23,9 @@ class InitializationError(StoreError):
|
||||
|
||||
|
||||
def process_store_error(cpe, program):
|
||||
message = cpe.output.decode('utf-8')
|
||||
if 'credentials not found in native keychain' in message:
|
||||
return CredentialsNotFound(
|
||||
f'No matching credentials in {program}'
|
||||
)
|
||||
message = cpe.output.decode("utf-8")
|
||||
if "credentials not found in native keychain" in message:
|
||||
return CredentialsNotFound(f"No matching credentials in {program}")
|
||||
return StoreError(
|
||||
f'Credentials store {program} exited with "{cpe.output.decode("utf-8").strip()}".'
|
||||
)
|
||||
|
||||
@@ -13,84 +13,81 @@ import errno
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from . import constants
|
||||
from . import errors
|
||||
from .utils import create_environment_dict
|
||||
from .utils import find_executable
|
||||
from . import constants, errors
|
||||
from .utils import create_environment_dict, find_executable
|
||||
|
||||
|
||||
class Store(object):
|
||||
def __init__(self, program, environment=None):
|
||||
""" Create a store object that acts as an interface to
|
||||
perform the basic operations for storing, retrieving
|
||||
and erasing credentials using `program`.
|
||||
"""Create a store object that acts as an interface to
|
||||
perform the basic operations for storing, retrieving
|
||||
and erasing credentials using `program`.
|
||||
"""
|
||||
self.program = constants.PROGRAM_PREFIX + program
|
||||
self.exe = find_executable(self.program)
|
||||
self.environment = environment
|
||||
if self.exe is None:
|
||||
raise errors.InitializationError(
|
||||
f'{self.program} not installed or not available in PATH'
|
||||
f"{self.program} not installed or not available in PATH"
|
||||
)
|
||||
|
||||
def get(self, server):
|
||||
""" Retrieve credentials for `server`. If no credentials are found,
|
||||
a `StoreError` will be raised.
|
||||
"""Retrieve credentials for `server`. If no credentials are found,
|
||||
a `StoreError` will be raised.
|
||||
"""
|
||||
if not isinstance(server, bytes):
|
||||
server = server.encode('utf-8')
|
||||
data = self._execute('get', server)
|
||||
result = json.loads(data.decode('utf-8'))
|
||||
server = server.encode("utf-8")
|
||||
data = self._execute("get", server)
|
||||
result = json.loads(data.decode("utf-8"))
|
||||
|
||||
# docker-credential-pass will return an object for inexistent servers
|
||||
# whereas other helpers will exit with returncode != 0. For
|
||||
# consistency, if no significant data is returned,
|
||||
# raise CredentialsNotFound
|
||||
if result['Username'] == '' and result['Secret'] == '':
|
||||
if result["Username"] == "" and result["Secret"] == "":
|
||||
raise errors.CredentialsNotFound(
|
||||
f'No matching credentials in {self.program}'
|
||||
f"No matching credentials in {self.program}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def store(self, server, username, secret):
|
||||
""" Store credentials for `server`. Raises a `StoreError` if an error
|
||||
occurs.
|
||||
"""Store credentials for `server`. Raises a `StoreError` if an error
|
||||
occurs.
|
||||
"""
|
||||
data_input = json.dumps({
|
||||
'ServerURL': server,
|
||||
'Username': username,
|
||||
'Secret': secret
|
||||
}).encode('utf-8')
|
||||
return self._execute('store', data_input)
|
||||
data_input = json.dumps(
|
||||
{"ServerURL": server, "Username": username, "Secret": secret}
|
||||
).encode("utf-8")
|
||||
return self._execute("store", data_input)
|
||||
|
||||
def erase(self, server):
|
||||
""" Erase credentials for `server`. Raises a `StoreError` if an error
|
||||
occurs.
|
||||
"""Erase credentials for `server`. Raises a `StoreError` if an error
|
||||
occurs.
|
||||
"""
|
||||
if not isinstance(server, bytes):
|
||||
server = server.encode('utf-8')
|
||||
self._execute('erase', server)
|
||||
server = server.encode("utf-8")
|
||||
self._execute("erase", server)
|
||||
|
||||
def list(self):
|
||||
""" List stored credentials. Requires v0.4.0+ of the helper.
|
||||
"""
|
||||
data = self._execute('list', None)
|
||||
return json.loads(data.decode('utf-8'))
|
||||
"""List stored credentials. Requires v0.4.0+ of the helper."""
|
||||
data = self._execute("list", None)
|
||||
return json.loads(data.decode("utf-8"))
|
||||
|
||||
def _execute(self, subcmd, data_input):
|
||||
output = None
|
||||
env = create_environment_dict(self.environment)
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[self.exe, subcmd], input=data_input, env=env,
|
||||
[self.exe, subcmd],
|
||||
input=data_input,
|
||||
env=env,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise errors.process_store_error(e, self.program)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise errors.StoreError(
|
||||
f'{self.program} not installed or not available in PATH'
|
||||
f"{self.program} not installed or not available in PATH"
|
||||
)
|
||||
else:
|
||||
raise errors.StoreError(
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from shutil import which
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ._import_helper import HTTPError as _HTTPError
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ._import_helper import HTTPError as _HTTPError
|
||||
|
||||
|
||||
class DockerException(Exception):
|
||||
"""
|
||||
@@ -29,15 +29,16 @@ def create_api_error_from_http_exception(e):
|
||||
"""
|
||||
response = e.response
|
||||
try:
|
||||
explanation = response.json()['message']
|
||||
explanation = response.json()["message"]
|
||||
except ValueError:
|
||||
explanation = to_native((response.content or '').strip())
|
||||
explanation = to_native((response.content or "").strip())
|
||||
cls = APIError
|
||||
if response.status_code == 404:
|
||||
if explanation and ('No such image' in str(explanation) or
|
||||
'not found: does not exist or no pull access'
|
||||
in str(explanation) or
|
||||
'repository does not exist' in str(explanation)):
|
||||
if explanation and (
|
||||
"No such image" in str(explanation)
|
||||
or "not found: does not exist or no pull access" in str(explanation)
|
||||
or "repository does not exist" in str(explanation)
|
||||
):
|
||||
cls = ImageNotFound
|
||||
else:
|
||||
cls = NotFound
|
||||
@@ -48,6 +49,7 @@ class APIError(_HTTPError, DockerException):
|
||||
"""
|
||||
An HTTP error from the API.
|
||||
"""
|
||||
|
||||
def __init__(self, message, response=None, explanation=None):
|
||||
# requests 1.2 supports response as a keyword argument, but
|
||||
# requests 1.1 does not
|
||||
@@ -59,10 +61,10 @@ class APIError(_HTTPError, DockerException):
|
||||
message = super(APIError, self).__str__()
|
||||
|
||||
if self.is_client_error():
|
||||
message = f'{self.response.status_code} Client Error for {self.response.url}: {self.response.reason}'
|
||||
message = f"{self.response.status_code} Client Error for {self.response.url}: {self.response.reason}"
|
||||
|
||||
elif self.is_server_error():
|
||||
message = f'{self.response.status_code} Server Error for {self.response.url}: {self.response.reason}'
|
||||
message = f"{self.response.status_code} Server Error for {self.response.url}: {self.response.reason}"
|
||||
|
||||
if self.explanation:
|
||||
message = f'{message} ("{self.explanation}")'
|
||||
@@ -121,10 +123,12 @@ class TLSParameterError(DockerException):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg + (". TLS configurations should map the Docker CLI "
|
||||
"client configurations. See "
|
||||
"https://docs.docker.com/engine/articles/https/ "
|
||||
"for API details.")
|
||||
return self.msg + (
|
||||
". TLS configurations should map the Docker CLI "
|
||||
"client configurations. See "
|
||||
"https://docs.docker.com/engine/articles/https/ "
|
||||
"for API details."
|
||||
)
|
||||
|
||||
|
||||
class NullResource(DockerException, ValueError):
|
||||
@@ -135,6 +139,7 @@ class ContainerError(DockerException):
|
||||
"""
|
||||
Represents a container that has exited with a non-zero exit code.
|
||||
"""
|
||||
|
||||
def __init__(self, container, exit_status, command, image, stderr):
|
||||
self.container = container
|
||||
self.exit_status = exit_status
|
||||
@@ -171,8 +176,8 @@ def create_unexpected_kwargs_error(name, kwargs):
|
||||
text.append("got an unexpected keyword argument ")
|
||||
else:
|
||||
text.append("got unexpected keyword arguments ")
|
||||
text.append(', '.join(quoted_kwargs))
|
||||
return TypeError(''.join(text))
|
||||
text.append(", ".join(quoted_kwargs))
|
||||
return TypeError("".join(text))
|
||||
|
||||
|
||||
class MissingContextParameter(DockerException):
|
||||
@@ -196,7 +201,7 @@ class ContextException(DockerException):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return (self.msg)
|
||||
return self.msg
|
||||
|
||||
|
||||
class ContextNotFound(DockerException):
|
||||
@@ -214,4 +219,4 @@ class MissingRequirementException(DockerException):
|
||||
self.import_exception = import_exception
|
||||
|
||||
def __str__(self):
|
||||
return (self.msg)
|
||||
return self.msg
|
||||
|
||||
@@ -31,13 +31,20 @@ class TLSConfig(object):
|
||||
.. _`SSL version`:
|
||||
https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1
|
||||
"""
|
||||
|
||||
cert = None
|
||||
ca_cert = None
|
||||
verify = None
|
||||
ssl_version = None
|
||||
|
||||
def __init__(self, client_cert=None, ca_cert=None, verify=None,
|
||||
ssl_version=None, assert_hostname=None):
|
||||
def __init__(
|
||||
self,
|
||||
client_cert=None,
|
||||
ca_cert=None,
|
||||
verify=None,
|
||||
ssl_version=None,
|
||||
assert_hostname=None,
|
||||
):
|
||||
# Argument compatibility/mapping with
|
||||
# https://docs.docker.com/engine/articles/https/
|
||||
# This diverges from the Docker CLI in that users can specify 'tls'
|
||||
@@ -61,15 +68,15 @@ class TLSConfig(object):
|
||||
tls_cert, tls_key = client_cert
|
||||
except ValueError:
|
||||
raise errors.TLSParameterError(
|
||||
'client_cert must be a tuple of'
|
||||
' (client certificate, key file)'
|
||||
"client_cert must be a tuple of" " (client certificate, key file)"
|
||||
)
|
||||
|
||||
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
|
||||
not os.path.isfile(tls_key)):
|
||||
if not (tls_cert and tls_key) or (
|
||||
not os.path.isfile(tls_cert) or not os.path.isfile(tls_key)
|
||||
):
|
||||
raise errors.TLSParameterError(
|
||||
'Path to a certificate and key files must be provided'
|
||||
' through the client_cert param'
|
||||
"Path to a certificate and key files must be provided"
|
||||
" through the client_cert param"
|
||||
)
|
||||
self.cert = (tls_cert, tls_key)
|
||||
|
||||
@@ -78,7 +85,7 @@ class TLSConfig(object):
|
||||
self.ca_cert = ca_cert
|
||||
if self.verify and self.ca_cert and not os.path.isfile(self.ca_cert):
|
||||
raise errors.TLSParameterError(
|
||||
'Invalid CA certificate provided for `ca_cert`.'
|
||||
"Invalid CA certificate provided for `ca_cert`."
|
||||
)
|
||||
|
||||
def configure_client(self, client):
|
||||
@@ -95,7 +102,10 @@ class TLSConfig(object):
|
||||
if self.cert:
|
||||
client.cert = self.cert
|
||||
|
||||
client.mount('https://', SSLHTTPAdapter(
|
||||
ssl_version=self.ssl_version,
|
||||
assert_hostname=self.assert_hostname,
|
||||
))
|
||||
client.mount(
|
||||
"https://",
|
||||
SSLHTTPAdapter(
|
||||
ssl_version=self.ssl_version,
|
||||
assert_hostname=self.assert_hostname,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ from .._import_helper import HTTPAdapter as _HTTPAdapter
|
||||
class BaseHTTPAdapter(_HTTPAdapter):
|
||||
def close(self):
|
||||
super(BaseHTTPAdapter, self).close()
|
||||
if hasattr(self, 'pools'):
|
||||
if hasattr(self, "pools"):
|
||||
self.pools.clear()
|
||||
|
||||
# Hotfix for requests 2.32.0 and 2.32.1: its commit
|
||||
@@ -23,7 +23,7 @@ class BaseHTTPAdapter(_HTTPAdapter):
|
||||
# changes requests.adapters.HTTPAdapter to no longer call get_connection() from
|
||||
# send(), but instead call _get_connection().
|
||||
def _get_connection(self, request, *args, **kwargs):
|
||||
return self.get_connection(request.url, kwargs.get('proxies'))
|
||||
return self.get_connection(request.url, kwargs.get("proxies"))
|
||||
|
||||
# Fix for requests 2.32.2+:
|
||||
# https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05
|
||||
|
||||
@@ -13,18 +13,16 @@ from queue import Empty
|
||||
|
||||
from .. import constants
|
||||
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
|
||||
|
||||
from .basehttpadapter import BaseHTTPAdapter
|
||||
from .npipesocket import NpipeSocket
|
||||
|
||||
|
||||
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||
|
||||
|
||||
class NpipeHTTPConnection(urllib3_connection.HTTPConnection, object):
|
||||
def __init__(self, npipe_path, timeout=60):
|
||||
super(NpipeHTTPConnection, self).__init__(
|
||||
'localhost', timeout=timeout
|
||||
)
|
||||
super(NpipeHTTPConnection, self).__init__("localhost", timeout=timeout)
|
||||
self.npipe_path = npipe_path
|
||||
self.timeout = timeout
|
||||
|
||||
@@ -38,15 +36,13 @@ class NpipeHTTPConnection(urllib3_connection.HTTPConnection, object):
|
||||
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
def __init__(self, npipe_path, timeout=60, maxsize=10):
|
||||
super(NpipeHTTPConnectionPool, self).__init__(
|
||||
'localhost', timeout=timeout, maxsize=maxsize
|
||||
"localhost", timeout=timeout, maxsize=maxsize
|
||||
)
|
||||
self.npipe_path = npipe_path
|
||||
self.timeout = timeout
|
||||
|
||||
def _new_conn(self):
|
||||
return NpipeHTTPConnection(
|
||||
self.npipe_path, self.timeout
|
||||
)
|
||||
return NpipeHTTPConnection(self.npipe_path, self.timeout)
|
||||
|
||||
# When re-using connections, urllib3 tries to call select() on our
|
||||
# NpipeSocket instance, causing a crash. To circumvent this, we override
|
||||
@@ -63,8 +59,7 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
if self.block:
|
||||
raise urllib3.exceptions.EmptyPoolError(
|
||||
self,
|
||||
"Pool reached maximum size and no more "
|
||||
"connections are allowed."
|
||||
"Pool reached maximum size and no more " "connections are allowed.",
|
||||
)
|
||||
pass # Oh well, we'll create a new connection then
|
||||
|
||||
@@ -73,15 +68,21 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
|
||||
class NpipeHTTPAdapter(BaseHTTPAdapter):
|
||||
|
||||
__attrs__ = HTTPAdapter.__attrs__ + ['npipe_path',
|
||||
'pools',
|
||||
'timeout',
|
||||
'max_pool_size']
|
||||
__attrs__ = HTTPAdapter.__attrs__ + [
|
||||
"npipe_path",
|
||||
"pools",
|
||||
"timeout",
|
||||
"max_pool_size",
|
||||
]
|
||||
|
||||
def __init__(self, base_url, timeout=60,
|
||||
pool_connections=constants.DEFAULT_NUM_POOLS,
|
||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
|
||||
self.npipe_path = base_url.replace('npipe://', '')
|
||||
def __init__(
|
||||
self,
|
||||
base_url,
|
||||
timeout=60,
|
||||
pool_connections=constants.DEFAULT_NUM_POOLS,
|
||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
|
||||
):
|
||||
self.npipe_path = base_url.replace("npipe://", "")
|
||||
self.timeout = timeout
|
||||
self.max_pool_size = max_pool_size
|
||||
self.pools = RecentlyUsedContainer(
|
||||
@@ -96,8 +97,7 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
|
||||
return pool
|
||||
|
||||
pool = NpipeHTTPConnectionPool(
|
||||
self.npipe_path, self.timeout,
|
||||
maxsize=self.max_pool_size
|
||||
self.npipe_path, self.timeout, maxsize=self.max_pool_size
|
||||
)
|
||||
self.pools[url] = pool
|
||||
|
||||
|
||||
@@ -14,18 +14,19 @@ import io
|
||||
import time
|
||||
import traceback
|
||||
|
||||
|
||||
PYWIN32_IMPORT_ERROR = None
|
||||
try:
|
||||
import pywintypes
|
||||
import win32api
|
||||
import win32event
|
||||
import win32file
|
||||
import win32pipe
|
||||
import pywintypes
|
||||
import win32event
|
||||
import win32api
|
||||
except ImportError:
|
||||
PYWIN32_IMPORT_ERROR = traceback.format_exc()
|
||||
|
||||
|
||||
cERROR_PIPE_BUSY = 0xe7
|
||||
cERROR_PIPE_BUSY = 0xE7
|
||||
cSECURITY_SQOS_PRESENT = 0x100000
|
||||
cSECURITY_ANONYMOUS = 0
|
||||
|
||||
@@ -36,18 +37,17 @@ def check_closed(f):
|
||||
@functools.wraps(f)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
if self._closed:
|
||||
raise RuntimeError(
|
||||
'Can not reuse socket after connection was closed.'
|
||||
)
|
||||
raise RuntimeError("Can not reuse socket after connection was closed.")
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class NpipeSocket(object):
|
||||
""" Partial implementation of the socket API over windows named pipes.
|
||||
This implementation is only designed to be used as a client socket,
|
||||
and server-specific methods (bind, listen, accept...) are not
|
||||
implemented.
|
||||
"""Partial implementation of the socket API over windows named pipes.
|
||||
This implementation is only designed to be used as a client socket,
|
||||
and server-specific methods (bind, listen, accept...) are not
|
||||
implemented.
|
||||
"""
|
||||
|
||||
def __init__(self, handle=None):
|
||||
@@ -74,10 +74,12 @@ class NpipeSocket(object):
|
||||
0,
|
||||
None,
|
||||
win32file.OPEN_EXISTING,
|
||||
(cSECURITY_ANONYMOUS
|
||||
(
|
||||
cSECURITY_ANONYMOUS
|
||||
| cSECURITY_SQOS_PRESENT
|
||||
| win32file.FILE_FLAG_OVERLAPPED),
|
||||
0
|
||||
| win32file.FILE_FLAG_OVERLAPPED
|
||||
),
|
||||
0,
|
||||
)
|
||||
except win32pipe.error as e:
|
||||
# See Remarks:
|
||||
@@ -87,7 +89,7 @@ class NpipeSocket(object):
|
||||
# before we got to it. Wait for availability and attempt to
|
||||
# connect again.
|
||||
retry_count = retry_count + 1
|
||||
if (retry_count < MAXIMUM_RETRY_COUNT):
|
||||
if retry_count < MAXIMUM_RETRY_COUNT:
|
||||
time.sleep(1)
|
||||
return self.connect(address, retry_count)
|
||||
raise e
|
||||
@@ -126,7 +128,7 @@ class NpipeSocket(object):
|
||||
raise NotImplementedError()
|
||||
|
||||
def makefile(self, mode=None, bufsize=None):
|
||||
if mode.strip('b') != 'r':
|
||||
if mode.strip("b") != "r":
|
||||
raise NotImplementedError()
|
||||
rawio = NpipeFileIOBase(self)
|
||||
if bufsize is None or bufsize <= 0:
|
||||
@@ -158,9 +160,7 @@ class NpipeSocket(object):
|
||||
overlapped = pywintypes.OVERLAPPED()
|
||||
overlapped.hEvent = event
|
||||
err, data = win32file.ReadFile(
|
||||
self._handle,
|
||||
readbuf[:nbytes] if nbytes else readbuf,
|
||||
overlapped
|
||||
self._handle, readbuf[:nbytes] if nbytes else readbuf, overlapped
|
||||
)
|
||||
wait_result = win32event.WaitForSingleObject(event, self._timeout)
|
||||
if wait_result == win32event.WAIT_TIMEOUT:
|
||||
@@ -204,7 +204,7 @@ class NpipeSocket(object):
|
||||
# Blocking mode
|
||||
self._timeout = win32event.INFINITE
|
||||
elif not isinstance(value, (float, int)) or value < 0:
|
||||
raise ValueError('Timeout value out of range')
|
||||
raise ValueError("Timeout value out of range")
|
||||
else:
|
||||
# Timeout mode - Value converted to milliseconds
|
||||
self._timeout = int(value * 1000)
|
||||
|
||||
@@ -18,10 +18,10 @@ import traceback
|
||||
from queue import Empty
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .basehttpadapter import BaseHTTPAdapter
|
||||
from .. import constants
|
||||
|
||||
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
|
||||
from .basehttpadapter import BaseHTTPAdapter
|
||||
|
||||
|
||||
PARAMIKO_IMPORT_ERROR = None
|
||||
try:
|
||||
@@ -35,51 +35,54 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||
|
||||
class SSHSocket(socket.socket):
|
||||
def __init__(self, host):
|
||||
super(SSHSocket, self).__init__(
|
||||
socket.AF_INET, socket.SOCK_STREAM)
|
||||
super(SSHSocket, self).__init__(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.host = host
|
||||
self.port = None
|
||||
self.user = None
|
||||
if ':' in self.host:
|
||||
self.host, self.port = self.host.split(':')
|
||||
if '@' in self.host:
|
||||
self.user, self.host = self.host.split('@')
|
||||
if ":" in self.host:
|
||||
self.host, self.port = self.host.split(":")
|
||||
if "@" in self.host:
|
||||
self.user, self.host = self.host.split("@")
|
||||
|
||||
self.proc = None
|
||||
|
||||
def connect(self, **kwargs):
|
||||
args = ['ssh']
|
||||
args = ["ssh"]
|
||||
if self.user:
|
||||
args = args + ['-l', self.user]
|
||||
args = args + ["-l", self.user]
|
||||
|
||||
if self.port:
|
||||
args = args + ['-p', self.port]
|
||||
args = args + ["-p", self.port]
|
||||
|
||||
args = args + ['--', self.host, 'docker system dial-stdio']
|
||||
args = args + ["--", self.host, "docker system dial-stdio"]
|
||||
|
||||
preexec_func = None
|
||||
if not constants.IS_WINDOWS_PLATFORM:
|
||||
|
||||
def f():
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
preexec_func = f
|
||||
|
||||
env = dict(os.environ)
|
||||
|
||||
# drop LD_LIBRARY_PATH and SSL_CERT_FILE
|
||||
env.pop('LD_LIBRARY_PATH', None)
|
||||
env.pop('SSL_CERT_FILE', None)
|
||||
env.pop("LD_LIBRARY_PATH", None)
|
||||
env.pop("SSL_CERT_FILE", None)
|
||||
|
||||
self.proc = subprocess.Popen(
|
||||
args,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
preexec_fn=preexec_func)
|
||||
preexec_fn=preexec_func,
|
||||
)
|
||||
|
||||
def _write(self, data):
|
||||
if not self.proc or self.proc.stdin.closed:
|
||||
raise Exception('SSH subprocess not initiated.'
|
||||
'connect() must be called first.')
|
||||
raise Exception(
|
||||
"SSH subprocess not initiated." "connect() must be called first."
|
||||
)
|
||||
written = self.proc.stdin.write(data)
|
||||
self.proc.stdin.flush()
|
||||
return written
|
||||
@@ -92,8 +95,9 @@ class SSHSocket(socket.socket):
|
||||
|
||||
def recv(self, n):
|
||||
if not self.proc:
|
||||
raise Exception('SSH subprocess not initiated.'
|
||||
'connect() must be called first.')
|
||||
raise Exception(
|
||||
"SSH subprocess not initiated." "connect() must be called first."
|
||||
)
|
||||
return self.proc.stdout.read(n)
|
||||
|
||||
def makefile(self, mode):
|
||||
@@ -106,16 +110,14 @@ class SSHSocket(socket.socket):
|
||||
def close(self):
|
||||
if not self.proc or self.proc.stdin.closed:
|
||||
return
|
||||
self.proc.stdin.write(b'\n\n')
|
||||
self.proc.stdin.write(b"\n\n")
|
||||
self.proc.stdin.flush()
|
||||
self.proc.terminate()
|
||||
|
||||
|
||||
class SSHConnection(urllib3_connection.HTTPConnection, object):
|
||||
def __init__(self, ssh_transport=None, timeout=60, host=None):
|
||||
super(SSHConnection, self).__init__(
|
||||
'localhost', timeout=timeout
|
||||
)
|
||||
super(SSHConnection, self).__init__("localhost", timeout=timeout)
|
||||
self.ssh_transport = ssh_transport
|
||||
self.timeout = timeout
|
||||
self.ssh_host = host
|
||||
@@ -124,7 +126,7 @@ class SSHConnection(urllib3_connection.HTTPConnection, object):
|
||||
if self.ssh_transport:
|
||||
sock = self.ssh_transport.open_session()
|
||||
sock.settimeout(self.timeout)
|
||||
sock.exec_command('docker system dial-stdio')
|
||||
sock.exec_command("docker system dial-stdio")
|
||||
else:
|
||||
sock = SSHSocket(self.ssh_host)
|
||||
sock.settimeout(self.timeout)
|
||||
@@ -134,11 +136,11 @@ class SSHConnection(urllib3_connection.HTTPConnection, object):
|
||||
|
||||
|
||||
class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
scheme = 'ssh'
|
||||
scheme = "ssh"
|
||||
|
||||
def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
|
||||
super(SSHConnectionPool, self).__init__(
|
||||
'localhost', timeout=timeout, maxsize=maxsize
|
||||
"localhost", timeout=timeout, maxsize=maxsize
|
||||
)
|
||||
self.ssh_transport = None
|
||||
self.timeout = timeout
|
||||
@@ -164,8 +166,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
if self.block:
|
||||
raise urllib3.exceptions.EmptyPoolError(
|
||||
self,
|
||||
"Pool reached maximum size and no more "
|
||||
"connections are allowed."
|
||||
"Pool reached maximum size and no more " "connections are allowed.",
|
||||
)
|
||||
pass # Oh well, we'll create a new connection then
|
||||
|
||||
@@ -175,21 +176,29 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||
|
||||
__attrs__ = HTTPAdapter.__attrs__ + [
|
||||
'pools', 'timeout', 'ssh_client', 'ssh_params', 'max_pool_size'
|
||||
"pools",
|
||||
"timeout",
|
||||
"ssh_client",
|
||||
"ssh_params",
|
||||
"max_pool_size",
|
||||
]
|
||||
|
||||
def __init__(self, base_url, timeout=60,
|
||||
pool_connections=constants.DEFAULT_NUM_POOLS,
|
||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
|
||||
shell_out=False):
|
||||
def __init__(
|
||||
self,
|
||||
base_url,
|
||||
timeout=60,
|
||||
pool_connections=constants.DEFAULT_NUM_POOLS,
|
||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
|
||||
shell_out=False,
|
||||
):
|
||||
self.ssh_client = None
|
||||
if not shell_out:
|
||||
self._create_paramiko_client(base_url)
|
||||
self._connect()
|
||||
|
||||
self.ssh_host = base_url
|
||||
if base_url.startswith('ssh://'):
|
||||
self.ssh_host = base_url[len('ssh://'):]
|
||||
if base_url.startswith("ssh://"):
|
||||
self.ssh_host = base_url[len("ssh://") :]
|
||||
|
||||
self.timeout = timeout
|
||||
self.max_pool_size = max_pool_size
|
||||
@@ -213,18 +222,18 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||
with open(ssh_config_file) as f:
|
||||
conf.parse(f)
|
||||
host_config = conf.lookup(base_url.hostname)
|
||||
if 'proxycommand' in host_config:
|
||||
if "proxycommand" in host_config:
|
||||
self.ssh_params["sock"] = paramiko.ProxyCommand(
|
||||
host_config['proxycommand']
|
||||
host_config["proxycommand"]
|
||||
)
|
||||
if 'hostname' in host_config:
|
||||
self.ssh_params['hostname'] = host_config['hostname']
|
||||
if base_url.port is None and 'port' in host_config:
|
||||
self.ssh_params['port'] = host_config['port']
|
||||
if base_url.username is None and 'user' in host_config:
|
||||
self.ssh_params['username'] = host_config['user']
|
||||
if 'identityfile' in host_config:
|
||||
self.ssh_params['key_filename'] = host_config['identityfile']
|
||||
if "hostname" in host_config:
|
||||
self.ssh_params["hostname"] = host_config["hostname"]
|
||||
if base_url.port is None and "port" in host_config:
|
||||
self.ssh_params["port"] = host_config["port"]
|
||||
if base_url.username is None and "user" in host_config:
|
||||
self.ssh_params["username"] = host_config["user"]
|
||||
if "identityfile" in host_config:
|
||||
self.ssh_params["key_filename"] = host_config["identityfile"]
|
||||
|
||||
self.ssh_client.load_system_host_keys()
|
||||
self.ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
||||
@@ -239,7 +248,7 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||
ssh_client=self.ssh_client,
|
||||
timeout=self.timeout,
|
||||
maxsize=self.max_pool_size,
|
||||
host=self.ssh_host
|
||||
host=self.ssh_host,
|
||||
)
|
||||
with self.pools.lock:
|
||||
pool = self.pools.get(url)
|
||||
@@ -254,7 +263,7 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
|
||||
ssh_client=self.ssh_client,
|
||||
timeout=self.timeout,
|
||||
maxsize=self.max_pool_size,
|
||||
host=self.ssh_host
|
||||
host=self.ssh_host,
|
||||
)
|
||||
self.pools[url] = pool
|
||||
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
""" Resolves OpenSSL issues in some servers:
|
||||
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
|
||||
https://github.com/kennethreitz/requests/pull/799
|
||||
"""
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
from .._import_helper import HTTPAdapter, urllib3
|
||||
from .basehttpadapter import BaseHTTPAdapter
|
||||
@@ -24,9 +27,9 @@ PoolManager = urllib3.poolmanager.PoolManager
|
||||
|
||||
|
||||
class SSLHTTPAdapter(BaseHTTPAdapter):
|
||||
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''
|
||||
"""An HTTPS Transport Adapter that uses an arbitrary SSL version."""
|
||||
|
||||
__attrs__ = HTTPAdapter.__attrs__ + ['assert_hostname', 'ssl_version']
|
||||
__attrs__ = HTTPAdapter.__attrs__ + ["assert_hostname", "ssl_version"]
|
||||
|
||||
def __init__(self, ssl_version=None, assert_hostname=None, **kwargs):
|
||||
self.ssl_version = ssl_version
|
||||
@@ -35,14 +38,14 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
|
||||
|
||||
def init_poolmanager(self, connections, maxsize, block=False):
|
||||
kwargs = {
|
||||
'num_pools': connections,
|
||||
'maxsize': maxsize,
|
||||
'block': block,
|
||||
"num_pools": connections,
|
||||
"maxsize": maxsize,
|
||||
"block": block,
|
||||
}
|
||||
if self.assert_hostname is not None:
|
||||
kwargs['assert_hostname'] = self.assert_hostname
|
||||
kwargs["assert_hostname"] = self.assert_hostname
|
||||
if self.ssl_version and self.can_override_ssl_version():
|
||||
kwargs['ssl_version'] = self.ssl_version
|
||||
kwargs["ssl_version"] = self.ssl_version
|
||||
|
||||
self.poolmanager = PoolManager(**kwargs)
|
||||
|
||||
@@ -55,14 +58,17 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
|
||||
But we still need to take care of when there is a proxy poolmanager
|
||||
"""
|
||||
conn = super(SSLHTTPAdapter, self).get_connection(*args, **kwargs)
|
||||
if self.assert_hostname is not None and conn.assert_hostname != self.assert_hostname:
|
||||
if (
|
||||
self.assert_hostname is not None
|
||||
and conn.assert_hostname != self.assert_hostname
|
||||
):
|
||||
conn.assert_hostname = self.assert_hostname
|
||||
return conn
|
||||
|
||||
def can_override_ssl_version(self):
|
||||
urllib_ver = urllib3.__version__.split('-')[0]
|
||||
urllib_ver = urllib3.__version__.split("-")[0]
|
||||
if urllib_ver is None:
|
||||
return False
|
||||
if urllib_ver == 'dev':
|
||||
if urllib_ver == "dev":
|
||||
return True
|
||||
return LooseVersion(urllib_ver) > LooseVersion('1.5')
|
||||
return LooseVersion(urllib_ver) > LooseVersion("1.5")
|
||||
|
||||
@@ -11,10 +11,9 @@ from __future__ import annotations
|
||||
|
||||
import socket
|
||||
|
||||
from .basehttpadapter import BaseHTTPAdapter
|
||||
from .. import constants
|
||||
|
||||
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
|
||||
from .basehttpadapter import BaseHTTPAdapter
|
||||
|
||||
|
||||
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||
@@ -23,9 +22,7 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
|
||||
class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
|
||||
|
||||
def __init__(self, base_url, unix_socket, timeout=60):
|
||||
super(UnixHTTPConnection, self).__init__(
|
||||
'localhost', timeout=timeout
|
||||
)
|
||||
super(UnixHTTPConnection, self).__init__("localhost", timeout=timeout)
|
||||
self.base_url = base_url
|
||||
self.unix_socket = unix_socket
|
||||
self.timeout = timeout
|
||||
@@ -39,7 +36,7 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
|
||||
|
||||
def putheader(self, header, *values):
|
||||
super(UnixHTTPConnection, self).putheader(header, *values)
|
||||
if header == 'Connection' and 'Upgrade' in values:
|
||||
if header == "Connection" and "Upgrade" in values:
|
||||
self.disable_buffering = True
|
||||
|
||||
def response_class(self, sock, *args, **kwargs):
|
||||
@@ -52,31 +49,35 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
|
||||
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
def __init__(self, base_url, socket_path, timeout=60, maxsize=10):
|
||||
super(UnixHTTPConnectionPool, self).__init__(
|
||||
'localhost', timeout=timeout, maxsize=maxsize
|
||||
"localhost", timeout=timeout, maxsize=maxsize
|
||||
)
|
||||
self.base_url = base_url
|
||||
self.socket_path = socket_path
|
||||
self.timeout = timeout
|
||||
|
||||
def _new_conn(self):
|
||||
return UnixHTTPConnection(
|
||||
self.base_url, self.socket_path, self.timeout
|
||||
)
|
||||
return UnixHTTPConnection(self.base_url, self.socket_path, self.timeout)
|
||||
|
||||
|
||||
class UnixHTTPAdapter(BaseHTTPAdapter):
|
||||
|
||||
__attrs__ = HTTPAdapter.__attrs__ + ['pools',
|
||||
'socket_path',
|
||||
'timeout',
|
||||
'max_pool_size']
|
||||
__attrs__ = HTTPAdapter.__attrs__ + [
|
||||
"pools",
|
||||
"socket_path",
|
||||
"timeout",
|
||||
"max_pool_size",
|
||||
]
|
||||
|
||||
def __init__(self, socket_url, timeout=60,
|
||||
pool_connections=constants.DEFAULT_NUM_POOLS,
|
||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
|
||||
socket_path = socket_url.replace('http+unix://', '')
|
||||
if not socket_path.startswith('/'):
|
||||
socket_path = '/' + socket_path
|
||||
def __init__(
|
||||
self,
|
||||
socket_url,
|
||||
timeout=60,
|
||||
pool_connections=constants.DEFAULT_NUM_POOLS,
|
||||
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
|
||||
):
|
||||
socket_path = socket_url.replace("http+unix://", "")
|
||||
if not socket_path.startswith("/"):
|
||||
socket_path = "/" + socket_path
|
||||
self.socket_path = socket_path
|
||||
self.timeout = timeout
|
||||
self.max_pool_size = max_pool_size
|
||||
@@ -92,8 +93,7 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
|
||||
return pool
|
||||
|
||||
pool = UnixHTTPConnectionPool(
|
||||
url, self.socket_path, self.timeout,
|
||||
maxsize=self.max_pool_size
|
||||
url, self.socket_path, self.timeout, maxsize=self.max_pool_size
|
||||
)
|
||||
self.pools[url] = pool
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ from __future__ import annotations
|
||||
import socket
|
||||
|
||||
from .._import_helper import urllib3
|
||||
|
||||
from ..errors import DockerException
|
||||
|
||||
|
||||
@@ -56,26 +55,27 @@ class CancellableStream(object):
|
||||
|
||||
sock_fp = self._response.raw._fp.fp
|
||||
|
||||
if hasattr(sock_fp, 'raw'):
|
||||
if hasattr(sock_fp, "raw"):
|
||||
sock_raw = sock_fp.raw
|
||||
|
||||
if hasattr(sock_raw, 'sock'):
|
||||
if hasattr(sock_raw, "sock"):
|
||||
sock = sock_raw.sock
|
||||
|
||||
elif hasattr(sock_raw, '_sock'):
|
||||
elif hasattr(sock_raw, "_sock"):
|
||||
sock = sock_raw._sock
|
||||
|
||||
elif hasattr(sock_fp, 'channel'):
|
||||
elif hasattr(sock_fp, "channel"):
|
||||
# We are working with a paramiko (SSH) channel, which does not
|
||||
# support cancelable streams with the current implementation
|
||||
raise DockerException(
|
||||
'Cancellable streams not supported for the SSH protocol'
|
||||
"Cancellable streams not supported for the SSH protocol"
|
||||
)
|
||||
else:
|
||||
sock = sock_fp._sock
|
||||
|
||||
if hasattr(urllib3.contrib, 'pyopenssl') and isinstance(
|
||||
sock, urllib3.contrib.pyopenssl.WrappedSocket):
|
||||
if hasattr(urllib3.contrib, "pyopenssl") and isinstance(
|
||||
sock, urllib3.contrib.pyopenssl.WrappedSocket
|
||||
):
|
||||
sock = sock.socket
|
||||
|
||||
sock.shutdown(socket.SHUT_RDWR)
|
||||
|
||||
@@ -16,11 +16,11 @@ import re
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
from . import fnmatch
|
||||
from ..constants import IS_WINDOWS_PLATFORM, WINDOWS_LONGPATH_PREFIX
|
||||
from . import fnmatch
|
||||
|
||||
|
||||
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
|
||||
_SEP = re.compile("/|\\\\") if IS_WINDOWS_PLATFORM else re.compile("/")
|
||||
|
||||
|
||||
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
|
||||
@@ -29,16 +29,19 @@ def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
|
||||
dockerfile = dockerfile or (None, None)
|
||||
extra_files = []
|
||||
if dockerfile[1] is not None:
|
||||
dockerignore_contents = '\n'.join(
|
||||
(exclude or ['.dockerignore']) + [dockerfile[0]]
|
||||
dockerignore_contents = "\n".join(
|
||||
(exclude or [".dockerignore"]) + [dockerfile[0]]
|
||||
)
|
||||
extra_files = [
|
||||
('.dockerignore', dockerignore_contents),
|
||||
(".dockerignore", dockerignore_contents),
|
||||
dockerfile,
|
||||
]
|
||||
return create_archive(
|
||||
files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile[0])),
|
||||
root=root, fileobj=fileobj, gzip=gzip, extra_files=extra_files
|
||||
root=root,
|
||||
fileobj=fileobj,
|
||||
gzip=gzip,
|
||||
extra_files=extra_files,
|
||||
)
|
||||
|
||||
|
||||
@@ -52,9 +55,9 @@ def exclude_paths(root, patterns, dockerfile=None):
|
||||
"""
|
||||
|
||||
if dockerfile is None:
|
||||
dockerfile = 'Dockerfile'
|
||||
dockerfile = "Dockerfile"
|
||||
|
||||
patterns.append('!' + dockerfile)
|
||||
patterns.append("!" + dockerfile)
|
||||
pm = PatternMatcher(patterns)
|
||||
return set(pm.walk(root))
|
||||
|
||||
@@ -64,19 +67,16 @@ def build_file_list(root):
|
||||
for dirname, dirnames, fnames in os.walk(root):
|
||||
for filename in fnames + dirnames:
|
||||
longpath = os.path.join(dirname, filename)
|
||||
files.append(
|
||||
longpath.replace(root, '', 1).lstrip('/')
|
||||
)
|
||||
files.append(longpath.replace(root, "", 1).lstrip("/"))
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def create_archive(root, files=None, fileobj=None, gzip=False,
|
||||
extra_files=None):
|
||||
def create_archive(root, files=None, fileobj=None, gzip=False, extra_files=None):
|
||||
extra_files = extra_files or []
|
||||
if not fileobj:
|
||||
fileobj = tempfile.NamedTemporaryFile()
|
||||
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
|
||||
t = tarfile.open(mode="w:gz" if gzip else "w", fileobj=fileobj)
|
||||
if files is None:
|
||||
files = build_file_list(root)
|
||||
extra_names = set(e[0] for e in extra_files)
|
||||
@@ -103,19 +103,17 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
|
||||
|
||||
if i.isfile():
|
||||
try:
|
||||
with open(full_path, 'rb') as f:
|
||||
with open(full_path, "rb") as f:
|
||||
t.addfile(i, f)
|
||||
except IOError:
|
||||
raise IOError(
|
||||
f'Can not read file in context: {full_path}'
|
||||
)
|
||||
raise IOError(f"Can not read file in context: {full_path}")
|
||||
else:
|
||||
# Directories, FIFOs, symlinks... do not need to be read.
|
||||
t.addfile(i, None)
|
||||
|
||||
for name, contents in extra_files:
|
||||
info = tarfile.TarInfo(name)
|
||||
contents_encoded = contents.encode('utf-8')
|
||||
contents_encoded = contents.encode("utf-8")
|
||||
info.size = len(contents_encoded)
|
||||
t.addfile(info, io.BytesIO(contents_encoded))
|
||||
|
||||
@@ -126,15 +124,15 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
|
||||
|
||||
def mkbuildcontext(dockerfile):
|
||||
f = tempfile.NamedTemporaryFile()
|
||||
t = tarfile.open(mode='w', fileobj=f)
|
||||
t = tarfile.open(mode="w", fileobj=f)
|
||||
if isinstance(dockerfile, io.StringIO):
|
||||
raise TypeError('Please use io.BytesIO to create in-memory Dockerfiles')
|
||||
raise TypeError("Please use io.BytesIO to create in-memory Dockerfiles")
|
||||
elif isinstance(dockerfile, io.BytesIO):
|
||||
dfinfo = tarfile.TarInfo('Dockerfile')
|
||||
dfinfo = tarfile.TarInfo("Dockerfile")
|
||||
dfinfo.size = len(dockerfile.getvalue())
|
||||
dockerfile.seek(0)
|
||||
else:
|
||||
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile')
|
||||
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname="Dockerfile")
|
||||
t.addfile(dfinfo, dockerfile)
|
||||
t.close()
|
||||
f.seek(0)
|
||||
@@ -142,12 +140,12 @@ def mkbuildcontext(dockerfile):
|
||||
|
||||
|
||||
def split_path(p):
|
||||
return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
|
||||
return [pt for pt in re.split(_SEP, p) if pt and pt != "."]
|
||||
|
||||
|
||||
def normalize_slashes(p):
|
||||
if IS_WINDOWS_PLATFORM:
|
||||
return '/'.join(split_path(p))
|
||||
return "/".join(split_path(p))
|
||||
return p
|
||||
|
||||
|
||||
@@ -160,10 +158,8 @@ def walk(root, patterns, default=True):
|
||||
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
|
||||
class PatternMatcher(object):
|
||||
def __init__(self, patterns):
|
||||
self.patterns = list(filter(
|
||||
lambda p: p.dirs, [Pattern(p) for p in patterns]
|
||||
))
|
||||
self.patterns.append(Pattern('!.dockerignore'))
|
||||
self.patterns = list(filter(lambda p: p.dirs, [Pattern(p) for p in patterns]))
|
||||
self.patterns.append(Pattern("!.dockerignore"))
|
||||
|
||||
def matches(self, filepath):
|
||||
matched = False
|
||||
@@ -173,10 +169,10 @@ class PatternMatcher(object):
|
||||
for pattern in self.patterns:
|
||||
negative = pattern.exclusion
|
||||
match = pattern.match(filepath)
|
||||
if not match and parent_path != '':
|
||||
if not match and parent_path != "":
|
||||
if len(pattern.dirs) <= len(parent_path_dirs):
|
||||
match = pattern.match(
|
||||
os.path.sep.join(parent_path_dirs[:len(pattern.dirs)])
|
||||
os.path.sep.join(parent_path_dirs[: len(pattern.dirs)])
|
||||
)
|
||||
|
||||
if match:
|
||||
@@ -187,10 +183,8 @@ class PatternMatcher(object):
|
||||
def walk(self, root):
|
||||
def rec_walk(current_dir):
|
||||
for f in os.listdir(current_dir):
|
||||
fpath = os.path.join(
|
||||
os.path.relpath(current_dir, root), f
|
||||
)
|
||||
if fpath.startswith('.' + os.path.sep):
|
||||
fpath = os.path.join(os.path.relpath(current_dir, root), f)
|
||||
if fpath.startswith("." + os.path.sep):
|
||||
fpath = fpath[2:]
|
||||
match = self.matches(fpath)
|
||||
if not match:
|
||||
@@ -210,8 +204,7 @@ class PatternMatcher(object):
|
||||
for pat in self.patterns:
|
||||
if not pat.exclusion:
|
||||
continue
|
||||
if pat.cleaned_pattern.startswith(
|
||||
normalize_slashes(fpath)):
|
||||
if pat.cleaned_pattern.startswith(normalize_slashes(fpath)):
|
||||
skip = False
|
||||
break
|
||||
if skip:
|
||||
@@ -224,12 +217,12 @@ class PatternMatcher(object):
|
||||
class Pattern(object):
|
||||
def __init__(self, pattern_str):
|
||||
self.exclusion = False
|
||||
if pattern_str.startswith('!'):
|
||||
if pattern_str.startswith("!"):
|
||||
self.exclusion = True
|
||||
pattern_str = pattern_str[1:]
|
||||
|
||||
self.dirs = self.normalize(pattern_str)
|
||||
self.cleaned_pattern = '/'.join(self.dirs)
|
||||
self.cleaned_pattern = "/".join(self.dirs)
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, p):
|
||||
@@ -249,7 +242,7 @@ class Pattern(object):
|
||||
i = 0
|
||||
split = split_path(p)
|
||||
while i < len(split):
|
||||
if split[i] == '..':
|
||||
if split[i] == "..":
|
||||
del split[i]
|
||||
if i > 0:
|
||||
del split[i - 1]
|
||||
@@ -269,17 +262,14 @@ def process_dockerfile(dockerfile, path):
|
||||
abs_dockerfile = dockerfile
|
||||
if not os.path.isabs(dockerfile):
|
||||
abs_dockerfile = os.path.join(path, dockerfile)
|
||||
if IS_WINDOWS_PLATFORM and path.startswith(
|
||||
WINDOWS_LONGPATH_PREFIX):
|
||||
abs_dockerfile = f'{WINDOWS_LONGPATH_PREFIX}{os.path.normpath(abs_dockerfile[len(WINDOWS_LONGPATH_PREFIX):])}'
|
||||
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
|
||||
os.path.relpath(abs_dockerfile, path).startswith('..')):
|
||||
if IS_WINDOWS_PLATFORM and path.startswith(WINDOWS_LONGPATH_PREFIX):
|
||||
abs_dockerfile = f"{WINDOWS_LONGPATH_PREFIX}{os.path.normpath(abs_dockerfile[len(WINDOWS_LONGPATH_PREFIX):])}"
|
||||
if os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[
|
||||
0
|
||||
] or os.path.relpath(abs_dockerfile, path).startswith(".."):
|
||||
# Dockerfile not in context - read data to insert into tar later
|
||||
with open(abs_dockerfile) as df:
|
||||
return (
|
||||
f'.dockerfile.{random.getrandbits(160):x}',
|
||||
df.read()
|
||||
)
|
||||
return (f".dockerfile.{random.getrandbits(160):x}", df.read())
|
||||
|
||||
# Dockerfile is inside the context - return path relative to context root
|
||||
if dockerfile == abs_dockerfile:
|
||||
|
||||
@@ -15,8 +15,9 @@ import os
|
||||
|
||||
from ..constants import IS_WINDOWS_PLATFORM
|
||||
|
||||
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
|
||||
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
|
||||
|
||||
DOCKER_CONFIG_FILENAME = os.path.join(".docker", "config.json")
|
||||
LEGACY_DOCKER_CONFIG_FILENAME = ".dockercfg"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,12 +28,17 @@ def get_default_config_file():
|
||||
|
||||
def find_config_file(config_path=None):
|
||||
homedir = home_dir()
|
||||
paths = list(filter(None, [
|
||||
config_path, # 1
|
||||
config_path_from_environment(), # 2
|
||||
os.path.join(homedir, DOCKER_CONFIG_FILENAME), # 3
|
||||
os.path.join(homedir, LEGACY_DOCKER_CONFIG_FILENAME), # 4
|
||||
]))
|
||||
paths = list(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
config_path, # 1
|
||||
config_path_from_environment(), # 2
|
||||
os.path.join(homedir, DOCKER_CONFIG_FILENAME), # 3
|
||||
os.path.join(homedir, LEGACY_DOCKER_CONFIG_FILENAME), # 4
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
log.debug("Trying paths: %s", repr(paths))
|
||||
|
||||
@@ -47,7 +53,7 @@ def find_config_file(config_path=None):
|
||||
|
||||
|
||||
def config_path_from_environment():
|
||||
config_dir = os.environ.get('DOCKER_CONFIG')
|
||||
config_dir = os.environ.get("DOCKER_CONFIG")
|
||||
if not config_dir:
|
||||
return None
|
||||
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
|
||||
@@ -59,9 +65,9 @@ def home_dir():
|
||||
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
|
||||
"""
|
||||
if IS_WINDOWS_PLATFORM:
|
||||
return os.environ.get('USERPROFILE', '')
|
||||
return os.environ.get("USERPROFILE", "")
|
||||
else:
|
||||
return os.path.expanduser('~')
|
||||
return os.path.expanduser("~")
|
||||
|
||||
|
||||
def load_general_config(config_path=None):
|
||||
|
||||
@@ -22,13 +22,13 @@ def check_resource(resource_name):
|
||||
if resource_id is None and kwargs.get(resource_name):
|
||||
resource_id = kwargs.pop(resource_name)
|
||||
if isinstance(resource_id, dict):
|
||||
resource_id = resource_id.get('Id', resource_id.get('ID'))
|
||||
resource_id = resource_id.get("Id", resource_id.get("ID"))
|
||||
if not resource_id:
|
||||
raise errors.NullResource(
|
||||
'Resource ID was not provided'
|
||||
)
|
||||
raise errors.NullResource("Resource ID was not provided")
|
||||
return f(self, resource_id, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@@ -38,19 +38,22 @@ def minimum_version(version):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if utils.version_lt(self._version, version):
|
||||
raise errors.InvalidVersion(
|
||||
f'{f.__name__} is not available for version < {version}'
|
||||
f"{f.__name__} is not available for version < {version}"
|
||||
)
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def update_headers(f):
|
||||
def inner(self, *args, **kwargs):
|
||||
if 'HttpHeaders' in self._general_configs:
|
||||
if not kwargs.get('headers'):
|
||||
kwargs['headers'] = self._general_configs['HttpHeaders']
|
||||
if "HttpHeaders" in self._general_configs:
|
||||
if not kwargs.get("headers"):
|
||||
kwargs["headers"] = self._general_configs["HttpHeaders"]
|
||||
else:
|
||||
kwargs['headers'].update(self._general_configs['HttpHeaders'])
|
||||
kwargs["headers"].update(self._general_configs["HttpHeaders"])
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
"""Filename matching with shell patterns.
|
||||
|
||||
fnmatch(FILENAME, PATTERN) matches according to the local convention.
|
||||
@@ -23,6 +24,7 @@ corresponding to PATTERN. (It does not compile it.)
|
||||
|
||||
import re
|
||||
|
||||
|
||||
__all__ = ["fnmatch", "fnmatchcase", "translate"]
|
||||
|
||||
_cache = {}
|
||||
@@ -77,50 +79,50 @@ def translate(pat):
|
||||
There is no way to quote meta-characters.
|
||||
"""
|
||||
i, n = 0, len(pat)
|
||||
res = '^'
|
||||
res = "^"
|
||||
while i < n:
|
||||
c = pat[i]
|
||||
i = i + 1
|
||||
if c == '*':
|
||||
if i < n and pat[i] == '*':
|
||||
if c == "*":
|
||||
if i < n and pat[i] == "*":
|
||||
# is some flavor of "**"
|
||||
i = i + 1
|
||||
# Treat **/ as ** so eat the "/"
|
||||
if i < n and pat[i] == '/':
|
||||
if i < n and pat[i] == "/":
|
||||
i = i + 1
|
||||
if i >= n:
|
||||
# is "**EOF" - to align with .gitignore just accept all
|
||||
res = res + '.*'
|
||||
res = res + ".*"
|
||||
else:
|
||||
# is "**"
|
||||
# Note that this allows for any # of /'s (even 0) because
|
||||
# the .* will eat everything, even /'s
|
||||
res = res + '(.*/)?'
|
||||
res = res + "(.*/)?"
|
||||
else:
|
||||
# is "*" so map it to anything but "/"
|
||||
res = res + '[^/]*'
|
||||
elif c == '?':
|
||||
res = res + "[^/]*"
|
||||
elif c == "?":
|
||||
# "?" is any char except "/"
|
||||
res = res + '[^/]'
|
||||
elif c == '[':
|
||||
res = res + "[^/]"
|
||||
elif c == "[":
|
||||
j = i
|
||||
if j < n and pat[j] == '!':
|
||||
if j < n and pat[j] == "!":
|
||||
j = j + 1
|
||||
if j < n and pat[j] == ']':
|
||||
if j < n and pat[j] == "]":
|
||||
j = j + 1
|
||||
while j < n and pat[j] != ']':
|
||||
while j < n and pat[j] != "]":
|
||||
j = j + 1
|
||||
if j >= n:
|
||||
res = res + '\\['
|
||||
res = res + "\\["
|
||||
else:
|
||||
stuff = pat[i:j].replace('\\', '\\\\')
|
||||
stuff = pat[i:j].replace("\\", "\\\\")
|
||||
i = j + 1
|
||||
if stuff[0] == '!':
|
||||
stuff = '^' + stuff[1:]
|
||||
elif stuff[0] == '^':
|
||||
stuff = '\\' + stuff
|
||||
res = f'{res}[{stuff}]'
|
||||
if stuff[0] == "!":
|
||||
stuff = "^" + stuff[1:]
|
||||
elif stuff[0] == "^":
|
||||
stuff = "\\" + stuff
|
||||
res = f"{res}[{stuff}]"
|
||||
else:
|
||||
res = res + re.escape(c)
|
||||
|
||||
return res + '$'
|
||||
return res + "$"
|
||||
|
||||
@@ -27,7 +27,7 @@ def stream_as_text(stream):
|
||||
"""
|
||||
for data in stream:
|
||||
if not isinstance(data, str):
|
||||
data = data.decode('utf-8', 'replace')
|
||||
data = data.decode("utf-8", "replace")
|
||||
yield data
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def json_splitter(buffer):
|
||||
buffer = buffer.strip()
|
||||
try:
|
||||
obj, index = json_decoder.raw_decode(buffer)
|
||||
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():]
|
||||
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end() :]
|
||||
return obj, rest
|
||||
except ValueError:
|
||||
return None
|
||||
@@ -52,11 +52,11 @@ def json_stream(stream):
|
||||
return split_buffer(stream, json_splitter, json_decoder.decode)
|
||||
|
||||
|
||||
def line_splitter(buffer, separator='\n'):
|
||||
def line_splitter(buffer, separator="\n"):
|
||||
index = buffer.find(str(separator))
|
||||
if index == -1:
|
||||
return None
|
||||
return buffer[:index + 1], buffer[index + 1:]
|
||||
return buffer[: index + 1], buffer[index + 1 :]
|
||||
|
||||
|
||||
def split_buffer(stream, splitter=None, decoder=lambda a: a):
|
||||
@@ -67,7 +67,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
|
||||
of the input.
|
||||
"""
|
||||
splitter = splitter or line_splitter
|
||||
buffered = ''
|
||||
buffered = ""
|
||||
|
||||
for data in stream_as_text(stream):
|
||||
buffered += data
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
|
||||
PORT_SPEC = re.compile(
|
||||
"^" # Match full string
|
||||
"(" # External part
|
||||
@@ -49,23 +50,25 @@ def build_port_bindings(ports):
|
||||
|
||||
|
||||
def _raise_invalid_port(port):
|
||||
raise ValueError(f'Invalid port "{port}", should be '
|
||||
'[[remote_ip:]remote_port[-remote_port]:]'
|
||||
'port[/protocol]')
|
||||
raise ValueError(
|
||||
f'Invalid port "{port}", should be '
|
||||
"[[remote_ip:]remote_port[-remote_port]:]"
|
||||
"port[/protocol]"
|
||||
)
|
||||
|
||||
|
||||
def port_range(start, end, proto, randomly_available_port=False):
|
||||
if not start:
|
||||
return start
|
||||
if not end:
|
||||
return [f'{start}{proto}']
|
||||
return [f"{start}{proto}"]
|
||||
if randomly_available_port:
|
||||
return [f'{start}-{end}{proto}']
|
||||
return [f'{port}{proto}' for port in range(int(start), int(end) + 1)]
|
||||
return [f"{start}-{end}{proto}"]
|
||||
return [f"{port}{proto}" for port in range(int(start), int(end) + 1)]
|
||||
|
||||
|
||||
def split_port(port):
|
||||
if hasattr(port, 'legacy_repr'):
|
||||
if hasattr(port, "legacy_repr"):
|
||||
# This is the worst hack, but it prevents a bug in Compose 1.14.0
|
||||
# https://github.com/docker/docker-py/issues/1668
|
||||
# TODO: remove once fixed in Compose stable
|
||||
@@ -76,19 +79,18 @@ def split_port(port):
|
||||
_raise_invalid_port(port)
|
||||
parts = match.groupdict()
|
||||
|
||||
host = parts['host']
|
||||
proto = parts['proto'] or ''
|
||||
internal = port_range(parts['int'], parts['int_end'], proto)
|
||||
external = port_range(
|
||||
parts['ext'], parts['ext_end'], '', len(internal) == 1)
|
||||
host = parts["host"]
|
||||
proto = parts["proto"] or ""
|
||||
internal = port_range(parts["int"], parts["int_end"], proto)
|
||||
external = port_range(parts["ext"], parts["ext_end"], "", len(internal) == 1)
|
||||
|
||||
if host is None:
|
||||
if external is not None and len(internal) != len(external):
|
||||
raise ValueError('Port ranges don\'t match in length')
|
||||
raise ValueError("Port ranges don't match in length")
|
||||
return internal, external
|
||||
else:
|
||||
if not external:
|
||||
external = [None] * len(internal)
|
||||
elif len(internal) != len(external):
|
||||
raise ValueError('Port ranges don\'t match in length')
|
||||
raise ValueError("Port ranges don't match in length")
|
||||
return internal, [(host, ext_port) for ext_port in external]
|
||||
|
||||
@@ -13,62 +13,63 @@ from .utils import format_environment
|
||||
|
||||
|
||||
class ProxyConfig(dict):
|
||||
'''
|
||||
"""
|
||||
Hold the client's proxy configuration
|
||||
'''
|
||||
"""
|
||||
|
||||
@property
|
||||
def http(self):
|
||||
return self.get('http')
|
||||
return self.get("http")
|
||||
|
||||
@property
|
||||
def https(self):
|
||||
return self.get('https')
|
||||
return self.get("https")
|
||||
|
||||
@property
|
||||
def ftp(self):
|
||||
return self.get('ftp')
|
||||
return self.get("ftp")
|
||||
|
||||
@property
|
||||
def no_proxy(self):
|
||||
return self.get('no_proxy')
|
||||
return self.get("no_proxy")
|
||||
|
||||
@staticmethod
|
||||
def from_dict(config):
|
||||
'''
|
||||
"""
|
||||
Instantiate a new ProxyConfig from a dictionary that represents a
|
||||
client configuration, as described in `the documentation`_.
|
||||
|
||||
.. _the documentation:
|
||||
https://docs.docker.com/network/proxy/#configure-the-docker-client
|
||||
'''
|
||||
"""
|
||||
return ProxyConfig(
|
||||
http=config.get('httpProxy'),
|
||||
https=config.get('httpsProxy'),
|
||||
ftp=config.get('ftpProxy'),
|
||||
no_proxy=config.get('noProxy'),
|
||||
http=config.get("httpProxy"),
|
||||
https=config.get("httpsProxy"),
|
||||
ftp=config.get("ftpProxy"),
|
||||
no_proxy=config.get("noProxy"),
|
||||
)
|
||||
|
||||
def get_environment(self):
|
||||
'''
|
||||
"""
|
||||
Return a dictionary representing the environment variables used to
|
||||
set the proxy settings.
|
||||
'''
|
||||
"""
|
||||
env = {}
|
||||
if self.http:
|
||||
env['http_proxy'] = env['HTTP_PROXY'] = self.http
|
||||
env["http_proxy"] = env["HTTP_PROXY"] = self.http
|
||||
if self.https:
|
||||
env['https_proxy'] = env['HTTPS_PROXY'] = self.https
|
||||
env["https_proxy"] = env["HTTPS_PROXY"] = self.https
|
||||
if self.ftp:
|
||||
env['ftp_proxy'] = env['FTP_PROXY'] = self.ftp
|
||||
env["ftp_proxy"] = env["FTP_PROXY"] = self.ftp
|
||||
if self.no_proxy:
|
||||
env['no_proxy'] = env['NO_PROXY'] = self.no_proxy
|
||||
env["no_proxy"] = env["NO_PROXY"] = self.no_proxy
|
||||
return env
|
||||
|
||||
def inject_proxy_environment(self, environment):
|
||||
'''
|
||||
"""
|
||||
Given a list of strings representing environment variables, prepend the
|
||||
environment variables corresponding to the proxy settings.
|
||||
'''
|
||||
"""
|
||||
if not self:
|
||||
return environment
|
||||
|
||||
@@ -80,4 +81,4 @@ class ProxyConfig(dict):
|
||||
return proxy_env + environment
|
||||
|
||||
def __str__(self):
|
||||
return f'ProxyConfig(http={self.http}, https={self.https}, ftp={self.ftp}, no_proxy={self.no_proxy})'
|
||||
return f"ProxyConfig(http={self.http}, https={self.https}, ftp={self.ftp}, no_proxy={self.no_proxy})"
|
||||
|
||||
@@ -48,22 +48,24 @@ def read(socket, n=4096):
|
||||
poll.poll()
|
||||
|
||||
try:
|
||||
if hasattr(socket, 'recv'):
|
||||
if hasattr(socket, "recv"):
|
||||
return socket.recv(n)
|
||||
if isinstance(socket, getattr(pysocket, 'SocketIO')):
|
||||
if isinstance(socket, getattr(pysocket, "SocketIO")):
|
||||
return socket.read(n)
|
||||
return os.read(socket.fileno(), n)
|
||||
except EnvironmentError as e:
|
||||
if e.errno not in recoverable_errors:
|
||||
raise
|
||||
except Exception as e:
|
||||
is_pipe_ended = (isinstance(socket, NpipeSocket) and
|
||||
len(e.args) > 0 and
|
||||
e.args[0] == NPIPE_ENDED)
|
||||
is_pipe_ended = (
|
||||
isinstance(socket, NpipeSocket)
|
||||
and len(e.args) > 0
|
||||
and e.args[0] == NPIPE_ENDED
|
||||
)
|
||||
if is_pipe_ended:
|
||||
# npipes do not support duplex sockets, so we interpret
|
||||
# a PIPE_ENDED error as a close operation (0-length read).
|
||||
return ''
|
||||
return ""
|
||||
raise
|
||||
|
||||
|
||||
@@ -72,7 +74,7 @@ def read_exactly(socket, n):
|
||||
Reads exactly n bytes from socket
|
||||
Raises SocketError if there is not enough data
|
||||
"""
|
||||
data = b''
|
||||
data = b""
|
||||
while len(data) < n:
|
||||
next_data = read(socket, n - len(data))
|
||||
if not next_data:
|
||||
@@ -93,7 +95,7 @@ def next_frame_header(socket):
|
||||
except SocketError:
|
||||
return (-1, -1)
|
||||
|
||||
stream, actual = struct.unpack('>BxxxL', data)
|
||||
stream, actual = struct.unpack(">BxxxL", data)
|
||||
return (stream, actual)
|
||||
|
||||
|
||||
@@ -160,7 +162,7 @@ def consume_socket_output(frames, demux=False):
|
||||
if demux is False:
|
||||
# If the streams are multiplexed, the generator returns strings, that
|
||||
# we just need to concatenate.
|
||||
return b''.join(frames)
|
||||
return b"".join(frames)
|
||||
|
||||
# If the streams are demultiplexed, the generator yields tuples
|
||||
# (stdout, stderr)
|
||||
@@ -169,7 +171,7 @@ def consume_socket_output(frames, demux=False):
|
||||
# It is guaranteed that for each frame, one and only one stream
|
||||
# is not None.
|
||||
if frame == (None, None):
|
||||
raise AssertionError(f'frame must be (None, None), but got {frame}')
|
||||
raise AssertionError(f"frame must be (None, None), but got {frame}")
|
||||
if frame[0] is not None:
|
||||
if out[0] is None:
|
||||
out[0] = frame[0]
|
||||
@@ -193,4 +195,4 @@ def demux_adaptor(stream_id, data):
|
||||
elif stream_id == STDERR:
|
||||
return (None, data)
|
||||
else:
|
||||
raise ValueError(f'{stream_id} is not a valid stream')
|
||||
raise ValueError(f"{stream_id} is not a valid stream")
|
||||
|
||||
@@ -16,40 +16,44 @@ import os
|
||||
import os.path
|
||||
import shlex
|
||||
import string
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import StrictVersion
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
StrictVersion,
|
||||
)
|
||||
|
||||
from .. import errors
|
||||
from ..constants import DEFAULT_HTTP_HOST
|
||||
from ..constants import DEFAULT_UNIX_SOCKET
|
||||
from ..constants import DEFAULT_NPIPE
|
||||
from ..constants import BYTE_UNITS
|
||||
from ..constants import (
|
||||
BYTE_UNITS,
|
||||
DEFAULT_HTTP_HOST,
|
||||
DEFAULT_NPIPE,
|
||||
DEFAULT_UNIX_SOCKET,
|
||||
)
|
||||
from ..tls import TLSConfig
|
||||
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
|
||||
URLComponents = collections.namedtuple(
|
||||
'URLComponents',
|
||||
'scheme netloc url params query fragment',
|
||||
"URLComponents",
|
||||
"scheme netloc url params query fragment",
|
||||
)
|
||||
|
||||
|
||||
def create_ipam_pool(*args, **kwargs):
|
||||
raise errors.DeprecatedMethod(
|
||||
'utils.create_ipam_pool has been removed. Please use a '
|
||||
'docker.types.IPAMPool object instead.'
|
||||
"utils.create_ipam_pool has been removed. Please use a "
|
||||
"docker.types.IPAMPool object instead."
|
||||
)
|
||||
|
||||
|
||||
def create_ipam_config(*args, **kwargs):
|
||||
raise errors.DeprecatedMethod(
|
||||
'utils.create_ipam_config has been removed. Please use a '
|
||||
'docker.types.IPAMConfig object instead.'
|
||||
"utils.create_ipam_config has been removed. Please use a "
|
||||
"docker.types.IPAMConfig object instead."
|
||||
)
|
||||
|
||||
|
||||
def decode_json_header(header):
|
||||
data = base64.b64decode(header).decode('utf-8')
|
||||
data = base64.b64decode(header).decode("utf-8")
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
@@ -84,29 +88,29 @@ def version_gte(v1, v2):
|
||||
|
||||
|
||||
def _convert_port_binding(binding):
|
||||
result = {'HostIp': '', 'HostPort': ''}
|
||||
result = {"HostIp": "", "HostPort": ""}
|
||||
if isinstance(binding, tuple):
|
||||
if len(binding) == 2:
|
||||
result['HostPort'] = binding[1]
|
||||
result['HostIp'] = binding[0]
|
||||
result["HostPort"] = binding[1]
|
||||
result["HostIp"] = binding[0]
|
||||
elif isinstance(binding[0], str):
|
||||
result['HostIp'] = binding[0]
|
||||
result["HostIp"] = binding[0]
|
||||
else:
|
||||
result['HostPort'] = binding[0]
|
||||
result["HostPort"] = binding[0]
|
||||
elif isinstance(binding, dict):
|
||||
if 'HostPort' in binding:
|
||||
result['HostPort'] = binding['HostPort']
|
||||
if 'HostIp' in binding:
|
||||
result['HostIp'] = binding['HostIp']
|
||||
if "HostPort" in binding:
|
||||
result["HostPort"] = binding["HostPort"]
|
||||
if "HostIp" in binding:
|
||||
result["HostIp"] = binding["HostIp"]
|
||||
else:
|
||||
raise ValueError(binding)
|
||||
else:
|
||||
result['HostPort'] = binding
|
||||
result["HostPort"] = binding
|
||||
|
||||
if result['HostPort'] is None:
|
||||
result['HostPort'] = ''
|
||||
if result["HostPort"] is None:
|
||||
result["HostPort"] = ""
|
||||
else:
|
||||
result['HostPort'] = str(result['HostPort'])
|
||||
result["HostPort"] = str(result["HostPort"])
|
||||
|
||||
return result
|
||||
|
||||
@@ -115,8 +119,8 @@ def convert_port_bindings(port_bindings):
|
||||
result = {}
|
||||
for k, v in port_bindings.items():
|
||||
key = str(k)
|
||||
if '/' not in key:
|
||||
key += '/tcp'
|
||||
if "/" not in key:
|
||||
key += "/tcp"
|
||||
if isinstance(v, list):
|
||||
result[key] = [_convert_port_binding(binding) for binding in v]
|
||||
else:
|
||||
@@ -131,46 +135,44 @@ def convert_volume_binds(binds):
|
||||
result = []
|
||||
for k, v in binds.items():
|
||||
if isinstance(k, bytes):
|
||||
k = k.decode('utf-8')
|
||||
k = k.decode("utf-8")
|
||||
|
||||
if isinstance(v, dict):
|
||||
if 'ro' in v and 'mode' in v:
|
||||
raise ValueError(
|
||||
f'Binding cannot contain both "ro" and "mode": {v!r}'
|
||||
)
|
||||
if "ro" in v and "mode" in v:
|
||||
raise ValueError(f'Binding cannot contain both "ro" and "mode": {v!r}')
|
||||
|
||||
bind = v['bind']
|
||||
bind = v["bind"]
|
||||
if isinstance(bind, bytes):
|
||||
bind = bind.decode('utf-8')
|
||||
bind = bind.decode("utf-8")
|
||||
|
||||
if 'ro' in v:
|
||||
mode = 'ro' if v['ro'] else 'rw'
|
||||
elif 'mode' in v:
|
||||
mode = v['mode']
|
||||
if "ro" in v:
|
||||
mode = "ro" if v["ro"] else "rw"
|
||||
elif "mode" in v:
|
||||
mode = v["mode"]
|
||||
else:
|
||||
mode = 'rw'
|
||||
mode = "rw"
|
||||
|
||||
# NOTE: this is only relevant for Linux hosts
|
||||
# (does not apply in Docker Desktop)
|
||||
propagation_modes = [
|
||||
'rshared',
|
||||
'shared',
|
||||
'rslave',
|
||||
'slave',
|
||||
'rprivate',
|
||||
'private',
|
||||
"rshared",
|
||||
"shared",
|
||||
"rslave",
|
||||
"slave",
|
||||
"rprivate",
|
||||
"private",
|
||||
]
|
||||
if 'propagation' in v and v['propagation'] in propagation_modes:
|
||||
if "propagation" in v and v["propagation"] in propagation_modes:
|
||||
if mode:
|
||||
mode = ','.join([mode, v['propagation']])
|
||||
mode = ",".join([mode, v["propagation"]])
|
||||
else:
|
||||
mode = v['propagation']
|
||||
mode = v["propagation"]
|
||||
|
||||
result.append(f'{k}:{bind}:{mode}')
|
||||
result.append(f"{k}:{bind}:{mode}")
|
||||
else:
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode('utf-8')
|
||||
result.append(f'{k}:{v}:rw')
|
||||
v = v.decode("utf-8")
|
||||
result.append(f"{k}:{v}:rw")
|
||||
return result
|
||||
|
||||
|
||||
@@ -180,7 +182,7 @@ def convert_tmpfs_mounts(tmpfs):
|
||||
|
||||
if not isinstance(tmpfs, list):
|
||||
raise ValueError(
|
||||
f'Expected tmpfs value to be either a list or a dict, found: {type(tmpfs).__name__}'
|
||||
f"Expected tmpfs value to be either a list or a dict, found: {type(tmpfs).__name__}"
|
||||
)
|
||||
|
||||
result = {}
|
||||
@@ -205,22 +207,22 @@ def convert_service_networks(networks):
|
||||
if not networks:
|
||||
return networks
|
||||
if not isinstance(networks, list):
|
||||
raise TypeError('networks parameter must be a list.')
|
||||
raise TypeError("networks parameter must be a list.")
|
||||
|
||||
result = []
|
||||
for n in networks:
|
||||
if isinstance(n, str):
|
||||
n = {'Target': n}
|
||||
n = {"Target": n}
|
||||
result.append(n)
|
||||
return result
|
||||
|
||||
|
||||
def parse_repository_tag(repo_name):
|
||||
parts = repo_name.rsplit('@', 1)
|
||||
parts = repo_name.rsplit("@", 1)
|
||||
if len(parts) == 2:
|
||||
return tuple(parts)
|
||||
parts = repo_name.rsplit(':', 1)
|
||||
if len(parts) == 2 and '/' not in parts[1]:
|
||||
parts = repo_name.rsplit(":", 1)
|
||||
if len(parts) == 2 and "/" not in parts[1]:
|
||||
return tuple(parts)
|
||||
return repo_name, None
|
||||
|
||||
@@ -229,86 +231,81 @@ def parse_host(addr, is_win32=False, tls=False):
|
||||
# Sensible defaults
|
||||
if not addr and is_win32:
|
||||
return DEFAULT_NPIPE
|
||||
if not addr or addr.strip() == 'unix://':
|
||||
if not addr or addr.strip() == "unix://":
|
||||
return DEFAULT_UNIX_SOCKET
|
||||
|
||||
addr = addr.strip()
|
||||
|
||||
parsed_url = urlparse(addr)
|
||||
proto = parsed_url.scheme
|
||||
if not proto or any(x not in string.ascii_letters + '+' for x in proto):
|
||||
if not proto or any(x not in string.ascii_letters + "+" for x in proto):
|
||||
# https://bugs.python.org/issue754016
|
||||
parsed_url = urlparse('//' + addr, 'tcp')
|
||||
proto = 'tcp'
|
||||
parsed_url = urlparse("//" + addr, "tcp")
|
||||
proto = "tcp"
|
||||
|
||||
if proto == 'fd':
|
||||
raise errors.DockerException('fd protocol is not implemented')
|
||||
if proto == "fd":
|
||||
raise errors.DockerException("fd protocol is not implemented")
|
||||
|
||||
# These protos are valid aliases for our library but not for the
|
||||
# official spec
|
||||
if proto == 'http' or proto == 'https':
|
||||
tls = proto == 'https'
|
||||
proto = 'tcp'
|
||||
elif proto == 'http+unix':
|
||||
proto = 'unix'
|
||||
if proto == "http" or proto == "https":
|
||||
tls = proto == "https"
|
||||
proto = "tcp"
|
||||
elif proto == "http+unix":
|
||||
proto = "unix"
|
||||
|
||||
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
|
||||
raise errors.DockerException(
|
||||
f"Invalid bind address protocol: {addr}"
|
||||
)
|
||||
if proto not in ("tcp", "unix", "npipe", "ssh"):
|
||||
raise errors.DockerException(f"Invalid bind address protocol: {addr}")
|
||||
|
||||
if proto == 'tcp' and not parsed_url.netloc:
|
||||
if proto == "tcp" and not parsed_url.netloc:
|
||||
# "tcp://" is exceptionally disallowed by convention;
|
||||
# omitting a hostname for other protocols is fine
|
||||
raise errors.DockerException(
|
||||
f'Invalid bind address format: {addr}'
|
||||
)
|
||||
raise errors.DockerException(f"Invalid bind address format: {addr}")
|
||||
|
||||
if any([
|
||||
parsed_url.params, parsed_url.query, parsed_url.fragment,
|
||||
parsed_url.password
|
||||
]):
|
||||
raise errors.DockerException(
|
||||
f'Invalid bind address format: {addr}'
|
||||
)
|
||||
if any(
|
||||
[parsed_url.params, parsed_url.query, parsed_url.fragment, parsed_url.password]
|
||||
):
|
||||
raise errors.DockerException(f"Invalid bind address format: {addr}")
|
||||
|
||||
if parsed_url.path and proto == 'ssh':
|
||||
if parsed_url.path and proto == "ssh":
|
||||
raise errors.DockerException(
|
||||
f'Invalid bind address format: no path allowed for this protocol: {addr}'
|
||||
f"Invalid bind address format: no path allowed for this protocol: {addr}"
|
||||
)
|
||||
else:
|
||||
path = parsed_url.path
|
||||
if proto == 'unix' and parsed_url.hostname is not None:
|
||||
if proto == "unix" and parsed_url.hostname is not None:
|
||||
# For legacy reasons, we consider unix://path
|
||||
# to be valid and equivalent to unix:///path
|
||||
path = '/'.join((parsed_url.hostname, path))
|
||||
path = "/".join((parsed_url.hostname, path))
|
||||
|
||||
netloc = parsed_url.netloc
|
||||
if proto in ('tcp', 'ssh'):
|
||||
if proto in ("tcp", "ssh"):
|
||||
port = parsed_url.port or 0
|
||||
if port <= 0:
|
||||
port = 22 if proto == 'ssh' else (2375 if tls else 2376)
|
||||
netloc = f'{parsed_url.netloc}:{port}'
|
||||
port = 22 if proto == "ssh" else (2375 if tls else 2376)
|
||||
netloc = f"{parsed_url.netloc}:{port}"
|
||||
|
||||
if not parsed_url.hostname:
|
||||
netloc = f'{DEFAULT_HTTP_HOST}:{port}'
|
||||
netloc = f"{DEFAULT_HTTP_HOST}:{port}"
|
||||
|
||||
# Rewrite schemes to fit library internals (requests adapters)
|
||||
if proto == 'tcp':
|
||||
if proto == "tcp":
|
||||
proto = f"http{'s' if tls else ''}"
|
||||
elif proto == 'unix':
|
||||
proto = 'http+unix'
|
||||
elif proto == "unix":
|
||||
proto = "http+unix"
|
||||
|
||||
if proto in ('http+unix', 'npipe'):
|
||||
return f"{proto}://{path}".rstrip('/')
|
||||
return urlunparse(URLComponents(
|
||||
scheme=proto,
|
||||
netloc=netloc,
|
||||
url=path,
|
||||
params='',
|
||||
query='',
|
||||
fragment='',
|
||||
)).rstrip('/')
|
||||
if proto in ("http+unix", "npipe"):
|
||||
return f"{proto}://{path}".rstrip("/")
|
||||
return urlunparse(
|
||||
URLComponents(
|
||||
scheme=proto,
|
||||
netloc=netloc,
|
||||
url=path,
|
||||
params="",
|
||||
query="",
|
||||
fragment="",
|
||||
)
|
||||
).rstrip("/")
|
||||
|
||||
|
||||
def parse_devices(devices):
|
||||
@@ -318,10 +315,8 @@ def parse_devices(devices):
|
||||
device_list.append(device)
|
||||
continue
|
||||
if not isinstance(device, str):
|
||||
raise errors.DockerException(
|
||||
f'Invalid device type {type(device)}'
|
||||
)
|
||||
device_mapping = device.split(':')
|
||||
raise errors.DockerException(f"Invalid device type {type(device)}")
|
||||
device_mapping = device.split(":")
|
||||
if device_mapping:
|
||||
path_on_host = device_mapping[0]
|
||||
if len(device_mapping) > 1:
|
||||
@@ -331,27 +326,29 @@ def parse_devices(devices):
|
||||
if len(device_mapping) > 2:
|
||||
permissions = device_mapping[2]
|
||||
else:
|
||||
permissions = 'rwm'
|
||||
device_list.append({
|
||||
'PathOnHost': path_on_host,
|
||||
'PathInContainer': path_in_container,
|
||||
'CgroupPermissions': permissions
|
||||
})
|
||||
permissions = "rwm"
|
||||
device_list.append(
|
||||
{
|
||||
"PathOnHost": path_on_host,
|
||||
"PathInContainer": path_in_container,
|
||||
"CgroupPermissions": permissions,
|
||||
}
|
||||
)
|
||||
return device_list
|
||||
|
||||
|
||||
def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
|
||||
if not environment:
|
||||
environment = os.environ
|
||||
host = environment.get('DOCKER_HOST')
|
||||
host = environment.get("DOCKER_HOST")
|
||||
|
||||
# empty string for cert path is the same as unset.
|
||||
cert_path = environment.get('DOCKER_CERT_PATH') or None
|
||||
cert_path = environment.get("DOCKER_CERT_PATH") or None
|
||||
|
||||
# empty string for tls verify counts as "false".
|
||||
# Any value or 'unset' counts as true.
|
||||
tls_verify = environment.get('DOCKER_TLS_VERIFY')
|
||||
if tls_verify == '':
|
||||
tls_verify = environment.get("DOCKER_TLS_VERIFY")
|
||||
if tls_verify == "":
|
||||
tls_verify = False
|
||||
else:
|
||||
tls_verify = tls_verify is not None
|
||||
@@ -360,23 +357,25 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
|
||||
params = {}
|
||||
|
||||
if host:
|
||||
params['base_url'] = host
|
||||
params["base_url"] = host
|
||||
|
||||
if not enable_tls:
|
||||
return params
|
||||
|
||||
if not cert_path:
|
||||
cert_path = os.path.join(os.path.expanduser('~'), '.docker')
|
||||
cert_path = os.path.join(os.path.expanduser("~"), ".docker")
|
||||
|
||||
if not tls_verify and assert_hostname is None:
|
||||
# assert_hostname is a subset of TLS verification,
|
||||
# so if it is not set already then set it to false.
|
||||
assert_hostname = False
|
||||
|
||||
params['tls'] = TLSConfig(
|
||||
client_cert=(os.path.join(cert_path, 'cert.pem'),
|
||||
os.path.join(cert_path, 'key.pem')),
|
||||
ca_cert=os.path.join(cert_path, 'ca.pem'),
|
||||
params["tls"] = TLSConfig(
|
||||
client_cert=(
|
||||
os.path.join(cert_path, "cert.pem"),
|
||||
os.path.join(cert_path, "key.pem"),
|
||||
),
|
||||
ca_cert=os.path.join(cert_path, "ca.pem"),
|
||||
verify=tls_verify,
|
||||
ssl_version=ssl_version,
|
||||
assert_hostname=assert_hostname,
|
||||
@@ -389,13 +388,12 @@ def convert_filters(filters):
|
||||
result = {}
|
||||
for k, v in filters.items():
|
||||
if isinstance(v, bool):
|
||||
v = 'true' if v else 'false'
|
||||
v = "true" if v else "false"
|
||||
if not isinstance(v, list):
|
||||
v = [v, ]
|
||||
result[k] = [
|
||||
str(item) if not isinstance(item, str) else item
|
||||
for item in v
|
||||
]
|
||||
v = [
|
||||
v,
|
||||
]
|
||||
result[k] = [str(item) if not isinstance(item, str) else item for item in v]
|
||||
return json.dumps(result)
|
||||
|
||||
|
||||
@@ -415,7 +413,7 @@ def parse_bytes(s):
|
||||
# without a units part. Assuming that the units are bytes.
|
||||
if suffix.isdigit():
|
||||
digits_part = s
|
||||
suffix = 'b'
|
||||
suffix = "b"
|
||||
else:
|
||||
digits_part = s[:-1]
|
||||
|
||||
@@ -424,14 +422,14 @@ def parse_bytes(s):
|
||||
digits = float(digits_part)
|
||||
except ValueError:
|
||||
raise errors.DockerException(
|
||||
f'Failed converting the string value for memory ({digits_part}) to an integer.'
|
||||
f"Failed converting the string value for memory ({digits_part}) to an integer."
|
||||
)
|
||||
|
||||
# Reconvert to long for the final result
|
||||
s = int(digits * units[suffix])
|
||||
else:
|
||||
raise errors.DockerException(
|
||||
f'The specified value for memory ({s}) should specify the units. The postfix should be one of the `b` `k` `m` `g` characters'
|
||||
f"The specified value for memory ({s}) should specify the units. The postfix should be one of the `b` `k` `m` `g` characters"
|
||||
)
|
||||
|
||||
return s
|
||||
@@ -441,7 +439,7 @@ def normalize_links(links):
|
||||
if isinstance(links, dict):
|
||||
links = links.items()
|
||||
|
||||
return [f'{k}:{v}' if v else k for k, v in sorted(links)]
|
||||
return [f"{k}:{v}" if v else k for k, v in sorted(links)]
|
||||
|
||||
|
||||
def parse_env_file(env_file):
|
||||
@@ -451,22 +449,24 @@ def parse_env_file(env_file):
|
||||
"""
|
||||
environment = {}
|
||||
|
||||
with open(env_file, 'r') as f:
|
||||
with open(env_file, "r") as f:
|
||||
for line in f:
|
||||
|
||||
if line[0] == '#':
|
||||
if line[0] == "#":
|
||||
continue
|
||||
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parse_line = line.split('=', 1)
|
||||
parse_line = line.split("=", 1)
|
||||
if len(parse_line) == 2:
|
||||
k, v = parse_line
|
||||
environment[k] = v
|
||||
else:
|
||||
raise errors.DockerException(f'Invalid line in environment file {env_file}:\n{line}')
|
||||
raise errors.DockerException(
|
||||
f"Invalid line in environment file {env_file}:\n{line}"
|
||||
)
|
||||
|
||||
return environment
|
||||
|
||||
@@ -480,26 +480,23 @@ def format_environment(environment):
|
||||
if value is None:
|
||||
return key
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
value = value.decode("utf-8")
|
||||
|
||||
return f"{key}={value}"
|
||||
|
||||
return f'{key}={value}'
|
||||
return [format_env(*var) for var in environment.items()]
|
||||
|
||||
|
||||
def format_extra_hosts(extra_hosts, task=False):
|
||||
# Use format dictated by Swarm API if container is part of a task
|
||||
if task:
|
||||
return [
|
||||
f'{v} {k}' for k, v in sorted(extra_hosts.items())
|
||||
]
|
||||
return [f"{v} {k}" for k, v in sorted(extra_hosts.items())]
|
||||
|
||||
return [
|
||||
f'{k}:{v}' for k, v in sorted(extra_hosts.items())
|
||||
]
|
||||
return [f"{k}:{v}" for k, v in sorted(extra_hosts.items())]
|
||||
|
||||
|
||||
def create_host_config(self, *args, **kwargs):
|
||||
raise errors.DeprecatedMethod(
|
||||
'utils.create_host_config has been removed. Please use a '
|
||||
'docker.types.HostConfig object instead.'
|
||||
"utils.create_host_config has been removed. Please use a "
|
||||
"docker.types.HostConfig object instead."
|
||||
)
|
||||
|
||||
@@ -29,44 +29,44 @@ class _Mode(object):
|
||||
|
||||
_ESCAPE_DICT = {
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
"\\": "\\",
|
||||
"'": "'",
|
||||
'/': '/',
|
||||
'b': '\b',
|
||||
'f': '\f',
|
||||
'n': '\n',
|
||||
'r': '\r',
|
||||
't': '\t',
|
||||
"/": "/",
|
||||
"b": "\b",
|
||||
"f": "\f",
|
||||
"n": "\n",
|
||||
"r": "\r",
|
||||
"t": "\t",
|
||||
}
|
||||
|
||||
_HEX_DICT = {
|
||||
'0': 0,
|
||||
'1': 1,
|
||||
'2': 2,
|
||||
'3': 3,
|
||||
'4': 4,
|
||||
'5': 5,
|
||||
'6': 6,
|
||||
'7': 7,
|
||||
'8': 8,
|
||||
'9': 9,
|
||||
'a': 0xA,
|
||||
'b': 0xB,
|
||||
'c': 0xC,
|
||||
'd': 0xD,
|
||||
'e': 0xE,
|
||||
'f': 0xF,
|
||||
'A': 0xA,
|
||||
'B': 0xB,
|
||||
'C': 0xC,
|
||||
'D': 0xD,
|
||||
'E': 0xE,
|
||||
'F': 0xF,
|
||||
"0": 0,
|
||||
"1": 1,
|
||||
"2": 2,
|
||||
"3": 3,
|
||||
"4": 4,
|
||||
"5": 5,
|
||||
"6": 6,
|
||||
"7": 7,
|
||||
"8": 8,
|
||||
"9": 9,
|
||||
"a": 0xA,
|
||||
"b": 0xB,
|
||||
"c": 0xC,
|
||||
"d": 0xD,
|
||||
"e": 0xE,
|
||||
"f": 0xF,
|
||||
"A": 0xA,
|
||||
"B": 0xB,
|
||||
"C": 0xC,
|
||||
"D": 0xD,
|
||||
"E": 0xE,
|
||||
"F": 0xF,
|
||||
}
|
||||
|
||||
|
||||
def _is_ident(cur):
|
||||
return cur > ' ' and cur not in ('"', '=')
|
||||
return cur > " " and cur not in ('"', "=")
|
||||
|
||||
|
||||
class _Parser(object):
|
||||
@@ -89,16 +89,18 @@ class _Parser(object):
|
||||
|
||||
def parse_unicode_sequence(self):
|
||||
if self.index + 6 > self.length:
|
||||
raise InvalidLogFmt('Not enough space for unicode escape')
|
||||
if self.line[self.index:self.index + 2] != '\\u':
|
||||
raise InvalidLogFmt('Invalid unicode escape start')
|
||||
raise InvalidLogFmt("Not enough space for unicode escape")
|
||||
if self.line[self.index : self.index + 2] != "\\u":
|
||||
raise InvalidLogFmt("Invalid unicode escape start")
|
||||
v = 0
|
||||
for i in range(self.index + 2, self.index + 6):
|
||||
v <<= 4
|
||||
try:
|
||||
v += _HEX_DICT[self.line[self.index]]
|
||||
except KeyError:
|
||||
raise InvalidLogFmt(f'Invalid unicode escape digit {self.line[self.index]!r}')
|
||||
raise InvalidLogFmt(
|
||||
f"Invalid unicode escape digit {self.line[self.index]!r}"
|
||||
)
|
||||
self.index += 6
|
||||
return chr(v)
|
||||
|
||||
@@ -111,8 +113,8 @@ def parse_line(line, logrus_mode=False):
|
||||
mode = _Mode.GARBAGE
|
||||
|
||||
def handle_kv(has_no_value=False):
|
||||
k = ''.join(key)
|
||||
v = None if has_no_value else ''.join(value)
|
||||
k = "".join(key)
|
||||
v = None if has_no_value else "".join(value)
|
||||
result[k] = v
|
||||
del key[:]
|
||||
del value[:]
|
||||
@@ -128,7 +130,7 @@ def parse_line(line, logrus_mode=False):
|
||||
key.append(cur)
|
||||
parser.next()
|
||||
return _Mode.KEY
|
||||
elif cur == '=':
|
||||
elif cur == "=":
|
||||
parser.next()
|
||||
return _Mode.EQUAL
|
||||
else:
|
||||
@@ -162,16 +164,16 @@ def parse_line(line, logrus_mode=False):
|
||||
return _Mode.GARBAGE
|
||||
|
||||
def parse_quoted_value(cur):
|
||||
if cur == '\\':
|
||||
if cur == "\\":
|
||||
parser.next()
|
||||
if parser.done():
|
||||
raise InvalidLogFmt('Unterminated escape sequence in quoted string')
|
||||
raise InvalidLogFmt("Unterminated escape sequence in quoted string")
|
||||
cur = parser.cur()
|
||||
if cur in _ESCAPE_DICT:
|
||||
value.append(_ESCAPE_DICT[cur])
|
||||
elif cur != 'u':
|
||||
elif cur != "u":
|
||||
es = f"\\{cur}"
|
||||
raise InvalidLogFmt(f'Unknown escape sequence {es!r}')
|
||||
raise InvalidLogFmt(f"Unknown escape sequence {es!r}")
|
||||
else:
|
||||
parser.prev()
|
||||
value.append(parser.parse_unicode_sequence())
|
||||
@@ -181,8 +183,8 @@ def parse_line(line, logrus_mode=False):
|
||||
handle_kv()
|
||||
parser.next()
|
||||
return _Mode.GARBAGE
|
||||
elif cur < ' ':
|
||||
raise InvalidLogFmt('Control characters in quoted string are not allowed')
|
||||
elif cur < " ":
|
||||
raise InvalidLogFmt("Control characters in quoted string are not allowed")
|
||||
else:
|
||||
value.append(cur)
|
||||
parser.next()
|
||||
@@ -204,5 +206,5 @@ def parse_line(line, logrus_mode=False):
|
||||
elif mode == _Mode.IDENT_VALUE:
|
||||
handle_kv()
|
||||
elif mode == _Mode.QUOTED_VALUE:
|
||||
raise InvalidLogFmt('Unterminated quoted string')
|
||||
raise InvalidLogFmt("Unterminated quoted string")
|
||||
return result
|
||||
|
||||
@@ -13,36 +13,74 @@ from __future__ import annotations
|
||||
import re
|
||||
|
||||
|
||||
_VALID_STR = re.compile('^[A-Za-z0-9_-]+$')
|
||||
_VALID_STR = re.compile("^[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
def _validate_part(string, part, part_name):
|
||||
if not part:
|
||||
raise ValueError(f'Invalid platform string "{string}": {part_name} is empty')
|
||||
if not _VALID_STR.match(part):
|
||||
raise ValueError(f'Invalid platform string "{string}": {part_name} has invalid characters')
|
||||
raise ValueError(
|
||||
f'Invalid platform string "{string}": {part_name} has invalid characters'
|
||||
)
|
||||
return part
|
||||
|
||||
|
||||
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L32-L38
|
||||
_KNOWN_OS = (
|
||||
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js",
|
||||
"linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos",
|
||||
"aix",
|
||||
"android",
|
||||
"darwin",
|
||||
"dragonfly",
|
||||
"freebsd",
|
||||
"hurd",
|
||||
"illumos",
|
||||
"ios",
|
||||
"js",
|
||||
"linux",
|
||||
"nacl",
|
||||
"netbsd",
|
||||
"openbsd",
|
||||
"plan9",
|
||||
"solaris",
|
||||
"windows",
|
||||
"zos",
|
||||
)
|
||||
|
||||
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L54-L60
|
||||
_KNOWN_ARCH = (
|
||||
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le",
|
||||
"loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le",
|
||||
"ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm",
|
||||
"386",
|
||||
"amd64",
|
||||
"amd64p32",
|
||||
"arm",
|
||||
"armbe",
|
||||
"arm64",
|
||||
"arm64be",
|
||||
"ppc64",
|
||||
"ppc64le",
|
||||
"loong64",
|
||||
"mips",
|
||||
"mipsle",
|
||||
"mips64",
|
||||
"mips64le",
|
||||
"mips64p32",
|
||||
"mips64p32le",
|
||||
"ppc",
|
||||
"riscv",
|
||||
"riscv64",
|
||||
"s390",
|
||||
"s390x",
|
||||
"sparc",
|
||||
"sparc64",
|
||||
"wasm",
|
||||
)
|
||||
|
||||
|
||||
def _normalize_os(os_str):
|
||||
# See normalizeOS() in https://github.com/containerd/containerd/blob/main/platforms/database.go
|
||||
os_str = os_str.lower()
|
||||
if os_str == 'macos':
|
||||
os_str = 'darwin'
|
||||
if os_str == "macos":
|
||||
os_str = "darwin"
|
||||
return os_str
|
||||
|
||||
|
||||
@@ -94,9 +132,9 @@ class _Platform(object):
|
||||
self.variant = variant
|
||||
if variant is not None:
|
||||
if arch is None:
|
||||
raise ValueError('If variant is given, architecture must be given too')
|
||||
raise ValueError("If variant is given, architecture must be given too")
|
||||
if os is None:
|
||||
raise ValueError('If variant is given, os must be given too')
|
||||
raise ValueError("If variant is given, os must be given too")
|
||||
|
||||
@classmethod
|
||||
def parse_platform_string(cls, string, daemon_os=None, daemon_arch=None):
|
||||
@@ -104,40 +142,48 @@ class _Platform(object):
|
||||
if string is None:
|
||||
return cls()
|
||||
if not string:
|
||||
raise ValueError('Platform string must be non-empty')
|
||||
parts = string.split('/', 2)
|
||||
raise ValueError("Platform string must be non-empty")
|
||||
parts = string.split("/", 2)
|
||||
arch = None
|
||||
variant = None
|
||||
if len(parts) == 1:
|
||||
_validate_part(string, string, 'OS/architecture')
|
||||
_validate_part(string, string, "OS/architecture")
|
||||
# The part is either OS or architecture
|
||||
os = _normalize_os(string)
|
||||
if os in _KNOWN_OS:
|
||||
if daemon_arch is not None:
|
||||
arch, variant = _normalize_arch(daemon_arch, '')
|
||||
arch, variant = _normalize_arch(daemon_arch, "")
|
||||
return cls(os=os, arch=arch, variant=variant)
|
||||
arch, variant = _normalize_arch(os, '')
|
||||
arch, variant = _normalize_arch(os, "")
|
||||
if arch in _KNOWN_ARCH:
|
||||
return cls(
|
||||
os=_normalize_os(daemon_os) if daemon_os else None,
|
||||
arch=arch or None,
|
||||
variant=variant or None,
|
||||
)
|
||||
raise ValueError(f'Invalid platform string "{string}": unknown OS or architecture')
|
||||
os = _validate_part(string, parts[0], 'OS')
|
||||
raise ValueError(
|
||||
f'Invalid platform string "{string}": unknown OS or architecture'
|
||||
)
|
||||
os = _validate_part(string, parts[0], "OS")
|
||||
if not os:
|
||||
raise ValueError(f'Invalid platform string "{string}": OS is empty')
|
||||
arch = _validate_part(string, parts[1], 'architecture') if len(parts) > 1 else None
|
||||
arch = (
|
||||
_validate_part(string, parts[1], "architecture") if len(parts) > 1 else None
|
||||
)
|
||||
if arch is not None and not arch:
|
||||
raise ValueError(f'Invalid platform string "{string}": architecture is empty')
|
||||
variant = _validate_part(string, parts[2], 'variant') if len(parts) > 2 else None
|
||||
raise ValueError(
|
||||
f'Invalid platform string "{string}": architecture is empty'
|
||||
)
|
||||
variant = (
|
||||
_validate_part(string, parts[2], "variant") if len(parts) > 2 else None
|
||||
)
|
||||
if variant is not None and not variant:
|
||||
raise ValueError(f'Invalid platform string "{string}": variant is empty')
|
||||
arch, variant = _normalize_arch(arch, variant or '')
|
||||
if len(parts) == 2 and arch == 'arm' and variant == 'v7':
|
||||
arch, variant = _normalize_arch(arch, variant or "")
|
||||
if len(parts) == 2 and arch == "arm" and variant == "v7":
|
||||
variant = None
|
||||
if len(parts) == 3 and arch == 'arm64' and variant == '':
|
||||
variant = 'v8'
|
||||
if len(parts) == 3 and arch == "arm64" and variant == "":
|
||||
variant = "v8"
|
||||
return cls(os=_normalize_os(os), arch=arch, variant=variant or None)
|
||||
|
||||
def __str__(self):
|
||||
@@ -152,27 +198,41 @@ class _Platform(object):
|
||||
parts = [self.arch]
|
||||
else:
|
||||
parts = []
|
||||
return '/'.join(parts)
|
||||
return "/".join(parts)
|
||||
|
||||
def __repr__(self):
|
||||
return f'_Platform(os={self.os!r}, arch={self.arch!r}, variant={self.variant!r})'
|
||||
return (
|
||||
f"_Platform(os={self.os!r}, arch={self.arch!r}, variant={self.variant!r})"
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.os == other.os and self.arch == other.arch and self.variant == other.variant
|
||||
return (
|
||||
self.os == other.os
|
||||
and self.arch == other.arch
|
||||
and self.variant == other.variant
|
||||
)
|
||||
|
||||
|
||||
def normalize_platform_string(string, daemon_os=None, daemon_arch=None):
|
||||
return str(_Platform.parse_platform_string(string, daemon_os=daemon_os, daemon_arch=daemon_arch))
|
||||
return str(
|
||||
_Platform.parse_platform_string(
|
||||
string, daemon_os=daemon_os, daemon_arch=daemon_arch
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def compose_platform_string(os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None):
|
||||
def compose_platform_string(
|
||||
os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None
|
||||
):
|
||||
if os is None and daemon_os is not None:
|
||||
os = _normalize_os(daemon_os)
|
||||
if arch is None and daemon_arch is not None:
|
||||
arch, variant = _normalize_arch(daemon_arch, variant or '')
|
||||
arch, variant = _normalize_arch(daemon_arch, variant or "")
|
||||
variant = variant or None
|
||||
return str(_Platform(os=os, arch=arch, variant=variant or None))
|
||||
|
||||
|
||||
def compare_platform_strings(string1, string2):
|
||||
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(string2)
|
||||
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(
|
||||
string2
|
||||
)
|
||||
|
||||
@@ -11,32 +11,32 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
|
||||
|
||||
|
||||
def generate_insecure_key():
|
||||
'''Do NOT use this for cryptographic purposes!'''
|
||||
"""Do NOT use this for cryptographic purposes!"""
|
||||
while True:
|
||||
# Generate a one-byte key. Right now the functions below do not use more
|
||||
# than one byte, so this is sufficient.
|
||||
key = bytes([random.randint(0, 255)])
|
||||
# Return anything that is not zero
|
||||
if key != b'\x00':
|
||||
if key != b"\x00":
|
||||
return key
|
||||
|
||||
|
||||
def scramble(value, key):
|
||||
'''Do NOT use this for cryptographic purposes!'''
|
||||
"""Do NOT use this for cryptographic purposes!"""
|
||||
if len(key) < 1:
|
||||
raise ValueError('Key must be at least one byte')
|
||||
raise ValueError("Key must be at least one byte")
|
||||
value = to_bytes(value)
|
||||
k = key[0]
|
||||
value = bytes([k ^ b for b in value])
|
||||
return '=S=' + to_native(base64.b64encode(value))
|
||||
return "=S=" + to_native(base64.b64encode(value))
|
||||
|
||||
|
||||
def unscramble(value, key):
|
||||
'''Do NOT use this for cryptographic purposes!'''
|
||||
"""Do NOT use this for cryptographic purposes!"""
|
||||
if len(key) < 1:
|
||||
raise ValueError('Key must be at least one byte')
|
||||
if not value.startswith('=S='):
|
||||
raise ValueError('Value does not start with indicator')
|
||||
raise ValueError("Key must be at least one byte")
|
||||
if not value.startswith("=S="):
|
||||
raise ValueError("Value does not start with indicator")
|
||||
value = base64.b64decode(value[3:])
|
||||
k = key[0]
|
||||
value = bytes([k ^ b for b in value])
|
||||
|
||||
+277
-158
@@ -4,7 +4,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import abc
|
||||
import os
|
||||
import platform
|
||||
@@ -14,9 +13,11 @@ import traceback
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
HAS_DOCKER_PY = True
|
||||
HAS_DOCKER_PY_2 = False
|
||||
@@ -26,15 +27,16 @@ HAS_DOCKER_TRACEBACK = None
|
||||
|
||||
try:
|
||||
from requests.exceptions import SSLError
|
||||
|
||||
from docker import __version__ as docker_version
|
||||
from docker import auth
|
||||
from docker.errors import APIError, NotFound, TLSParameterError
|
||||
from docker.tls import TLSConfig
|
||||
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
|
||||
from docker import APIClient as Client
|
||||
elif LooseVersion(docker_version) >= LooseVersion('2.0.0'):
|
||||
elif LooseVersion(docker_version) >= LooseVersion("2.0.0"):
|
||||
HAS_DOCKER_PY_2 = True
|
||||
from docker import APIClient as Client
|
||||
else:
|
||||
@@ -52,6 +54,7 @@ except ImportError as exc:
|
||||
try:
|
||||
# docker (Docker SDK for Python >= 2.0.0)
|
||||
import docker.models # noqa: F401, pylint: disable=unused-import
|
||||
|
||||
HAS_DOCKER_MODELS = True
|
||||
except ImportError:
|
||||
HAS_DOCKER_MODELS = False
|
||||
@@ -59,13 +62,16 @@ except ImportError:
|
||||
try:
|
||||
# docker-py (Docker SDK for Python < 2.0.0)
|
||||
import docker.ssladapter # noqa: F401, pylint: disable=unused-import
|
||||
|
||||
HAS_DOCKER_SSLADAPTER = True
|
||||
except ImportError:
|
||||
HAS_DOCKER_SSLADAPTER = False
|
||||
|
||||
|
||||
try:
|
||||
from requests.exceptions import RequestException # noqa: F401, pylint: disable=unused-import
|
||||
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
|
||||
RequestException,
|
||||
)
|
||||
except ImportError:
|
||||
# Either Docker SDK for Python is no longer using requests, or Docker SDK for Python is not around either,
|
||||
# or Docker SDK for Python's dependency requests is missing. In any case, define an exception
|
||||
@@ -73,30 +79,59 @@ except ImportError:
|
||||
class RequestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
BYTE_SUFFIXES, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DEFAULT_DOCKER_REGISTRY, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DEFAULT_TLS_HOSTNAME, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DOCKER_COMMON_ARGS_VARS, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DifferenceTracker, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
clean_dict_booleans_for_docker_api, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
compare_dict_allow_more_present, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
compare_generic, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
convert_duration_to_nanosecond, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
is_image_name_id, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
is_valid_tag, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
omit_none_from_dict, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
parse_healthcheck, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
|
||||
DEFAULT_DOCKER_HOST,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
DEFAULT_TLS,
|
||||
DEFAULT_TLS_VERIFY,
|
||||
DEFAULT_TLS_HOSTNAME, # TODO: remove
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
DOCKER_COMMON_ARGS,
|
||||
DOCKER_COMMON_ARGS_VARS, # TODO: remove
|
||||
DOCKER_MUTUALLY_EXCLUSIVE,
|
||||
DOCKER_REQUIRED_TOGETHER,
|
||||
DEFAULT_DOCKER_REGISTRY, # TODO: remove
|
||||
BYTE_SUFFIXES, # TODO: remove
|
||||
is_image_name_id, # TODO: remove
|
||||
is_valid_tag, # TODO: remove
|
||||
sanitize_result,
|
||||
DockerBaseClass, # TODO: remove
|
||||
update_tls_hostname,
|
||||
compare_dict_allow_more_present, # TODO: remove
|
||||
compare_generic, # TODO: remove
|
||||
DifferenceTracker, # TODO: remove
|
||||
clean_dict_booleans_for_docker_api, # TODO: remove
|
||||
convert_duration_to_nanosecond, # TODO: remove
|
||||
parse_healthcheck, # TODO: remove
|
||||
omit_none_from_dict, # TODO: remove
|
||||
)
|
||||
|
||||
|
||||
@@ -120,8 +155,10 @@ if not HAS_DOCKER_PY:
|
||||
|
||||
|
||||
def _get_tls_config(fail_function, **kwargs):
|
||||
if 'assert_hostname' in kwargs and LooseVersion(docker_version) >= LooseVersion('7.0.0b1'):
|
||||
assert_hostname = kwargs.pop('assert_hostname')
|
||||
if "assert_hostname" in kwargs and LooseVersion(docker_version) >= LooseVersion(
|
||||
"7.0.0b1"
|
||||
):
|
||||
assert_hostname = kwargs.pop("assert_hostname")
|
||||
if assert_hostname is not None:
|
||||
fail_function(
|
||||
"tls_hostname is not compatible with Docker SDK for Python 7.0.0+. You are using"
|
||||
@@ -139,51 +176,55 @@ def _get_tls_config(fail_function, **kwargs):
|
||||
|
||||
|
||||
def is_using_tls(auth):
|
||||
return auth['tls_verify'] or auth['tls']
|
||||
return auth["tls_verify"] or auth["tls"]
|
||||
|
||||
|
||||
def get_connect_params(auth, fail_function):
|
||||
if is_using_tls(auth):
|
||||
auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')
|
||||
auth["docker_host"] = auth["docker_host"].replace("tcp://", "https://")
|
||||
|
||||
result = dict(
|
||||
base_url=auth['docker_host'],
|
||||
version=auth['api_version'],
|
||||
timeout=auth['timeout'],
|
||||
base_url=auth["docker_host"],
|
||||
version=auth["api_version"],
|
||||
timeout=auth["timeout"],
|
||||
)
|
||||
|
||||
if auth['tls_verify']:
|
||||
if auth["tls_verify"]:
|
||||
# TLS with verification
|
||||
tls_config = dict(
|
||||
verify=True,
|
||||
assert_hostname=auth['tls_hostname'],
|
||||
assert_hostname=auth["tls_hostname"],
|
||||
fail_function=fail_function,
|
||||
)
|
||||
if auth['cert_path'] and auth['key_path']:
|
||||
tls_config['client_cert'] = (auth['cert_path'], auth['key_path'])
|
||||
if auth['cacert_path']:
|
||||
tls_config['ca_cert'] = auth['cacert_path']
|
||||
result['tls'] = _get_tls_config(**tls_config)
|
||||
elif auth['tls']:
|
||||
if auth["cert_path"] and auth["key_path"]:
|
||||
tls_config["client_cert"] = (auth["cert_path"], auth["key_path"])
|
||||
if auth["cacert_path"]:
|
||||
tls_config["ca_cert"] = auth["cacert_path"]
|
||||
result["tls"] = _get_tls_config(**tls_config)
|
||||
elif auth["tls"]:
|
||||
# TLS without verification
|
||||
tls_config = dict(
|
||||
verify=False,
|
||||
fail_function=fail_function,
|
||||
)
|
||||
if auth['cert_path'] and auth['key_path']:
|
||||
tls_config['client_cert'] = (auth['cert_path'], auth['key_path'])
|
||||
result['tls'] = _get_tls_config(**tls_config)
|
||||
if auth["cert_path"] and auth["key_path"]:
|
||||
tls_config["client_cert"] = (auth["cert_path"], auth["key_path"])
|
||||
result["tls"] = _get_tls_config(**tls_config)
|
||||
|
||||
if auth.get('use_ssh_client'):
|
||||
if LooseVersion(docker_version) < LooseVersion('4.4.0'):
|
||||
fail_function("use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer")
|
||||
result['use_ssh_client'] = True
|
||||
if auth.get("use_ssh_client"):
|
||||
if LooseVersion(docker_version) < LooseVersion("4.4.0"):
|
||||
fail_function(
|
||||
"use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer"
|
||||
)
|
||||
result["use_ssh_client"] = True
|
||||
|
||||
# No TLS
|
||||
return result
|
||||
|
||||
|
||||
DOCKERPYUPGRADE_SWITCH_TO_DOCKER = "Try `pip uninstall docker-py` followed by `pip install docker`."
|
||||
DOCKERPYUPGRADE_SWITCH_TO_DOCKER = (
|
||||
"Try `pip uninstall docker-py` followed by `pip install docker`."
|
||||
)
|
||||
DOCKERPYUPGRADE_UPGRADE_DOCKER = "Use `pip install --upgrade docker` to upgrade."
|
||||
DOCKERPYUPGRADE_RECOMMEND_DOCKER = "Use `pip install --upgrade docker-py` to upgrade."
|
||||
|
||||
@@ -192,17 +233,19 @@ class AnsibleDockerClientBase(Client):
|
||||
def __init__(self, min_docker_version=None, min_docker_api_version=None):
|
||||
if min_docker_version is None:
|
||||
min_docker_version = MIN_DOCKER_VERSION
|
||||
NEEDS_DOCKER_PY2 = (LooseVersion(min_docker_version) >= LooseVersion('2.0.0'))
|
||||
NEEDS_DOCKER_PY2 = LooseVersion(min_docker_version) >= LooseVersion("2.0.0")
|
||||
|
||||
self.docker_py_version = LooseVersion(docker_version)
|
||||
|
||||
if HAS_DOCKER_MODELS and HAS_DOCKER_SSLADAPTER:
|
||||
self.fail("Cannot have both the docker-py and docker python modules (old and new version of Docker "
|
||||
"SDK for Python) installed together as they use the same namespace and cause a corrupt "
|
||||
"installation. Please uninstall both packages, and re-install only the docker-py or docker "
|
||||
f"python module (for {platform.node()}'s Python {sys.executable}). It is recommended to install the docker module. Please "
|
||||
"note that simply uninstalling one of the modules can leave the other module in a broken "
|
||||
"state.")
|
||||
self.fail(
|
||||
"Cannot have both the docker-py and docker python modules (old and new version of Docker "
|
||||
"SDK for Python) installed together as they use the same namespace and cause a corrupt "
|
||||
"installation. Please uninstall both packages, and re-install only the docker-py or docker "
|
||||
f"python module (for {platform.node()}'s Python {sys.executable}). It is recommended to install the docker module. Please "
|
||||
"note that simply uninstalling one of the modules can leave the other module in a broken "
|
||||
"state."
|
||||
)
|
||||
|
||||
if not HAS_DOCKER_PY:
|
||||
msg = missing_required_lib("Docker SDK for Python: docker>=5.0.0")
|
||||
@@ -218,13 +261,15 @@ class AnsibleDockerClientBase(Client):
|
||||
# The minimal required version is < 2.0 (and the current version as well).
|
||||
# Advertise docker (instead of docker-py).
|
||||
msg += DOCKERPYUPGRADE_RECOMMEND_DOCKER
|
||||
elif docker_version < LooseVersion('2.0'):
|
||||
elif docker_version < LooseVersion("2.0"):
|
||||
msg += DOCKERPYUPGRADE_SWITCH_TO_DOCKER
|
||||
else:
|
||||
msg += DOCKERPYUPGRADE_UPGRADE_DOCKER
|
||||
self.fail(msg)
|
||||
|
||||
self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
|
||||
self._connect_params = get_connect_params(
|
||||
self.auth_params, fail_function=self.fail
|
||||
)
|
||||
|
||||
try:
|
||||
super(AnsibleDockerClientBase, self).__init__(**self._connect_params)
|
||||
@@ -235,9 +280,11 @@ class AnsibleDockerClientBase(Client):
|
||||
self.fail(f"Error connecting: {exc}")
|
||||
|
||||
self.docker_api_version = LooseVersion(self.docker_api_version_str)
|
||||
min_docker_api_version = min_docker_api_version or '1.25'
|
||||
min_docker_api_version = min_docker_api_version or "1.25"
|
||||
if self.docker_api_version < LooseVersion(min_docker_api_version):
|
||||
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
|
||||
self.fail(
|
||||
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
|
||||
)
|
||||
|
||||
def log(self, msg, pretty_print=False):
|
||||
pass
|
||||
@@ -253,16 +300,16 @@ class AnsibleDockerClientBase(Client):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_value(param_name, param_value, env_variable, default_value, type='str'):
|
||||
def _get_value(param_name, param_value, env_variable, default_value, type="str"):
|
||||
if param_value is not None:
|
||||
# take module parameter value
|
||||
if type == 'bool':
|
||||
if type == "bool":
|
||||
if param_value in BOOLEANS_TRUE:
|
||||
return True
|
||||
if param_value in BOOLEANS_FALSE:
|
||||
return False
|
||||
return bool(param_value)
|
||||
if type == 'int':
|
||||
if type == "int":
|
||||
return int(param_value)
|
||||
return param_value
|
||||
|
||||
@@ -270,19 +317,19 @@ class AnsibleDockerClientBase(Client):
|
||||
env_value = os.environ.get(env_variable)
|
||||
if env_value is not None:
|
||||
# take the env variable value
|
||||
if param_name == 'cert_path':
|
||||
return os.path.join(env_value, 'cert.pem')
|
||||
if param_name == 'cacert_path':
|
||||
return os.path.join(env_value, 'ca.pem')
|
||||
if param_name == 'key_path':
|
||||
return os.path.join(env_value, 'key.pem')
|
||||
if type == 'bool':
|
||||
if param_name == "cert_path":
|
||||
return os.path.join(env_value, "cert.pem")
|
||||
if param_name == "cacert_path":
|
||||
return os.path.join(env_value, "ca.pem")
|
||||
if param_name == "key_path":
|
||||
return os.path.join(env_value, "key.pem")
|
||||
if type == "bool":
|
||||
if env_value in BOOLEANS_TRUE:
|
||||
return True
|
||||
if env_value in BOOLEANS_FALSE:
|
||||
return False
|
||||
return bool(env_value)
|
||||
if type == 'int':
|
||||
if type == "int":
|
||||
return int(env_value)
|
||||
return env_value
|
||||
|
||||
@@ -298,7 +345,7 @@ class AnsibleDockerClientBase(Client):
|
||||
# Get authentication credentials.
|
||||
# Precedence: module parameters-> environment variables-> defaults.
|
||||
|
||||
self.log('Getting credentials')
|
||||
self.log("Getting credentials")
|
||||
|
||||
client_params = self._get_params()
|
||||
|
||||
@@ -307,21 +354,56 @@ class AnsibleDockerClientBase(Client):
|
||||
params[key] = client_params.get(key)
|
||||
|
||||
result = dict(
|
||||
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
|
||||
DEFAULT_DOCKER_HOST, type='str'),
|
||||
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
|
||||
'DOCKER_TLS_HOSTNAME', None, type='str'),
|
||||
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
|
||||
'auto', type='str'),
|
||||
cacert_path=self._get_value('cacert_path', params['ca_path'], 'DOCKER_CERT_PATH', None, type='str'),
|
||||
cert_path=self._get_value('cert_path', params['client_cert'], 'DOCKER_CERT_PATH', None, type='str'),
|
||||
key_path=self._get_value('key_path', params['client_key'], 'DOCKER_CERT_PATH', None, type='str'),
|
||||
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS, type='bool'),
|
||||
tls_verify=self._get_value('validate_certs', params['validate_certs'], 'DOCKER_TLS_VERIFY',
|
||||
DEFAULT_TLS_VERIFY, type='bool'),
|
||||
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
|
||||
DEFAULT_TIMEOUT_SECONDS, type='int'),
|
||||
use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False, type='bool'),
|
||||
docker_host=self._get_value(
|
||||
"docker_host",
|
||||
params["docker_host"],
|
||||
"DOCKER_HOST",
|
||||
DEFAULT_DOCKER_HOST,
|
||||
type="str",
|
||||
),
|
||||
tls_hostname=self._get_value(
|
||||
"tls_hostname",
|
||||
params["tls_hostname"],
|
||||
"DOCKER_TLS_HOSTNAME",
|
||||
None,
|
||||
type="str",
|
||||
),
|
||||
api_version=self._get_value(
|
||||
"api_version",
|
||||
params["api_version"],
|
||||
"DOCKER_API_VERSION",
|
||||
"auto",
|
||||
type="str",
|
||||
),
|
||||
cacert_path=self._get_value(
|
||||
"cacert_path", params["ca_path"], "DOCKER_CERT_PATH", None, type="str"
|
||||
),
|
||||
cert_path=self._get_value(
|
||||
"cert_path", params["client_cert"], "DOCKER_CERT_PATH", None, type="str"
|
||||
),
|
||||
key_path=self._get_value(
|
||||
"key_path", params["client_key"], "DOCKER_CERT_PATH", None, type="str"
|
||||
),
|
||||
tls=self._get_value(
|
||||
"tls", params["tls"], "DOCKER_TLS", DEFAULT_TLS, type="bool"
|
||||
),
|
||||
tls_verify=self._get_value(
|
||||
"validate_certs",
|
||||
params["validate_certs"],
|
||||
"DOCKER_TLS_VERIFY",
|
||||
DEFAULT_TLS_VERIFY,
|
||||
type="bool",
|
||||
),
|
||||
timeout=self._get_value(
|
||||
"timeout",
|
||||
params["timeout"],
|
||||
"DOCKER_TIMEOUT",
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
type="int",
|
||||
),
|
||||
use_ssh_client=self._get_value(
|
||||
"use_ssh_client", params["use_ssh_client"], None, False, type="bool"
|
||||
),
|
||||
)
|
||||
|
||||
update_tls_hostname(result)
|
||||
@@ -331,11 +413,13 @@ class AnsibleDockerClientBase(Client):
|
||||
def _handle_ssl_error(self, error):
|
||||
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
|
||||
if match:
|
||||
hostname = self.auth_params['tls_hostname']
|
||||
self.fail(f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
|
||||
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
|
||||
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
|
||||
"setting the `tls` parameter to true.")
|
||||
hostname = self.auth_params["tls_hostname"]
|
||||
self.fail(
|
||||
f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
|
||||
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
|
||||
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
|
||||
"setting the `tls` parameter to true."
|
||||
)
|
||||
self.fail(f"SSL Exception: {error}")
|
||||
|
||||
def get_container_by_id(self, container_id):
|
||||
@@ -350,27 +434,30 @@ class AnsibleDockerClientBase(Client):
|
||||
self.fail(f"Error inspecting container: {exc}")
|
||||
|
||||
def get_container(self, name=None):
|
||||
'''
|
||||
"""
|
||||
Lookup a container and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
search_name = name
|
||||
if not name.startswith('/'):
|
||||
search_name = '/' + name
|
||||
if not name.startswith("/"):
|
||||
search_name = "/" + name
|
||||
|
||||
result = None
|
||||
try:
|
||||
for container in self.containers(all=True):
|
||||
self.log(f"testing container: {container['Names']}")
|
||||
if isinstance(container['Names'], list) and search_name in container['Names']:
|
||||
if (
|
||||
isinstance(container["Names"], list)
|
||||
and search_name in container["Names"]
|
||||
):
|
||||
result = container
|
||||
break
|
||||
if container['Id'].startswith(name):
|
||||
if container["Id"].startswith(name):
|
||||
result = container
|
||||
break
|
||||
if container['Id'] == name:
|
||||
if container["Id"] == name:
|
||||
result = container
|
||||
break
|
||||
except SSLError as exc:
|
||||
@@ -381,12 +468,12 @@ class AnsibleDockerClientBase(Client):
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return self.get_container_by_id(result['Id'])
|
||||
return self.get_container_by_id(result["Id"])
|
||||
|
||||
def get_network(self, name=None, network_id=None):
|
||||
'''
|
||||
"""
|
||||
Lookup a network and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if name is None and network_id is None:
|
||||
return None
|
||||
|
||||
@@ -396,10 +483,10 @@ class AnsibleDockerClientBase(Client):
|
||||
try:
|
||||
for network in self.networks():
|
||||
self.log(f"testing network: {network['Name']}")
|
||||
if name == network['Name']:
|
||||
if name == network["Name"]:
|
||||
result = network
|
||||
break
|
||||
if network['Id'].startswith(name):
|
||||
if network["Id"].startswith(name):
|
||||
result = network
|
||||
break
|
||||
except SSLError as exc:
|
||||
@@ -408,7 +495,7 @@ class AnsibleDockerClientBase(Client):
|
||||
self.fail(f"Error retrieving network list: {exc}")
|
||||
|
||||
if result is not None:
|
||||
network_id = result['Id']
|
||||
network_id = result["Id"]
|
||||
|
||||
if network_id is not None:
|
||||
try:
|
||||
@@ -423,9 +510,9 @@ class AnsibleDockerClientBase(Client):
|
||||
return result
|
||||
|
||||
def find_image(self, name, tag):
|
||||
'''
|
||||
"""
|
||||
Lookup an image (by name and tag) and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
|
||||
@@ -434,14 +521,14 @@ class AnsibleDockerClientBase(Client):
|
||||
if not images:
|
||||
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
|
||||
registry, repo_name = auth.resolve_repository_name(name)
|
||||
if registry == 'docker.io':
|
||||
if registry == "docker.io":
|
||||
# If docker.io is explicitly there in name, the image
|
||||
# is not found in some cases (#41509)
|
||||
self.log(f"Check for docker.io image: {repo_name}")
|
||||
images = self._image_lookup(repo_name, tag)
|
||||
if not images and repo_name.startswith('library/'):
|
||||
if not images and repo_name.startswith("library/"):
|
||||
# Sometimes library/xxx images are not found
|
||||
lookup = repo_name[len('library/'):]
|
||||
lookup = repo_name[len("library/") :]
|
||||
self.log(f"Check for docker.io image: {lookup}")
|
||||
images = self._image_lookup(lookup, tag)
|
||||
if not images:
|
||||
@@ -451,7 +538,7 @@ class AnsibleDockerClientBase(Client):
|
||||
lookup = f"{registry}/{repo_name}"
|
||||
self.log(f"Check for docker.io image: {lookup}")
|
||||
images = self._image_lookup(lookup, tag)
|
||||
if not images and '/' not in repo_name:
|
||||
if not images and "/" not in repo_name:
|
||||
# This seems to be happening with podman-docker
|
||||
# (https://github.com/ansible-collections/community.docker/issues/291)
|
||||
lookup = f"{registry}/library/{repo_name}"
|
||||
@@ -463,7 +550,7 @@ class AnsibleDockerClientBase(Client):
|
||||
|
||||
if len(images) == 1:
|
||||
try:
|
||||
inspection = self.inspect_image(images[0]['Id'])
|
||||
inspection = self.inspect_image(images[0]["Id"])
|
||||
except NotFound:
|
||||
self.log(f"Image {name}:{tag} not found.")
|
||||
return None
|
||||
@@ -475,9 +562,9 @@ class AnsibleDockerClientBase(Client):
|
||||
return None
|
||||
|
||||
def find_image_by_id(self, image_id, accept_missing_image=False):
|
||||
'''
|
||||
"""
|
||||
Lookup an image (by ID) and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if not image_id:
|
||||
return None
|
||||
|
||||
@@ -494,11 +581,11 @@ class AnsibleDockerClientBase(Client):
|
||||
return inspection
|
||||
|
||||
def _image_lookup(self, name, tag):
|
||||
'''
|
||||
"""
|
||||
Including a tag in the name parameter sent to the Docker SDK for Python images method
|
||||
does not work consistently. Instead, get the result set for name and manually check
|
||||
if the tag exists.
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
response = self.images(name=name)
|
||||
except Exception as exc:
|
||||
@@ -509,33 +596,35 @@ class AnsibleDockerClientBase(Client):
|
||||
lookup_digest = f"{name}@{tag}"
|
||||
images = []
|
||||
for image in response:
|
||||
tags = image.get('RepoTags')
|
||||
digests = image.get('RepoDigests')
|
||||
tags = image.get("RepoTags")
|
||||
digests = image.get("RepoDigests")
|
||||
if (tags and lookup in tags) or (digests and lookup_digest in digests):
|
||||
images = [image]
|
||||
break
|
||||
return images
|
||||
|
||||
def pull_image(self, name, tag="latest", platform=None):
|
||||
'''
|
||||
"""
|
||||
Pull an image
|
||||
'''
|
||||
"""
|
||||
kwargs = dict(
|
||||
tag=tag,
|
||||
stream=True,
|
||||
decode=True,
|
||||
)
|
||||
if platform is not None:
|
||||
kwargs['platform'] = platform
|
||||
kwargs["platform"] = platform
|
||||
self.log(f"Pulling image {name}:{tag}")
|
||||
old_tag = self.find_image(name, tag)
|
||||
try:
|
||||
for line in self.pull(name, **kwargs):
|
||||
self.log(line, pretty_print=True)
|
||||
if line.get('error'):
|
||||
if line.get('errorDetail'):
|
||||
error_detail = line.get('errorDetail')
|
||||
self.fail(f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}")
|
||||
if line.get("error"):
|
||||
if line.get("errorDetail"):
|
||||
error_detail = line.get("errorDetail")
|
||||
self.fail(
|
||||
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
|
||||
)
|
||||
else:
|
||||
self.fail(f"Error pulling {name} - {line.get('error')}")
|
||||
except Exception as exc:
|
||||
@@ -546,27 +635,43 @@ class AnsibleDockerClientBase(Client):
|
||||
return new_tag, old_tag == new_tag
|
||||
|
||||
def inspect_distribution(self, image, **kwargs):
|
||||
'''
|
||||
"""
|
||||
Get image digest by directly calling the Docker API when running Docker SDK < 4.0.0
|
||||
since prior versions did not support accessing private repositories.
|
||||
'''
|
||||
if self.docker_py_version < LooseVersion('4.0.0'):
|
||||
"""
|
||||
if self.docker_py_version < LooseVersion("4.0.0"):
|
||||
registry = auth.resolve_repository_name(image)[0]
|
||||
header = auth.get_config_header(self, registry)
|
||||
if header:
|
||||
return self._result(self._get(
|
||||
self._url('/distribution/{0}/json', image),
|
||||
headers={'X-Registry-Auth': header}
|
||||
), json=True)
|
||||
return super(AnsibleDockerClientBase, self).inspect_distribution(image, **kwargs)
|
||||
return self._result(
|
||||
self._get(
|
||||
self._url("/distribution/{0}/json", image),
|
||||
headers={"X-Registry-Auth": header},
|
||||
),
|
||||
json=True,
|
||||
)
|
||||
return super(AnsibleDockerClientBase, self).inspect_distribution(
|
||||
image, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
|
||||
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
|
||||
required_together=None, required_if=None, required_one_of=None, required_by=None,
|
||||
min_docker_version=None, min_docker_api_version=None, option_minimal_versions=None,
|
||||
option_minimal_versions_ignore_params=None, fail_results=None):
|
||||
def __init__(
|
||||
self,
|
||||
argument_spec=None,
|
||||
supports_check_mode=False,
|
||||
mutually_exclusive=None,
|
||||
required_together=None,
|
||||
required_if=None,
|
||||
required_one_of=None,
|
||||
required_by=None,
|
||||
min_docker_version=None,
|
||||
min_docker_api_version=None,
|
||||
option_minimal_versions=None,
|
||||
option_minimal_versions_ignore_params=None,
|
||||
fail_results=None,
|
||||
):
|
||||
|
||||
# Modules can put information in here which will always be returned
|
||||
# in case client.fail() is called.
|
||||
@@ -598,22 +703,27 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
required_by=required_by or {},
|
||||
)
|
||||
|
||||
self.debug = self.module.params.get('debug')
|
||||
self.debug = self.module.params.get("debug")
|
||||
self.check_mode = self.module.check_mode
|
||||
|
||||
super(AnsibleDockerClient, self).__init__(
|
||||
min_docker_version=min_docker_version,
|
||||
min_docker_api_version=min_docker_api_version)
|
||||
min_docker_api_version=min_docker_api_version,
|
||||
)
|
||||
|
||||
if option_minimal_versions is not None:
|
||||
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
|
||||
self._get_minimal_versions(
|
||||
option_minimal_versions, option_minimal_versions_ignore_params
|
||||
)
|
||||
|
||||
def fail(self, msg, **kwargs):
|
||||
self.fail_results.update(kwargs)
|
||||
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
|
||||
|
||||
def deprecate(self, msg, version=None, date=None, collection_name=None):
|
||||
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
|
||||
self.module.deprecate(
|
||||
msg, version=version, date=date, collection_name=collection_name
|
||||
)
|
||||
|
||||
def _get_params(self):
|
||||
return self.module.params
|
||||
@@ -631,26 +741,33 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
# Test whether option is supported, and store result
|
||||
support_docker_py = True
|
||||
support_docker_api = True
|
||||
if 'docker_py_version' in data:
|
||||
support_docker_py = self.docker_py_version >= LooseVersion(data['docker_py_version'])
|
||||
if 'docker_api_version' in data:
|
||||
support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version'])
|
||||
data['supported'] = support_docker_py and support_docker_api
|
||||
if "docker_py_version" in data:
|
||||
support_docker_py = self.docker_py_version >= LooseVersion(
|
||||
data["docker_py_version"]
|
||||
)
|
||||
if "docker_api_version" in data:
|
||||
support_docker_api = self.docker_api_version >= LooseVersion(
|
||||
data["docker_api_version"]
|
||||
)
|
||||
data["supported"] = support_docker_py and support_docker_api
|
||||
# Fail if option is not supported but used
|
||||
if not data['supported']:
|
||||
if not data["supported"]:
|
||||
# Test whether option is specified
|
||||
if 'detect_usage' in data:
|
||||
used = data['detect_usage'](self)
|
||||
if "detect_usage" in data:
|
||||
used = data["detect_usage"](self)
|
||||
else:
|
||||
used = self.module.params.get(option) is not None
|
||||
if used and 'default' in self.module.argument_spec[option]:
|
||||
used = self.module.params[option] != self.module.argument_spec[option]['default']
|
||||
if used and "default" in self.module.argument_spec[option]:
|
||||
used = (
|
||||
self.module.params[option]
|
||||
!= self.module.argument_spec[option]["default"]
|
||||
)
|
||||
if used:
|
||||
# If the option is used, compose error message.
|
||||
if 'usage_msg' in data:
|
||||
usg = data['usage_msg']
|
||||
if "usage_msg" in data:
|
||||
usg = data["usage_msg"]
|
||||
else:
|
||||
usg = f'set {option} option'
|
||||
usg = f"set {option} option"
|
||||
if not support_docker_api:
|
||||
msg = f"Docker API version is {self.docker_api_version_str}. Minimum version required is {data['docker_api_version']} to {usg}."
|
||||
elif not support_docker_py:
|
||||
@@ -658,19 +775,21 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
f"Docker SDK for Python version is {docker_version} ({platform.node()}'s Python {sys.executable})."
|
||||
f" Minimum version required is {data['docker_py_version']} to {usg}. "
|
||||
)
|
||||
if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'):
|
||||
if LooseVersion(data["docker_py_version"]) < LooseVersion(
|
||||
"2.0.0"
|
||||
):
|
||||
msg += DOCKERPYUPGRADE_RECOMMEND_DOCKER
|
||||
elif self.docker_py_version < LooseVersion('2.0.0'):
|
||||
elif self.docker_py_version < LooseVersion("2.0.0"):
|
||||
msg += DOCKERPYUPGRADE_SWITCH_TO_DOCKER
|
||||
else:
|
||||
msg += DOCKERPYUPGRADE_UPGRADE_DOCKER
|
||||
else:
|
||||
# should not happen
|
||||
msg = f'Cannot {usg} with your configuration.'
|
||||
msg = f"Cannot {usg} with your configuration."
|
||||
self.fail(msg)
|
||||
|
||||
def report_warnings(self, result, warnings_key=None):
|
||||
'''
|
||||
"""
|
||||
Checks result of client operation for warnings, and if present, outputs them.
|
||||
|
||||
warnings_key should be a list of keys used to crawl the result dictionary.
|
||||
@@ -681,15 +800,15 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
|
||||
In most cases (if warnings are returned at all), warnings_key should be
|
||||
['Warnings'] or ['Warning']. The default value (if not specified) is ['Warnings'].
|
||||
'''
|
||||
"""
|
||||
if warnings_key is None:
|
||||
warnings_key = ['Warnings']
|
||||
warnings_key = ["Warnings"]
|
||||
for key in warnings_key:
|
||||
if not isinstance(result, Mapping):
|
||||
return
|
||||
result = result.get(key)
|
||||
if isinstance(result, Sequence):
|
||||
for warning in result:
|
||||
self.module.warn(f'Docker warning: {warning}')
|
||||
self.module.warn(f"Docker warning: {warning}")
|
||||
elif isinstance(result, str) and result:
|
||||
self.module.warn(f'Docker warning: {result}')
|
||||
self.module.warn(f"Docker warning: {result}")
|
||||
|
||||
+235
-142
@@ -5,30 +5,37 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import abc
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from requests.exceptions import RequestException, SSLError # noqa: F401, pylint: disable=unused-import
|
||||
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
|
||||
RequestException,
|
||||
SSLError,
|
||||
)
|
||||
except ImportError:
|
||||
# Define an exception class RequestException so that our code does not break.
|
||||
class RequestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api import auth
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.api.client import APIClient as Client
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.api.client import (
|
||||
APIClient as Client,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
NotFound,
|
||||
MissingRequirementException,
|
||||
NotFound,
|
||||
TLSParameterError,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.tls import TLSConfig
|
||||
@@ -36,19 +43,26 @@ from ansible_collections.community.docker.plugins.module_utils._api.utils.utils
|
||||
convert_filters,
|
||||
parse_repository_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DEFAULT_DOCKER_REGISTRY, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DEFAULT_TLS_HOSTNAME, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
is_image_name_id, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
is_valid_tag, # TODO: remove
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
|
||||
DEFAULT_DOCKER_HOST,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
DEFAULT_TLS,
|
||||
DEFAULT_TLS_VERIFY,
|
||||
DEFAULT_TLS_HOSTNAME, # TODO: remove
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
DOCKER_COMMON_ARGS,
|
||||
DOCKER_MUTUALLY_EXCLUSIVE,
|
||||
DOCKER_REQUIRED_TOGETHER,
|
||||
DEFAULT_DOCKER_REGISTRY, # TODO: remove
|
||||
is_image_name_id, # TODO: remove
|
||||
is_valid_tag, # TODO: remove
|
||||
sanitize_result,
|
||||
update_tls_hostname,
|
||||
)
|
||||
@@ -63,43 +77,45 @@ def _get_tls_config(fail_function, **kwargs):
|
||||
|
||||
|
||||
def is_using_tls(auth_data):
|
||||
return auth_data['tls_verify'] or auth_data['tls']
|
||||
return auth_data["tls_verify"] or auth_data["tls"]
|
||||
|
||||
|
||||
def get_connect_params(auth_data, fail_function):
|
||||
if is_using_tls(auth_data):
|
||||
auth_data['docker_host'] = auth_data['docker_host'].replace('tcp://', 'https://')
|
||||
auth_data["docker_host"] = auth_data["docker_host"].replace(
|
||||
"tcp://", "https://"
|
||||
)
|
||||
|
||||
result = dict(
|
||||
base_url=auth_data['docker_host'],
|
||||
version=auth_data['api_version'],
|
||||
timeout=auth_data['timeout'],
|
||||
base_url=auth_data["docker_host"],
|
||||
version=auth_data["api_version"],
|
||||
timeout=auth_data["timeout"],
|
||||
)
|
||||
|
||||
if auth_data['tls_verify']:
|
||||
if auth_data["tls_verify"]:
|
||||
# TLS with verification
|
||||
tls_config = dict(
|
||||
verify=True,
|
||||
assert_hostname=auth_data['tls_hostname'],
|
||||
assert_hostname=auth_data["tls_hostname"],
|
||||
fail_function=fail_function,
|
||||
)
|
||||
if auth_data['cert_path'] and auth_data['key_path']:
|
||||
tls_config['client_cert'] = (auth_data['cert_path'], auth_data['key_path'])
|
||||
if auth_data['cacert_path']:
|
||||
tls_config['ca_cert'] = auth_data['cacert_path']
|
||||
result['tls'] = _get_tls_config(**tls_config)
|
||||
elif auth_data['tls']:
|
||||
if auth_data["cert_path"] and auth_data["key_path"]:
|
||||
tls_config["client_cert"] = (auth_data["cert_path"], auth_data["key_path"])
|
||||
if auth_data["cacert_path"]:
|
||||
tls_config["ca_cert"] = auth_data["cacert_path"]
|
||||
result["tls"] = _get_tls_config(**tls_config)
|
||||
elif auth_data["tls"]:
|
||||
# TLS without verification
|
||||
tls_config = dict(
|
||||
verify=False,
|
||||
fail_function=fail_function,
|
||||
)
|
||||
if auth_data['cert_path'] and auth_data['key_path']:
|
||||
tls_config['client_cert'] = (auth_data['cert_path'], auth_data['key_path'])
|
||||
result['tls'] = _get_tls_config(**tls_config)
|
||||
if auth_data["cert_path"] and auth_data["key_path"]:
|
||||
tls_config["client_cert"] = (auth_data["cert_path"], auth_data["key_path"])
|
||||
result["tls"] = _get_tls_config(**tls_config)
|
||||
|
||||
if auth_data.get('use_ssh_client'):
|
||||
result['use_ssh_client'] = True
|
||||
if auth_data.get("use_ssh_client"):
|
||||
result["use_ssh_client"] = True
|
||||
|
||||
# No TLS
|
||||
return result
|
||||
@@ -107,22 +123,28 @@ def get_connect_params(auth_data, fail_function):
|
||||
|
||||
class AnsibleDockerClientBase(Client):
|
||||
def __init__(self, min_docker_api_version=None):
|
||||
self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
|
||||
self._connect_params = get_connect_params(
|
||||
self.auth_params, fail_function=self.fail
|
||||
)
|
||||
|
||||
try:
|
||||
super(AnsibleDockerClientBase, self).__init__(**self._connect_params)
|
||||
self.docker_api_version_str = self.api_version
|
||||
except MissingRequirementException as exc:
|
||||
self.fail(missing_required_lib(exc.requirement), exception=exc.import_exception)
|
||||
self.fail(
|
||||
missing_required_lib(exc.requirement), exception=exc.import_exception
|
||||
)
|
||||
except APIError as exc:
|
||||
self.fail(f"Docker API error: {exc}")
|
||||
except Exception as exc:
|
||||
self.fail(f"Error connecting: {exc}")
|
||||
|
||||
self.docker_api_version = LooseVersion(self.docker_api_version_str)
|
||||
min_docker_api_version = min_docker_api_version or '1.25'
|
||||
min_docker_api_version = min_docker_api_version or "1.25"
|
||||
if self.docker_api_version < LooseVersion(min_docker_api_version):
|
||||
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
|
||||
self.fail(
|
||||
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
|
||||
)
|
||||
|
||||
def log(self, msg, pretty_print=False):
|
||||
pass
|
||||
@@ -138,16 +160,16 @@ class AnsibleDockerClientBase(Client):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_value(param_name, param_value, env_variable, default_value, type='str'):
|
||||
def _get_value(param_name, param_value, env_variable, default_value, type="str"):
|
||||
if param_value is not None:
|
||||
# take module parameter value
|
||||
if type == 'bool':
|
||||
if type == "bool":
|
||||
if param_value in BOOLEANS_TRUE:
|
||||
return True
|
||||
if param_value in BOOLEANS_FALSE:
|
||||
return False
|
||||
return bool(param_value)
|
||||
if type == 'int':
|
||||
if type == "int":
|
||||
return int(param_value)
|
||||
return param_value
|
||||
|
||||
@@ -155,19 +177,19 @@ class AnsibleDockerClientBase(Client):
|
||||
env_value = os.environ.get(env_variable)
|
||||
if env_value is not None:
|
||||
# take the env variable value
|
||||
if param_name == 'cert_path':
|
||||
return os.path.join(env_value, 'cert.pem')
|
||||
if param_name == 'cacert_path':
|
||||
return os.path.join(env_value, 'ca.pem')
|
||||
if param_name == 'key_path':
|
||||
return os.path.join(env_value, 'key.pem')
|
||||
if type == 'bool':
|
||||
if param_name == "cert_path":
|
||||
return os.path.join(env_value, "cert.pem")
|
||||
if param_name == "cacert_path":
|
||||
return os.path.join(env_value, "ca.pem")
|
||||
if param_name == "key_path":
|
||||
return os.path.join(env_value, "key.pem")
|
||||
if type == "bool":
|
||||
if env_value in BOOLEANS_TRUE:
|
||||
return True
|
||||
if env_value in BOOLEANS_FALSE:
|
||||
return False
|
||||
return bool(env_value)
|
||||
if type == 'int':
|
||||
if type == "int":
|
||||
return int(env_value)
|
||||
return env_value
|
||||
|
||||
@@ -183,7 +205,7 @@ class AnsibleDockerClientBase(Client):
|
||||
# Get authentication credentials.
|
||||
# Precedence: module parameters-> environment variables-> defaults.
|
||||
|
||||
self.log('Getting credentials')
|
||||
self.log("Getting credentials")
|
||||
|
||||
client_params = self._get_params()
|
||||
|
||||
@@ -192,44 +214,86 @@ class AnsibleDockerClientBase(Client):
|
||||
params[key] = client_params.get(key)
|
||||
|
||||
result = dict(
|
||||
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
|
||||
DEFAULT_DOCKER_HOST, type='str'),
|
||||
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
|
||||
'DOCKER_TLS_HOSTNAME', None, type='str'),
|
||||
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
|
||||
'auto', type='str'),
|
||||
cacert_path=self._get_value('cacert_path', params['ca_path'], 'DOCKER_CERT_PATH', None, type='str'),
|
||||
cert_path=self._get_value('cert_path', params['client_cert'], 'DOCKER_CERT_PATH', None, type='str'),
|
||||
key_path=self._get_value('key_path', params['client_key'], 'DOCKER_CERT_PATH', None, type='str'),
|
||||
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS, type='bool'),
|
||||
tls_verify=self._get_value('validate_certs', params['validate_certs'], 'DOCKER_TLS_VERIFY',
|
||||
DEFAULT_TLS_VERIFY, type='bool'),
|
||||
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
|
||||
DEFAULT_TIMEOUT_SECONDS, type='int'),
|
||||
use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False, type='bool'),
|
||||
docker_host=self._get_value(
|
||||
"docker_host",
|
||||
params["docker_host"],
|
||||
"DOCKER_HOST",
|
||||
DEFAULT_DOCKER_HOST,
|
||||
type="str",
|
||||
),
|
||||
tls_hostname=self._get_value(
|
||||
"tls_hostname",
|
||||
params["tls_hostname"],
|
||||
"DOCKER_TLS_HOSTNAME",
|
||||
None,
|
||||
type="str",
|
||||
),
|
||||
api_version=self._get_value(
|
||||
"api_version",
|
||||
params["api_version"],
|
||||
"DOCKER_API_VERSION",
|
||||
"auto",
|
||||
type="str",
|
||||
),
|
||||
cacert_path=self._get_value(
|
||||
"cacert_path", params["ca_path"], "DOCKER_CERT_PATH", None, type="str"
|
||||
),
|
||||
cert_path=self._get_value(
|
||||
"cert_path", params["client_cert"], "DOCKER_CERT_PATH", None, type="str"
|
||||
),
|
||||
key_path=self._get_value(
|
||||
"key_path", params["client_key"], "DOCKER_CERT_PATH", None, type="str"
|
||||
),
|
||||
tls=self._get_value(
|
||||
"tls", params["tls"], "DOCKER_TLS", DEFAULT_TLS, type="bool"
|
||||
),
|
||||
tls_verify=self._get_value(
|
||||
"validate_certs",
|
||||
params["validate_certs"],
|
||||
"DOCKER_TLS_VERIFY",
|
||||
DEFAULT_TLS_VERIFY,
|
||||
type="bool",
|
||||
),
|
||||
timeout=self._get_value(
|
||||
"timeout",
|
||||
params["timeout"],
|
||||
"DOCKER_TIMEOUT",
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
type="int",
|
||||
),
|
||||
use_ssh_client=self._get_value(
|
||||
"use_ssh_client", params["use_ssh_client"], None, False, type="bool"
|
||||
),
|
||||
)
|
||||
|
||||
def depr(*args, **kwargs):
|
||||
self.deprecate(*args, **kwargs)
|
||||
|
||||
update_tls_hostname(result, old_behavior=True, deprecate_function=depr, uses_tls=is_using_tls(result))
|
||||
update_tls_hostname(
|
||||
result,
|
||||
old_behavior=True,
|
||||
deprecate_function=depr,
|
||||
uses_tls=is_using_tls(result),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _handle_ssl_error(self, error):
|
||||
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
|
||||
if match:
|
||||
hostname = self.auth_params['tls_hostname']
|
||||
self.fail(f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
|
||||
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
|
||||
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
|
||||
"setting the `tls` parameter to true.")
|
||||
hostname = self.auth_params["tls_hostname"]
|
||||
self.fail(
|
||||
f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
|
||||
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
|
||||
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
|
||||
"setting the `tls` parameter to true."
|
||||
)
|
||||
self.fail(f"SSL Exception: {error}")
|
||||
|
||||
def get_container_by_id(self, container_id):
|
||||
try:
|
||||
self.log(f"Inspecting container Id {container_id}")
|
||||
result = self.get_json('/containers/{0}/json', container_id)
|
||||
result = self.get_json("/containers/{0}/json", container_id)
|
||||
self.log("Completed container inspection")
|
||||
return result
|
||||
except NotFound as dummy:
|
||||
@@ -238,34 +302,37 @@ class AnsibleDockerClientBase(Client):
|
||||
self.fail(f"Error inspecting container: {exc}")
|
||||
|
||||
def get_container(self, name=None):
|
||||
'''
|
||||
"""
|
||||
Lookup a container and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
search_name = name
|
||||
if not name.startswith('/'):
|
||||
search_name = '/' + name
|
||||
if not name.startswith("/"):
|
||||
search_name = "/" + name
|
||||
|
||||
result = None
|
||||
try:
|
||||
params = {
|
||||
'limit': -1,
|
||||
'all': 1,
|
||||
'size': 0,
|
||||
'trunc_cmd': 0,
|
||||
"limit": -1,
|
||||
"all": 1,
|
||||
"size": 0,
|
||||
"trunc_cmd": 0,
|
||||
}
|
||||
containers = self.get_json("/containers/json", params=params)
|
||||
for container in containers:
|
||||
self.log(f"testing container: {container['Names']}")
|
||||
if isinstance(container['Names'], list) and search_name in container['Names']:
|
||||
if (
|
||||
isinstance(container["Names"], list)
|
||||
and search_name in container["Names"]
|
||||
):
|
||||
result = container
|
||||
break
|
||||
if container['Id'].startswith(name):
|
||||
if container["Id"].startswith(name):
|
||||
result = container
|
||||
break
|
||||
if container['Id'] == name:
|
||||
if container["Id"] == name:
|
||||
result = container
|
||||
break
|
||||
except SSLError as exc:
|
||||
@@ -276,12 +343,12 @@ class AnsibleDockerClientBase(Client):
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
return self.get_container_by_id(result['Id'])
|
||||
return self.get_container_by_id(result["Id"])
|
||||
|
||||
def get_network(self, name=None, network_id=None):
|
||||
'''
|
||||
"""
|
||||
Lookup a network and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if name is None and network_id is None:
|
||||
return None
|
||||
|
||||
@@ -292,10 +359,10 @@ class AnsibleDockerClientBase(Client):
|
||||
networks = self.get_json("/networks")
|
||||
for network in networks:
|
||||
self.log(f"testing network: {network['Name']}")
|
||||
if name == network['Name']:
|
||||
if name == network["Name"]:
|
||||
result = network
|
||||
break
|
||||
if network['Id'].startswith(name):
|
||||
if network["Id"].startswith(name):
|
||||
result = network
|
||||
break
|
||||
except SSLError as exc:
|
||||
@@ -304,12 +371,12 @@ class AnsibleDockerClientBase(Client):
|
||||
self.fail(f"Error retrieving network list: {exc}")
|
||||
|
||||
if result is not None:
|
||||
network_id = result['Id']
|
||||
network_id = result["Id"]
|
||||
|
||||
if network_id is not None:
|
||||
try:
|
||||
self.log(f"Inspecting network Id {network_id}")
|
||||
result = self.get_json('/networks/{0}', network_id)
|
||||
result = self.get_json("/networks/{0}", network_id)
|
||||
self.log("Completed network inspection")
|
||||
except NotFound as dummy:
|
||||
return None
|
||||
@@ -319,21 +386,21 @@ class AnsibleDockerClientBase(Client):
|
||||
return result
|
||||
|
||||
def _image_lookup(self, name, tag):
|
||||
'''
|
||||
"""
|
||||
Including a tag in the name parameter sent to the Docker SDK for Python images method
|
||||
does not work consistently. Instead, get the result set for name and manually check
|
||||
if the tag exists.
|
||||
'''
|
||||
"""
|
||||
try:
|
||||
params = {
|
||||
'only_ids': 0,
|
||||
'all': 0,
|
||||
"only_ids": 0,
|
||||
"all": 0,
|
||||
}
|
||||
if LooseVersion(self.api_version) < LooseVersion('1.25'):
|
||||
if LooseVersion(self.api_version) < LooseVersion("1.25"):
|
||||
# only use "filter" on API 1.24 and under, as it is deprecated
|
||||
params['filter'] = name
|
||||
params["filter"] = name
|
||||
else:
|
||||
params['filters'] = convert_filters({'reference': name})
|
||||
params["filters"] = convert_filters({"reference": name})
|
||||
images = self.get_json("/images/json", params=params)
|
||||
except Exception as exc:
|
||||
self.fail(f"Error searching for image {name} - {exc}")
|
||||
@@ -343,17 +410,17 @@ class AnsibleDockerClientBase(Client):
|
||||
response = images
|
||||
images = []
|
||||
for image in response:
|
||||
tags = image.get('RepoTags')
|
||||
digests = image.get('RepoDigests')
|
||||
tags = image.get("RepoTags")
|
||||
digests = image.get("RepoDigests")
|
||||
if (tags and lookup in tags) or (digests and lookup_digest in digests):
|
||||
images = [image]
|
||||
break
|
||||
return images
|
||||
|
||||
def find_image(self, name, tag):
|
||||
'''
|
||||
"""
|
||||
Lookup an image (by name and tag) and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
|
||||
@@ -362,14 +429,14 @@ class AnsibleDockerClientBase(Client):
|
||||
if not images:
|
||||
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
|
||||
registry, repo_name = auth.resolve_repository_name(name)
|
||||
if registry == 'docker.io':
|
||||
if registry == "docker.io":
|
||||
# If docker.io is explicitly there in name, the image
|
||||
# is not found in some cases (#41509)
|
||||
self.log(f"Check for docker.io image: {repo_name}")
|
||||
images = self._image_lookup(repo_name, tag)
|
||||
if not images and repo_name.startswith('library/'):
|
||||
if not images and repo_name.startswith("library/"):
|
||||
# Sometimes library/xxx images are not found
|
||||
lookup = repo_name[len('library/'):]
|
||||
lookup = repo_name[len("library/") :]
|
||||
self.log(f"Check for docker.io image: {lookup}")
|
||||
images = self._image_lookup(lookup, tag)
|
||||
if not images:
|
||||
@@ -379,7 +446,7 @@ class AnsibleDockerClientBase(Client):
|
||||
lookup = f"{registry}/{repo_name}"
|
||||
self.log(f"Check for docker.io image: {lookup}")
|
||||
images = self._image_lookup(lookup, tag)
|
||||
if not images and '/' not in repo_name:
|
||||
if not images and "/" not in repo_name:
|
||||
# This seems to be happening with podman-docker
|
||||
# (https://github.com/ansible-collections/community.docker/issues/291)
|
||||
lookup = f"{registry}/library/{repo_name}"
|
||||
@@ -391,7 +458,7 @@ class AnsibleDockerClientBase(Client):
|
||||
|
||||
if len(images) == 1:
|
||||
try:
|
||||
return self.get_json('/images/{0}/json', images[0]['Id'])
|
||||
return self.get_json("/images/{0}/json", images[0]["Id"])
|
||||
except NotFound:
|
||||
self.log(f"Image {name}:{tag} not found.")
|
||||
return None
|
||||
@@ -402,15 +469,15 @@ class AnsibleDockerClientBase(Client):
|
||||
return None
|
||||
|
||||
def find_image_by_id(self, image_id, accept_missing_image=False):
|
||||
'''
|
||||
"""
|
||||
Lookup an image (by ID) and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if not image_id:
|
||||
return None
|
||||
|
||||
self.log(f"Find image {image_id} (by ID)")
|
||||
try:
|
||||
return self.get_json('/images/{0}/json', image_id)
|
||||
return self.get_json("/images/{0}/json", image_id)
|
||||
except NotFound as exc:
|
||||
if not accept_missing_image:
|
||||
self.fail(f"Error inspecting image ID {image_id} - {exc}")
|
||||
@@ -420,37 +487,42 @@ class AnsibleDockerClientBase(Client):
|
||||
self.fail(f"Error inspecting image ID {image_id} - {exc}")
|
||||
|
||||
def pull_image(self, name, tag="latest", platform=None):
|
||||
'''
|
||||
"""
|
||||
Pull an image
|
||||
'''
|
||||
"""
|
||||
self.log(f"Pulling image {name}:{tag}")
|
||||
old_tag = self.find_image(name, tag)
|
||||
try:
|
||||
repository, image_tag = parse_repository_tag(name)
|
||||
registry, repo_name = auth.resolve_repository_name(repository)
|
||||
params = {
|
||||
'tag': tag or image_tag or 'latest',
|
||||
'fromImage': repository,
|
||||
"tag": tag or image_tag or "latest",
|
||||
"fromImage": repository,
|
||||
}
|
||||
if platform is not None:
|
||||
params['platform'] = platform
|
||||
params["platform"] = platform
|
||||
|
||||
headers = {}
|
||||
header = auth.get_config_header(self, registry)
|
||||
if header:
|
||||
headers['X-Registry-Auth'] = header
|
||||
headers["X-Registry-Auth"] = header
|
||||
|
||||
response = self._post(
|
||||
self._url('/images/create'), params=params, headers=headers,
|
||||
stream=True, timeout=None
|
||||
self._url("/images/create"),
|
||||
params=params,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
timeout=None,
|
||||
)
|
||||
self._raise_for_status(response)
|
||||
for line in self._stream_helper(response, decode=True):
|
||||
self.log(line, pretty_print=True)
|
||||
if line.get('error'):
|
||||
if line.get('errorDetail'):
|
||||
error_detail = line.get('errorDetail')
|
||||
self.fail(f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}")
|
||||
if line.get("error"):
|
||||
if line.get("errorDetail"):
|
||||
error_detail = line.get("errorDetail")
|
||||
self.fail(
|
||||
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
|
||||
)
|
||||
else:
|
||||
self.fail(f"Error pulling {name} - {line.get('error')}")
|
||||
except Exception as exc:
|
||||
@@ -463,10 +535,20 @@ class AnsibleDockerClientBase(Client):
|
||||
|
||||
class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
|
||||
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
|
||||
required_together=None, required_if=None, required_one_of=None, required_by=None,
|
||||
min_docker_api_version=None, option_minimal_versions=None,
|
||||
option_minimal_versions_ignore_params=None, fail_results=None):
|
||||
def __init__(
|
||||
self,
|
||||
argument_spec=None,
|
||||
supports_check_mode=False,
|
||||
mutually_exclusive=None,
|
||||
required_together=None,
|
||||
required_if=None,
|
||||
required_one_of=None,
|
||||
required_by=None,
|
||||
min_docker_api_version=None,
|
||||
option_minimal_versions=None,
|
||||
option_minimal_versions_ignore_params=None,
|
||||
fail_results=None,
|
||||
):
|
||||
|
||||
# Modules can put information in here which will always be returned
|
||||
# in case client.fail() is called.
|
||||
@@ -498,20 +580,26 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
required_by=required_by or {},
|
||||
)
|
||||
|
||||
self.debug = self.module.params.get('debug')
|
||||
self.debug = self.module.params.get("debug")
|
||||
self.check_mode = self.module.check_mode
|
||||
|
||||
super(AnsibleDockerClient, self).__init__(min_docker_api_version=min_docker_api_version)
|
||||
super(AnsibleDockerClient, self).__init__(
|
||||
min_docker_api_version=min_docker_api_version
|
||||
)
|
||||
|
||||
if option_minimal_versions is not None:
|
||||
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
|
||||
self._get_minimal_versions(
|
||||
option_minimal_versions, option_minimal_versions_ignore_params
|
||||
)
|
||||
|
||||
def fail(self, msg, **kwargs):
|
||||
self.fail_results.update(kwargs)
|
||||
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
|
||||
|
||||
def deprecate(self, msg, version=None, date=None, collection_name=None):
|
||||
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
|
||||
self.module.deprecate(
|
||||
msg, version=version, date=date, collection_name=collection_name
|
||||
)
|
||||
|
||||
def _get_params(self):
|
||||
return self.module.params
|
||||
@@ -528,33 +616,38 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
for option, data in self.option_minimal_versions.items():
|
||||
# Test whether option is supported, and store result
|
||||
support_docker_api = True
|
||||
if 'docker_api_version' in data:
|
||||
support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version'])
|
||||
data['supported'] = support_docker_api
|
||||
if "docker_api_version" in data:
|
||||
support_docker_api = self.docker_api_version >= LooseVersion(
|
||||
data["docker_api_version"]
|
||||
)
|
||||
data["supported"] = support_docker_api
|
||||
# Fail if option is not supported but used
|
||||
if not data['supported']:
|
||||
if not data["supported"]:
|
||||
# Test whether option is specified
|
||||
if 'detect_usage' in data:
|
||||
used = data['detect_usage'](self)
|
||||
if "detect_usage" in data:
|
||||
used = data["detect_usage"](self)
|
||||
else:
|
||||
used = self.module.params.get(option) is not None
|
||||
if used and 'default' in self.module.argument_spec[option]:
|
||||
used = self.module.params[option] != self.module.argument_spec[option]['default']
|
||||
if used and "default" in self.module.argument_spec[option]:
|
||||
used = (
|
||||
self.module.params[option]
|
||||
!= self.module.argument_spec[option]["default"]
|
||||
)
|
||||
if used:
|
||||
# If the option is used, compose error message.
|
||||
if 'usage_msg' in data:
|
||||
usg = data['usage_msg']
|
||||
if "usage_msg" in data:
|
||||
usg = data["usage_msg"]
|
||||
else:
|
||||
usg = f'set {option} option'
|
||||
usg = f"set {option} option"
|
||||
if not support_docker_api:
|
||||
msg = f"Docker API version is {self.docker_api_version_str}. Minimum version required is {data['docker_api_version']} to {usg}."
|
||||
else:
|
||||
# should not happen
|
||||
msg = f'Cannot {usg} with your configuration.'
|
||||
msg = f"Cannot {usg} with your configuration."
|
||||
self.fail(msg)
|
||||
|
||||
def report_warnings(self, result, warnings_key=None):
|
||||
'''
|
||||
"""
|
||||
Checks result of client operation for warnings, and if present, outputs them.
|
||||
|
||||
warnings_key should be a list of keys used to crawl the result dictionary.
|
||||
@@ -565,15 +658,15 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
|
||||
In most cases (if warnings are returned at all), warnings_key should be
|
||||
['Warnings'] or ['Warning']. The default value (if not specified) is ['Warnings'].
|
||||
'''
|
||||
"""
|
||||
if warnings_key is None:
|
||||
warnings_key = ['Warnings']
|
||||
warnings_key = ["Warnings"]
|
||||
for key in warnings_key:
|
||||
if not isinstance(result, Mapping):
|
||||
return
|
||||
result = result.get(key)
|
||||
if isinstance(result, Sequence):
|
||||
for warning in result:
|
||||
self.module.warn(f'Docker warning: {warning}')
|
||||
self.module.warn(f"Docker warning: {warning}")
|
||||
elif isinstance(result, str) and result:
|
||||
self.module.warn(f'Docker warning: {result}')
|
||||
self.module.warn(f"Docker warning: {result}")
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import abc
|
||||
import json
|
||||
import shlex
|
||||
@@ -12,11 +11,9 @@ import shlex
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.auth import resolve_repository_name
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
|
||||
resolve_repository_name,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
|
||||
DEFAULT_DOCKER_HOST,
|
||||
DEFAULT_TLS,
|
||||
@@ -25,20 +22,35 @@ from ansible_collections.community.docker.plugins.module_utils.util import ( #
|
||||
DOCKER_REQUIRED_TOGETHER,
|
||||
sanitize_result,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
DOCKER_COMMON_ARGS = dict(
|
||||
docker_cli=dict(type='path'),
|
||||
docker_host=dict(type='str', fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
|
||||
tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
|
||||
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
|
||||
ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']),
|
||||
client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
|
||||
client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
|
||||
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
|
||||
validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
|
||||
docker_cli=dict(type="path"),
|
||||
docker_host=dict(
|
||||
type="str", fallback=(env_fallback, ["DOCKER_HOST"]), aliases=["docker_url"]
|
||||
),
|
||||
tls_hostname=dict(type="str", fallback=(env_fallback, ["DOCKER_TLS_HOSTNAME"])),
|
||||
api_version=dict(
|
||||
type="str",
|
||||
default="auto",
|
||||
fallback=(env_fallback, ["DOCKER_API_VERSION"]),
|
||||
aliases=["docker_api_version"],
|
||||
),
|
||||
ca_path=dict(type="path", aliases=["ca_cert", "tls_ca_cert", "cacert_path"]),
|
||||
client_cert=dict(type="path", aliases=["tls_client_cert", "cert_path"]),
|
||||
client_key=dict(type="path", aliases=["tls_client_key", "key_path"]),
|
||||
tls=dict(type="bool", default=DEFAULT_TLS, fallback=(env_fallback, ["DOCKER_TLS"])),
|
||||
validate_certs=dict(
|
||||
type="bool",
|
||||
default=DEFAULT_TLS_VERIFY,
|
||||
fallback=(env_fallback, ["DOCKER_TLS_VERIFY"]),
|
||||
aliases=["tls_verify"],
|
||||
),
|
||||
# debug=dict(type='bool', default=False),
|
||||
cli_context=dict(type='str'),
|
||||
cli_context=dict(type="str"),
|
||||
)
|
||||
|
||||
|
||||
@@ -47,55 +59,71 @@ class DockerException(Exception):
|
||||
|
||||
|
||||
class AnsibleDockerClientBase(object):
|
||||
def __init__(self, common_args, min_docker_api_version=None, needs_api_version=True):
|
||||
def __init__(
|
||||
self, common_args, min_docker_api_version=None, needs_api_version=True
|
||||
):
|
||||
self._environment = {}
|
||||
if common_args['tls_hostname']:
|
||||
self._environment['DOCKER_TLS_HOSTNAME'] = common_args['tls_hostname']
|
||||
if common_args['api_version'] and common_args['api_version'] != 'auto':
|
||||
self._environment['DOCKER_API_VERSION'] = common_args['api_version']
|
||||
self._cli = common_args.get('docker_cli')
|
||||
if common_args["tls_hostname"]:
|
||||
self._environment["DOCKER_TLS_HOSTNAME"] = common_args["tls_hostname"]
|
||||
if common_args["api_version"] and common_args["api_version"] != "auto":
|
||||
self._environment["DOCKER_API_VERSION"] = common_args["api_version"]
|
||||
self._cli = common_args.get("docker_cli")
|
||||
if self._cli is None:
|
||||
try:
|
||||
self._cli = get_bin_path('docker')
|
||||
self._cli = get_bin_path("docker")
|
||||
except ValueError:
|
||||
self.fail('Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter')
|
||||
self.fail(
|
||||
"Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter"
|
||||
)
|
||||
|
||||
self._cli_base = [self._cli]
|
||||
docker_host = common_args['docker_host']
|
||||
if not docker_host and not common_args['cli_context']:
|
||||
docker_host = common_args["docker_host"]
|
||||
if not docker_host and not common_args["cli_context"]:
|
||||
docker_host = DEFAULT_DOCKER_HOST
|
||||
if docker_host:
|
||||
self._cli_base.extend(['--host', docker_host])
|
||||
if common_args['validate_certs']:
|
||||
self._cli_base.append('--tlsverify')
|
||||
elif common_args['tls']:
|
||||
self._cli_base.append('--tls')
|
||||
if common_args['ca_path']:
|
||||
self._cli_base.extend(['--tlscacert', common_args['ca_path']])
|
||||
if common_args['client_cert']:
|
||||
self._cli_base.extend(['--tlscert', common_args['client_cert']])
|
||||
if common_args['client_key']:
|
||||
self._cli_base.extend(['--tlskey', common_args['client_key']])
|
||||
if common_args['cli_context']:
|
||||
self._cli_base.extend(['--context', common_args['cli_context']])
|
||||
self._cli_base.extend(["--host", docker_host])
|
||||
if common_args["validate_certs"]:
|
||||
self._cli_base.append("--tlsverify")
|
||||
elif common_args["tls"]:
|
||||
self._cli_base.append("--tls")
|
||||
if common_args["ca_path"]:
|
||||
self._cli_base.extend(["--tlscacert", common_args["ca_path"]])
|
||||
if common_args["client_cert"]:
|
||||
self._cli_base.extend(["--tlscert", common_args["client_cert"]])
|
||||
if common_args["client_key"]:
|
||||
self._cli_base.extend(["--tlskey", common_args["client_key"]])
|
||||
if common_args["cli_context"]:
|
||||
self._cli_base.extend(["--context", common_args["cli_context"]])
|
||||
|
||||
# `--format json` was only added as a shorthand for `--format {{ json . }}` in Docker 23.0
|
||||
dummy, self._version, dummy = self.call_cli_json('version', '--format', '{{ json . }}', check_rc=True)
|
||||
dummy, self._version, dummy = self.call_cli_json(
|
||||
"version", "--format", "{{ json . }}", check_rc=True
|
||||
)
|
||||
self._info = None
|
||||
|
||||
if needs_api_version:
|
||||
if not isinstance(self._version.get('Server'), dict) or not isinstance(self._version['Server'].get('ApiVersion'), str):
|
||||
self.fail('Cannot determine Docker Daemon information. Are you maybe using podman instead of docker?')
|
||||
self.docker_api_version_str = to_native(self._version['Server']['ApiVersion'])
|
||||
if not isinstance(self._version.get("Server"), dict) or not isinstance(
|
||||
self._version["Server"].get("ApiVersion"), str
|
||||
):
|
||||
self.fail(
|
||||
"Cannot determine Docker Daemon information. Are you maybe using podman instead of docker?"
|
||||
)
|
||||
self.docker_api_version_str = to_native(
|
||||
self._version["Server"]["ApiVersion"]
|
||||
)
|
||||
self.docker_api_version = LooseVersion(self.docker_api_version_str)
|
||||
min_docker_api_version = min_docker_api_version or '1.25'
|
||||
min_docker_api_version = min_docker_api_version or "1.25"
|
||||
if self.docker_api_version < LooseVersion(min_docker_api_version):
|
||||
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
|
||||
self.fail(
|
||||
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
|
||||
)
|
||||
else:
|
||||
self.docker_api_version_str = None
|
||||
self.docker_api_version = None
|
||||
if min_docker_api_version is not None:
|
||||
self.fail('Internal error: cannot have needs_api_version=False with min_docker_api_version not None')
|
||||
self.fail(
|
||||
"Internal error: cannot have needs_api_version=False with min_docker_api_version not None"
|
||||
)
|
||||
|
||||
def log(self, msg, pretty_print=False):
|
||||
pass
|
||||
@@ -113,7 +141,7 @@ class AnsibleDockerClientBase(object):
|
||||
return self._cli_base + list(args)
|
||||
|
||||
def _compose_cmd_str(self, args):
|
||||
return ' '.join(shlex.quote(a) for a in self._compose_cmd(args))
|
||||
return " ".join(shlex.quote(a) for a in self._compose_cmd(args))
|
||||
|
||||
@abc.abstractmethod
|
||||
def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None):
|
||||
@@ -121,19 +149,21 @@ class AnsibleDockerClientBase(object):
|
||||
|
||||
# def call_cli_json(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False):
|
||||
def call_cli_json(self, *args, **kwargs):
|
||||
warn_on_stderr = kwargs.pop('warn_on_stderr', False)
|
||||
warn_on_stderr = kwargs.pop("warn_on_stderr", False)
|
||||
rc, stdout, stderr = self.call_cli(*args, **kwargs)
|
||||
if warn_on_stderr and stderr:
|
||||
self.warn(to_native(stderr))
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
except Exception as exc:
|
||||
self.fail(f'Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}')
|
||||
self.fail(
|
||||
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}"
|
||||
)
|
||||
return rc, data, stderr
|
||||
|
||||
# def call_cli_json_stream(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False):
|
||||
def call_cli_json_stream(self, *args, **kwargs):
|
||||
warn_on_stderr = kwargs.pop('warn_on_stderr', False)
|
||||
warn_on_stderr = kwargs.pop("warn_on_stderr", False)
|
||||
rc, stdout, stderr = self.call_cli(*args, **kwargs)
|
||||
if warn_on_stderr and stderr:
|
||||
self.warn(to_native(stderr))
|
||||
@@ -141,10 +171,12 @@ class AnsibleDockerClientBase(object):
|
||||
try:
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith(b'{'):
|
||||
if line.startswith(b"{"):
|
||||
result.append(json.loads(line))
|
||||
except Exception as exc:
|
||||
self.fail(f'Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}')
|
||||
self.fail(
|
||||
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}"
|
||||
)
|
||||
return rc, result, stderr
|
||||
|
||||
@abc.abstractmethod
|
||||
@@ -161,26 +193,36 @@ class AnsibleDockerClientBase(object):
|
||||
|
||||
def get_cli_info(self):
|
||||
if self._info is None:
|
||||
dummy, self._info, dummy = self.call_cli_json('info', '--format', '{{ json . }}', check_rc=True)
|
||||
dummy, self._info, dummy = self.call_cli_json(
|
||||
"info", "--format", "{{ json . }}", check_rc=True
|
||||
)
|
||||
return self._info
|
||||
|
||||
def get_client_plugin_info(self, component):
|
||||
cli_info = self.get_cli_info()
|
||||
if not isinstance(cli_info.get('ClientInfo'), dict):
|
||||
self.fail('Cannot determine Docker client information. Are you maybe using podman instead of docker?')
|
||||
for plugin in cli_info['ClientInfo'].get('Plugins') or []:
|
||||
if plugin.get('Name') == component:
|
||||
if not isinstance(cli_info.get("ClientInfo"), dict):
|
||||
self.fail(
|
||||
"Cannot determine Docker client information. Are you maybe using podman instead of docker?"
|
||||
)
|
||||
for plugin in cli_info["ClientInfo"].get("Plugins") or []:
|
||||
if plugin.get("Name") == component:
|
||||
return plugin
|
||||
return None
|
||||
|
||||
def _image_lookup(self, name, tag):
|
||||
'''
|
||||
"""
|
||||
Including a tag in the name parameter sent to the Docker SDK for Python images method
|
||||
does not work consistently. Instead, get the result set for name and manually check
|
||||
if the tag exists.
|
||||
'''
|
||||
"""
|
||||
dummy, images, dummy = self.call_cli_json_stream(
|
||||
'image', 'ls', '--format', '{{ json . }}', '--no-trunc', '--filter', f'reference={name}',
|
||||
"image",
|
||||
"ls",
|
||||
"--format",
|
||||
"{{ json . }}",
|
||||
"--no-trunc",
|
||||
"--filter",
|
||||
f"reference={name}",
|
||||
check_rc=True,
|
||||
)
|
||||
if tag:
|
||||
@@ -189,15 +231,15 @@ class AnsibleDockerClientBase(object):
|
||||
response = images
|
||||
images = []
|
||||
for image in response:
|
||||
if image.get('Tag') == tag or image.get('Digest') == tag:
|
||||
if image.get("Tag") == tag or image.get("Digest") == tag:
|
||||
images = [image]
|
||||
break
|
||||
return images
|
||||
|
||||
def find_image(self, name, tag):
|
||||
'''
|
||||
"""
|
||||
Lookup an image (by name and tag) and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
|
||||
@@ -206,14 +248,14 @@ class AnsibleDockerClientBase(object):
|
||||
if not images:
|
||||
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
|
||||
registry, repo_name = resolve_repository_name(name)
|
||||
if registry == 'docker.io':
|
||||
if registry == "docker.io":
|
||||
# If docker.io is explicitly there in name, the image
|
||||
# is not found in some cases (#41509)
|
||||
self.log(f"Check for docker.io image: {repo_name}")
|
||||
images = self._image_lookup(repo_name, tag)
|
||||
if not images and repo_name.startswith('library/'):
|
||||
if not images and repo_name.startswith("library/"):
|
||||
# Sometimes library/xxx images are not found
|
||||
lookup = repo_name[len('library/'):]
|
||||
lookup = repo_name[len("library/") :]
|
||||
self.log(f"Check for docker.io image: {lookup}")
|
||||
images = self._image_lookup(lookup, tag)
|
||||
if not images:
|
||||
@@ -223,7 +265,7 @@ class AnsibleDockerClientBase(object):
|
||||
lookup = f"{registry}/{repo_name}"
|
||||
self.log(f"Check for docker.io image: {lookup}")
|
||||
images = self._image_lookup(lookup, tag)
|
||||
if not images and '/' not in repo_name:
|
||||
if not images and "/" not in repo_name:
|
||||
# This seems to be happening with podman-docker
|
||||
# (https://github.com/ansible-collections/community.docker/issues/291)
|
||||
lookup = f"{registry}/library/{repo_name}"
|
||||
@@ -234,7 +276,7 @@ class AnsibleDockerClientBase(object):
|
||||
self.fail(f"Daemon returned more than one result for {name}:{tag}")
|
||||
|
||||
if len(images) == 1:
|
||||
rc, image, stderr = self.call_cli_json('image', 'inspect', images[0]['ID'])
|
||||
rc, image, stderr = self.call_cli_json("image", "inspect", images[0]["ID"])
|
||||
if not image:
|
||||
self.log(f"Image {name}:{tag} not found.")
|
||||
return None
|
||||
@@ -246,14 +288,14 @@ class AnsibleDockerClientBase(object):
|
||||
return None
|
||||
|
||||
def find_image_by_id(self, image_id, accept_missing_image=False):
|
||||
'''
|
||||
"""
|
||||
Lookup an image (by ID) and return the inspection results.
|
||||
'''
|
||||
"""
|
||||
if not image_id:
|
||||
return None
|
||||
|
||||
self.log(f"Find image {image_id} (by ID)")
|
||||
rc, image, stderr = self.call_cli_json('image', 'inspect', image_id)
|
||||
rc, image, stderr = self.call_cli_json("image", "inspect", image_id)
|
||||
if not image:
|
||||
if not accept_missing_image:
|
||||
self.fail(f"Error inspecting image ID {image_id} - {to_native(stderr)}")
|
||||
@@ -265,9 +307,19 @@ class AnsibleDockerClientBase(object):
|
||||
|
||||
|
||||
class AnsibleModuleDockerClient(AnsibleDockerClientBase):
|
||||
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
|
||||
required_together=None, required_if=None, required_one_of=None, required_by=None,
|
||||
min_docker_api_version=None, fail_results=None, needs_api_version=True):
|
||||
def __init__(
|
||||
self,
|
||||
argument_spec=None,
|
||||
supports_check_mode=False,
|
||||
mutually_exclusive=None,
|
||||
required_together=None,
|
||||
required_if=None,
|
||||
required_one_of=None,
|
||||
required_by=None,
|
||||
min_docker_api_version=None,
|
||||
fail_results=None,
|
||||
needs_api_version=True,
|
||||
):
|
||||
|
||||
# Modules can put information in here which will always be returned
|
||||
# in case client.fail() is called.
|
||||
@@ -279,7 +331,7 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
|
||||
merged_arg_spec.update(argument_spec)
|
||||
self.arg_spec = merged_arg_spec
|
||||
|
||||
mutually_exclusive_params = [('docker_host', 'cli_context')]
|
||||
mutually_exclusive_params = [("docker_host", "cli_context")]
|
||||
mutually_exclusive_params += DOCKER_MUTUALLY_EXCLUSIVE
|
||||
if mutually_exclusive:
|
||||
mutually_exclusive_params += mutually_exclusive
|
||||
@@ -305,7 +357,9 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
|
||||
|
||||
common_args = dict((k, self.module.params[k]) for k in DOCKER_COMMON_ARGS)
|
||||
super(AnsibleModuleDockerClient, self).__init__(
|
||||
common_args, min_docker_api_version=min_docker_api_version, needs_api_version=needs_api_version,
|
||||
common_args,
|
||||
min_docker_api_version=min_docker_api_version,
|
||||
needs_api_version=needs_api_version,
|
||||
)
|
||||
|
||||
def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None):
|
||||
@@ -333,4 +387,6 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
|
||||
self.module.warn(msg)
|
||||
|
||||
def deprecate(self, msg, version=None, date=None, collection_name=None):
|
||||
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
|
||||
self.module.deprecate(
|
||||
msg, version=version, date=date, collection_name=collection_name
|
||||
)
|
||||
|
||||
+438
-348
File diff suppressed because it is too large
Load Diff
+174
-84
@@ -15,8 +15,10 @@ import stat
|
||||
import tarfile
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, NotFound
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
NotFound,
|
||||
)
|
||||
|
||||
|
||||
class DockerFileCopyError(Exception):
|
||||
@@ -34,52 +36,64 @@ class DockerFileNotFound(DockerFileCopyError):
|
||||
def _put_archive(client, container, path, data):
|
||||
# data can also be file object for streaming. This is because _put uses requests's put().
|
||||
# See https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads
|
||||
url = client._url('/containers/{0}/archive', container)
|
||||
res = client._put(url, params={'path': path}, data=data)
|
||||
url = client._url("/containers/{0}/archive", container)
|
||||
res = client._put(url, params={"path": path}, data=data)
|
||||
client._raise_for_status(res)
|
||||
return res.status_code == 200
|
||||
|
||||
|
||||
def _symlink_tar_creator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
|
||||
def _symlink_tar_creator(
|
||||
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
|
||||
):
|
||||
if not stat.S_ISLNK(file_stat.st_mode):
|
||||
raise DockerUnexpectedError('stat information is not for a symlink')
|
||||
raise DockerUnexpectedError("stat information is not for a symlink")
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode='w|', dereference=False, encoding='utf-8') as tar:
|
||||
with tarfile.open(
|
||||
fileobj=bio, mode="w|", dereference=False, encoding="utf-8"
|
||||
) as tar:
|
||||
# Note that without both name (bytes) and arcname (unicode), this either fails for
|
||||
# Python 2.7, Python 3.5/3.6, or Python 3.7+. Only when passing both (in this
|
||||
# form) it works with Python 2.7, 3.5, 3.6, and 3.7 up to 3.11
|
||||
tarinfo = tar.gettarinfo(b_in_path, arcname=to_text(out_file))
|
||||
tarinfo.uid = user_id
|
||||
tarinfo.uname = ''
|
||||
tarinfo.uname = ""
|
||||
if user_name:
|
||||
tarinfo.uname = user_name
|
||||
tarinfo.gid = group_id
|
||||
tarinfo.gname = ''
|
||||
tarinfo.gname = ""
|
||||
tarinfo.mode &= 0o700
|
||||
if mode is not None:
|
||||
tarinfo.mode = mode
|
||||
if not tarinfo.issym():
|
||||
raise DockerUnexpectedError('stat information is not for a symlink')
|
||||
raise DockerUnexpectedError("stat information is not for a symlink")
|
||||
tar.addfile(tarinfo)
|
||||
return bio.getvalue()
|
||||
|
||||
|
||||
def _symlink_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
|
||||
yield _symlink_tar_creator(b_in_path, file_stat, out_file, user_id, group_id, mode, user_name)
|
||||
def _symlink_tar_generator(
|
||||
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
|
||||
):
|
||||
yield _symlink_tar_creator(
|
||||
b_in_path, file_stat, out_file, user_id, group_id, mode, user_name
|
||||
)
|
||||
|
||||
|
||||
def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
|
||||
def _regular_file_tar_generator(
|
||||
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
|
||||
):
|
||||
if not stat.S_ISREG(file_stat.st_mode):
|
||||
raise DockerUnexpectedError('stat information is not for a regular file')
|
||||
raise DockerUnexpectedError("stat information is not for a regular file")
|
||||
tarinfo = tarfile.TarInfo()
|
||||
tarinfo.name = os.path.splitdrive(to_text(out_file))[1].replace(os.sep, '/').lstrip('/')
|
||||
tarinfo.name = (
|
||||
os.path.splitdrive(to_text(out_file))[1].replace(os.sep, "/").lstrip("/")
|
||||
)
|
||||
tarinfo.mode = (file_stat.st_mode & 0o700) if mode is None else mode
|
||||
tarinfo.uid = user_id
|
||||
tarinfo.gid = group_id
|
||||
tarinfo.size = file_stat.st_size
|
||||
tarinfo.mtime = file_stat.st_mtime
|
||||
tarinfo.type = tarfile.REGTYPE
|
||||
tarinfo.linkname = ''
|
||||
tarinfo.linkname = ""
|
||||
if user_name:
|
||||
tarinfo.uname = user_name
|
||||
|
||||
@@ -89,7 +103,7 @@ def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_i
|
||||
|
||||
size = tarinfo.size
|
||||
total_size += size
|
||||
with open(b_in_path, 'rb') as f:
|
||||
with open(b_in_path, "rb") as f:
|
||||
while size > 0:
|
||||
to_read = min(size, 65536)
|
||||
buf = f.read(to_read)
|
||||
@@ -117,16 +131,20 @@ def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_i
|
||||
yield tarfile.NUL * (tarfile.RECORDSIZE - remainder)
|
||||
|
||||
|
||||
def _regular_content_tar_generator(content, out_file, user_id, group_id, mode, user_name=None):
|
||||
def _regular_content_tar_generator(
|
||||
content, out_file, user_id, group_id, mode, user_name=None
|
||||
):
|
||||
tarinfo = tarfile.TarInfo()
|
||||
tarinfo.name = os.path.splitdrive(to_text(out_file))[1].replace(os.sep, '/').lstrip('/')
|
||||
tarinfo.name = (
|
||||
os.path.splitdrive(to_text(out_file))[1].replace(os.sep, "/").lstrip("/")
|
||||
)
|
||||
tarinfo.mode = mode
|
||||
tarinfo.uid = user_id
|
||||
tarinfo.gid = group_id
|
||||
tarinfo.size = len(content)
|
||||
tarinfo.mtime = int(datetime.datetime.now().timestamp())
|
||||
tarinfo.type = tarfile.REGTYPE
|
||||
tarinfo.linkname = ''
|
||||
tarinfo.linkname = ""
|
||||
if user_name:
|
||||
tarinfo.uname = user_name
|
||||
|
||||
@@ -152,13 +170,22 @@ def _regular_content_tar_generator(content, out_file, user_id, group_id, mode, u
|
||||
yield tarfile.NUL * (tarfile.RECORDSIZE - remainder)
|
||||
|
||||
|
||||
def put_file(client, container, in_path, out_path, user_id, group_id, mode=None, user_name=None, follow_links=False):
|
||||
def put_file(
|
||||
client,
|
||||
container,
|
||||
in_path,
|
||||
out_path,
|
||||
user_id,
|
||||
group_id,
|
||||
mode=None,
|
||||
user_name=None,
|
||||
follow_links=False,
|
||||
):
|
||||
"""Transfer a file from local to Docker container."""
|
||||
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
|
||||
raise DockerFileNotFound(
|
||||
f"file or module does not exist: {to_native(in_path)}")
|
||||
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
|
||||
raise DockerFileNotFound(f"file or module does not exist: {to_native(in_path)}")
|
||||
|
||||
b_in_path = to_bytes(in_path, errors='surrogate_or_strict')
|
||||
b_in_path = to_bytes(in_path, errors="surrogate_or_strict")
|
||||
|
||||
out_dir, out_file = os.path.split(out_path)
|
||||
|
||||
@@ -168,28 +195,53 @@ def put_file(client, container, in_path, out_path, user_id, group_id, mode=None,
|
||||
file_stat = os.lstat(b_in_path)
|
||||
|
||||
if stat.S_ISREG(file_stat.st_mode):
|
||||
stream = _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=mode, user_name=user_name)
|
||||
stream = _regular_file_tar_generator(
|
||||
b_in_path,
|
||||
file_stat,
|
||||
out_file,
|
||||
user_id,
|
||||
group_id,
|
||||
mode=mode,
|
||||
user_name=user_name,
|
||||
)
|
||||
elif stat.S_ISLNK(file_stat.st_mode):
|
||||
stream = _symlink_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=mode, user_name=user_name)
|
||||
stream = _symlink_tar_generator(
|
||||
b_in_path,
|
||||
file_stat,
|
||||
out_file,
|
||||
user_id,
|
||||
group_id,
|
||||
mode=mode,
|
||||
user_name=user_name,
|
||||
)
|
||||
else:
|
||||
file_part = ' referenced by' if follow_links else ''
|
||||
file_part = " referenced by" if follow_links else ""
|
||||
raise DockerFileCopyError(
|
||||
f'File{file_part} {in_path} is neither a regular file nor a symlink (stat mode {oct(file_stat.st_mode)}).')
|
||||
f"File{file_part} {in_path} is neither a regular file nor a symlink (stat mode {oct(file_stat.st_mode)})."
|
||||
)
|
||||
|
||||
ok = _put_archive(client, container, out_dir, stream)
|
||||
if not ok:
|
||||
raise DockerUnexpectedError(f'Unknown error while creating file "{out_path}" in container "{container}".')
|
||||
raise DockerUnexpectedError(
|
||||
f'Unknown error while creating file "{out_path}" in container "{container}".'
|
||||
)
|
||||
|
||||
|
||||
def put_file_content(client, container, content, out_path, user_id, group_id, mode, user_name=None):
|
||||
def put_file_content(
|
||||
client, container, content, out_path, user_id, group_id, mode, user_name=None
|
||||
):
|
||||
"""Transfer a file from local to Docker container."""
|
||||
out_dir, out_file = os.path.split(out_path)
|
||||
|
||||
stream = _regular_content_tar_generator(content, out_file, user_id, group_id, mode, user_name=user_name)
|
||||
stream = _regular_content_tar_generator(
|
||||
content, out_file, user_id, group_id, mode, user_name=user_name
|
||||
)
|
||||
|
||||
ok = _put_archive(client, container, out_dir, stream)
|
||||
if not ok:
|
||||
raise DockerUnexpectedError(f'Unknown error while creating file "{out_path}" in container "{container}".')
|
||||
raise DockerUnexpectedError(
|
||||
f'Unknown error while creating file "{out_path}" in container "{container}".'
|
||||
)
|
||||
|
||||
|
||||
def stat_file(client, container, in_path, follow_links=False, log=None):
|
||||
@@ -208,30 +260,32 @@ def stat_file(client, container, in_path, follow_links=False, log=None):
|
||||
|
||||
while True:
|
||||
if in_path in considered_in_paths:
|
||||
raise DockerFileCopyError(f'Found infinite symbolic link loop when trying to stating "{in_path}"')
|
||||
raise DockerFileCopyError(
|
||||
f'Found infinite symbolic link loop when trying to stating "{in_path}"'
|
||||
)
|
||||
considered_in_paths.add(in_path)
|
||||
|
||||
if log:
|
||||
log(f'FETCH: Stating "{in_path}"')
|
||||
|
||||
response = client._head(
|
||||
client._url('/containers/{0}/archive', container),
|
||||
params={'path': in_path},
|
||||
client._url("/containers/{0}/archive", container),
|
||||
params={"path": in_path},
|
||||
)
|
||||
if response.status_code == 404:
|
||||
return in_path, None, None
|
||||
client._raise_for_status(response)
|
||||
header = response.headers.get('x-docker-container-path-stat')
|
||||
header = response.headers.get("x-docker-container-path-stat")
|
||||
try:
|
||||
stat_data = json.loads(base64.b64decode(header))
|
||||
except Exception as exc:
|
||||
raise DockerUnexpectedError(
|
||||
f'When retrieving information for {in_path} from {container}, obtained header {header!r} that cannot be loaded as JSON: {exc}'
|
||||
f"When retrieving information for {in_path} from {container}, obtained header {header!r} that cannot be loaded as JSON: {exc}"
|
||||
)
|
||||
|
||||
# https://pkg.go.dev/io/fs#FileMode: bit 32 - 5 means ModeSymlink
|
||||
if stat_data['mode'] & (1 << (32 - 5)) != 0:
|
||||
link_target = stat_data['linkTarget']
|
||||
if stat_data["mode"] & (1 << (32 - 5)) != 0:
|
||||
link_target = stat_data["linkTarget"]
|
||||
if not follow_links:
|
||||
return in_path, stat_data, link_target
|
||||
in_path = os.path.join(os.path.split(in_path)[0], link_target)
|
||||
@@ -243,7 +297,7 @@ def stat_file(client, container, in_path, follow_links=False, log=None):
|
||||
class _RawGeneratorFileobj(io.RawIOBase):
|
||||
def __init__(self, stream):
|
||||
self._stream = stream
|
||||
self._buf = b''
|
||||
self._buf = b""
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
@@ -251,7 +305,7 @@ class _RawGeneratorFileobj(io.RawIOBase):
|
||||
def _readinto_from_buf(self, b, index, length):
|
||||
cpy = min(length - index, len(self._buf))
|
||||
if cpy:
|
||||
b[index:index + cpy] = self._buf[:cpy]
|
||||
b[index : index + cpy] = self._buf[:cpy]
|
||||
self._buf = self._buf[cpy:]
|
||||
index += cpy
|
||||
return index
|
||||
@@ -273,38 +327,55 @@ class _RawGeneratorFileobj(io.RawIOBase):
|
||||
|
||||
|
||||
def _stream_generator_to_fileobj(stream):
|
||||
'''Given a generator that generates chunks of bytes, create a readable buffered stream.'''
|
||||
"""Given a generator that generates chunks of bytes, create a readable buffered stream."""
|
||||
raw = _RawGeneratorFileobj(stream)
|
||||
return io.BufferedReader(raw)
|
||||
|
||||
|
||||
def fetch_file_ex(client, container, in_path, process_none, process_regular, process_symlink, process_other, follow_links=False, log=None):
|
||||
def fetch_file_ex(
|
||||
client,
|
||||
container,
|
||||
in_path,
|
||||
process_none,
|
||||
process_regular,
|
||||
process_symlink,
|
||||
process_other,
|
||||
follow_links=False,
|
||||
log=None,
|
||||
):
|
||||
"""Fetch a file (as a tar file entry) from a Docker container to local."""
|
||||
considered_in_paths = set()
|
||||
|
||||
while True:
|
||||
if in_path in considered_in_paths:
|
||||
raise DockerFileCopyError(f'Found infinite symbolic link loop when trying to fetch "{in_path}"')
|
||||
raise DockerFileCopyError(
|
||||
f'Found infinite symbolic link loop when trying to fetch "{in_path}"'
|
||||
)
|
||||
considered_in_paths.add(in_path)
|
||||
|
||||
if log:
|
||||
log(f'FETCH: Fetching "{in_path}"')
|
||||
try:
|
||||
stream = client.get_raw_stream(
|
||||
'/containers/{0}/archive', container,
|
||||
params={'path': in_path},
|
||||
headers={'Accept-Encoding': 'identity'},
|
||||
"/containers/{0}/archive",
|
||||
container,
|
||||
params={"path": in_path},
|
||||
headers={"Accept-Encoding": "identity"},
|
||||
)
|
||||
except NotFound:
|
||||
return process_none(in_path)
|
||||
|
||||
with tarfile.open(fileobj=_stream_generator_to_fileobj(stream), mode='r|') as tar:
|
||||
with tarfile.open(
|
||||
fileobj=_stream_generator_to_fileobj(stream), mode="r|"
|
||||
) as tar:
|
||||
symlink_member = None
|
||||
result = None
|
||||
found = False
|
||||
for member in tar:
|
||||
if found:
|
||||
raise DockerUnexpectedError('Received tarfile contains more than one file!')
|
||||
raise DockerUnexpectedError(
|
||||
"Received tarfile contains more than one file!"
|
||||
)
|
||||
found = True
|
||||
if member.issym():
|
||||
symlink_member = member
|
||||
@@ -316,21 +387,23 @@ def fetch_file_ex(client, container, in_path, process_none, process_regular, pro
|
||||
if symlink_member:
|
||||
if not follow_links:
|
||||
return process_symlink(in_path, symlink_member)
|
||||
in_path = os.path.join(os.path.split(in_path)[0], symlink_member.linkname)
|
||||
in_path = os.path.join(
|
||||
os.path.split(in_path)[0], symlink_member.linkname
|
||||
)
|
||||
if log:
|
||||
log(f'FETCH: Following symbolic link to "{in_path}"')
|
||||
continue
|
||||
if found:
|
||||
return result
|
||||
raise DockerUnexpectedError('Received tarfile is empty!')
|
||||
raise DockerUnexpectedError("Received tarfile is empty!")
|
||||
|
||||
|
||||
def fetch_file(client, container, in_path, out_path, follow_links=False, log=None):
|
||||
b_out_path = to_bytes(out_path, errors='surrogate_or_strict')
|
||||
b_out_path = to_bytes(out_path, errors="surrogate_or_strict")
|
||||
|
||||
def process_none(in_path):
|
||||
raise DockerFileNotFound(
|
||||
f'File {in_path} does not exist in container {container}'
|
||||
f"File {in_path} does not exist in container {container}"
|
||||
)
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
@@ -338,7 +411,7 @@ def fetch_file(client, container, in_path, out_path, follow_links=False, log=Non
|
||||
os.unlink(b_out_path)
|
||||
|
||||
with tar.extractfile(member) as in_f:
|
||||
with open(b_out_path, 'wb') as out_f:
|
||||
with open(b_out_path, "wb") as out_f:
|
||||
shutil.copyfileobj(in_f, out_f)
|
||||
return in_path
|
||||
|
||||
@@ -350,56 +423,71 @@ def fetch_file(client, container, in_path, out_path, follow_links=False, log=Non
|
||||
return in_path
|
||||
|
||||
def process_other(in_path, member):
|
||||
raise DockerFileCopyError(f'Remote file "{in_path}" is not a regular file or a symbolic link')
|
||||
raise DockerFileCopyError(
|
||||
f'Remote file "{in_path}" is not a regular file or a symbolic link'
|
||||
)
|
||||
|
||||
return fetch_file_ex(client, container, in_path, process_none, process_regular, process_symlink, process_other, follow_links=follow_links, log=log)
|
||||
return fetch_file_ex(
|
||||
client,
|
||||
container,
|
||||
in_path,
|
||||
process_none,
|
||||
process_regular,
|
||||
process_symlink,
|
||||
process_other,
|
||||
follow_links=follow_links,
|
||||
log=log,
|
||||
)
|
||||
|
||||
|
||||
def _execute_command(client, container, command, log=None, check_rc=False):
|
||||
if log:
|
||||
log(f'Executing {command} in {container}')
|
||||
log(f"Executing {command} in {container}")
|
||||
|
||||
data = {
|
||||
'Container': container,
|
||||
'User': '',
|
||||
'Privileged': False,
|
||||
'Tty': False,
|
||||
'AttachStdin': False,
|
||||
'AttachStdout': True,
|
||||
'AttachStderr': True,
|
||||
'Cmd': command,
|
||||
"Container": container,
|
||||
"User": "",
|
||||
"Privileged": False,
|
||||
"Tty": False,
|
||||
"AttachStdin": False,
|
||||
"AttachStdout": True,
|
||||
"AttachStderr": True,
|
||||
"Cmd": command,
|
||||
}
|
||||
|
||||
if 'detachKeys' in client._general_configs:
|
||||
data['detachKeys'] = client._general_configs['detachKeys']
|
||||
if "detachKeys" in client._general_configs:
|
||||
data["detachKeys"] = client._general_configs["detachKeys"]
|
||||
|
||||
try:
|
||||
exec_data = client.post_json_to_json('/containers/{0}/exec', container, data=data)
|
||||
exec_data = client.post_json_to_json(
|
||||
"/containers/{0}/exec", container, data=data
|
||||
)
|
||||
except NotFound as e:
|
||||
raise DockerFileCopyError(f'Could not find container "{container}"') from e
|
||||
except APIError as e:
|
||||
if e.response is not None and e.response.status_code == 409:
|
||||
raise DockerFileCopyError(f'Cannot execute command in paused container "{container}"') from e
|
||||
raise DockerFileCopyError(
|
||||
f'Cannot execute command in paused container "{container}"'
|
||||
) from e
|
||||
raise
|
||||
exec_id = exec_data['Id']
|
||||
exec_id = exec_data["Id"]
|
||||
|
||||
data = {
|
||||
'Tty': False,
|
||||
'Detach': False
|
||||
}
|
||||
stdout, stderr = client.post_json_to_stream('/exec/{0}/start', exec_id, stream=False, demux=True, tty=False)
|
||||
data = {"Tty": False, "Detach": False}
|
||||
stdout, stderr = client.post_json_to_stream(
|
||||
"/exec/{0}/start", exec_id, stream=False, demux=True, tty=False
|
||||
)
|
||||
|
||||
result = client.get_json('/exec/{0}/json', exec_id)
|
||||
result = client.get_json("/exec/{0}/json", exec_id)
|
||||
|
||||
rc = result.get('ExitCode') or 0
|
||||
stdout = stdout or b''
|
||||
stderr = stderr or b''
|
||||
rc = result.get("ExitCode") or 0
|
||||
stdout = stdout or b""
|
||||
stderr = stderr or b""
|
||||
|
||||
if log:
|
||||
log(f'Exit code {rc}, stdout {stdout!r}, stderr {stderr!r}')
|
||||
log(f"Exit code {rc}, stdout {stdout!r}, stderr {stderr!r}")
|
||||
|
||||
if check_rc and rc != 0:
|
||||
command_str = ' '.join(command)
|
||||
command_str = " ".join(command)
|
||||
raise DockerUnexpectedError(
|
||||
f'Obtained unexpected exit code {rc} when running "{command_str}" in {container}.\nSTDOUT: {stdout}\nSTDERR: {stderr}'
|
||||
)
|
||||
@@ -408,12 +496,14 @@ def _execute_command(client, container, command, log=None, check_rc=False):
|
||||
|
||||
|
||||
def determine_user_group(client, container, log=None):
|
||||
dummy, stdout, stderr = _execute_command(client, container, ['/bin/sh', '-c', 'id -u && id -g'], check_rc=True, log=log)
|
||||
dummy, stdout, stderr = _execute_command(
|
||||
client, container, ["/bin/sh", "-c", "id -u && id -g"], check_rc=True, log=log
|
||||
)
|
||||
|
||||
stdout_lines = stdout.splitlines()
|
||||
if len(stdout_lines) != 2:
|
||||
raise DockerUnexpectedError(
|
||||
f'Expected two-line output to obtain user and group ID for container {container}, but got {len(stdout_lines)} lines:\n{stdout}'
|
||||
f"Expected two-line output to obtain user and group ID for container {container}, but got {len(stdout_lines)} lines:\n{stdout}"
|
||||
)
|
||||
|
||||
user_id, group_id = stdout_lines
|
||||
|
||||
@@ -10,18 +10,18 @@ import tarfile
|
||||
|
||||
|
||||
class ImageArchiveManifestSummary(object):
|
||||
'''
|
||||
"""
|
||||
Represents data extracted from a manifest.json found in the tar archive output of the
|
||||
"docker image save some:tag > some.tar" command.
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self, image_id, repo_tags):
|
||||
'''
|
||||
"""
|
||||
:param image_id: File name portion of Config entry, e.g. abcde12345 from abcde12345.json
|
||||
:type image_id: str
|
||||
:param repo_tags Docker image names, e.g. ["hello-world:latest"]
|
||||
:type repo_tags: list[str]
|
||||
'''
|
||||
"""
|
||||
|
||||
self.image_id = image_id
|
||||
self.repo_tags = repo_tags
|
||||
@@ -32,7 +32,7 @@ class ImageArchiveInvalidException(Exception):
|
||||
|
||||
|
||||
def api_image_id(archive_image_id):
|
||||
'''
|
||||
"""
|
||||
Accepts an image hash in the format stored in manifest.json, and returns an equivalent identifier
|
||||
that represents the same image hash, but in the format presented by the Docker Engine API.
|
||||
|
||||
@@ -41,13 +41,13 @@ def api_image_id(archive_image_id):
|
||||
|
||||
:returns: Prefixed hash used by REST api
|
||||
:rtype: str
|
||||
'''
|
||||
"""
|
||||
|
||||
return f'sha256:{archive_image_id}'
|
||||
return f"sha256:{archive_image_id}"
|
||||
|
||||
|
||||
def load_archived_image_manifest(archive_path):
|
||||
'''
|
||||
"""
|
||||
Attempts to get image IDs and image names from metadata stored in the image
|
||||
archive tar file.
|
||||
|
||||
@@ -63,17 +63,17 @@ def load_archived_image_manifest(archive_path):
|
||||
|
||||
:return: None, if no file at archive_path, or a list of ImageArchiveManifestSummary objects.
|
||||
:rtype: ImageArchiveManifestSummary
|
||||
'''
|
||||
"""
|
||||
|
||||
try:
|
||||
# FileNotFoundError does not exist in Python 2
|
||||
if not os.path.isfile(archive_path):
|
||||
return None
|
||||
|
||||
with tarfile.open(archive_path, 'r') as tf:
|
||||
with tarfile.open(archive_path, "r") as tf:
|
||||
try:
|
||||
try:
|
||||
with tf.extractfile('manifest.json') as ef:
|
||||
with tf.extractfile("manifest.json") as ef:
|
||||
manifest = json.load(ef)
|
||||
except Exception as exc:
|
||||
raise ImageArchiveInvalidException(
|
||||
@@ -88,7 +88,7 @@ def load_archived_image_manifest(archive_path):
|
||||
result = []
|
||||
for index, meta in enumerate(manifest):
|
||||
try:
|
||||
config_file = meta['Config']
|
||||
config_file = meta["Config"]
|
||||
except KeyError as exc:
|
||||
raise ImageArchiveInvalidException(
|
||||
f"Failed to get Config entry from {index + 1}th manifest in manifest.json: {exc}"
|
||||
@@ -103,23 +103,22 @@ def load_archived_image_manifest(archive_path):
|
||||
f"Failed to extract image id from config file name {config_file}: {exc}"
|
||||
) from exc
|
||||
|
||||
for prefix in (
|
||||
'blobs/sha256/', # Moby 25.0.0, Docker API 1.44
|
||||
):
|
||||
for prefix in ("blobs/sha256/",): # Moby 25.0.0, Docker API 1.44
|
||||
if image_id.startswith(prefix):
|
||||
image_id = image_id[len(prefix):]
|
||||
image_id = image_id[len(prefix) :]
|
||||
|
||||
try:
|
||||
repo_tags = meta['RepoTags']
|
||||
repo_tags = meta["RepoTags"]
|
||||
except KeyError as exc:
|
||||
raise ImageArchiveInvalidException(
|
||||
f"Failed to get RepoTags entry from {index + 1}th manifest in manifest.json: {exc}"
|
||||
) from exc
|
||||
|
||||
result.append(ImageArchiveManifestSummary(
|
||||
image_id=image_id,
|
||||
repo_tags=repo_tags
|
||||
))
|
||||
result.append(
|
||||
ImageArchiveManifestSummary(
|
||||
image_id=image_id, repo_tags=repo_tags
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
except ImageArchiveInvalidException:
|
||||
@@ -132,11 +131,13 @@ def load_archived_image_manifest(archive_path):
|
||||
except ImageArchiveInvalidException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise ImageArchiveInvalidException(f"Failed to open tar file {archive_path}: {exc}") from exc
|
||||
raise ImageArchiveInvalidException(
|
||||
f"Failed to open tar file {archive_path}: {exc}"
|
||||
) from exc
|
||||
|
||||
|
||||
def archived_image_manifest(archive_path):
|
||||
'''
|
||||
"""
|
||||
Attempts to get Image.Id and image name from metadata stored in the image
|
||||
archive tar file.
|
||||
|
||||
@@ -152,7 +153,7 @@ def archived_image_manifest(archive_path):
|
||||
|
||||
:return: None, if no file at archive_path, or the extracted image ID, which will not have a sha256: prefix.
|
||||
:rtype: ImageArchiveManifestSummary
|
||||
'''
|
||||
"""
|
||||
|
||||
results = load_archived_image_manifest(archive_path)
|
||||
if results is None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,5 +8,4 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import selectors # noqa: F401, pylint: disable=unused-import
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import socket as pysocket
|
||||
import struct
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils import socket as docker_socket
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils import (
|
||||
socket as docker_socket,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.socket_helper import (
|
||||
make_unblocking,
|
||||
shutdown_writing,
|
||||
@@ -31,19 +31,21 @@ class DockerSocketHandlerBase(object):
|
||||
self._log = log
|
||||
else:
|
||||
self._log = lambda msg: True
|
||||
self._paramiko_read_workaround = hasattr(sock, 'send_ready') and 'paramiko' in str(type(sock))
|
||||
self._paramiko_read_workaround = hasattr(
|
||||
sock, "send_ready"
|
||||
) and "paramiko" in str(type(sock))
|
||||
|
||||
self._sock = sock
|
||||
self._block_done_callback = None
|
||||
self._block_buffer = []
|
||||
self._eof = False
|
||||
self._read_buffer = b''
|
||||
self._write_buffer = b''
|
||||
self._read_buffer = b""
|
||||
self._write_buffer = b""
|
||||
self._end_of_writing = False
|
||||
|
||||
self._current_stream = None
|
||||
self._current_missing = 0
|
||||
self._current_buffer = b''
|
||||
self._current_buffer = b""
|
||||
|
||||
self._selector = self._selectors.DefaultSelector()
|
||||
self._selector.register(self._sock, self._selectors.EVENT_READ)
|
||||
@@ -70,26 +72,26 @@ class DockerSocketHandlerBase(object):
|
||||
def _read(self):
|
||||
if self._eof:
|
||||
return
|
||||
if hasattr(self._sock, 'recv'):
|
||||
if hasattr(self._sock, "recv"):
|
||||
try:
|
||||
data = self._sock.recv(262144)
|
||||
except Exception as e:
|
||||
# After calling self._sock.shutdown(), OpenSSL's/urllib3's
|
||||
# WrappedSocket seems to eventually raise ZeroReturnError in
|
||||
# case of EOF
|
||||
if 'OpenSSL.SSL.ZeroReturnError' in str(type(e)):
|
||||
if "OpenSSL.SSL.ZeroReturnError" in str(type(e)):
|
||||
self._eof = True
|
||||
return
|
||||
else:
|
||||
raise
|
||||
elif isinstance(self._sock, getattr(pysocket, 'SocketIO')):
|
||||
elif isinstance(self._sock, getattr(pysocket, "SocketIO")):
|
||||
data = self._sock.read()
|
||||
else:
|
||||
data = os.read(self._sock.fileno())
|
||||
if data is None:
|
||||
# no data available
|
||||
return
|
||||
self._log(f'read {len(data)} bytes')
|
||||
self._log(f"read {len(data)} bytes")
|
||||
if len(data) == 0:
|
||||
# Stream EOF
|
||||
self._eof = True
|
||||
@@ -103,10 +105,12 @@ class DockerSocketHandlerBase(object):
|
||||
self._current_missing -= n
|
||||
if self._current_missing == 0:
|
||||
self._add_block(self._current_stream, self._current_buffer)
|
||||
self._current_buffer = b''
|
||||
self._current_buffer = b""
|
||||
if len(self._read_buffer) < 8:
|
||||
break
|
||||
self._current_stream, self._current_missing = struct.unpack('>BxxxL', self._read_buffer[:8])
|
||||
self._current_stream, self._current_missing = struct.unpack(
|
||||
">BxxxL", self._read_buffer[:8]
|
||||
)
|
||||
self._read_buffer = self._read_buffer[8:]
|
||||
if self._current_missing < 0:
|
||||
# Stream EOF (as reported by docker daemon)
|
||||
@@ -116,22 +120,28 @@ class DockerSocketHandlerBase(object):
|
||||
def _handle_end_of_writing(self):
|
||||
if self._end_of_writing and len(self._write_buffer) == 0:
|
||||
self._end_of_writing = False
|
||||
self._log('Shutting socket down for writing')
|
||||
self._log("Shutting socket down for writing")
|
||||
shutdown_writing(self._sock, self._log)
|
||||
|
||||
def _write(self):
|
||||
if len(self._write_buffer) > 0:
|
||||
written = write_to_socket(self._sock, self._write_buffer)
|
||||
self._write_buffer = self._write_buffer[written:]
|
||||
self._log(f'wrote {written} bytes, {len(self._write_buffer)} are left')
|
||||
self._log(f"wrote {written} bytes, {len(self._write_buffer)} are left")
|
||||
if len(self._write_buffer) > 0:
|
||||
self._selector.modify(self._sock, self._selectors.EVENT_READ | self._selectors.EVENT_WRITE)
|
||||
self._selector.modify(
|
||||
self._sock, self._selectors.EVENT_READ | self._selectors.EVENT_WRITE
|
||||
)
|
||||
else:
|
||||
self._selector.modify(self._sock, self._selectors.EVENT_READ)
|
||||
self._handle_end_of_writing()
|
||||
|
||||
def select(self, timeout=None, _internal_recursion=False):
|
||||
if not _internal_recursion and self._paramiko_read_workaround and len(self._write_buffer) > 0:
|
||||
if (
|
||||
not _internal_recursion
|
||||
and self._paramiko_read_workaround
|
||||
and len(self._write_buffer) > 0
|
||||
):
|
||||
# When the SSH transport is used, Docker SDK for Python internally uses Paramiko, whose
|
||||
# Channel object supports select(), but only for reading
|
||||
# (https://github.com/paramiko/paramiko/issues/695).
|
||||
@@ -147,13 +157,13 @@ class DockerSocketHandlerBase(object):
|
||||
return True
|
||||
if timeout is not None:
|
||||
timeout -= PARAMIKO_POLL_TIMEOUT
|
||||
self._log(f'select... ({timeout})')
|
||||
self._log(f"select... ({timeout})")
|
||||
events = self._selector.select(timeout)
|
||||
for key, event in events:
|
||||
if key.fileobj == self._sock:
|
||||
ev_read = event & self._selectors.EVENT_READ != 0
|
||||
ev_write = event & self._selectors.EVENT_WRITE != 0
|
||||
self._log(f'select event read:{ev_read} write:{ev_write}')
|
||||
self._log(f"select event read:{ev_read} write:{ev_write}")
|
||||
if event & self._selectors.EVENT_READ != 0:
|
||||
self._read()
|
||||
if event & self._selectors.EVENT_WRITE != 0:
|
||||
@@ -182,14 +192,14 @@ class DockerSocketHandlerBase(object):
|
||||
elif stream_id == docker_socket.STDERR:
|
||||
stderr.append(data)
|
||||
else:
|
||||
raise ValueError(f'{stream_id} is not a valid stream ID')
|
||||
raise ValueError(f"{stream_id} is not a valid stream ID")
|
||||
|
||||
self.end_of_writing()
|
||||
|
||||
self.set_block_done_callback(append_block)
|
||||
while not self._eof:
|
||||
self.select()
|
||||
return b''.join(stdout), b''.join(stderr)
|
||||
return b"".join(stdout), b"".join(stderr)
|
||||
|
||||
def write(self, str):
|
||||
self._write_buffer += str
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import os.path
|
||||
@@ -12,17 +11,25 @@ import socket as pysocket
|
||||
|
||||
|
||||
def make_file_unblocking(file):
|
||||
fcntl.fcntl(file.fileno(), fcntl.F_SETFL, fcntl.fcntl(file.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
|
||||
fcntl.fcntl(
|
||||
file.fileno(),
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(file.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK,
|
||||
)
|
||||
|
||||
|
||||
def make_file_blocking(file):
|
||||
fcntl.fcntl(file.fileno(), fcntl.F_SETFL, fcntl.fcntl(file.fileno(), fcntl.F_GETFL) & ~os.O_NONBLOCK)
|
||||
fcntl.fcntl(
|
||||
file.fileno(),
|
||||
fcntl.F_SETFL,
|
||||
fcntl.fcntl(file.fileno(), fcntl.F_GETFL) & ~os.O_NONBLOCK,
|
||||
)
|
||||
|
||||
|
||||
def make_unblocking(sock):
|
||||
if hasattr(sock, '_sock'):
|
||||
if hasattr(sock, "_sock"):
|
||||
sock._sock.setblocking(0)
|
||||
elif hasattr(sock, 'setblocking'):
|
||||
elif hasattr(sock, "setblocking"):
|
||||
sock.setblocking(0)
|
||||
else:
|
||||
make_file_unblocking(sock)
|
||||
@@ -37,27 +44,27 @@ def shutdown_writing(sock, log=_empty_writer):
|
||||
# a close_notify TLS alert without completely shutting down the connection.
|
||||
# Calling sock.shutdown(pysocket.SHUT_WR) simply turns of TLS encryption and from that
|
||||
# point on the raw encrypted data is returned when sock.recv() is called. :-(
|
||||
if hasattr(sock, 'shutdown_write'):
|
||||
if hasattr(sock, "shutdown_write"):
|
||||
sock.shutdown_write()
|
||||
elif hasattr(sock, 'shutdown'):
|
||||
elif hasattr(sock, "shutdown"):
|
||||
try:
|
||||
sock.shutdown(pysocket.SHUT_WR)
|
||||
except TypeError as e:
|
||||
# probably: "TypeError: shutdown() takes 1 positional argument but 2 were given"
|
||||
log(f'Shutting down for writing not possible; trying shutdown instead: {e}')
|
||||
log(f"Shutting down for writing not possible; trying shutdown instead: {e}")
|
||||
sock.shutdown()
|
||||
elif isinstance(sock, getattr(pysocket, 'SocketIO')):
|
||||
elif isinstance(sock, getattr(pysocket, "SocketIO")):
|
||||
sock._sock.shutdown(pysocket.SHUT_WR)
|
||||
else:
|
||||
log('No idea how to signal end of writing')
|
||||
log("No idea how to signal end of writing")
|
||||
|
||||
|
||||
def write_to_socket(sock, data):
|
||||
if hasattr(sock, '_send_until_done'):
|
||||
if hasattr(sock, "_send_until_done"):
|
||||
# WrappedSocket (urllib3/contrib/pyopenssl) does not have `send`, but
|
||||
# only `sendall`, which uses `_send_until_done` under the hood.
|
||||
return sock._send_until_done(data)
|
||||
elif hasattr(sock, 'send'):
|
||||
elif hasattr(sock, "send"):
|
||||
return sock.send(data)
|
||||
else:
|
||||
return os.write(sock.fileno(), data)
|
||||
|
||||
@@ -5,19 +5,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import json
|
||||
from time import sleep
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import APIError, NotFound
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
|
||||
pass
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import AnsibleDockerClient
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
AnsibleDockerClient,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
@@ -41,8 +44,8 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
if info:
|
||||
json_str = json.dumps(info, ensure_ascii=False)
|
||||
swarm_info = json.loads(json_str)
|
||||
if swarm_info['Swarm']['NodeID']:
|
||||
return swarm_info['Swarm']['NodeID']
|
||||
if swarm_info["Swarm"]["NodeID"]:
|
||||
return swarm_info["Swarm"]["NodeID"]
|
||||
return None
|
||||
|
||||
def check_if_swarm_node(self, node_id=None):
|
||||
@@ -66,9 +69,13 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
if info:
|
||||
json_str = json.dumps(info, ensure_ascii=False)
|
||||
swarm_info = json.loads(json_str)
|
||||
if swarm_info['Swarm']['NodeID']:
|
||||
if swarm_info["Swarm"]["NodeID"]:
|
||||
return True
|
||||
if swarm_info['Swarm']['LocalNodeState'] in ('active', 'pending', 'locked'):
|
||||
if swarm_info["Swarm"]["LocalNodeState"] in (
|
||||
"active",
|
||||
"pending",
|
||||
"locked",
|
||||
):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
@@ -77,7 +84,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
except APIError:
|
||||
return
|
||||
|
||||
if node_info['ID'] is not None:
|
||||
if node_info["ID"] is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -100,7 +107,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
If host is not a swarm manager then Ansible task on this host should end with 'failed' state
|
||||
"""
|
||||
if not self.check_if_swarm_manager():
|
||||
self.fail("Error running docker swarm module: must run on swarm manager node")
|
||||
self.fail(
|
||||
"Error running docker swarm module: must run on swarm manager node"
|
||||
)
|
||||
|
||||
def check_if_swarm_worker(self):
|
||||
"""
|
||||
@@ -136,7 +145,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
if retry > 0:
|
||||
sleep(5)
|
||||
node_info = self.get_node_inspect(node_id=node_id)
|
||||
if node_info['Status']['State'] == 'down':
|
||||
if node_info["Status"]["State"] == "down":
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -160,7 +169,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
node_info = self.inspect_node(node_id=node_id)
|
||||
except APIError as exc:
|
||||
if exc.status_code == 503:
|
||||
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
|
||||
self.fail(
|
||||
"Cannot inspect node: To inspect node execute module on Swarm Manager"
|
||||
)
|
||||
if exc.status_code == 404:
|
||||
if skip_missing:
|
||||
return None
|
||||
@@ -171,16 +182,19 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
json_str = json.dumps(node_info, ensure_ascii=False)
|
||||
node_info = json.loads(json_str)
|
||||
|
||||
if 'ManagerStatus' in node_info:
|
||||
if node_info['ManagerStatus'].get('Leader'):
|
||||
if "ManagerStatus" in node_info:
|
||||
if node_info["ManagerStatus"].get("Leader"):
|
||||
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
|
||||
# Check moby/moby#35437 for details
|
||||
count_colons = node_info['ManagerStatus']['Addr'].count(":")
|
||||
count_colons = node_info["ManagerStatus"]["Addr"].count(":")
|
||||
if count_colons == 1:
|
||||
swarm_leader_ip = node_info['ManagerStatus']['Addr'].split(":", 1)[0] or node_info['Status']['Addr']
|
||||
swarm_leader_ip = (
|
||||
node_info["ManagerStatus"]["Addr"].split(":", 1)[0]
|
||||
or node_info["Status"]["Addr"]
|
||||
)
|
||||
else:
|
||||
swarm_leader_ip = node_info['Status']['Addr']
|
||||
node_info['Status']['Addr'] = swarm_leader_ip
|
||||
swarm_leader_ip = node_info["Status"]["Addr"]
|
||||
node_info["Status"]["Addr"] = swarm_leader_ip
|
||||
return node_info
|
||||
|
||||
def get_all_nodes_inspect(self):
|
||||
@@ -194,7 +208,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
node_info = self.nodes()
|
||||
except APIError as exc:
|
||||
if exc.status_code == 503:
|
||||
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
|
||||
self.fail(
|
||||
"Cannot inspect node: To inspect node execute module on Swarm Manager"
|
||||
)
|
||||
self.fail(f"Error while reading from Swarm manager: {exc}")
|
||||
except Exception as exc:
|
||||
self.fail(f"Error inspecting swarm node: {exc}")
|
||||
@@ -203,7 +219,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
node_info = json.loads(json_str)
|
||||
return node_info
|
||||
|
||||
def get_all_nodes_list(self, output='short'):
|
||||
def get_all_nodes_list(self, output="short"):
|
||||
"""
|
||||
Returns list of nodes registered in Swarm
|
||||
|
||||
@@ -219,22 +235,26 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
if nodes_inspect is None:
|
||||
return None
|
||||
|
||||
if output == 'short':
|
||||
if output == "short":
|
||||
for node in nodes_inspect:
|
||||
nodes_list.append(node['Description']['Hostname'])
|
||||
elif output == 'long':
|
||||
nodes_list.append(node["Description"]["Hostname"])
|
||||
elif output == "long":
|
||||
for node in nodes_inspect:
|
||||
node_property = {}
|
||||
|
||||
node_property.update({'ID': node['ID']})
|
||||
node_property.update({'Hostname': node['Description']['Hostname']})
|
||||
node_property.update({'Status': node['Status']['State']})
|
||||
node_property.update({'Availability': node['Spec']['Availability']})
|
||||
if 'ManagerStatus' in node:
|
||||
if node['ManagerStatus']['Leader'] is True:
|
||||
node_property.update({'Leader': True})
|
||||
node_property.update({'ManagerStatus': node['ManagerStatus']['Reachability']})
|
||||
node_property.update({'EngineVersion': node['Description']['Engine']['EngineVersion']})
|
||||
node_property.update({"ID": node["ID"]})
|
||||
node_property.update({"Hostname": node["Description"]["Hostname"]})
|
||||
node_property.update({"Status": node["Status"]["State"]})
|
||||
node_property.update({"Availability": node["Spec"]["Availability"]})
|
||||
if "ManagerStatus" in node:
|
||||
if node["ManagerStatus"]["Leader"] is True:
|
||||
node_property.update({"Leader": True})
|
||||
node_property.update(
|
||||
{"ManagerStatus": node["ManagerStatus"]["Reachability"]}
|
||||
)
|
||||
node_property.update(
|
||||
{"EngineVersion": node["Description"]["Engine"]["EngineVersion"]}
|
||||
)
|
||||
|
||||
nodes_list.append(node_property)
|
||||
else:
|
||||
@@ -243,10 +263,10 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
return nodes_list
|
||||
|
||||
def get_node_name_by_id(self, nodeid):
|
||||
return self.get_node_inspect(nodeid)['Description']['Hostname']
|
||||
return self.get_node_inspect(nodeid)["Description"]["Hostname"]
|
||||
|
||||
def get_unlock_key(self):
|
||||
if self.docker_py_version < LooseVersion('2.7.0'):
|
||||
if self.docker_py_version < LooseVersion("2.7.0"):
|
||||
return None
|
||||
return super(AnsibleDockerSwarmClient, self).get_unlock_key()
|
||||
|
||||
@@ -268,7 +288,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
|
||||
return None
|
||||
except APIError as exc:
|
||||
if exc.status_code == 503:
|
||||
self.fail("Cannot inspect service: To inspect service execute module on Swarm Manager")
|
||||
self.fail(
|
||||
"Cannot inspect service: To inspect service execute module on Swarm Manager"
|
||||
)
|
||||
self.fail(f"Error inspecting swarm service: {exc}")
|
||||
except Exception as exc:
|
||||
self.fail(f"Error inspecting swarm service: {exc}")
|
||||
|
||||
+134
-93
@@ -4,7 +4,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import timedelta
|
||||
@@ -15,45 +14,64 @@ from ansible.module_utils.common.collections import is_sequence
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
|
||||
DEFAULT_DOCKER_HOST = 'unix:///var/run/docker.sock'
|
||||
DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock"
|
||||
DEFAULT_TLS = False
|
||||
DEFAULT_TLS_VERIFY = False
|
||||
DEFAULT_TLS_HOSTNAME = 'localhost' # deprecated
|
||||
DEFAULT_TLS_HOSTNAME = "localhost" # deprecated
|
||||
DEFAULT_TIMEOUT_SECONDS = 60
|
||||
|
||||
DOCKER_COMMON_ARGS = dict(
|
||||
docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
|
||||
tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
|
||||
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
|
||||
timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])),
|
||||
ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']),
|
||||
client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
|
||||
client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
|
||||
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
|
||||
use_ssh_client=dict(type='bool', default=False),
|
||||
validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
|
||||
debug=dict(type='bool', default=False)
|
||||
docker_host=dict(
|
||||
type="str",
|
||||
default=DEFAULT_DOCKER_HOST,
|
||||
fallback=(env_fallback, ["DOCKER_HOST"]),
|
||||
aliases=["docker_url"],
|
||||
),
|
||||
tls_hostname=dict(type="str", fallback=(env_fallback, ["DOCKER_TLS_HOSTNAME"])),
|
||||
api_version=dict(
|
||||
type="str",
|
||||
default="auto",
|
||||
fallback=(env_fallback, ["DOCKER_API_VERSION"]),
|
||||
aliases=["docker_api_version"],
|
||||
),
|
||||
timeout=dict(
|
||||
type="int",
|
||||
default=DEFAULT_TIMEOUT_SECONDS,
|
||||
fallback=(env_fallback, ["DOCKER_TIMEOUT"]),
|
||||
),
|
||||
ca_path=dict(type="path", aliases=["ca_cert", "tls_ca_cert", "cacert_path"]),
|
||||
client_cert=dict(type="path", aliases=["tls_client_cert", "cert_path"]),
|
||||
client_key=dict(type="path", aliases=["tls_client_key", "key_path"]),
|
||||
tls=dict(type="bool", default=DEFAULT_TLS, fallback=(env_fallback, ["DOCKER_TLS"])),
|
||||
use_ssh_client=dict(type="bool", default=False),
|
||||
validate_certs=dict(
|
||||
type="bool",
|
||||
default=DEFAULT_TLS_VERIFY,
|
||||
fallback=(env_fallback, ["DOCKER_TLS_VERIFY"]),
|
||||
aliases=["tls_verify"],
|
||||
),
|
||||
debug=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
DOCKER_COMMON_ARGS_VARS = dict([
|
||||
[option_name, f'ansible_docker_{option_name}']
|
||||
for option_name in DOCKER_COMMON_ARGS
|
||||
if option_name != 'debug'
|
||||
])
|
||||
DOCKER_COMMON_ARGS_VARS = dict(
|
||||
[
|
||||
[option_name, f"ansible_docker_{option_name}"]
|
||||
for option_name in DOCKER_COMMON_ARGS
|
||||
if option_name != "debug"
|
||||
]
|
||||
)
|
||||
|
||||
DOCKER_MUTUALLY_EXCLUSIVE = []
|
||||
|
||||
DOCKER_REQUIRED_TOGETHER = [
|
||||
['client_cert', 'client_key']
|
||||
]
|
||||
DOCKER_REQUIRED_TOGETHER = [["client_cert", "client_key"]]
|
||||
|
||||
DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/'
|
||||
BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
DEFAULT_DOCKER_REGISTRY = "https://index.docker.io/v1/"
|
||||
BYTE_SUFFIXES = ["B", "KB", "MB", "GB", "TB", "PB"]
|
||||
|
||||
|
||||
def is_image_name_id(name):
|
||||
"""Check whether the given image name is in fact an image ID (hash)."""
|
||||
if re.match('^sha256:[0-9a-fA-F]{64}$', name):
|
||||
if re.match("^sha256:[0-9a-fA-F]{64}$", name):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -64,7 +82,7 @@ def is_valid_tag(tag, allow_empty=False):
|
||||
return allow_empty
|
||||
# See here ("Extended description") for a definition what tags can be:
|
||||
# https://docs.docker.com/engine/reference/commandline/tag/
|
||||
return bool(re.match('^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$', tag))
|
||||
return bool(re.match("^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$", tag))
|
||||
|
||||
|
||||
def sanitize_result(data):
|
||||
@@ -90,10 +108,12 @@ def log_debug(msg, pretty_print=False):
|
||||
|
||||
If ``pretty_print=True``, the message will be pretty-printed as JSON.
|
||||
"""
|
||||
with open('docker.log', 'a') as log_file:
|
||||
with open("docker.log", "a") as log_file:
|
||||
if pretty_print:
|
||||
log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': ')))
|
||||
log_file.write('\n')
|
||||
log_file.write(
|
||||
json.dumps(msg, sort_keys=True, indent=4, separators=(",", ": "))
|
||||
)
|
||||
log_file.write("\n")
|
||||
else:
|
||||
log_file.write(f"{msg}\n")
|
||||
|
||||
@@ -108,17 +128,19 @@ class DockerBaseClass(object):
|
||||
# log_debug(msg, pretty_print=pretty_print)
|
||||
|
||||
|
||||
def update_tls_hostname(result, old_behavior=False, deprecate_function=None, uses_tls=True):
|
||||
if result['tls_hostname'] is None:
|
||||
def update_tls_hostname(
|
||||
result, old_behavior=False, deprecate_function=None, uses_tls=True
|
||||
):
|
||||
if result["tls_hostname"] is None:
|
||||
# get default machine name from the url
|
||||
parsed_url = urlparse(result['docker_host'])
|
||||
result['tls_hostname'] = parsed_url.netloc.rsplit(':', 1)[0]
|
||||
parsed_url = urlparse(result["docker_host"])
|
||||
result["tls_hostname"] = parsed_url.netloc.rsplit(":", 1)[0]
|
||||
|
||||
|
||||
def compare_dict_allow_more_present(av, bv):
|
||||
'''
|
||||
"""
|
||||
Compare two dictionaries for whether every entry of the first is in the second.
|
||||
'''
|
||||
"""
|
||||
for key, value in av.items():
|
||||
if key not in bv:
|
||||
return False
|
||||
@@ -128,7 +150,7 @@ def compare_dict_allow_more_present(av, bv):
|
||||
|
||||
|
||||
def compare_generic(a, b, method, datatype):
|
||||
'''
|
||||
"""
|
||||
Compare values a and b as described by method and datatype.
|
||||
|
||||
Returns ``True`` if the values compare equal, and ``False`` if not.
|
||||
@@ -151,8 +173,8 @@ def compare_generic(a, b, method, datatype):
|
||||
not matter and which contain ``dict``s; ``allow_more_present`` is used
|
||||
for the ``dict``s, and these are assumed to be dictionaries of values;
|
||||
- ``dict``: for dictionaries of values.
|
||||
'''
|
||||
if method == 'ignore':
|
||||
"""
|
||||
if method == "ignore":
|
||||
return True
|
||||
# If a or b is None:
|
||||
if a is None or b is None:
|
||||
@@ -161,18 +183,18 @@ def compare_generic(a, b, method, datatype):
|
||||
return True
|
||||
# Otherwise, not equal for values, and equal
|
||||
# if the other is empty for set/list/dict
|
||||
if datatype == 'value':
|
||||
if datatype == "value":
|
||||
return False
|
||||
# For allow_more_present, allow a to be None
|
||||
if method == 'allow_more_present' and a is None:
|
||||
if method == "allow_more_present" and a is None:
|
||||
return True
|
||||
# Otherwise, the iterable object which is not None must have length 0
|
||||
return len(b if a is None else a) == 0
|
||||
# Do proper comparison (both objects not None)
|
||||
if datatype == 'value':
|
||||
if datatype == "value":
|
||||
return a == b
|
||||
elif datatype == 'list':
|
||||
if method == 'strict':
|
||||
elif datatype == "list":
|
||||
if method == "strict":
|
||||
return a == b
|
||||
else:
|
||||
i = 0
|
||||
@@ -183,19 +205,19 @@ def compare_generic(a, b, method, datatype):
|
||||
return False
|
||||
i += 1
|
||||
return True
|
||||
elif datatype == 'dict':
|
||||
if method == 'strict':
|
||||
elif datatype == "dict":
|
||||
if method == "strict":
|
||||
return a == b
|
||||
else:
|
||||
return compare_dict_allow_more_present(a, b)
|
||||
elif datatype == 'set':
|
||||
elif datatype == "set":
|
||||
set_a = set(a)
|
||||
set_b = set(b)
|
||||
if method == 'strict':
|
||||
if method == "strict":
|
||||
return set_a == set_b
|
||||
else:
|
||||
return set_b >= set_a
|
||||
elif datatype == 'set(dict)':
|
||||
elif datatype == "set(dict)":
|
||||
for av in a:
|
||||
found = False
|
||||
for bv in b:
|
||||
@@ -204,7 +226,7 @@ def compare_generic(a, b, method, datatype):
|
||||
break
|
||||
if not found:
|
||||
return False
|
||||
if method == 'strict':
|
||||
if method == "strict":
|
||||
# If we would know that both a and b do not contain duplicates,
|
||||
# we could simply compare len(a) to len(b) to finish this test.
|
||||
# We can assume that b has no duplicates (as it is returned by
|
||||
@@ -225,11 +247,13 @@ class DifferenceTracker(object):
|
||||
self._diff = []
|
||||
|
||||
def add(self, name, parameter=None, active=None):
|
||||
self._diff.append(dict(
|
||||
name=name,
|
||||
parameter=parameter,
|
||||
active=active,
|
||||
))
|
||||
self._diff.append(
|
||||
dict(
|
||||
name=name,
|
||||
parameter=parameter,
|
||||
active=active,
|
||||
)
|
||||
)
|
||||
|
||||
def merge(self, other_tracker):
|
||||
self._diff.extend(other_tracker._diff)
|
||||
@@ -239,41 +263,41 @@ class DifferenceTracker(object):
|
||||
return len(self._diff) == 0
|
||||
|
||||
def get_before_after(self):
|
||||
'''
|
||||
"""
|
||||
Return texts ``before`` and ``after``.
|
||||
'''
|
||||
"""
|
||||
before = dict()
|
||||
after = dict()
|
||||
for item in self._diff:
|
||||
before[item['name']] = item['active']
|
||||
after[item['name']] = item['parameter']
|
||||
before[item["name"]] = item["active"]
|
||||
after[item["name"]] = item["parameter"]
|
||||
return before, after
|
||||
|
||||
def has_difference_for(self, name):
|
||||
'''
|
||||
"""
|
||||
Returns a boolean if a difference exists for name
|
||||
'''
|
||||
return any(diff for diff in self._diff if diff['name'] == name)
|
||||
"""
|
||||
return any(diff for diff in self._diff if diff["name"] == name)
|
||||
|
||||
def get_legacy_docker_container_diffs(self):
|
||||
'''
|
||||
"""
|
||||
Return differences in the docker_container legacy format.
|
||||
'''
|
||||
"""
|
||||
result = []
|
||||
for entry in self._diff:
|
||||
item = dict()
|
||||
item[entry['name']] = dict(
|
||||
parameter=entry['parameter'],
|
||||
container=entry['active'],
|
||||
item[entry["name"]] = dict(
|
||||
parameter=entry["parameter"],
|
||||
container=entry["active"],
|
||||
)
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
def get_legacy_docker_diffs(self):
|
||||
'''
|
||||
"""
|
||||
Return differences in the docker_container legacy format.
|
||||
'''
|
||||
result = [entry['name'] for entry in self._diff]
|
||||
"""
|
||||
result = [entry["name"] for entry in self._diff]
|
||||
return result
|
||||
|
||||
|
||||
@@ -291,31 +315,38 @@ def sanitize_labels(labels, labels_field, client=None, module=None):
|
||||
if not isinstance(k, str):
|
||||
fail(f"The key {k!r} of {labels_field} is not a string!")
|
||||
if isinstance(v, (bool, float)):
|
||||
fail(f"The value {v!r} for {k!r} of {labels_field} is not a string or something than can be safely converted to a string!")
|
||||
fail(
|
||||
f"The value {v!r} for {k!r} of {labels_field} is not a string or something than can be safely converted to a string!"
|
||||
)
|
||||
labels[k] = to_text(v)
|
||||
|
||||
|
||||
def clean_dict_booleans_for_docker_api(data, allow_sequences=False):
|
||||
'''
|
||||
"""
|
||||
Go does not like Python booleans 'True' or 'False', while Ansible is just
|
||||
fine with them in YAML. As such, they need to be converted in cases where
|
||||
we pass dictionaries to the Docker API (e.g. docker_network's
|
||||
driver_options and docker_prune's filters). When `allow_sequences=True`
|
||||
YAML sequences (lists, tuples) are converted to [str] instead of str([...])
|
||||
which is the expected format of filters which accept lists such as labels.
|
||||
'''
|
||||
"""
|
||||
|
||||
def sanitize(value):
|
||||
if value is True:
|
||||
return 'true'
|
||||
return "true"
|
||||
elif value is False:
|
||||
return 'false'
|
||||
return "false"
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
result = dict()
|
||||
if data is not None:
|
||||
for k, v in data.items():
|
||||
result[str(k)] = [sanitize(e) for e in v] if allow_sequences and is_sequence(v) else sanitize(v)
|
||||
result[str(k)] = (
|
||||
[sanitize(e) for e in v]
|
||||
if allow_sequences and is_sequence(v)
|
||||
else sanitize(v)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -324,30 +355,30 @@ def convert_duration_to_nanosecond(time_str):
|
||||
Return time duration in nanosecond.
|
||||
"""
|
||||
if not isinstance(time_str, str):
|
||||
raise ValueError(f'Missing unit in duration - {time_str}')
|
||||
raise ValueError(f"Missing unit in duration - {time_str}")
|
||||
|
||||
regex = re.compile(
|
||||
r'^(((?P<hours>\d+)h)?'
|
||||
r'((?P<minutes>\d+)m(?!s))?'
|
||||
r'((?P<seconds>\d+)s)?'
|
||||
r'((?P<milliseconds>\d+)ms)?'
|
||||
r'((?P<microseconds>\d+)us)?)$'
|
||||
r"^(((?P<hours>\d+)h)?"
|
||||
r"((?P<minutes>\d+)m(?!s))?"
|
||||
r"((?P<seconds>\d+)s)?"
|
||||
r"((?P<milliseconds>\d+)ms)?"
|
||||
r"((?P<microseconds>\d+)us)?)$"
|
||||
)
|
||||
parts = regex.match(time_str)
|
||||
|
||||
if not parts:
|
||||
raise ValueError(f'Invalid time duration - {time_str}')
|
||||
raise ValueError(f"Invalid time duration - {time_str}")
|
||||
|
||||
parts = parts.groupdict()
|
||||
time_params = {}
|
||||
for (name, value) in parts.items():
|
||||
for name, value in parts.items():
|
||||
if value:
|
||||
time_params[name] = int(value)
|
||||
|
||||
delta = timedelta(**time_params)
|
||||
time_in_nanoseconds = (
|
||||
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6
|
||||
) * 10 ** 3
|
||||
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
|
||||
) * 10**3
|
||||
|
||||
return time_in_nanoseconds
|
||||
|
||||
@@ -355,7 +386,7 @@ def convert_duration_to_nanosecond(time_str):
|
||||
def normalize_healthcheck_test(test):
|
||||
if isinstance(test, (tuple, list)):
|
||||
return [str(e) for e in test]
|
||||
return ['CMD-SHELL', str(test)]
|
||||
return ["CMD-SHELL", str(test)]
|
||||
|
||||
|
||||
def normalize_healthcheck(healthcheck, normalize_test=False):
|
||||
@@ -365,9 +396,17 @@ def normalize_healthcheck(healthcheck, normalize_test=False):
|
||||
result = dict()
|
||||
|
||||
# All supported healthcheck parameters
|
||||
options = ('test', 'test_cli_compatible', 'interval', 'timeout', 'start_period', 'start_interval', 'retries')
|
||||
options = (
|
||||
"test",
|
||||
"test_cli_compatible",
|
||||
"interval",
|
||||
"timeout",
|
||||
"start_period",
|
||||
"start_interval",
|
||||
"retries",
|
||||
)
|
||||
|
||||
duration_options = ('interval', 'timeout', 'start_period', 'start_interval')
|
||||
duration_options = ("interval", "timeout", "start_period", "start_interval")
|
||||
|
||||
for key in options:
|
||||
if key in healthcheck:
|
||||
@@ -378,16 +417,18 @@ def normalize_healthcheck(healthcheck, normalize_test=False):
|
||||
continue
|
||||
if key in duration_options:
|
||||
value = convert_duration_to_nanosecond(value)
|
||||
if not value and not (healthcheck.get('test_cli_compatible') and key == 'test'):
|
||||
if not value and not (
|
||||
healthcheck.get("test_cli_compatible") and key == "test"
|
||||
):
|
||||
continue
|
||||
if key == 'retries':
|
||||
if key == "retries":
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f'Cannot parse number of retries for healthcheck. Expected an integer, got "{value}".'
|
||||
)
|
||||
if key == 'test' and value and normalize_test:
|
||||
if key == "test" and value and normalize_test:
|
||||
value = normalize_healthcheck_test(value)
|
||||
result[key] = value
|
||||
|
||||
@@ -399,12 +440,12 @@ def parse_healthcheck(healthcheck):
|
||||
Return dictionary of healthcheck parameters and boolean if
|
||||
healthcheck defined in image was requested to be disabled.
|
||||
"""
|
||||
if (not healthcheck) or (not healthcheck.get('test')):
|
||||
if (not healthcheck) or (not healthcheck.get("test")):
|
||||
return None, None
|
||||
|
||||
result = normalize_healthcheck(healthcheck, normalize_test=True)
|
||||
|
||||
if result['test'] == ['NONE']:
|
||||
if result["test"] == ["NONE"]:
|
||||
# If the user explicitly disables the healthcheck, return None
|
||||
# as the healthcheck object, and set disable_healthcheck to True
|
||||
return None, True
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from ansible.module_utils.compat.version import LooseVersion, StrictVersion # noqa: F401, pylint: disable=unused-import
|
||||
from ansible.module_utils.compat.version import ( # noqa: F401, pylint: disable=unused-import
|
||||
LooseVersion,
|
||||
StrictVersion,
|
||||
)
|
||||
|
||||
@@ -82,11 +82,11 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
def main():
|
||||
module = AnsibleModule(dict(), supports_check_mode=True)
|
||||
|
||||
cpuset_path = '/proc/self/cpuset'
|
||||
mountinfo_path = '/proc/self/mountinfo'
|
||||
cpuset_path = "/proc/self/cpuset"
|
||||
mountinfo_path = "/proc/self/mountinfo"
|
||||
|
||||
container_id = ''
|
||||
container_type = ''
|
||||
container_id = ""
|
||||
container_type = ""
|
||||
|
||||
contents = None
|
||||
if os.path.exists(cpuset_path):
|
||||
@@ -98,49 +98,51 @@ def main():
|
||||
# While this was true and worked well for a long time, this seems to be no longer accurate
|
||||
# with newer Docker / Podman versions and/or with cgroupv2. That's why the /proc/self/mountinfo
|
||||
# detection further down is done when this test is inconclusive.
|
||||
with open(cpuset_path, 'rb') as f:
|
||||
contents = f.read().decode('utf-8')
|
||||
with open(cpuset_path, "rb") as f:
|
||||
contents = f.read().decode("utf-8")
|
||||
|
||||
cgroup_path, cgroup_name = os.path.split(contents.strip())
|
||||
|
||||
if cgroup_path == '/docker':
|
||||
if cgroup_path == "/docker":
|
||||
container_id = cgroup_name
|
||||
container_type = 'docker'
|
||||
container_type = "docker"
|
||||
|
||||
if cgroup_path == '/azpl_job':
|
||||
if cgroup_path == "/azpl_job":
|
||||
container_id = cgroup_name
|
||||
container_type = 'azure_pipelines'
|
||||
container_type = "azure_pipelines"
|
||||
|
||||
if cgroup_path == '/actions_job':
|
||||
if cgroup_path == "/actions_job":
|
||||
container_id = cgroup_name
|
||||
container_type = 'github_actions'
|
||||
container_type = "github_actions"
|
||||
|
||||
if not container_id and os.path.exists(mountinfo_path):
|
||||
with open(mountinfo_path, 'rb') as f:
|
||||
contents = f.read().decode('utf-8')
|
||||
with open(mountinfo_path, "rb") as f:
|
||||
contents = f.read().decode("utf-8")
|
||||
|
||||
# As to why this works, see the explanations by Matt Clay in
|
||||
# https://github.com/ansible/ansible/blob/80d2f8da02052f64396da6b8caaf820eedbf18e2/test/lib/ansible_test/_internal/docker_util.py#L571-L610
|
||||
|
||||
for line in contents.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 5 and parts[4] == '/etc/hostname':
|
||||
m = re.match('.*/([a-f0-9]{64})/hostname$', parts[3])
|
||||
if len(parts) >= 5 and parts[4] == "/etc/hostname":
|
||||
m = re.match(".*/([a-f0-9]{64})/hostname$", parts[3])
|
||||
if m:
|
||||
container_id = m.group(1)
|
||||
container_type = 'docker'
|
||||
container_type = "docker"
|
||||
|
||||
m = re.match('.*/([a-f0-9]{64})/userdata/hostname$', parts[3])
|
||||
m = re.match(".*/([a-f0-9]{64})/userdata/hostname$", parts[3])
|
||||
if m:
|
||||
container_id = m.group(1)
|
||||
container_type = 'podman'
|
||||
container_type = "podman"
|
||||
|
||||
module.exit_json(ansible_facts=dict(
|
||||
ansible_module_running_in_container=container_id != '',
|
||||
ansible_module_container_id=container_id,
|
||||
ansible_module_container_type=container_type,
|
||||
))
|
||||
module.exit_json(
|
||||
ansible_facts=dict(
|
||||
ansible_module_running_in_container=container_id != "",
|
||||
ansible_module_container_id=container_id,
|
||||
ansible_module_container_type=container_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -439,19 +439,18 @@ actions:
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.validation import check_type_int
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
|
||||
BaseComposeManager,
|
||||
common_compose_argspec_ex,
|
||||
is_failed,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
class ServicesManager(BaseComposeManager):
|
||||
@@ -459,86 +458,90 @@ class ServicesManager(BaseComposeManager):
|
||||
super(ServicesManager, self).__init__(client)
|
||||
parameters = self.client.module.params
|
||||
|
||||
self.state = parameters['state']
|
||||
self.dependencies = parameters['dependencies']
|
||||
self.pull = parameters['pull']
|
||||
self.build = parameters['build']
|
||||
self.ignore_build_events = parameters['ignore_build_events']
|
||||
self.recreate = parameters['recreate']
|
||||
self.remove_images = parameters['remove_images']
|
||||
self.remove_volumes = parameters['remove_volumes']
|
||||
self.remove_orphans = parameters['remove_orphans']
|
||||
self.renew_anon_volumes = parameters['renew_anon_volumes']
|
||||
self.timeout = parameters['timeout']
|
||||
self.services = parameters['services'] or []
|
||||
self.scale = parameters['scale'] or {}
|
||||
self.wait = parameters['wait']
|
||||
self.wait_timeout = parameters['wait_timeout']
|
||||
self.yes = parameters['assume_yes']
|
||||
if self.compose_version < LooseVersion('2.32.0') and self.yes:
|
||||
self.fail(f'assume_yes=true needs Docker Compose 2.32.0 or newer, not version {self.compose_version}')
|
||||
self.state = parameters["state"]
|
||||
self.dependencies = parameters["dependencies"]
|
||||
self.pull = parameters["pull"]
|
||||
self.build = parameters["build"]
|
||||
self.ignore_build_events = parameters["ignore_build_events"]
|
||||
self.recreate = parameters["recreate"]
|
||||
self.remove_images = parameters["remove_images"]
|
||||
self.remove_volumes = parameters["remove_volumes"]
|
||||
self.remove_orphans = parameters["remove_orphans"]
|
||||
self.renew_anon_volumes = parameters["renew_anon_volumes"]
|
||||
self.timeout = parameters["timeout"]
|
||||
self.services = parameters["services"] or []
|
||||
self.scale = parameters["scale"] or {}
|
||||
self.wait = parameters["wait"]
|
||||
self.wait_timeout = parameters["wait_timeout"]
|
||||
self.yes = parameters["assume_yes"]
|
||||
if self.compose_version < LooseVersion("2.32.0") and self.yes:
|
||||
self.fail(
|
||||
f"assume_yes=true needs Docker Compose 2.32.0 or newer, not version {self.compose_version}"
|
||||
)
|
||||
|
||||
for key, value in self.scale.items():
|
||||
if not isinstance(key, str):
|
||||
self.fail(f'The key {key!r} for `scale` is not a string')
|
||||
self.fail(f"The key {key!r} for `scale` is not a string")
|
||||
try:
|
||||
value = check_type_int(value)
|
||||
except TypeError as exc:
|
||||
self.fail(f'The value {value!r} for `scale[{key!r}]` is not an integer')
|
||||
self.fail(f"The value {value!r} for `scale[{key!r}]` is not an integer")
|
||||
if value < 0:
|
||||
self.fail(f'The value {value!r} for `scale[{key!r}]` is negative')
|
||||
self.fail(f"The value {value!r} for `scale[{key!r}]` is negative")
|
||||
self.scale[key] = value
|
||||
|
||||
def run(self):
|
||||
if self.state == 'present':
|
||||
if self.state == "present":
|
||||
result = self.cmd_up()
|
||||
elif self.state == 'stopped':
|
||||
elif self.state == "stopped":
|
||||
result = self.cmd_stop()
|
||||
elif self.state == 'restarted':
|
||||
elif self.state == "restarted":
|
||||
result = self.cmd_restart()
|
||||
elif self.state == 'absent':
|
||||
elif self.state == "absent":
|
||||
result = self.cmd_down()
|
||||
|
||||
result['containers'] = self.list_containers()
|
||||
result['images'] = self.list_images()
|
||||
result["containers"] = self.list_containers()
|
||||
result["images"] = self.list_images()
|
||||
self.cleanup_result(result)
|
||||
return result
|
||||
|
||||
def get_up_cmd(self, dry_run, no_start=False):
|
||||
args = self.get_base_args() + ['up', '--detach', '--no-color', '--quiet-pull']
|
||||
if self.pull != 'policy':
|
||||
args.extend(['--pull', self.pull])
|
||||
args = self.get_base_args() + ["up", "--detach", "--no-color", "--quiet-pull"]
|
||||
if self.pull != "policy":
|
||||
args.extend(["--pull", self.pull])
|
||||
if self.remove_orphans:
|
||||
args.append('--remove-orphans')
|
||||
if self.recreate == 'always':
|
||||
args.append('--force-recreate')
|
||||
if self.recreate == 'never':
|
||||
args.append('--no-recreate')
|
||||
args.append("--remove-orphans")
|
||||
if self.recreate == "always":
|
||||
args.append("--force-recreate")
|
||||
if self.recreate == "never":
|
||||
args.append("--no-recreate")
|
||||
if self.renew_anon_volumes:
|
||||
args.append('--renew-anon-volumes')
|
||||
args.append("--renew-anon-volumes")
|
||||
if not self.dependencies:
|
||||
args.append('--no-deps')
|
||||
args.append("--no-deps")
|
||||
if self.timeout is not None:
|
||||
args.extend(['--timeout', f'{self.timeout}'])
|
||||
if self.build == 'always':
|
||||
args.append('--build')
|
||||
elif self.build == 'never':
|
||||
args.append('--no-build')
|
||||
args.extend(["--timeout", f"{self.timeout}"])
|
||||
if self.build == "always":
|
||||
args.append("--build")
|
||||
elif self.build == "never":
|
||||
args.append("--no-build")
|
||||
for key, value in sorted(self.scale.items()):
|
||||
args.extend(['--scale', f'{key}={value}'])
|
||||
args.extend(["--scale", f"{key}={value}"])
|
||||
if self.wait:
|
||||
args.append('--wait')
|
||||
args.append("--wait")
|
||||
if self.wait_timeout is not None:
|
||||
args.extend(['--wait-timeout', str(self.wait_timeout)])
|
||||
args.extend(["--wait-timeout", str(self.wait_timeout)])
|
||||
if no_start:
|
||||
args.append('--no-start')
|
||||
args.append("--no-start")
|
||||
if dry_run:
|
||||
args.append('--dry-run')
|
||||
args.append("--dry-run")
|
||||
if self.yes:
|
||||
# Note that for Docker Compose 2.32.x and 2.33.x, the long form is '--y' and not '--yes'.
|
||||
# This was fixed in Docker Compose 2.34.0 (https://github.com/docker/compose/releases/tag/v2.34.0).
|
||||
args.append('-y' if self.compose_version < LooseVersion('2.34.0') else '--yes')
|
||||
args.append('--')
|
||||
args.append(
|
||||
"-y" if self.compose_version < LooseVersion("2.34.0") else "--yes"
|
||||
)
|
||||
args.append("--")
|
||||
for service in self.services:
|
||||
args.append(service)
|
||||
return args
|
||||
@@ -549,24 +552,31 @@ class ServicesManager(BaseComposeManager):
|
||||
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
|
||||
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
|
||||
self.emit_warnings(events)
|
||||
self.update_result(result, events, stdout, stderr, ignore_service_pull_events=True, ignore_build_events=self.ignore_build_events)
|
||||
self.update_result(
|
||||
result,
|
||||
events,
|
||||
stdout,
|
||||
stderr,
|
||||
ignore_service_pull_events=True,
|
||||
ignore_build_events=self.ignore_build_events,
|
||||
)
|
||||
self.update_failed(result, events, args, stdout, stderr, rc)
|
||||
return result
|
||||
|
||||
def get_stop_cmd(self, dry_run):
|
||||
args = self.get_base_args() + ['stop']
|
||||
args = self.get_base_args() + ["stop"]
|
||||
if self.timeout is not None:
|
||||
args.extend(['--timeout', f'{self.timeout}'])
|
||||
args.extend(["--timeout", f"{self.timeout}"])
|
||||
if dry_run:
|
||||
args.append('--dry-run')
|
||||
args.append('--')
|
||||
args.append("--dry-run")
|
||||
args.append("--")
|
||||
for service in self.services:
|
||||
args.append(service)
|
||||
return args
|
||||
|
||||
def _are_containers_stopped(self):
|
||||
for container in self.list_containers_raw():
|
||||
if container['State'] not in ('created', 'exited', 'stopped', 'killed'):
|
||||
if container["State"] not in ("created", "exited", "stopped", "killed"):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -578,20 +588,33 @@ class ServicesManager(BaseComposeManager):
|
||||
# Make sure all containers are created
|
||||
args_1 = self.get_up_cmd(self.check_mode, no_start=True)
|
||||
rc_1, stdout_1, stderr_1 = self.client.call_cli(*args_1, cwd=self.project_src)
|
||||
events_1 = self.parse_events(stderr_1, dry_run=self.check_mode, nonzero_rc=rc_1 != 0)
|
||||
events_1 = self.parse_events(
|
||||
stderr_1, dry_run=self.check_mode, nonzero_rc=rc_1 != 0
|
||||
)
|
||||
self.emit_warnings(events_1)
|
||||
self.update_result(result, events_1, stdout_1, stderr_1, ignore_service_pull_events=True, ignore_build_events=self.ignore_build_events)
|
||||
self.update_result(
|
||||
result,
|
||||
events_1,
|
||||
stdout_1,
|
||||
stderr_1,
|
||||
ignore_service_pull_events=True,
|
||||
ignore_build_events=self.ignore_build_events,
|
||||
)
|
||||
is_failed_1 = is_failed(events_1, rc_1)
|
||||
if not is_failed_1 and not self._are_containers_stopped():
|
||||
# Make sure all containers are stopped
|
||||
args_2 = self.get_stop_cmd(self.check_mode)
|
||||
rc_2, stdout_2, stderr_2 = self.client.call_cli(*args_2, cwd=self.project_src)
|
||||
events_2 = self.parse_events(stderr_2, dry_run=self.check_mode, nonzero_rc=rc_2 != 0)
|
||||
rc_2, stdout_2, stderr_2 = self.client.call_cli(
|
||||
*args_2, cwd=self.project_src
|
||||
)
|
||||
events_2 = self.parse_events(
|
||||
stderr_2, dry_run=self.check_mode, nonzero_rc=rc_2 != 0
|
||||
)
|
||||
self.emit_warnings(events_2)
|
||||
self.update_result(result, events_2, stdout_2, stderr_2)
|
||||
else:
|
||||
args_2 = []
|
||||
rc_2, stdout_2, stderr_2 = 0, b'', b''
|
||||
rc_2, stdout_2, stderr_2 = 0, b"", b""
|
||||
events_2 = []
|
||||
# Compose result
|
||||
self.update_failed(
|
||||
@@ -605,14 +628,14 @@ class ServicesManager(BaseComposeManager):
|
||||
return result
|
||||
|
||||
def get_restart_cmd(self, dry_run):
|
||||
args = self.get_base_args() + ['restart']
|
||||
args = self.get_base_args() + ["restart"]
|
||||
if not self.dependencies:
|
||||
args.append('--no-deps')
|
||||
args.append("--no-deps")
|
||||
if self.timeout is not None:
|
||||
args.extend(['--timeout', f'{self.timeout}'])
|
||||
args.extend(["--timeout", f"{self.timeout}"])
|
||||
if dry_run:
|
||||
args.append('--dry-run')
|
||||
args.append('--')
|
||||
args.append("--dry-run")
|
||||
args.append("--")
|
||||
for service in self.services:
|
||||
args.append(service)
|
||||
return args
|
||||
@@ -628,18 +651,18 @@ class ServicesManager(BaseComposeManager):
|
||||
return result
|
||||
|
||||
def get_down_cmd(self, dry_run):
|
||||
args = self.get_base_args() + ['down']
|
||||
args = self.get_base_args() + ["down"]
|
||||
if self.remove_orphans:
|
||||
args.append('--remove-orphans')
|
||||
args.append("--remove-orphans")
|
||||
if self.remove_images:
|
||||
args.extend(['--rmi', self.remove_images])
|
||||
args.extend(["--rmi", self.remove_images])
|
||||
if self.remove_volumes:
|
||||
args.append('--volumes')
|
||||
args.append("--volumes")
|
||||
if self.timeout is not None:
|
||||
args.extend(['--timeout', f'{self.timeout}'])
|
||||
args.extend(["--timeout", f"{self.timeout}"])
|
||||
if dry_run:
|
||||
args.append('--dry-run')
|
||||
args.append('--')
|
||||
args.append("--dry-run")
|
||||
args.append("--")
|
||||
for service in self.services:
|
||||
args.append(service)
|
||||
return args
|
||||
@@ -657,31 +680,39 @@ class ServicesManager(BaseComposeManager):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
state=dict(type='str', default='present', choices=['absent', 'present', 'stopped', 'restarted']),
|
||||
dependencies=dict(type='bool', default=True),
|
||||
pull=dict(type='str', choices=['always', 'missing', 'never', 'policy'], default='policy'),
|
||||
build=dict(type='str', choices=['always', 'never', 'policy'], default='policy'),
|
||||
recreate=dict(type='str', default='auto', choices=['always', 'never', 'auto']),
|
||||
renew_anon_volumes=dict(type='bool', default=False),
|
||||
remove_images=dict(type='str', choices=['all', 'local']),
|
||||
remove_volumes=dict(type='bool', default=False),
|
||||
remove_orphans=dict(type='bool', default=False),
|
||||
timeout=dict(type='int'),
|
||||
services=dict(type='list', elements='str'),
|
||||
scale=dict(type='dict'),
|
||||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(type='int'),
|
||||
ignore_build_events=dict(type='bool', default=True),
|
||||
assume_yes=dict(type='bool', default=False),
|
||||
state=dict(
|
||||
type="str",
|
||||
default="present",
|
||||
choices=["absent", "present", "stopped", "restarted"],
|
||||
),
|
||||
dependencies=dict(type="bool", default=True),
|
||||
pull=dict(
|
||||
type="str",
|
||||
choices=["always", "missing", "never", "policy"],
|
||||
default="policy",
|
||||
),
|
||||
build=dict(type="str", choices=["always", "never", "policy"], default="policy"),
|
||||
recreate=dict(type="str", default="auto", choices=["always", "never", "auto"]),
|
||||
renew_anon_volumes=dict(type="bool", default=False),
|
||||
remove_images=dict(type="str", choices=["all", "local"]),
|
||||
remove_volumes=dict(type="bool", default=False),
|
||||
remove_orphans=dict(type="bool", default=False),
|
||||
timeout=dict(type="int"),
|
||||
services=dict(type="list", elements="str"),
|
||||
scale=dict(type="dict"),
|
||||
wait=dict(type="bool", default=False),
|
||||
wait_timeout=dict(type="int"),
|
||||
ignore_build_events=dict(type="bool", default=True),
|
||||
assume_yes=dict(type="bool", default=False),
|
||||
)
|
||||
argspec_ex = common_compose_argspec_ex()
|
||||
argument_spec.update(argspec_ex.pop('argspec'))
|
||||
argument_spec.update(argspec_ex.pop("argspec"))
|
||||
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
needs_api_version=False,
|
||||
**argspec_ex
|
||||
**argspec_ex,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -690,8 +721,11 @@ def main():
|
||||
manager.cleanup()
|
||||
client.module.exit_json(**result)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -168,12 +168,10 @@ import shlex
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
|
||||
BaseComposeManager,
|
||||
common_compose_argspec_ex,
|
||||
@@ -185,26 +183,26 @@ class ExecManager(BaseComposeManager):
|
||||
super(ExecManager, self).__init__(client)
|
||||
parameters = self.client.module.params
|
||||
|
||||
self.service = parameters['service']
|
||||
self.index = parameters['index']
|
||||
self.chdir = parameters['chdir']
|
||||
self.detach = parameters['detach']
|
||||
self.user = parameters['user']
|
||||
self.stdin = parameters['stdin']
|
||||
self.strip_empty_ends = parameters['strip_empty_ends']
|
||||
self.privileged = parameters['privileged']
|
||||
self.tty = parameters['tty']
|
||||
self.env = parameters['env']
|
||||
self.service = parameters["service"]
|
||||
self.index = parameters["index"]
|
||||
self.chdir = parameters["chdir"]
|
||||
self.detach = parameters["detach"]
|
||||
self.user = parameters["user"]
|
||||
self.stdin = parameters["stdin"]
|
||||
self.strip_empty_ends = parameters["strip_empty_ends"]
|
||||
self.privileged = parameters["privileged"]
|
||||
self.tty = parameters["tty"]
|
||||
self.env = parameters["env"]
|
||||
|
||||
self.argv = parameters['argv']
|
||||
if parameters['command'] is not None:
|
||||
self.argv = shlex.split(parameters['command'])
|
||||
self.argv = parameters["argv"]
|
||||
if parameters["command"] is not None:
|
||||
self.argv = shlex.split(parameters["command"])
|
||||
|
||||
if self.detach and self.stdin is not None:
|
||||
self.mail('If detach=true, stdin cannot be provided.')
|
||||
self.mail("If detach=true, stdin cannot be provided.")
|
||||
|
||||
if self.stdin is not None and parameters['stdin_add_newline']:
|
||||
self.stdin += '\n'
|
||||
if self.stdin is not None and parameters["stdin_add_newline"]:
|
||||
self.stdin += "\n"
|
||||
|
||||
if self.env is not None:
|
||||
for name, value in list(self.env.items()):
|
||||
@@ -213,27 +211,27 @@ class ExecManager(BaseComposeManager):
|
||||
"Non-string value found for env option. Ambiguous env options must be "
|
||||
f"wrapped in quotes to avoid them being interpreted. Key: {name}"
|
||||
)
|
||||
self.env[name] = to_text(value, errors='surrogate_or_strict')
|
||||
self.env[name] = to_text(value, errors="surrogate_or_strict")
|
||||
|
||||
def get_exec_cmd(self, dry_run, no_start=False):
|
||||
args = self.get_base_args(plain_progress=True) + ['exec']
|
||||
args = self.get_base_args(plain_progress=True) + ["exec"]
|
||||
if self.index is not None:
|
||||
args.extend(['--index', str(self.index)])
|
||||
args.extend(["--index", str(self.index)])
|
||||
if self.chdir is not None:
|
||||
args.extend(['--workdir', self.chdir])
|
||||
args.extend(["--workdir", self.chdir])
|
||||
if self.detach:
|
||||
args.extend(['--detach'])
|
||||
args.extend(["--detach"])
|
||||
if self.user is not None:
|
||||
args.extend(['--user', self.user])
|
||||
args.extend(["--user", self.user])
|
||||
if self.privileged:
|
||||
args.append('--privileged')
|
||||
args.append("--privileged")
|
||||
if not self.tty:
|
||||
args.append('--no-TTY')
|
||||
args.append("--no-TTY")
|
||||
if self.env:
|
||||
for name, value in list(self.env.items()):
|
||||
args.append('--env')
|
||||
args.append(f'{name}={value}')
|
||||
args.append('--')
|
||||
args.append("--env")
|
||||
args.append(f"{name}={value}")
|
||||
args.append("--")
|
||||
args.append(self.service)
|
||||
args.extend(self.argv)
|
||||
return args
|
||||
@@ -241,52 +239,52 @@ class ExecManager(BaseComposeManager):
|
||||
def run(self):
|
||||
args = self.get_exec_cmd(self.check_mode)
|
||||
kwargs = {
|
||||
'cwd': self.project_src,
|
||||
"cwd": self.project_src,
|
||||
}
|
||||
if self.stdin is not None:
|
||||
kwargs['data'] = self.stdin.encode('utf-8')
|
||||
kwargs["data"] = self.stdin.encode("utf-8")
|
||||
if self.detach:
|
||||
kwargs['check_rc'] = True
|
||||
kwargs["check_rc"] = True
|
||||
rc, stdout, stderr = self.client.call_cli(*args, **kwargs)
|
||||
if self.detach:
|
||||
return {}
|
||||
stdout = to_text(stdout)
|
||||
stderr = to_text(stderr)
|
||||
if self.strip_empty_ends:
|
||||
stdout = stdout.rstrip('\r\n')
|
||||
stderr = stderr.rstrip('\r\n')
|
||||
stdout = stdout.rstrip("\r\n")
|
||||
stderr = stderr.rstrip("\r\n")
|
||||
return {
|
||||
'changed': True,
|
||||
'rc': rc,
|
||||
'stdout': stdout,
|
||||
'stderr': stderr,
|
||||
"changed": True,
|
||||
"rc": rc,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
service=dict(type='str', required=True),
|
||||
index=dict(type='int'),
|
||||
argv=dict(type='list', elements='str'),
|
||||
command=dict(type='str'),
|
||||
chdir=dict(type='str'),
|
||||
detach=dict(type='bool', default=False),
|
||||
user=dict(type='str'),
|
||||
stdin=dict(type='str'),
|
||||
stdin_add_newline=dict(type='bool', default=True),
|
||||
strip_empty_ends=dict(type='bool', default=True),
|
||||
privileged=dict(type='bool', default=False),
|
||||
tty=dict(type='bool', default=True),
|
||||
env=dict(type='dict'),
|
||||
service=dict(type="str", required=True),
|
||||
index=dict(type="int"),
|
||||
argv=dict(type="list", elements="str"),
|
||||
command=dict(type="str"),
|
||||
chdir=dict(type="str"),
|
||||
detach=dict(type="bool", default=False),
|
||||
user=dict(type="str"),
|
||||
stdin=dict(type="str"),
|
||||
stdin_add_newline=dict(type="bool", default=True),
|
||||
strip_empty_ends=dict(type="bool", default=True),
|
||||
privileged=dict(type="bool", default=False),
|
||||
tty=dict(type="bool", default=True),
|
||||
env=dict(type="dict"),
|
||||
)
|
||||
argspec_ex = common_compose_argspec_ex()
|
||||
argument_spec.update(argspec_ex.pop('argspec'))
|
||||
argument_spec.update(argspec_ex.pop("argspec"))
|
||||
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=False,
|
||||
needs_api_version=False,
|
||||
**argspec_ex
|
||||
**argspec_ex,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -295,8 +293,11 @@ def main():
|
||||
manager.cleanup()
|
||||
client.module.exit_json(**result)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -116,13 +116,13 @@ from ansible_collections.community.docker.plugins.module_utils.common_cli import
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
|
||||
BaseComposeManager,
|
||||
common_compose_argspec_ex,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
class PullManager(BaseComposeManager):
|
||||
@@ -130,31 +130,33 @@ class PullManager(BaseComposeManager):
|
||||
super(PullManager, self).__init__(client)
|
||||
parameters = self.client.module.params
|
||||
|
||||
self.policy = parameters['policy']
|
||||
self.ignore_buildable = parameters['ignore_buildable']
|
||||
self.include_deps = parameters['include_deps']
|
||||
self.services = parameters['services'] or []
|
||||
self.policy = parameters["policy"]
|
||||
self.ignore_buildable = parameters["ignore_buildable"]
|
||||
self.include_deps = parameters["include_deps"]
|
||||
self.services = parameters["services"] or []
|
||||
|
||||
if self.policy != 'always' and self.compose_version < LooseVersion('2.22.0'):
|
||||
if self.policy != "always" and self.compose_version < LooseVersion("2.22.0"):
|
||||
# https://github.com/docker/compose/pull/10981 - 2.22.0
|
||||
self.fail(
|
||||
f'A pull policy other than always is only supported since Docker Compose 2.22.0. {self.client.get_cli()} has version {self.compose_version}')
|
||||
if self.ignore_buildable and self.compose_version < LooseVersion('2.15.0'):
|
||||
f"A pull policy other than always is only supported since Docker Compose 2.22.0. {self.client.get_cli()} has version {self.compose_version}"
|
||||
)
|
||||
if self.ignore_buildable and self.compose_version < LooseVersion("2.15.0"):
|
||||
# https://github.com/docker/compose/pull/10134 - 2.15.0
|
||||
self.fail(
|
||||
f'--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}')
|
||||
f"--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}"
|
||||
)
|
||||
|
||||
def get_pull_cmd(self, dry_run, no_start=False):
|
||||
args = self.get_base_args() + ['pull']
|
||||
if self.policy != 'always':
|
||||
args.extend(['--policy', self.policy])
|
||||
args = self.get_base_args() + ["pull"]
|
||||
if self.policy != "always":
|
||||
args.extend(["--policy", self.policy])
|
||||
if self.ignore_buildable:
|
||||
args.append('--ignore-buildable')
|
||||
args.append("--ignore-buildable")
|
||||
if self.include_deps:
|
||||
args.append('--include-deps')
|
||||
args.append("--include-deps")
|
||||
if dry_run:
|
||||
args.append('--dry-run')
|
||||
args.append('--')
|
||||
args.append("--dry-run")
|
||||
args.append("--")
|
||||
for service in self.services:
|
||||
args.append(service)
|
||||
return args
|
||||
@@ -165,7 +167,13 @@ class PullManager(BaseComposeManager):
|
||||
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
|
||||
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
|
||||
self.emit_warnings(events)
|
||||
self.update_result(result, events, stdout, stderr, ignore_service_pull_events=self.policy != 'missing' and not self.check_mode)
|
||||
self.update_result(
|
||||
result,
|
||||
events,
|
||||
stdout,
|
||||
stderr,
|
||||
ignore_service_pull_events=self.policy != "missing" and not self.check_mode,
|
||||
)
|
||||
self.update_failed(result, events, args, stdout, stderr, rc)
|
||||
self.cleanup_result(result)
|
||||
return result
|
||||
@@ -173,19 +181,19 @@ class PullManager(BaseComposeManager):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
policy=dict(type='str', choices=['always', 'missing'], default='always'),
|
||||
ignore_buildable=dict(type='bool', default=False),
|
||||
include_deps=dict(type='bool', default=False),
|
||||
services=dict(type='list', elements='str'),
|
||||
policy=dict(type="str", choices=["always", "missing"], default="always"),
|
||||
ignore_buildable=dict(type="bool", default=False),
|
||||
include_deps=dict(type="bool", default=False),
|
||||
services=dict(type="list", elements="str"),
|
||||
)
|
||||
argspec_ex = common_compose_argspec_ex()
|
||||
argument_spec.update(argspec_ex.pop('argspec'))
|
||||
argument_spec.update(argspec_ex.pop("argspec"))
|
||||
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
needs_api_version=False,
|
||||
**argspec_ex
|
||||
**argspec_ex,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -194,8 +202,11 @@ def main():
|
||||
manager.cleanup()
|
||||
client.module.exit_json(**result)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -241,12 +241,10 @@ import shlex
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.compose_v2 import (
|
||||
BaseComposeManager,
|
||||
common_compose_argspec_ex,
|
||||
@@ -258,39 +256,39 @@ class ExecManager(BaseComposeManager):
|
||||
super(ExecManager, self).__init__(client)
|
||||
parameters = self.client.module.params
|
||||
|
||||
self.service = parameters['service']
|
||||
self.build = parameters['build']
|
||||
self.cap_add = parameters['cap_add']
|
||||
self.cap_drop = parameters['cap_drop']
|
||||
self.entrypoint = parameters['entrypoint']
|
||||
self.interactive = parameters['interactive']
|
||||
self.labels = parameters['labels']
|
||||
self.name = parameters['name']
|
||||
self.no_deps = parameters['no_deps']
|
||||
self.publish = parameters['publish']
|
||||
self.quiet_pull = parameters['quiet_pull']
|
||||
self.remove_orphans = parameters['remove_orphans']
|
||||
self.do_cleanup = parameters['cleanup']
|
||||
self.service_ports = parameters['service_ports']
|
||||
self.use_aliases = parameters['use_aliases']
|
||||
self.volumes = parameters['volumes']
|
||||
self.chdir = parameters['chdir']
|
||||
self.detach = parameters['detach']
|
||||
self.user = parameters['user']
|
||||
self.stdin = parameters['stdin']
|
||||
self.strip_empty_ends = parameters['strip_empty_ends']
|
||||
self.tty = parameters['tty']
|
||||
self.env = parameters['env']
|
||||
self.service = parameters["service"]
|
||||
self.build = parameters["build"]
|
||||
self.cap_add = parameters["cap_add"]
|
||||
self.cap_drop = parameters["cap_drop"]
|
||||
self.entrypoint = parameters["entrypoint"]
|
||||
self.interactive = parameters["interactive"]
|
||||
self.labels = parameters["labels"]
|
||||
self.name = parameters["name"]
|
||||
self.no_deps = parameters["no_deps"]
|
||||
self.publish = parameters["publish"]
|
||||
self.quiet_pull = parameters["quiet_pull"]
|
||||
self.remove_orphans = parameters["remove_orphans"]
|
||||
self.do_cleanup = parameters["cleanup"]
|
||||
self.service_ports = parameters["service_ports"]
|
||||
self.use_aliases = parameters["use_aliases"]
|
||||
self.volumes = parameters["volumes"]
|
||||
self.chdir = parameters["chdir"]
|
||||
self.detach = parameters["detach"]
|
||||
self.user = parameters["user"]
|
||||
self.stdin = parameters["stdin"]
|
||||
self.strip_empty_ends = parameters["strip_empty_ends"]
|
||||
self.tty = parameters["tty"]
|
||||
self.env = parameters["env"]
|
||||
|
||||
self.argv = parameters['argv']
|
||||
if parameters['command'] is not None:
|
||||
self.argv = shlex.split(parameters['command'])
|
||||
self.argv = parameters["argv"]
|
||||
if parameters["command"] is not None:
|
||||
self.argv = shlex.split(parameters["command"])
|
||||
|
||||
if self.detach and self.stdin is not None:
|
||||
self.mail('If detach=true, stdin cannot be provided.')
|
||||
self.mail("If detach=true, stdin cannot be provided.")
|
||||
|
||||
if self.stdin is not None and parameters['stdin_add_newline']:
|
||||
self.stdin += '\n'
|
||||
if self.stdin is not None and parameters["stdin_add_newline"]:
|
||||
self.stdin += "\n"
|
||||
|
||||
if self.env is not None:
|
||||
for name, value in list(self.env.items()):
|
||||
@@ -299,58 +297,58 @@ class ExecManager(BaseComposeManager):
|
||||
"Non-string value found for env option. Ambiguous env options must be "
|
||||
f"wrapped in quotes to avoid them being interpreted. Key: {name}"
|
||||
)
|
||||
self.env[name] = to_text(value, errors='surrogate_or_strict')
|
||||
self.env[name] = to_text(value, errors="surrogate_or_strict")
|
||||
|
||||
def get_run_cmd(self, dry_run, no_start=False):
|
||||
args = self.get_base_args(plain_progress=True) + ['run']
|
||||
args = self.get_base_args(plain_progress=True) + ["run"]
|
||||
if self.build:
|
||||
args.append('--build')
|
||||
args.append("--build")
|
||||
if self.cap_add:
|
||||
for cap in self.cap_add:
|
||||
args.extend(['--cap-add', cap])
|
||||
args.extend(["--cap-add", cap])
|
||||
if self.cap_drop:
|
||||
for cap in self.cap_drop:
|
||||
args.extend(['--cap-drop', cap])
|
||||
args.extend(["--cap-drop", cap])
|
||||
if self.entrypoint is not None:
|
||||
args.extend(['--entrypoint', self.entrypoint])
|
||||
args.extend(["--entrypoint", self.entrypoint])
|
||||
if not self.interactive:
|
||||
args.append('--no-interactive')
|
||||
args.append("--no-interactive")
|
||||
if self.labels:
|
||||
for label in self.labels:
|
||||
args.extend(['--label', label])
|
||||
args.extend(["--label", label])
|
||||
if self.name is not None:
|
||||
args.extend(['--name', self.name])
|
||||
args.extend(["--name", self.name])
|
||||
if self.no_deps:
|
||||
args.append('--no-deps')
|
||||
args.append("--no-deps")
|
||||
if self.publish:
|
||||
for publish in self.publish:
|
||||
args.extend(['--publish', publish])
|
||||
args.extend(["--publish", publish])
|
||||
if self.quiet_pull:
|
||||
args.append('--quiet-pull')
|
||||
args.append("--quiet-pull")
|
||||
if self.remove_orphans:
|
||||
args.append('--remove-orphans')
|
||||
args.append("--remove-orphans")
|
||||
if self.do_cleanup:
|
||||
args.append('--rm')
|
||||
args.append("--rm")
|
||||
if self.service_ports:
|
||||
args.append('--service-ports')
|
||||
args.append("--service-ports")
|
||||
if self.use_aliases:
|
||||
args.append('--use-aliases')
|
||||
args.append("--use-aliases")
|
||||
if self.volumes:
|
||||
for volume in self.volumes:
|
||||
args.extend(['--volume', volume])
|
||||
args.extend(["--volume", volume])
|
||||
if self.chdir is not None:
|
||||
args.extend(['--workdir', self.chdir])
|
||||
args.extend(["--workdir", self.chdir])
|
||||
if self.detach:
|
||||
args.extend(['--detach'])
|
||||
args.extend(["--detach"])
|
||||
if self.user is not None:
|
||||
args.extend(['--user', self.user])
|
||||
args.extend(["--user", self.user])
|
||||
if not self.tty:
|
||||
args.append('--no-TTY')
|
||||
args.append("--no-TTY")
|
||||
if self.env:
|
||||
for name, value in list(self.env.items()):
|
||||
args.append('--env')
|
||||
args.append(f'{name}={value}')
|
||||
args.append('--')
|
||||
args.append("--env")
|
||||
args.append(f"{name}={value}")
|
||||
args.append("--")
|
||||
args.append(self.service)
|
||||
if self.argv:
|
||||
args.extend(self.argv)
|
||||
@@ -359,67 +357,67 @@ class ExecManager(BaseComposeManager):
|
||||
def run(self):
|
||||
args = self.get_run_cmd(self.check_mode)
|
||||
kwargs = {
|
||||
'cwd': self.project_src,
|
||||
"cwd": self.project_src,
|
||||
}
|
||||
if self.stdin is not None:
|
||||
kwargs['data'] = self.stdin.encode('utf-8')
|
||||
kwargs["data"] = self.stdin.encode("utf-8")
|
||||
if self.detach:
|
||||
kwargs['check_rc'] = True
|
||||
kwargs["check_rc"] = True
|
||||
rc, stdout, stderr = self.client.call_cli(*args, **kwargs)
|
||||
if self.detach:
|
||||
return {
|
||||
'container_id': stdout.strip(),
|
||||
"container_id": stdout.strip(),
|
||||
}
|
||||
stdout = to_text(stdout)
|
||||
stderr = to_text(stderr)
|
||||
if self.strip_empty_ends:
|
||||
stdout = stdout.rstrip('\r\n')
|
||||
stderr = stderr.rstrip('\r\n')
|
||||
stdout = stdout.rstrip("\r\n")
|
||||
stderr = stderr.rstrip("\r\n")
|
||||
return {
|
||||
'changed': True,
|
||||
'rc': rc,
|
||||
'stdout': stdout,
|
||||
'stderr': stderr,
|
||||
"changed": True,
|
||||
"rc": rc,
|
||||
"stdout": stdout,
|
||||
"stderr": stderr,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
service=dict(type='str', required=True),
|
||||
argv=dict(type='list', elements='str'),
|
||||
command=dict(type='str'),
|
||||
build=dict(type='bool', default=False),
|
||||
cap_add=dict(type='list', elements='str'),
|
||||
cap_drop=dict(type='list', elements='str'),
|
||||
entrypoint=dict(type='str'),
|
||||
interactive=dict(type='bool', default=True),
|
||||
labels=dict(type='list', elements='str'),
|
||||
name=dict(type='str'),
|
||||
no_deps=dict(type='bool', default=False),
|
||||
publish=dict(type='list', elements='str'),
|
||||
quiet_pull=dict(type='bool', default=False),
|
||||
remove_orphans=dict(type='bool', default=False),
|
||||
cleanup=dict(type='bool', default=False),
|
||||
service_ports=dict(type='bool', default=False),
|
||||
use_aliases=dict(type='bool', default=False),
|
||||
volumes=dict(type='list', elements='str'),
|
||||
chdir=dict(type='str'),
|
||||
detach=dict(type='bool', default=False),
|
||||
user=dict(type='str'),
|
||||
stdin=dict(type='str'),
|
||||
stdin_add_newline=dict(type='bool', default=True),
|
||||
strip_empty_ends=dict(type='bool', default=True),
|
||||
tty=dict(type='bool', default=True),
|
||||
env=dict(type='dict'),
|
||||
service=dict(type="str", required=True),
|
||||
argv=dict(type="list", elements="str"),
|
||||
command=dict(type="str"),
|
||||
build=dict(type="bool", default=False),
|
||||
cap_add=dict(type="list", elements="str"),
|
||||
cap_drop=dict(type="list", elements="str"),
|
||||
entrypoint=dict(type="str"),
|
||||
interactive=dict(type="bool", default=True),
|
||||
labels=dict(type="list", elements="str"),
|
||||
name=dict(type="str"),
|
||||
no_deps=dict(type="bool", default=False),
|
||||
publish=dict(type="list", elements="str"),
|
||||
quiet_pull=dict(type="bool", default=False),
|
||||
remove_orphans=dict(type="bool", default=False),
|
||||
cleanup=dict(type="bool", default=False),
|
||||
service_ports=dict(type="bool", default=False),
|
||||
use_aliases=dict(type="bool", default=False),
|
||||
volumes=dict(type="list", elements="str"),
|
||||
chdir=dict(type="str"),
|
||||
detach=dict(type="bool", default=False),
|
||||
user=dict(type="str"),
|
||||
stdin=dict(type="str"),
|
||||
stdin_add_newline=dict(type="bool", default=True),
|
||||
strip_empty_ends=dict(type="bool", default=True),
|
||||
tty=dict(type="bool", default=True),
|
||||
env=dict(type="dict"),
|
||||
)
|
||||
argspec_ex = common_compose_argspec_ex()
|
||||
argument_spec.update(argspec_ex.pop('argspec'))
|
||||
argument_spec.update(argspec_ex.pop("argspec"))
|
||||
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=False,
|
||||
needs_api_version=False,
|
||||
**argspec_ex
|
||||
**argspec_ex,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -428,8 +426,11 @@ def main():
|
||||
manager.cleanup()
|
||||
client.module.exit_json(**result)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -199,12 +199,14 @@ import base64
|
||||
import hashlib
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException, APIError
|
||||
from docker.errors import APIError, DockerException
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
|
||||
pass
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
@@ -214,7 +216,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
compare_generic,
|
||||
sanitize_labels,
|
||||
)
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
|
||||
class ConfigManager(DockerBaseClass):
|
||||
@@ -228,26 +229,26 @@ class ConfigManager(DockerBaseClass):
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
parameters = self.client.module.params
|
||||
self.name = parameters.get('name')
|
||||
self.state = parameters.get('state')
|
||||
self.data = parameters.get('data')
|
||||
self.name = parameters.get("name")
|
||||
self.state = parameters.get("state")
|
||||
self.data = parameters.get("data")
|
||||
if self.data is not None:
|
||||
if parameters.get('data_is_b64'):
|
||||
if parameters.get("data_is_b64"):
|
||||
self.data = base64.b64decode(self.data)
|
||||
else:
|
||||
self.data = to_bytes(self.data)
|
||||
data_src = parameters.get('data_src')
|
||||
data_src = parameters.get("data_src")
|
||||
if data_src is not None:
|
||||
try:
|
||||
with open(data_src, 'rb') as f:
|
||||
with open(data_src, "rb") as f:
|
||||
self.data = f.read()
|
||||
except Exception as exc:
|
||||
self.client.fail(f'Error while reading {data_src}: {exc}')
|
||||
self.labels = parameters.get('labels')
|
||||
self.force = parameters.get('force')
|
||||
self.rolling_versions = parameters.get('rolling_versions')
|
||||
self.versions_to_keep = parameters.get('versions_to_keep')
|
||||
self.template_driver = parameters.get('template_driver')
|
||||
self.client.fail(f"Error while reading {data_src}: {exc}")
|
||||
self.labels = parameters.get("labels")
|
||||
self.force = parameters.get("force")
|
||||
self.rolling_versions = parameters.get("rolling_versions")
|
||||
self.versions_to_keep = parameters.get("versions_to_keep")
|
||||
self.template_driver = parameters.get("template_driver")
|
||||
|
||||
if self.rolling_versions:
|
||||
self.version = 0
|
||||
@@ -256,16 +257,18 @@ class ConfigManager(DockerBaseClass):
|
||||
|
||||
def __call__(self):
|
||||
self.get_config()
|
||||
if self.state == 'present':
|
||||
if self.state == "present":
|
||||
self.data_key = hashlib.sha224(self.data).hexdigest()
|
||||
self.present()
|
||||
self.remove_old_versions()
|
||||
elif self.state == 'absent':
|
||||
elif self.state == "absent":
|
||||
self.absent()
|
||||
|
||||
def get_version(self, config):
|
||||
try:
|
||||
return int(config.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
|
||||
return int(
|
||||
config.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
|
||||
)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
@@ -277,9 +280,9 @@ class ConfigManager(DockerBaseClass):
|
||||
self.remove_config(self.configs.pop(0))
|
||||
|
||||
def get_config(self):
|
||||
''' Find an existing config. '''
|
||||
"""Find an existing config."""
|
||||
try:
|
||||
configs = self.client.configs(filters={'name': self.name})
|
||||
configs = self.client.configs(filters={"name": self.name})
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error accessing config {self.name}: {exc}")
|
||||
|
||||
@@ -287,25 +290,23 @@ class ConfigManager(DockerBaseClass):
|
||||
self.configs = [
|
||||
config
|
||||
for config in configs
|
||||
if config['Spec']['Name'].startswith(f'{self.name}_v')
|
||||
if config["Spec"]["Name"].startswith(f"{self.name}_v")
|
||||
]
|
||||
self.configs.sort(key=self.get_version)
|
||||
else:
|
||||
self.configs = [
|
||||
config for config in configs if config['Spec']['Name'] == self.name
|
||||
config for config in configs if config["Spec"]["Name"] == self.name
|
||||
]
|
||||
|
||||
def create_config(self):
|
||||
''' Create a new config '''
|
||||
"""Create a new config"""
|
||||
config_id = None
|
||||
# We ca not see the data after creation, so adding a label we can use for idempotency check
|
||||
labels = {
|
||||
'ansible_key': self.data_key
|
||||
}
|
||||
labels = {"ansible_key": self.data_key}
|
||||
if self.rolling_versions:
|
||||
self.version += 1
|
||||
labels['ansible_version'] = str(self.version)
|
||||
self.name = f'{self.name}_v{self.version}'
|
||||
labels["ansible_version"] = str(self.version)
|
||||
self.name = f"{self.name}_v{self.version}"
|
||||
if self.labels:
|
||||
labels.update(self.labels)
|
||||
|
||||
@@ -314,49 +315,53 @@ class ConfigManager(DockerBaseClass):
|
||||
# only use templating argument when self.template_driver is defined
|
||||
kwargs = {}
|
||||
if self.template_driver:
|
||||
kwargs['templating'] = {
|
||||
'name': self.template_driver
|
||||
}
|
||||
config_id = self.client.create_config(self.name, self.data, labels=labels, **kwargs)
|
||||
self.configs += self.client.configs(filters={'id': config_id})
|
||||
kwargs["templating"] = {"name": self.template_driver}
|
||||
config_id = self.client.create_config(
|
||||
self.name, self.data, labels=labels, **kwargs
|
||||
)
|
||||
self.configs += self.client.configs(filters={"id": config_id})
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error creating config: {exc}")
|
||||
|
||||
if isinstance(config_id, dict):
|
||||
config_id = config_id['ID']
|
||||
config_id = config_id["ID"]
|
||||
|
||||
return config_id
|
||||
|
||||
def remove_config(self, config):
|
||||
try:
|
||||
if not self.check_mode:
|
||||
self.client.remove_config(config['ID'])
|
||||
self.client.remove_config(config["ID"])
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error removing config {config['Spec']['Name']}: {exc}")
|
||||
|
||||
def present(self):
|
||||
''' Handles state == 'present', creating or updating the config '''
|
||||
"""Handles state == 'present', creating or updating the config"""
|
||||
if self.configs:
|
||||
config = self.configs[-1]
|
||||
self.results['config_id'] = config['ID']
|
||||
self.results['config_name'] = config['Spec']['Name']
|
||||
self.results["config_id"] = config["ID"]
|
||||
self.results["config_name"] = config["Spec"]["Name"]
|
||||
data_changed = False
|
||||
template_driver_changed = False
|
||||
attrs = config.get('Spec', {})
|
||||
if attrs.get('Labels', {}).get('ansible_key'):
|
||||
if attrs['Labels']['ansible_key'] != self.data_key:
|
||||
attrs = config.get("Spec", {})
|
||||
if attrs.get("Labels", {}).get("ansible_key"):
|
||||
if attrs["Labels"]["ansible_key"] != self.data_key:
|
||||
data_changed = True
|
||||
else:
|
||||
if not self.force:
|
||||
self.client.module.warn("'ansible_key' label not found. Config will not be changed unless the force parameter is set to 'true'")
|
||||
self.client.module.warn(
|
||||
"'ansible_key' label not found. Config will not be changed unless the force parameter is set to 'true'"
|
||||
)
|
||||
# template_driver has changed if it was set in the previous config
|
||||
# and now it differs, or if it was not set but now it is.
|
||||
if attrs.get('Templating', {}).get('Name'):
|
||||
if attrs['Templating']['Name'] != self.template_driver:
|
||||
if attrs.get("Templating", {}).get("Name"):
|
||||
if attrs["Templating"]["Name"] != self.template_driver:
|
||||
template_driver_changed = True
|
||||
elif self.template_driver:
|
||||
template_driver_changed = True
|
||||
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
|
||||
labels_changed = not compare_generic(
|
||||
self.labels, attrs.get("Labels"), "allow_more_present", "dict"
|
||||
)
|
||||
if self.rolling_versions:
|
||||
self.version = self.get_version(config)
|
||||
if data_changed or template_driver_changed or labels_changed or self.force:
|
||||
@@ -364,46 +369,46 @@ class ConfigManager(DockerBaseClass):
|
||||
if not self.rolling_versions:
|
||||
self.absent()
|
||||
config_id = self.create_config()
|
||||
self.results['changed'] = True
|
||||
self.results['config_id'] = config_id
|
||||
self.results['config_name'] = self.name
|
||||
self.results["changed"] = True
|
||||
self.results["config_id"] = config_id
|
||||
self.results["config_name"] = self.name
|
||||
else:
|
||||
self.results['changed'] = True
|
||||
self.results['config_id'] = self.create_config()
|
||||
self.results['config_name'] = self.name
|
||||
self.results["changed"] = True
|
||||
self.results["config_id"] = self.create_config()
|
||||
self.results["config_name"] = self.name
|
||||
|
||||
def absent(self):
|
||||
''' Handles state == 'absent', removing the config '''
|
||||
"""Handles state == 'absent', removing the config"""
|
||||
if self.configs:
|
||||
for config in self.configs:
|
||||
self.remove_config(config)
|
||||
self.results['changed'] = True
|
||||
self.results["changed"] = True
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
data=dict(type='str'),
|
||||
data_is_b64=dict(type='bool', default=False),
|
||||
data_src=dict(type='path'),
|
||||
labels=dict(type='dict'),
|
||||
force=dict(type='bool', default=False),
|
||||
rolling_versions=dict(type='bool', default=False),
|
||||
versions_to_keep=dict(type='int', default=5),
|
||||
template_driver=dict(type='str', choices=['golang']),
|
||||
name=dict(type="str", required=True),
|
||||
state=dict(type="str", default="present", choices=["absent", "present"]),
|
||||
data=dict(type="str"),
|
||||
data_is_b64=dict(type="bool", default=False),
|
||||
data_src=dict(type="path"),
|
||||
labels=dict(type="dict"),
|
||||
force=dict(type="bool", default=False),
|
||||
rolling_versions=dict(type="bool", default=False),
|
||||
versions_to_keep=dict(type="int", default=5),
|
||||
template_driver=dict(type="str", choices=["golang"]),
|
||||
)
|
||||
|
||||
required_if = [
|
||||
('state', 'present', ['data', 'data_src'], True),
|
||||
("state", "present", ["data", "data_src"], True),
|
||||
]
|
||||
|
||||
mutually_exclusive = [
|
||||
('data', 'data_src'),
|
||||
("data", "data_src"),
|
||||
]
|
||||
|
||||
option_minimal_versions = dict(
|
||||
template_driver=dict(docker_py_version='5.0.3', docker_api_version='1.37'),
|
||||
template_driver=dict(docker_py_version="5.0.3", docker_api_version="1.37"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -411,11 +416,11 @@ def main():
|
||||
supports_check_mode=True,
|
||||
required_if=required_if,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
min_docker_version='2.6.0',
|
||||
min_docker_api_version='1.30',
|
||||
min_docker_version="2.6.0",
|
||||
min_docker_api_version="1.30",
|
||||
option_minimal_versions=option_minimal_versions,
|
||||
)
|
||||
sanitize_labels(client.module.params['labels'], 'labels', client)
|
||||
sanitize_labels(client.module.params["labels"], "labels", client)
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
@@ -425,12 +430,16 @@ def main():
|
||||
ConfigManager(client, results)()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1334,7 +1334,6 @@ status:
|
||||
from ansible_collections.community.docker.plugins.module_utils.module_container.docker_api import (
|
||||
DockerAPIEngineDriver,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.module_container.module import (
|
||||
run_module,
|
||||
)
|
||||
@@ -1345,5 +1344,5 @@ def main():
|
||||
run_module(engine_driver)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -172,14 +172,19 @@ import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.common.validation import check_type_int
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._scramble import (
|
||||
generate_insecure_key,
|
||||
scramble,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.copy import (
|
||||
DockerFileCopyError,
|
||||
DockerFileNotFound,
|
||||
@@ -191,14 +196,12 @@ from ansible_collections.community.docker.plugins.module_utils.copy import (
|
||||
stat_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._scramble import generate_insecure_key, scramble
|
||||
|
||||
|
||||
def are_fileobjs_equal(f1, f2):
|
||||
'''Given two (buffered) file objects, compare their contents.'''
|
||||
"""Given two (buffered) file objects, compare their contents."""
|
||||
blocksize = 65536
|
||||
b1buf = b''
|
||||
b2buf = b''
|
||||
b1buf = b""
|
||||
b2buf = b""
|
||||
while True:
|
||||
if f1 and len(b1buf) < blocksize:
|
||||
f1b = f1.read(blocksize)
|
||||
@@ -226,14 +229,14 @@ def are_fileobjs_equal(f1, f2):
|
||||
|
||||
|
||||
def are_fileobjs_equal_read_first(f1, f2):
|
||||
'''Given two (buffered) file objects, compare their contents.
|
||||
"""Given two (buffered) file objects, compare their contents.
|
||||
|
||||
Returns a tuple (is_equal, content_of_f1), where the first element indicates
|
||||
whether the two file objects have the same content, and the second element is
|
||||
the content of the first file object.'''
|
||||
the content of the first file object."""
|
||||
blocksize = 65536
|
||||
b1buf = b''
|
||||
b2buf = b''
|
||||
b1buf = b""
|
||||
b2buf = b""
|
||||
is_equal = True
|
||||
content = []
|
||||
while True:
|
||||
@@ -268,7 +271,7 @@ def are_fileobjs_equal_read_first(f1, f2):
|
||||
if f1:
|
||||
content.append(f1.read())
|
||||
|
||||
return is_equal, b''.join(content)
|
||||
return is_equal, b"".join(content)
|
||||
|
||||
|
||||
def is_container_file_not_regular_file(container_stat):
|
||||
@@ -283,18 +286,18 @@ def is_container_file_not_regular_file(container_stat):
|
||||
32 - 11, # ModeCharDevice
|
||||
32 - 13, # ModeIrregular
|
||||
):
|
||||
if container_stat['mode'] & (1 << bit) != 0:
|
||||
if container_stat["mode"] & (1 << bit) != 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_container_file_mode(container_stat):
|
||||
mode = container_stat['mode'] & 0xFFF
|
||||
if container_stat['mode'] & (1 << (32 - 9)) != 0: # ModeSetuid
|
||||
mode = container_stat["mode"] & 0xFFF
|
||||
if container_stat["mode"] & (1 << (32 - 9)) != 0: # ModeSetuid
|
||||
mode |= stat.S_ISUID # set UID bit
|
||||
if container_stat['mode'] & (1 << (32 - 10)) != 0: # ModeSetgid
|
||||
if container_stat["mode"] & (1 << (32 - 10)) != 0: # ModeSetgid
|
||||
mode |= stat.S_ISGID # set GID bit
|
||||
if container_stat['mode'] & (1 << (32 - 12)) != 0: # ModeSticky
|
||||
if container_stat["mode"] & (1 << (32 - 12)) != 0: # ModeSticky
|
||||
mode |= stat.S_ISVTX # sticky bit
|
||||
return mode
|
||||
|
||||
@@ -302,77 +305,88 @@ def get_container_file_mode(container_stat):
|
||||
def add_other_diff(diff, in_path, member):
|
||||
if diff is None:
|
||||
return
|
||||
diff['before_header'] = in_path
|
||||
diff["before_header"] = in_path
|
||||
if member.isdir():
|
||||
diff['before'] = '(directory)'
|
||||
diff["before"] = "(directory)"
|
||||
elif member.issym() or member.islnk():
|
||||
diff['before'] = member.linkname
|
||||
diff["before"] = member.linkname
|
||||
elif member.ischr():
|
||||
diff['before'] = '(character device)'
|
||||
diff["before"] = "(character device)"
|
||||
elif member.isblk():
|
||||
diff['before'] = '(block device)'
|
||||
diff["before"] = "(block device)"
|
||||
elif member.isfifo():
|
||||
diff['before'] = '(fifo)'
|
||||
diff["before"] = "(fifo)"
|
||||
elif member.isdev():
|
||||
diff['before'] = '(device)'
|
||||
diff["before"] = "(device)"
|
||||
elif member.isfile():
|
||||
raise DockerUnexpectedError('should not be a regular file')
|
||||
raise DockerUnexpectedError("should not be a regular file")
|
||||
else:
|
||||
diff['before'] = '(unknown filesystem object)'
|
||||
diff["before"] = "(unknown filesystem object)"
|
||||
|
||||
|
||||
def retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat=None, link_target=None):
|
||||
def retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat=None,
|
||||
link_target=None,
|
||||
):
|
||||
if diff is None:
|
||||
return
|
||||
if regular_stat is not None:
|
||||
# First handle all filesystem object types that are not regular files
|
||||
if regular_stat['mode'] & (1 << (32 - 1)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(directory)'
|
||||
if regular_stat["mode"] & (1 << (32 - 1)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(directory)"
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 4)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(temporary file)'
|
||||
elif regular_stat["mode"] & (1 << (32 - 4)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(temporary file)"
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 5)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = link_target
|
||||
elif regular_stat["mode"] & (1 << (32 - 5)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = link_target
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 6)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(device)'
|
||||
elif regular_stat["mode"] & (1 << (32 - 6)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(device)"
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 7)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(named pipe)'
|
||||
elif regular_stat["mode"] & (1 << (32 - 7)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(named pipe)"
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 8)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(socket)'
|
||||
elif regular_stat["mode"] & (1 << (32 - 8)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(socket)"
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 11)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(character device)'
|
||||
elif regular_stat["mode"] & (1 << (32 - 11)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(character device)"
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 13)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(unknown filesystem object)'
|
||||
elif regular_stat["mode"] & (1 << (32 - 13)) != 0:
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = "(unknown filesystem object)"
|
||||
return
|
||||
# Check whether file is too large
|
||||
if regular_stat['size'] > max_file_size_for_diff > 0:
|
||||
diff['dst_larger'] = max_file_size_for_diff
|
||||
if regular_stat["size"] > max_file_size_for_diff > 0:
|
||||
diff["dst_larger"] = max_file_size_for_diff
|
||||
return
|
||||
|
||||
# We need to get hold of the content
|
||||
def process_none(in_path):
|
||||
diff['before'] = ''
|
||||
diff["before"] = ""
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
|
||||
add_diff_dst_from_regular_member(
|
||||
diff, max_file_size_for_diff, in_path, tar, member
|
||||
)
|
||||
|
||||
def process_symlink(in_path, member):
|
||||
diff['before_header'] = in_path
|
||||
diff['before'] = member.linkname
|
||||
diff["before_header"] = in_path
|
||||
diff["before"] = member.linkname
|
||||
|
||||
def process_other(in_path, member):
|
||||
add_other_diff(diff, in_path, member)
|
||||
@@ -390,53 +404,57 @@ def retrieve_diff(client, container, container_path, follow_links, diff, max_fil
|
||||
|
||||
|
||||
def is_binary(content):
|
||||
if b'\x00' in content:
|
||||
if b"\x00" in content:
|
||||
return True
|
||||
# TODO: better detection
|
||||
# (ansible-core also just checks for 0x00, and even just sticks to the first 8k, so this is not too bad...)
|
||||
return False
|
||||
|
||||
|
||||
def are_fileobjs_equal_with_diff_of_first(f1, f2, size, diff, max_file_size_for_diff, container_path):
|
||||
def are_fileobjs_equal_with_diff_of_first(
|
||||
f1, f2, size, diff, max_file_size_for_diff, container_path
|
||||
):
|
||||
if diff is None:
|
||||
return are_fileobjs_equal(f1, f2)
|
||||
if size > max_file_size_for_diff > 0:
|
||||
diff['dst_larger'] = max_file_size_for_diff
|
||||
diff["dst_larger"] = max_file_size_for_diff
|
||||
return are_fileobjs_equal(f1, f2)
|
||||
is_equal, content = are_fileobjs_equal_read_first(f1, f2)
|
||||
if is_binary(content):
|
||||
diff['dst_binary'] = 1
|
||||
diff["dst_binary"] = 1
|
||||
else:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = to_text(content)
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = to_text(content)
|
||||
return is_equal
|
||||
|
||||
|
||||
def add_diff_dst_from_regular_member(diff, max_file_size_for_diff, container_path, tar, member):
|
||||
def add_diff_dst_from_regular_member(
|
||||
diff, max_file_size_for_diff, container_path, tar, member
|
||||
):
|
||||
if diff is None:
|
||||
return
|
||||
if member.size > max_file_size_for_diff > 0:
|
||||
diff['dst_larger'] = max_file_size_for_diff
|
||||
diff["dst_larger"] = max_file_size_for_diff
|
||||
return
|
||||
|
||||
with tar.extractfile(member) as tar_f:
|
||||
content = tar_f.read()
|
||||
|
||||
if is_binary(content):
|
||||
diff['dst_binary'] = 1
|
||||
diff["dst_binary"] = 1
|
||||
else:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = to_text(content)
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = to_text(content)
|
||||
|
||||
|
||||
def copy_dst_to_src(diff):
|
||||
if diff is None:
|
||||
return
|
||||
for f, t in [
|
||||
('dst_size', 'src_size'),
|
||||
('dst_binary', 'src_binary'),
|
||||
('before_header', 'after_header'),
|
||||
('before', 'after'),
|
||||
("dst_size", "src_size"),
|
||||
("dst_binary", "src_binary"),
|
||||
("before_header", "after_header"),
|
||||
("before", "after"),
|
||||
]:
|
||||
if f in diff:
|
||||
diff[t] = diff[f]
|
||||
@@ -444,38 +462,61 @@ def copy_dst_to_src(diff):
|
||||
diff.pop(t)
|
||||
|
||||
|
||||
def is_file_idempotent(client, container, managed_path, container_path, follow_links, local_follow_links, owner_id, group_id, mode,
|
||||
force=False, diff=None, max_file_size_for_diff=1):
|
||||
def is_file_idempotent(
|
||||
client,
|
||||
container,
|
||||
managed_path,
|
||||
container_path,
|
||||
follow_links,
|
||||
local_follow_links,
|
||||
owner_id,
|
||||
group_id,
|
||||
mode,
|
||||
force=False,
|
||||
diff=None,
|
||||
max_file_size_for_diff=1,
|
||||
):
|
||||
# Retrieve information of local file
|
||||
try:
|
||||
file_stat = os.stat(managed_path) if local_follow_links else os.lstat(managed_path)
|
||||
file_stat = (
|
||||
os.stat(managed_path) if local_follow_links else os.lstat(managed_path)
|
||||
)
|
||||
except OSError as exc:
|
||||
if exc.errno == 2:
|
||||
raise DockerFileNotFound(f'Cannot find local file {managed_path}')
|
||||
raise DockerFileNotFound(f"Cannot find local file {managed_path}")
|
||||
raise
|
||||
if mode is None:
|
||||
mode = stat.S_IMODE(file_stat.st_mode)
|
||||
if not stat.S_ISLNK(file_stat.st_mode) and not stat.S_ISREG(file_stat.st_mode):
|
||||
raise DockerFileCopyError('Local path {managed_path} is not a symbolic link or file')
|
||||
raise DockerFileCopyError(
|
||||
"Local path {managed_path} is not a symbolic link or file"
|
||||
)
|
||||
|
||||
if diff is not None:
|
||||
if file_stat.st_size > max_file_size_for_diff > 0:
|
||||
diff['src_larger'] = max_file_size_for_diff
|
||||
diff["src_larger"] = max_file_size_for_diff
|
||||
elif stat.S_ISLNK(file_stat.st_mode):
|
||||
diff['after_header'] = managed_path
|
||||
diff['after'] = os.readlink(managed_path)
|
||||
diff["after_header"] = managed_path
|
||||
diff["after"] = os.readlink(managed_path)
|
||||
else:
|
||||
with open(managed_path, 'rb') as f:
|
||||
with open(managed_path, "rb") as f:
|
||||
content = f.read()
|
||||
if is_binary(content):
|
||||
diff['src_binary'] = 1
|
||||
diff["src_binary"] = 1
|
||||
else:
|
||||
diff['after_header'] = managed_path
|
||||
diff['after'] = to_text(content)
|
||||
diff["after_header"] = managed_path
|
||||
diff["after"] = to_text(content)
|
||||
|
||||
# When forcing and we are not following links in the container, go!
|
||||
if force and not follow_links:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
# Resolve symlinks in the container (if requested), and get information on container's file
|
||||
@@ -493,40 +534,112 @@ def is_file_idempotent(client, container, managed_path, container_path, follow_l
|
||||
# If the file was not found, continue
|
||||
if regular_stat is None:
|
||||
if diff is not None:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = ''
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = ""
|
||||
return container_path, mode, False
|
||||
|
||||
# When forcing, go!
|
||||
if force:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
# If force is set to False, and the destination exists, assume there's nothing to do
|
||||
if force is False:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
copy_dst_to_src(diff)
|
||||
return container_path, mode, True
|
||||
|
||||
# Basic idempotency checks
|
||||
if stat.S_ISLNK(file_stat.st_mode):
|
||||
if link_target is None:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
local_link_target = os.readlink(managed_path)
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, local_link_target == link_target
|
||||
if link_target is not None:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
if is_container_file_not_regular_file(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
if file_stat.st_size != regular_stat['size']:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
if file_stat.st_size != regular_stat["size"]:
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
if mode != get_container_file_mode(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
# Fetch file from container
|
||||
@@ -535,25 +648,31 @@ def is_file_idempotent(client, container, managed_path, container_path, follow_l
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
# Check things like user/group ID and mode
|
||||
if any([
|
||||
member.mode & 0xFFF != mode,
|
||||
member.uid != owner_id,
|
||||
member.gid != group_id,
|
||||
not stat.S_ISREG(file_stat.st_mode),
|
||||
member.size != file_stat.st_size,
|
||||
]):
|
||||
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
|
||||
if any(
|
||||
[
|
||||
member.mode & 0xFFF != mode,
|
||||
member.uid != owner_id,
|
||||
member.gid != group_id,
|
||||
not stat.S_ISREG(file_stat.st_mode),
|
||||
member.size != file_stat.st_size,
|
||||
]
|
||||
):
|
||||
add_diff_dst_from_regular_member(
|
||||
diff, max_file_size_for_diff, in_path, tar, member
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
with tar.extractfile(member) as tar_f:
|
||||
with open(managed_path, 'rb') as local_f:
|
||||
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path)
|
||||
with open(managed_path, "rb") as local_f:
|
||||
is_equal = are_fileobjs_equal_with_diff_of_first(
|
||||
tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path
|
||||
)
|
||||
return container_path, mode, is_equal
|
||||
|
||||
def process_symlink(in_path, member):
|
||||
if diff is not None:
|
||||
diff['before_header'] = in_path
|
||||
diff['before'] = member.linkname
|
||||
diff["before_header"] = in_path
|
||||
diff["before"] = member.linkname
|
||||
|
||||
# Check things like user/group ID and mode
|
||||
if member.mode & 0xFFF != mode:
|
||||
@@ -585,8 +704,20 @@ def is_file_idempotent(client, container, managed_path, container_path, follow_l
|
||||
)
|
||||
|
||||
|
||||
def copy_file_into_container(client, container, managed_path, container_path, follow_links, local_follow_links,
|
||||
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
|
||||
def copy_file_into_container(
|
||||
client,
|
||||
container,
|
||||
managed_path,
|
||||
container_path,
|
||||
follow_links,
|
||||
local_follow_links,
|
||||
owner_id,
|
||||
group_id,
|
||||
mode,
|
||||
force=False,
|
||||
diff=False,
|
||||
max_file_size_for_diff=1,
|
||||
):
|
||||
if diff:
|
||||
diff = {}
|
||||
else:
|
||||
@@ -625,24 +756,42 @@ def copy_file_into_container(client, container, managed_path, container_path, fo
|
||||
changed=changed,
|
||||
)
|
||||
if diff:
|
||||
result['diff'] = diff
|
||||
result["diff"] = diff
|
||||
client.module.exit_json(**result)
|
||||
|
||||
|
||||
def is_content_idempotent(client, container, content, container_path, follow_links, owner_id, group_id, mode,
|
||||
force=False, diff=None, max_file_size_for_diff=1):
|
||||
def is_content_idempotent(
|
||||
client,
|
||||
container,
|
||||
content,
|
||||
container_path,
|
||||
follow_links,
|
||||
owner_id,
|
||||
group_id,
|
||||
mode,
|
||||
force=False,
|
||||
diff=None,
|
||||
max_file_size_for_diff=1,
|
||||
):
|
||||
if diff is not None:
|
||||
if len(content) > max_file_size_for_diff > 0:
|
||||
diff['src_larger'] = max_file_size_for_diff
|
||||
diff["src_larger"] = max_file_size_for_diff
|
||||
elif is_binary(content):
|
||||
diff['src_binary'] = 1
|
||||
diff["src_binary"] = 1
|
||||
else:
|
||||
diff['after_header'] = 'dynamically generated'
|
||||
diff['after'] = to_text(content)
|
||||
diff["after_header"] = "dynamically generated"
|
||||
diff["after"] = to_text(content)
|
||||
|
||||
# When forcing and we are not following links in the container, go!
|
||||
if force and not follow_links:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
# Resolve symlinks in the container (if requested), and get information on container's file
|
||||
@@ -660,60 +809,125 @@ def is_content_idempotent(client, container, content, container_path, follow_lin
|
||||
# If the file was not found, continue
|
||||
if regular_stat is None:
|
||||
if diff is not None:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = ''
|
||||
diff["before_header"] = container_path
|
||||
diff["before"] = ""
|
||||
return container_path, mode, False
|
||||
|
||||
# When forcing, go!
|
||||
if force:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
# If force is set to False, and the destination exists, assume there's nothing to do
|
||||
if force is False:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
copy_dst_to_src(diff)
|
||||
return container_path, mode, True
|
||||
|
||||
# Basic idempotency checks
|
||||
if link_target is not None:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
if is_container_file_not_regular_file(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
if len(content) != regular_stat['size']:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
if len(content) != regular_stat["size"]:
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
if mode != get_container_file_mode(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
retrieve_diff(
|
||||
client,
|
||||
container,
|
||||
container_path,
|
||||
follow_links,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
regular_stat,
|
||||
link_target,
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
# Fetch file from container
|
||||
def process_none(in_path):
|
||||
if diff is not None:
|
||||
diff['before'] = ''
|
||||
diff["before"] = ""
|
||||
return container_path, mode, False
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
# Check things like user/group ID and mode
|
||||
if any([
|
||||
member.mode & 0xFFF != mode,
|
||||
member.uid != owner_id,
|
||||
member.gid != group_id,
|
||||
member.size != len(content),
|
||||
]):
|
||||
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
|
||||
if any(
|
||||
[
|
||||
member.mode & 0xFFF != mode,
|
||||
member.uid != owner_id,
|
||||
member.gid != group_id,
|
||||
member.size != len(content),
|
||||
]
|
||||
):
|
||||
add_diff_dst_from_regular_member(
|
||||
diff, max_file_size_for_diff, in_path, tar, member
|
||||
)
|
||||
return container_path, mode, False
|
||||
|
||||
with tar.extractfile(member) as tar_f:
|
||||
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, io.BytesIO(content), member.size, diff, max_file_size_for_diff, in_path)
|
||||
is_equal = are_fileobjs_equal_with_diff_of_first(
|
||||
tar_f,
|
||||
io.BytesIO(content),
|
||||
member.size,
|
||||
diff,
|
||||
max_file_size_for_diff,
|
||||
in_path,
|
||||
)
|
||||
return container_path, mode, is_equal
|
||||
|
||||
def process_symlink(in_path, member):
|
||||
if diff is not None:
|
||||
diff['before_header'] = in_path
|
||||
diff['before'] = member.linkname
|
||||
diff["before_header"] = in_path
|
||||
diff["before"] = member.linkname
|
||||
|
||||
return container_path, mode, False
|
||||
|
||||
@@ -733,8 +947,19 @@ def is_content_idempotent(client, container, content, container_path, follow_lin
|
||||
)
|
||||
|
||||
|
||||
def copy_content_into_container(client, container, content, container_path, follow_links,
|
||||
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
|
||||
def copy_content_into_container(
|
||||
client,
|
||||
container,
|
||||
content,
|
||||
container_path,
|
||||
follow_links,
|
||||
owner_id,
|
||||
group_id,
|
||||
mode,
|
||||
force=False,
|
||||
diff=False,
|
||||
max_file_size_for_diff=1,
|
||||
):
|
||||
if diff:
|
||||
diff = {}
|
||||
else:
|
||||
@@ -773,11 +998,11 @@ def copy_content_into_container(client, container, content, container_path, foll
|
||||
if diff:
|
||||
# Since the content is no_log, make sure that the before/after strings look sufficiently different
|
||||
key = generate_insecure_key()
|
||||
diff['scrambled_diff'] = base64.b64encode(key)
|
||||
for k in ('before', 'after'):
|
||||
diff["scrambled_diff"] = base64.b64encode(key)
|
||||
for k in ("before", "after"):
|
||||
if k in diff:
|
||||
diff[k] = scramble(diff[k], key)
|
||||
result['diff'] = diff
|
||||
result["diff"] = diff
|
||||
client.module.exit_json(**result)
|
||||
|
||||
|
||||
@@ -786,65 +1011,68 @@ def parse_modern(mode):
|
||||
return int(to_native(mode), 8)
|
||||
if isinstance(mode, int):
|
||||
return mode
|
||||
raise TypeError(f'must be an octal string or an integer, got {mode!r}')
|
||||
raise TypeError(f"must be an octal string or an integer, got {mode!r}")
|
||||
|
||||
|
||||
def parse_octal_string_only(mode):
|
||||
if isinstance(mode, str):
|
||||
return int(to_native(mode), 8)
|
||||
raise TypeError(f'must be an octal string, got {mode!r}')
|
||||
raise TypeError(f"must be an octal string, got {mode!r}")
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
container=dict(type='str', required=True),
|
||||
path=dict(type='path'),
|
||||
container_path=dict(type='str', required=True),
|
||||
follow=dict(type='bool', default=False),
|
||||
local_follow=dict(type='bool', default=True),
|
||||
owner_id=dict(type='int'),
|
||||
group_id=dict(type='int'),
|
||||
mode=dict(type='raw'),
|
||||
mode_parse=dict(type='str', choices=['legacy', 'modern', 'octal_string_only'], default='legacy'),
|
||||
force=dict(type='bool'),
|
||||
content=dict(type='str', no_log=True),
|
||||
content_is_b64=dict(type='bool', default=False),
|
||||
|
||||
container=dict(type="str", required=True),
|
||||
path=dict(type="path"),
|
||||
container_path=dict(type="str", required=True),
|
||||
follow=dict(type="bool", default=False),
|
||||
local_follow=dict(type="bool", default=True),
|
||||
owner_id=dict(type="int"),
|
||||
group_id=dict(type="int"),
|
||||
mode=dict(type="raw"),
|
||||
mode_parse=dict(
|
||||
type="str",
|
||||
choices=["legacy", "modern", "octal_string_only"],
|
||||
default="legacy",
|
||||
),
|
||||
force=dict(type="bool"),
|
||||
content=dict(type="str", no_log=True),
|
||||
content_is_b64=dict(type="bool", default=False),
|
||||
# Undocumented parameters for use by the action plugin
|
||||
_max_file_size_for_diff=dict(type='int'),
|
||||
_max_file_size_for_diff=dict(type="int"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
min_docker_api_version='1.20',
|
||||
min_docker_api_version="1.20",
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[('path', 'content')],
|
||||
required_together=[('owner_id', 'group_id')],
|
||||
mutually_exclusive=[("path", "content")],
|
||||
required_together=[("owner_id", "group_id")],
|
||||
required_by={
|
||||
'content': ['mode'],
|
||||
"content": ["mode"],
|
||||
},
|
||||
)
|
||||
|
||||
container = client.module.params['container']
|
||||
managed_path = client.module.params['path']
|
||||
container_path = client.module.params['container_path']
|
||||
follow = client.module.params['follow']
|
||||
local_follow = client.module.params['local_follow']
|
||||
owner_id = client.module.params['owner_id']
|
||||
group_id = client.module.params['group_id']
|
||||
mode = client.module.params['mode']
|
||||
force = client.module.params['force']
|
||||
content = client.module.params['content']
|
||||
max_file_size_for_diff = client.module.params['_max_file_size_for_diff'] or 1
|
||||
container = client.module.params["container"]
|
||||
managed_path = client.module.params["path"]
|
||||
container_path = client.module.params["container_path"]
|
||||
follow = client.module.params["follow"]
|
||||
local_follow = client.module.params["local_follow"]
|
||||
owner_id = client.module.params["owner_id"]
|
||||
group_id = client.module.params["group_id"]
|
||||
mode = client.module.params["mode"]
|
||||
force = client.module.params["force"]
|
||||
content = client.module.params["content"]
|
||||
max_file_size_for_diff = client.module.params["_max_file_size_for_diff"] or 1
|
||||
|
||||
if mode is not None:
|
||||
mode_parse = client.module.params['mode_parse']
|
||||
mode_parse = client.module.params["mode_parse"]
|
||||
try:
|
||||
if mode_parse == 'legacy':
|
||||
if mode_parse == "legacy":
|
||||
mode = check_type_int(mode)
|
||||
elif mode_parse == 'modern':
|
||||
elif mode_parse == "modern":
|
||||
mode = parse_modern(mode)
|
||||
elif mode_parse == 'octal_string_only':
|
||||
elif mode_parse == "octal_string_only":
|
||||
mode = parse_octal_string_only(mode)
|
||||
except (TypeError, ValueError) as e:
|
||||
client.fail(f"Error while parsing 'mode': {e}")
|
||||
@@ -852,11 +1080,13 @@ def main():
|
||||
client.fail(f"'mode' must not be negative; got {mode}")
|
||||
|
||||
if content is not None:
|
||||
if client.module.params['content_is_b64']:
|
||||
if client.module.params["content_is_b64"]:
|
||||
try:
|
||||
content = base64.b64decode(content)
|
||||
except Exception as e: # depending on Python version and error, multiple different exceptions can be raised
|
||||
client.fail(f'Cannot Base64 decode the content option: {e}')
|
||||
except (
|
||||
Exception
|
||||
) as e: # depending on Python version and error, multiple different exceptions can be raised
|
||||
client.fail(f"Cannot Base64 decode the content option: {e}")
|
||||
else:
|
||||
content = to_bytes(content)
|
||||
|
||||
@@ -899,24 +1129,31 @@ def main():
|
||||
)
|
||||
else:
|
||||
# Can happen if a user explicitly passes `content: null` or `path: null`...
|
||||
client.fail('One of path and content must be supplied')
|
||||
client.fail("One of path and content must be supplied")
|
||||
except NotFound as exc:
|
||||
client.fail(f'Could not find container "{container}" or resource in it ({exc})')
|
||||
except APIError as exc:
|
||||
client.fail(f'An unexpected Docker error occurred for container "{container}": {exc}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f'An unexpected Docker error occurred for container "{container}": {exc}',
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except DockerException as exc:
|
||||
client.fail(f'An unexpected Docker error occurred for container "{container}": {exc}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f'An unexpected Docker error occurred for container "{container}": {exc}',
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as exc:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred for container "{container}" when trying to talk to the Docker daemon: {exc}',
|
||||
exception=traceback.format_exc())
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except DockerUnexpectedError as exc:
|
||||
client.fail(f'Unexpected error: {exc}', exception=traceback.format_exc())
|
||||
client.fail(f"Unexpected error: {exc}", exception=traceback.format_exc())
|
||||
except DockerFileCopyError as exc:
|
||||
client.fail(to_native(exc))
|
||||
except OSError as exc:
|
||||
client.fail(f'Unexpected error: {exc}', exception=traceback.format_exc())
|
||||
client.fail(f"Unexpected error: {exc}", exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -167,110 +167,116 @@ import selectors
|
||||
import shlex
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_text, to_bytes
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.socket_handler import (
|
||||
DockerSocketHandlerModule,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import format_environment
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
format_environment,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.socket_handler import (
|
||||
DockerSocketHandlerModule,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
container=dict(type='str', required=True),
|
||||
argv=dict(type='list', elements='str'),
|
||||
command=dict(type='str'),
|
||||
chdir=dict(type='str'),
|
||||
detach=dict(type='bool', default=False),
|
||||
user=dict(type='str'),
|
||||
stdin=dict(type='str'),
|
||||
stdin_add_newline=dict(type='bool', default=True),
|
||||
strip_empty_ends=dict(type='bool', default=True),
|
||||
tty=dict(type='bool', default=False),
|
||||
env=dict(type='dict'),
|
||||
container=dict(type="str", required=True),
|
||||
argv=dict(type="list", elements="str"),
|
||||
command=dict(type="str"),
|
||||
chdir=dict(type="str"),
|
||||
detach=dict(type="bool", default=False),
|
||||
user=dict(type="str"),
|
||||
stdin=dict(type="str"),
|
||||
stdin_add_newline=dict(type="bool", default=True),
|
||||
strip_empty_ends=dict(type="bool", default=True),
|
||||
tty=dict(type="bool", default=False),
|
||||
env=dict(type="dict"),
|
||||
)
|
||||
|
||||
option_minimal_versions = dict(
|
||||
chdir=dict(docker_api_version='1.35'),
|
||||
chdir=dict(docker_api_version="1.35"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
option_minimal_versions=option_minimal_versions,
|
||||
mutually_exclusive=[('argv', 'command')],
|
||||
required_one_of=[('argv', 'command')],
|
||||
mutually_exclusive=[("argv", "command")],
|
||||
required_one_of=[("argv", "command")],
|
||||
)
|
||||
|
||||
container = client.module.params['container']
|
||||
argv = client.module.params['argv']
|
||||
command = client.module.params['command']
|
||||
chdir = client.module.params['chdir']
|
||||
detach = client.module.params['detach']
|
||||
user = client.module.params['user']
|
||||
stdin = client.module.params['stdin']
|
||||
strip_empty_ends = client.module.params['strip_empty_ends']
|
||||
tty = client.module.params['tty']
|
||||
env = client.module.params['env']
|
||||
container = client.module.params["container"]
|
||||
argv = client.module.params["argv"]
|
||||
command = client.module.params["command"]
|
||||
chdir = client.module.params["chdir"]
|
||||
detach = client.module.params["detach"]
|
||||
user = client.module.params["user"]
|
||||
stdin = client.module.params["stdin"]
|
||||
strip_empty_ends = client.module.params["strip_empty_ends"]
|
||||
tty = client.module.params["tty"]
|
||||
env = client.module.params["env"]
|
||||
|
||||
if env is not None:
|
||||
for name, value in list(env.items()):
|
||||
if not isinstance(value, str):
|
||||
client.module.fail_json(
|
||||
msg="Non-string value found for env option. Ambiguous env options must be "
|
||||
f"wrapped in quotes to avoid them being interpreted. Key: {name}")
|
||||
env[name] = to_text(value, errors='surrogate_or_strict')
|
||||
f"wrapped in quotes to avoid them being interpreted. Key: {name}"
|
||||
)
|
||||
env[name] = to_text(value, errors="surrogate_or_strict")
|
||||
|
||||
if command is not None:
|
||||
argv = shlex.split(command)
|
||||
|
||||
if detach and stdin is not None:
|
||||
client.module.fail_json(msg='If detach=true, stdin cannot be provided.')
|
||||
client.module.fail_json(msg="If detach=true, stdin cannot be provided.")
|
||||
|
||||
if stdin is not None and client.module.params['stdin_add_newline']:
|
||||
stdin += '\n'
|
||||
if stdin is not None and client.module.params["stdin_add_newline"]:
|
||||
stdin += "\n"
|
||||
|
||||
try:
|
||||
data = {
|
||||
'Container': container,
|
||||
'User': user or '',
|
||||
'Privileged': False,
|
||||
'Tty': False,
|
||||
'AttachStdin': bool(stdin),
|
||||
'AttachStdout': True,
|
||||
'AttachStderr': True,
|
||||
'Cmd': argv,
|
||||
'Env': format_environment(env) if env is not None else None,
|
||||
"Container": container,
|
||||
"User": user or "",
|
||||
"Privileged": False,
|
||||
"Tty": False,
|
||||
"AttachStdin": bool(stdin),
|
||||
"AttachStdout": True,
|
||||
"AttachStderr": True,
|
||||
"Cmd": argv,
|
||||
"Env": format_environment(env) if env is not None else None,
|
||||
}
|
||||
if chdir is not None:
|
||||
data['WorkingDir'] = chdir
|
||||
data["WorkingDir"] = chdir
|
||||
|
||||
exec_data = client.post_json_to_json('/containers/{0}/exec', container, data=data)
|
||||
exec_id = exec_data['Id']
|
||||
exec_data = client.post_json_to_json(
|
||||
"/containers/{0}/exec", container, data=data
|
||||
)
|
||||
exec_id = exec_data["Id"]
|
||||
|
||||
data = {
|
||||
'Tty': tty,
|
||||
'Detach': detach,
|
||||
"Tty": tty,
|
||||
"Detach": detach,
|
||||
}
|
||||
if detach:
|
||||
client.post_json_to_text('/exec/{0}/start', exec_id, data=data)
|
||||
client.post_json_to_text("/exec/{0}/start", exec_id, data=data)
|
||||
client.module.exit_json(changed=True, exec_id=exec_id)
|
||||
|
||||
else:
|
||||
if stdin and not detach:
|
||||
exec_socket = client.post_json_to_stream_socket('/exec/{0}/start', exec_id, data=data)
|
||||
exec_socket = client.post_json_to_stream_socket(
|
||||
"/exec/{0}/start", exec_id, data=data
|
||||
)
|
||||
try:
|
||||
with DockerSocketHandlerModule(exec_socket, client.module, selectors) as exec_socket_handler:
|
||||
with DockerSocketHandlerModule(
|
||||
exec_socket, client.module, selectors
|
||||
) as exec_socket_handler:
|
||||
if stdin:
|
||||
exec_socket_handler.write(to_bytes(stdin))
|
||||
|
||||
@@ -278,35 +284,49 @@ def main():
|
||||
finally:
|
||||
exec_socket.close()
|
||||
else:
|
||||
stdout, stderr = client.post_json_to_stream('/exec/{0}/start', exec_id, data=data, stream=False, tty=tty, demux=True)
|
||||
stdout, stderr = client.post_json_to_stream(
|
||||
"/exec/{0}/start",
|
||||
exec_id,
|
||||
data=data,
|
||||
stream=False,
|
||||
tty=tty,
|
||||
demux=True,
|
||||
)
|
||||
|
||||
result = client.get_json('/exec/{0}/json', exec_id)
|
||||
result = client.get_json("/exec/{0}/json", exec_id)
|
||||
|
||||
stdout = to_text(stdout or b'')
|
||||
stderr = to_text(stderr or b'')
|
||||
stdout = to_text(stdout or b"")
|
||||
stderr = to_text(stderr or b"")
|
||||
if strip_empty_ends:
|
||||
stdout = stdout.rstrip('\r\n')
|
||||
stderr = stderr.rstrip('\r\n')
|
||||
stdout = stdout.rstrip("\r\n")
|
||||
stderr = stderr.rstrip("\r\n")
|
||||
|
||||
client.module.exit_json(
|
||||
changed=True,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
rc=result.get('ExitCode') or 0,
|
||||
rc=result.get("ExitCode") or 0,
|
||||
)
|
||||
except NotFound:
|
||||
client.fail(f'Could not find container "{container}"')
|
||||
except APIError as e:
|
||||
if e.response is not None and e.response.status_code == 409:
|
||||
client.fail(f'The container "{container}" has been paused ({e})')
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -77,16 +77,18 @@ container:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
name=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -95,7 +97,7 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
container = client.get_container(client.module.params['name'])
|
||||
container = client.get_container(client.module.params["name"])
|
||||
|
||||
client.module.exit_json(
|
||||
changed=False,
|
||||
@@ -103,12 +105,16 @@ def main():
|
||||
container=container,
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -176,7 +176,6 @@ import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.context.api import (
|
||||
ContextAPI,
|
||||
)
|
||||
@@ -196,10 +195,10 @@ def tls_context_to_json(context):
|
||||
if context is None:
|
||||
return None
|
||||
return {
|
||||
'client_cert': context.cert[0] if context.cert else None,
|
||||
'client_key': context.cert[1] if context.cert else None,
|
||||
'ca_cert': context.ca_cert,
|
||||
'verify': context.verify,
|
||||
"client_cert": context.cert[0] if context.cert else None,
|
||||
"client_key": context.cert[1] if context.cert else None,
|
||||
"ca_cert": context.ca_cert,
|
||||
"verify": context.verify,
|
||||
# 'ssl_version': context.ssl_version, -- this isn't used anymore
|
||||
}
|
||||
|
||||
@@ -210,52 +209,52 @@ def to_bool(value):
|
||||
|
||||
def context_to_json(context, current):
|
||||
module_config = {}
|
||||
if 'docker' in context.endpoints:
|
||||
endpoint = context.endpoints['docker']
|
||||
if isinstance(endpoint.get('Host'), str):
|
||||
host_str = to_text(endpoint['Host'])
|
||||
if "docker" in context.endpoints:
|
||||
endpoint = context.endpoints["docker"]
|
||||
if isinstance(endpoint.get("Host"), str):
|
||||
host_str = to_text(endpoint["Host"])
|
||||
|
||||
# Adjust protocol name so that it works with the Docker CLI tool as well
|
||||
proto = None
|
||||
idx = host_str.find('://')
|
||||
idx = host_str.find("://")
|
||||
if idx >= 0:
|
||||
proto = host_str[:idx]
|
||||
host_str = host_str[idx + 3:]
|
||||
if proto in ('http', 'https'):
|
||||
proto = 'tcp'
|
||||
if proto == 'http+unix':
|
||||
proto = 'unix'
|
||||
host_str = host_str[idx + 3 :]
|
||||
if proto in ("http", "https"):
|
||||
proto = "tcp"
|
||||
if proto == "http+unix":
|
||||
proto = "unix"
|
||||
if proto:
|
||||
host_str = f"{proto}://{host_str}"
|
||||
|
||||
# Create config for the modules
|
||||
module_config['docker_host'] = host_str
|
||||
if context.tls_cfg.get('docker'):
|
||||
tls_cfg = context.tls_cfg['docker']
|
||||
module_config["docker_host"] = host_str
|
||||
if context.tls_cfg.get("docker"):
|
||||
tls_cfg = context.tls_cfg["docker"]
|
||||
if tls_cfg.ca_cert:
|
||||
module_config['ca_path'] = tls_cfg.ca_cert
|
||||
module_config["ca_path"] = tls_cfg.ca_cert
|
||||
if tls_cfg.cert:
|
||||
module_config['client_cert'] = tls_cfg.cert[0]
|
||||
module_config['client_key'] = tls_cfg.cert[1]
|
||||
module_config['validate_certs'] = tls_cfg.verify
|
||||
module_config['tls'] = True
|
||||
module_config["client_cert"] = tls_cfg.cert[0]
|
||||
module_config["client_key"] = tls_cfg.cert[1]
|
||||
module_config["validate_certs"] = tls_cfg.verify
|
||||
module_config["tls"] = True
|
||||
else:
|
||||
module_config['tls'] = to_bool(endpoint.get('SkipTLSVerify'))
|
||||
module_config["tls"] = to_bool(endpoint.get("SkipTLSVerify"))
|
||||
return {
|
||||
'current': current,
|
||||
'name': context.name,
|
||||
'description': context.description,
|
||||
'meta_path': None if context.meta_path is IN_MEMORY else context.meta_path,
|
||||
'tls_path': None if context.tls_path is IN_MEMORY else context.tls_path,
|
||||
'config': module_config,
|
||||
"current": current,
|
||||
"name": context.name,
|
||||
"description": context.description,
|
||||
"meta_path": None if context.meta_path is IN_MEMORY else context.meta_path,
|
||||
"tls_path": None if context.tls_path is IN_MEMORY else context.tls_path,
|
||||
"config": module_config,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
only_current=dict(type='bool', default=False),
|
||||
name=dict(type='str'),
|
||||
cli_context=dict(type='str'),
|
||||
only_current=dict(type="bool", default=False),
|
||||
name=dict(type="str"),
|
||||
cli_context=dict(type="str"),
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
@@ -267,15 +266,22 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
if module.params['cli_context']:
|
||||
current_context_name, current_context_source = module.params['cli_context'], "cli_context module option"
|
||||
if module.params["cli_context"]:
|
||||
current_context_name, current_context_source = (
|
||||
module.params["cli_context"],
|
||||
"cli_context module option",
|
||||
)
|
||||
else:
|
||||
current_context_name, current_context_source = get_current_context_name_with_source()
|
||||
if module.params['name']:
|
||||
contexts = [ContextAPI.get_context(module.params['name'])]
|
||||
current_context_name, current_context_source = (
|
||||
get_current_context_name_with_source()
|
||||
)
|
||||
if module.params["name"]:
|
||||
contexts = [ContextAPI.get_context(module.params["name"])]
|
||||
if not contexts[0]:
|
||||
module.fail_json(msg=f"There is no context of name {module.params['name']!r}")
|
||||
elif module.params['only_current']:
|
||||
module.fail_json(
|
||||
msg=f"There is no context of name {module.params['name']!r}"
|
||||
)
|
||||
elif module.params["only_current"]:
|
||||
contexts = [ContextAPI.get_context(current_context_name)]
|
||||
if not contexts[0]:
|
||||
module.fail_json(
|
||||
@@ -284,10 +290,13 @@ def main():
|
||||
else:
|
||||
contexts = ContextAPI.contexts()
|
||||
|
||||
json_contexts = sorted([
|
||||
context_to_json(context, context.name == current_context_name)
|
||||
for context in contexts
|
||||
], key=lambda entry: entry['name'])
|
||||
json_contexts = sorted(
|
||||
[
|
||||
context_to_json(context, context.name == current_context_name)
|
||||
for context in contexts
|
||||
],
|
||||
key=lambda entry: entry["name"],
|
||||
)
|
||||
|
||||
module.exit_json(
|
||||
changed=False,
|
||||
@@ -295,10 +304,16 @@ def main():
|
||||
current_context_name=current_context_name,
|
||||
)
|
||||
except ContextException as e:
|
||||
module.fail_json(msg=f'Error when handling Docker contexts: {e}', exception=traceback.format_exc())
|
||||
module.fail_json(
|
||||
msg=f"Error when handling Docker contexts: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except DockerException as e:
|
||||
module.fail_json(msg=f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
module.fail_json(
|
||||
msg=f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -213,17 +213,21 @@ disk_usage:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
convert_filters,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
clean_dict_booleans_for_docker_api,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, APIError
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import convert_filters
|
||||
|
||||
|
||||
class DockerHostManager(DockerBaseClass):
|
||||
@@ -234,24 +238,28 @@ class DockerHostManager(DockerBaseClass):
|
||||
|
||||
self.client = client
|
||||
self.results = results
|
||||
self.verbose_output = self.client.module.params['verbose_output']
|
||||
self.verbose_output = self.client.module.params["verbose_output"]
|
||||
|
||||
listed_objects = ['volumes', 'networks', 'containers', 'images']
|
||||
listed_objects = ["volumes", "networks", "containers", "images"]
|
||||
|
||||
self.results['host_info'] = self.get_docker_host_info()
|
||||
self.results["host_info"] = self.get_docker_host_info()
|
||||
# At this point we definitely know that we can talk to the Docker daemon
|
||||
self.results['can_talk_to_docker'] = True
|
||||
self.client.fail_results['can_talk_to_docker'] = True
|
||||
self.results["can_talk_to_docker"] = True
|
||||
self.client.fail_results["can_talk_to_docker"] = True
|
||||
|
||||
if self.client.module.params['disk_usage']:
|
||||
self.results['disk_usage'] = self.get_docker_disk_usage_facts()
|
||||
if self.client.module.params["disk_usage"]:
|
||||
self.results["disk_usage"] = self.get_docker_disk_usage_facts()
|
||||
|
||||
for docker_object in listed_objects:
|
||||
if self.client.module.params[docker_object]:
|
||||
returned_name = docker_object
|
||||
filter_name = docker_object + "_filters"
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get(filter_name), True)
|
||||
self.results[returned_name] = self.get_docker_items_list(docker_object, filters)
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get(filter_name), True
|
||||
)
|
||||
self.results[returned_name] = self.get_docker_items_list(
|
||||
docker_object, filters
|
||||
)
|
||||
|
||||
def get_docker_host_info(self):
|
||||
try:
|
||||
@@ -264,7 +272,7 @@ class DockerHostManager(DockerBaseClass):
|
||||
if self.verbose_output:
|
||||
return self.client.df()
|
||||
else:
|
||||
return dict(LayersSize=self.client.df()['LayersSize'])
|
||||
return dict(LayersSize=self.client.df()["LayersSize"])
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error inspecting docker host: {exc}")
|
||||
|
||||
@@ -272,44 +280,52 @@ class DockerHostManager(DockerBaseClass):
|
||||
items = None
|
||||
items_list = []
|
||||
|
||||
header_containers = ['Id', 'Image', 'Command', 'Created', 'Status', 'Ports', 'Names']
|
||||
header_volumes = ['Driver', 'Name']
|
||||
header_images = ['Id', 'RepoTags', 'Created', 'Size']
|
||||
header_networks = ['Id', 'Driver', 'Name', 'Scope']
|
||||
header_containers = [
|
||||
"Id",
|
||||
"Image",
|
||||
"Command",
|
||||
"Created",
|
||||
"Status",
|
||||
"Ports",
|
||||
"Names",
|
||||
]
|
||||
header_volumes = ["Driver", "Name"]
|
||||
header_images = ["Id", "RepoTags", "Created", "Size"]
|
||||
header_networks = ["Id", "Driver", "Name", "Scope"]
|
||||
|
||||
filter_arg = dict()
|
||||
if filters:
|
||||
filter_arg['filters'] = filters
|
||||
filter_arg["filters"] = filters
|
||||
try:
|
||||
if docker_object == 'containers':
|
||||
if docker_object == "containers":
|
||||
params = {
|
||||
'limit': -1,
|
||||
'all': 1 if self.client.module.params['containers_all'] else 0,
|
||||
'size': 0,
|
||||
'trunc_cmd': 0,
|
||||
'filters': convert_filters(filters) if filters else None,
|
||||
"limit": -1,
|
||||
"all": 1 if self.client.module.params["containers_all"] else 0,
|
||||
"size": 0,
|
||||
"trunc_cmd": 0,
|
||||
"filters": convert_filters(filters) if filters else None,
|
||||
}
|
||||
items = self.client.get_json("/containers/json", params=params)
|
||||
elif docker_object == 'networks':
|
||||
params = {
|
||||
'filters': convert_filters(filters or {})
|
||||
}
|
||||
elif docker_object == "networks":
|
||||
params = {"filters": convert_filters(filters or {})}
|
||||
items = self.client.get_json("/networks", params=params)
|
||||
elif docker_object == 'images':
|
||||
elif docker_object == "images":
|
||||
params = {
|
||||
'only_ids': 0,
|
||||
'all': 0,
|
||||
'filters': convert_filters(filters) if filters else None,
|
||||
"only_ids": 0,
|
||||
"all": 0,
|
||||
"filters": convert_filters(filters) if filters else None,
|
||||
}
|
||||
items = self.client.get_json("/images/json", params=params)
|
||||
elif docker_object == 'volumes':
|
||||
elif docker_object == "volumes":
|
||||
params = {
|
||||
'filters': convert_filters(filters) if filters else None,
|
||||
"filters": convert_filters(filters) if filters else None,
|
||||
}
|
||||
items = self.client.get_json('/volumes', params=params)
|
||||
items = items['Volumes']
|
||||
items = self.client.get_json("/volumes", params=params)
|
||||
items = items["Volumes"]
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error inspecting docker host for object '{docker_object}': {exc}")
|
||||
self.client.fail(
|
||||
f"Error inspecting docker host for object '{docker_object}': {exc}"
|
||||
)
|
||||
|
||||
if self.verbose_output:
|
||||
return items
|
||||
@@ -317,16 +333,16 @@ class DockerHostManager(DockerBaseClass):
|
||||
for item in items:
|
||||
item_record = dict()
|
||||
|
||||
if docker_object == 'containers':
|
||||
if docker_object == "containers":
|
||||
for key in header_containers:
|
||||
item_record[key] = item.get(key)
|
||||
elif docker_object == 'networks':
|
||||
elif docker_object == "networks":
|
||||
for key in header_networks:
|
||||
item_record[key] = item.get(key)
|
||||
elif docker_object == 'images':
|
||||
elif docker_object == "images":
|
||||
for key in header_images:
|
||||
item_record[key] = item.get(key)
|
||||
elif docker_object == 'volumes':
|
||||
elif docker_object == "volumes":
|
||||
for key in header_volumes:
|
||||
item_record[key] = item.get(key)
|
||||
items_list.append(item_record)
|
||||
@@ -336,17 +352,17 @@ class DockerHostManager(DockerBaseClass):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
containers=dict(type='bool', default=False),
|
||||
containers_all=dict(type='bool', default=False),
|
||||
containers_filters=dict(type='dict'),
|
||||
images=dict(type='bool', default=False),
|
||||
images_filters=dict(type='dict'),
|
||||
networks=dict(type='bool', default=False),
|
||||
networks_filters=dict(type='dict'),
|
||||
volumes=dict(type='bool', default=False),
|
||||
volumes_filters=dict(type='dict'),
|
||||
disk_usage=dict(type='bool', default=False),
|
||||
verbose_output=dict(type='bool', default=False),
|
||||
containers=dict(type="bool", default=False),
|
||||
containers_all=dict(type="bool", default=False),
|
||||
containers_filters=dict(type="dict"),
|
||||
images=dict(type="bool", default=False),
|
||||
images_filters=dict(type="dict"),
|
||||
networks=dict(type="bool", default=False),
|
||||
networks_filters=dict(type="dict"),
|
||||
volumes=dict(type="bool", default=False),
|
||||
volumes_filters=dict(type="dict"),
|
||||
disk_usage=dict(type="bool", default=False),
|
||||
verbose_output=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -356,9 +372,12 @@ def main():
|
||||
can_talk_to_docker=False,
|
||||
),
|
||||
)
|
||||
if client.module.params['api_version'] is None or client.module.params['api_version'].lower() == 'auto':
|
||||
if (
|
||||
client.module.params["api_version"] is None
|
||||
or client.module.params["api_version"].lower() == "auto"
|
||||
):
|
||||
# At this point we know that we can talk to Docker, since we asked it for the API version
|
||||
client.fail_results['can_talk_to_docker'] = True
|
||||
client.fail_results["can_talk_to_docker"] = True
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
@@ -368,12 +387,16 @@ def main():
|
||||
DockerHostManager(client, results)
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+379
-263
File diff suppressed because it is too large
Load Diff
@@ -285,23 +285,21 @@ import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.common.text.formatters import human_to_bytes
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
clean_dict_booleans_for_docker_api,
|
||||
is_image_name_id,
|
||||
is_valid_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
from ansible_collections.community.docker.plugins.module_utils.version import (
|
||||
LooseVersion,
|
||||
)
|
||||
|
||||
|
||||
@@ -309,15 +307,15 @@ def convert_to_bytes(value, module, name, unlimited_value=None):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
if unlimited_value is not None and value in ('unlimited', str(unlimited_value)):
|
||||
if unlimited_value is not None and value in ("unlimited", str(unlimited_value)):
|
||||
return unlimited_value
|
||||
return human_to_bytes(value)
|
||||
except ValueError as exc:
|
||||
module.fail_json(msg=f'Failed to convert {name} to bytes: {exc}')
|
||||
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
|
||||
|
||||
|
||||
def dict_to_list(dictionary, concat='='):
|
||||
return [f'{k}{concat}{v}' for k, v in sorted(dictionary.items())]
|
||||
def dict_to_list(dictionary, concat="="):
|
||||
return [f"{k}{concat}{v}" for k, v in sorted(dictionary.items())]
|
||||
|
||||
|
||||
def _quote_csv(input):
|
||||
@@ -334,47 +332,59 @@ class ImageBuilder(DockerBaseClass):
|
||||
self.check_mode = self.client.check_mode
|
||||
parameters = self.client.module.params
|
||||
|
||||
self.cache_from = parameters['cache_from']
|
||||
self.pull = parameters['pull']
|
||||
self.network = parameters['network']
|
||||
self.nocache = parameters['nocache']
|
||||
self.etc_hosts = clean_dict_booleans_for_docker_api(parameters['etc_hosts'])
|
||||
self.args = clean_dict_booleans_for_docker_api(parameters['args'])
|
||||
self.target = parameters['target']
|
||||
self.platform = parameters['platform']
|
||||
self.shm_size = convert_to_bytes(parameters['shm_size'], self.client.module, 'shm_size')
|
||||
self.labels = clean_dict_booleans_for_docker_api(parameters['labels'])
|
||||
self.rebuild = parameters['rebuild']
|
||||
self.secrets = parameters['secrets']
|
||||
self.outputs = parameters['outputs']
|
||||
self.cache_from = parameters["cache_from"]
|
||||
self.pull = parameters["pull"]
|
||||
self.network = parameters["network"]
|
||||
self.nocache = parameters["nocache"]
|
||||
self.etc_hosts = clean_dict_booleans_for_docker_api(parameters["etc_hosts"])
|
||||
self.args = clean_dict_booleans_for_docker_api(parameters["args"])
|
||||
self.target = parameters["target"]
|
||||
self.platform = parameters["platform"]
|
||||
self.shm_size = convert_to_bytes(
|
||||
parameters["shm_size"], self.client.module, "shm_size"
|
||||
)
|
||||
self.labels = clean_dict_booleans_for_docker_api(parameters["labels"])
|
||||
self.rebuild = parameters["rebuild"]
|
||||
self.secrets = parameters["secrets"]
|
||||
self.outputs = parameters["outputs"]
|
||||
|
||||
buildx = self.client.get_client_plugin_info('buildx')
|
||||
buildx = self.client.get_client_plugin_info("buildx")
|
||||
if buildx is None:
|
||||
self.fail(f'Docker CLI {self.client.get_cli()} does not have the buildx plugin installed')
|
||||
buildx_version = buildx['Version'].lstrip('v')
|
||||
self.fail(
|
||||
f"Docker CLI {self.client.get_cli()} does not have the buildx plugin installed"
|
||||
)
|
||||
buildx_version = buildx["Version"].lstrip("v")
|
||||
|
||||
if self.secrets:
|
||||
for secret in self.secrets:
|
||||
if secret['type'] in ('env', 'value'):
|
||||
if LooseVersion(buildx_version) < LooseVersion('0.6.0'):
|
||||
self.fail(f'The Docker buildx plugin has version {buildx_version}, but 0.6.0 is needed for secrets of type=env and type=value')
|
||||
if secret["type"] in ("env", "value"):
|
||||
if LooseVersion(buildx_version) < LooseVersion("0.6.0"):
|
||||
self.fail(
|
||||
f"The Docker buildx plugin has version {buildx_version}, but 0.6.0 is needed for secrets of type=env and type=value"
|
||||
)
|
||||
if self.outputs and len(self.outputs) > 1:
|
||||
if LooseVersion(buildx_version) < LooseVersion('0.13.0'):
|
||||
self.fail(f'The Docker buildx plugin has version {buildx_version}, but 0.13.0 is needed to specify more than one output')
|
||||
if LooseVersion(buildx_version) < LooseVersion("0.13.0"):
|
||||
self.fail(
|
||||
f"The Docker buildx plugin has version {buildx_version}, but 0.13.0 is needed to specify more than one output"
|
||||
)
|
||||
|
||||
self.path = parameters['path']
|
||||
self.path = parameters["path"]
|
||||
if not os.path.isdir(self.path):
|
||||
self.fail(f'"{self.path}" is not an existing directory')
|
||||
self.dockerfile = parameters['dockerfile']
|
||||
if self.dockerfile and not os.path.isfile(os.path.join(self.path, self.dockerfile)):
|
||||
self.fail(f'"{os.path.join(self.path, self.dockerfile)}" is not an existing file')
|
||||
self.dockerfile = parameters["dockerfile"]
|
||||
if self.dockerfile and not os.path.isfile(
|
||||
os.path.join(self.path, self.dockerfile)
|
||||
):
|
||||
self.fail(
|
||||
f'"{os.path.join(self.path, self.dockerfile)}" is not an existing file'
|
||||
)
|
||||
|
||||
self.name = parameters['name']
|
||||
self.tag = parameters['tag']
|
||||
self.name = parameters["name"]
|
||||
self.tag = parameters["tag"]
|
||||
if not is_valid_tag(self.tag, allow_empty=True):
|
||||
self.fail(f'"{self.tag}" is not a valid docker tag')
|
||||
if is_image_name_id(self.name):
|
||||
self.fail('Image name must not be a digest')
|
||||
self.fail("Image name must not be a digest")
|
||||
|
||||
# If name contains a tag, it takes precedence over tag parameter.
|
||||
repo, repo_tag = parse_repository_tag(self.name)
|
||||
@@ -383,25 +393,27 @@ class ImageBuilder(DockerBaseClass):
|
||||
self.tag = repo_tag
|
||||
|
||||
if is_image_name_id(self.tag):
|
||||
self.fail('Image name must not contain a digest, but have a tag')
|
||||
self.fail("Image name must not contain a digest, but have a tag")
|
||||
|
||||
if self.outputs:
|
||||
found = False
|
||||
name_tag = f'{self.name}:{self.tag}'
|
||||
name_tag = f"{self.name}:{self.tag}"
|
||||
for output in self.outputs:
|
||||
if output['type'] == 'image':
|
||||
if not output['name']:
|
||||
if output["type"] == "image":
|
||||
if not output["name"]:
|
||||
# Since we no longer pass --tag if --output is provided, we need to set this manually
|
||||
output['name'] = [name_tag]
|
||||
if output['name'] and name_tag in output['name']:
|
||||
output["name"] = [name_tag]
|
||||
if output["name"] and name_tag in output["name"]:
|
||||
found = True
|
||||
if not found:
|
||||
self.outputs.append({
|
||||
'type': 'image',
|
||||
'name': [name_tag],
|
||||
'push': False,
|
||||
})
|
||||
if LooseVersion(buildx_version) < LooseVersion('0.13.0'):
|
||||
self.outputs.append(
|
||||
{
|
||||
"type": "image",
|
||||
"name": [name_tag],
|
||||
"push": False,
|
||||
}
|
||||
)
|
||||
if LooseVersion(buildx_version) < LooseVersion("0.13.0"):
|
||||
self.fail(
|
||||
f"The output does not include an image with name {name_tag}, and the Docker"
|
||||
f" buildx plugin has version {buildx_version} which only supports one output."
|
||||
@@ -417,78 +429,86 @@ class ImageBuilder(DockerBaseClass):
|
||||
def add_args(self, args):
|
||||
environ_update = {}
|
||||
if not self.outputs:
|
||||
args.extend(['--tag', f'{self.name}:{self.tag}'])
|
||||
args.extend(["--tag", f"{self.name}:{self.tag}"])
|
||||
if self.dockerfile:
|
||||
args.extend(['--file', os.path.join(self.path, self.dockerfile)])
|
||||
args.extend(["--file", os.path.join(self.path, self.dockerfile)])
|
||||
if self.cache_from:
|
||||
self.add_list_arg(args, '--cache-from', self.cache_from)
|
||||
self.add_list_arg(args, "--cache-from", self.cache_from)
|
||||
if self.pull:
|
||||
args.append('--pull')
|
||||
args.append("--pull")
|
||||
if self.network:
|
||||
args.extend(['--network', self.network])
|
||||
args.extend(["--network", self.network])
|
||||
if self.nocache:
|
||||
args.append('--no-cache')
|
||||
args.append("--no-cache")
|
||||
if self.etc_hosts:
|
||||
self.add_list_arg(args, '--add-host', dict_to_list(self.etc_hosts, ':'))
|
||||
self.add_list_arg(args, "--add-host", dict_to_list(self.etc_hosts, ":"))
|
||||
if self.args:
|
||||
self.add_list_arg(args, '--build-arg', dict_to_list(self.args))
|
||||
self.add_list_arg(args, "--build-arg", dict_to_list(self.args))
|
||||
if self.target:
|
||||
args.extend(['--target', self.target])
|
||||
args.extend(["--target", self.target])
|
||||
if self.platform:
|
||||
for platform in self.platform:
|
||||
args.extend(['--platform', platform])
|
||||
args.extend(["--platform", platform])
|
||||
if self.shm_size:
|
||||
args.extend(['--shm-size', str(self.shm_size)])
|
||||
args.extend(["--shm-size", str(self.shm_size)])
|
||||
if self.labels:
|
||||
self.add_list_arg(args, '--label', dict_to_list(self.labels))
|
||||
self.add_list_arg(args, "--label", dict_to_list(self.labels))
|
||||
if self.secrets:
|
||||
random_prefix = None
|
||||
for index, secret in enumerate(self.secrets):
|
||||
sid = secret['id']
|
||||
if secret['type'] == 'file':
|
||||
src = secret['src']
|
||||
args.extend(['--secret', f'id={sid},type=file,src={src}'])
|
||||
if secret['type'] == 'env':
|
||||
env = secret['src']
|
||||
args.extend(['--secret', f'id={sid},type=env,env={env}'])
|
||||
if secret['type'] == 'value':
|
||||
sid = secret["id"]
|
||||
if secret["type"] == "file":
|
||||
src = secret["src"]
|
||||
args.extend(["--secret", f"id={sid},type=file,src={src}"])
|
||||
if secret["type"] == "env":
|
||||
env = secret["src"]
|
||||
args.extend(["--secret", f"id={sid},type=env,env={env}"])
|
||||
if secret["type"] == "value":
|
||||
# We pass values on using environment variables. The user has been warned in the documentation
|
||||
# that they should only use this mechanism when being comfortable with it.
|
||||
if random_prefix is None:
|
||||
# Use /dev/urandom to generate some entropy to make the environment variable's name unguessable
|
||||
random_prefix = base64.b64encode(os.urandom(16)).decode('utf-8').replace('=', '')
|
||||
env_name = f'ANSIBLE_DOCKER_COMPOSE_ENV_SECRET_{random_prefix}_{index}'
|
||||
environ_update[env_name] = secret['value']
|
||||
args.extend(['--secret', f'id={sid},type=env,env={env_name}'])
|
||||
random_prefix = (
|
||||
base64.b64encode(os.urandom(16))
|
||||
.decode("utf-8")
|
||||
.replace("=", "")
|
||||
)
|
||||
env_name = (
|
||||
f"ANSIBLE_DOCKER_COMPOSE_ENV_SECRET_{random_prefix}_{index}"
|
||||
)
|
||||
environ_update[env_name] = secret["value"]
|
||||
args.extend(["--secret", f"id={sid},type=env,env={env_name}"])
|
||||
if self.outputs:
|
||||
for output in self.outputs:
|
||||
subargs = []
|
||||
if output['type'] == 'local':
|
||||
dest = output['dest']
|
||||
subargs.extend(['type=local', f'dest={dest}'])
|
||||
if output['type'] == 'tar':
|
||||
dest = output['dest']
|
||||
subargs.extend(['type=tar', f'dest={dest}'])
|
||||
if output['type'] == 'oci':
|
||||
dest = output['dest']
|
||||
subargs.extend(['type=oci', f'dest={dest}'])
|
||||
if output['type'] == 'docker':
|
||||
subargs.append('type=docker')
|
||||
dest = output['dest']
|
||||
if output['dest'] is not None:
|
||||
subargs.append(f'dest={dest}')
|
||||
if output['context'] is not None:
|
||||
context = output['context']
|
||||
subargs.append(f'context={context}')
|
||||
if output['type'] == 'image':
|
||||
subargs.append('type=image')
|
||||
if output['name'] is not None:
|
||||
name = ','.join(output['name'])
|
||||
subargs.append(f'name={name}')
|
||||
if output['push']:
|
||||
subargs.append('push=true')
|
||||
if output["type"] == "local":
|
||||
dest = output["dest"]
|
||||
subargs.extend(["type=local", f"dest={dest}"])
|
||||
if output["type"] == "tar":
|
||||
dest = output["dest"]
|
||||
subargs.extend(["type=tar", f"dest={dest}"])
|
||||
if output["type"] == "oci":
|
||||
dest = output["dest"]
|
||||
subargs.extend(["type=oci", f"dest={dest}"])
|
||||
if output["type"] == "docker":
|
||||
subargs.append("type=docker")
|
||||
dest = output["dest"]
|
||||
if output["dest"] is not None:
|
||||
subargs.append(f"dest={dest}")
|
||||
if output["context"] is not None:
|
||||
context = output["context"]
|
||||
subargs.append(f"context={context}")
|
||||
if output["type"] == "image":
|
||||
subargs.append("type=image")
|
||||
if output["name"] is not None:
|
||||
name = ",".join(output["name"])
|
||||
subargs.append(f"name={name}")
|
||||
if output["push"]:
|
||||
subargs.append("push=true")
|
||||
if subargs:
|
||||
args.extend(['--output', ','.join(_quote_csv(subarg) for subarg in subargs)])
|
||||
args.extend(
|
||||
["--output", ",".join(_quote_csv(subarg) for subarg in subargs)]
|
||||
)
|
||||
return environ_update
|
||||
|
||||
def build_image(self):
|
||||
@@ -500,82 +520,93 @@ class ImageBuilder(DockerBaseClass):
|
||||
)
|
||||
|
||||
if image:
|
||||
if self.rebuild == 'never':
|
||||
if self.rebuild == "never":
|
||||
return results
|
||||
|
||||
results['changed'] = True
|
||||
results["changed"] = True
|
||||
if not self.check_mode:
|
||||
args = ['buildx', 'build', '--progress', 'plain']
|
||||
args = ["buildx", "build", "--progress", "plain"]
|
||||
environ_update = self.add_args(args)
|
||||
args.extend(['--', self.path])
|
||||
rc, stdout, stderr = self.client.call_cli(*args, environ_update=environ_update)
|
||||
args.extend(["--", self.path])
|
||||
rc, stdout, stderr = self.client.call_cli(
|
||||
*args, environ_update=environ_update
|
||||
)
|
||||
if rc != 0:
|
||||
self.fail(f'Building {self.name}:{self.tag} failed', stdout=to_native(stdout), stderr=to_native(stderr), command=args)
|
||||
results['stdout'] = to_native(stdout)
|
||||
results['stderr'] = to_native(stderr)
|
||||
results['image'] = self.client.find_image(self.name, self.tag) or {}
|
||||
results['command'] = args
|
||||
self.fail(
|
||||
f"Building {self.name}:{self.tag} failed",
|
||||
stdout=to_native(stdout),
|
||||
stderr=to_native(stderr),
|
||||
command=args,
|
||||
)
|
||||
results["stdout"] = to_native(stdout)
|
||||
results["stderr"] = to_native(stderr)
|
||||
results["image"] = self.client.find_image(self.name, self.tag) or {}
|
||||
results["command"] = args
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
tag=dict(type='str', default='latest'),
|
||||
path=dict(type='path', required=True),
|
||||
dockerfile=dict(type='str'),
|
||||
cache_from=dict(type='list', elements='str'),
|
||||
pull=dict(type='bool', default=False),
|
||||
network=dict(type='str'),
|
||||
nocache=dict(type='bool', default=False),
|
||||
etc_hosts=dict(type='dict'),
|
||||
args=dict(type='dict'),
|
||||
target=dict(type='str'),
|
||||
platform=dict(type='list', elements='str'),
|
||||
shm_size=dict(type='str'),
|
||||
labels=dict(type='dict'),
|
||||
rebuild=dict(type='str', choices=['never', 'always'], default='never'),
|
||||
name=dict(type="str", required=True),
|
||||
tag=dict(type="str", default="latest"),
|
||||
path=dict(type="path", required=True),
|
||||
dockerfile=dict(type="str"),
|
||||
cache_from=dict(type="list", elements="str"),
|
||||
pull=dict(type="bool", default=False),
|
||||
network=dict(type="str"),
|
||||
nocache=dict(type="bool", default=False),
|
||||
etc_hosts=dict(type="dict"),
|
||||
args=dict(type="dict"),
|
||||
target=dict(type="str"),
|
||||
platform=dict(type="list", elements="str"),
|
||||
shm_size=dict(type="str"),
|
||||
labels=dict(type="dict"),
|
||||
rebuild=dict(type="str", choices=["never", "always"], default="never"),
|
||||
secrets=dict(
|
||||
type='list',
|
||||
elements='dict',
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(
|
||||
id=dict(type='str', required=True),
|
||||
type=dict(type='str', choices=['file', 'env', 'value'], required=True),
|
||||
src=dict(type='path'),
|
||||
env=dict(type='str'),
|
||||
value=dict(type='str', no_log=True),
|
||||
id=dict(type="str", required=True),
|
||||
type=dict(type="str", choices=["file", "env", "value"], required=True),
|
||||
src=dict(type="path"),
|
||||
env=dict(type="str"),
|
||||
value=dict(type="str", no_log=True),
|
||||
),
|
||||
required_if=[
|
||||
('type', 'file', ['src']),
|
||||
('type', 'env', ['env']),
|
||||
('type', 'value', ['value']),
|
||||
("type", "file", ["src"]),
|
||||
("type", "env", ["env"]),
|
||||
("type", "value", ["value"]),
|
||||
],
|
||||
mutually_exclusive=[
|
||||
('src', 'env', 'value'),
|
||||
("src", "env", "value"),
|
||||
],
|
||||
no_log=False,
|
||||
),
|
||||
outputs=dict(
|
||||
type='list',
|
||||
elements='dict',
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(
|
||||
type=dict(type='str', choices=['local', 'tar', 'oci', 'docker', 'image'], required=True),
|
||||
dest=dict(type='path'),
|
||||
context=dict(type='str'),
|
||||
name=dict(type='list', elements='str'),
|
||||
push=dict(type='bool', default=False),
|
||||
type=dict(
|
||||
type="str",
|
||||
choices=["local", "tar", "oci", "docker", "image"],
|
||||
required=True,
|
||||
),
|
||||
dest=dict(type="path"),
|
||||
context=dict(type="str"),
|
||||
name=dict(type="list", elements="str"),
|
||||
push=dict(type="bool", default=False),
|
||||
),
|
||||
required_if=[
|
||||
('type', 'local', ['dest']),
|
||||
('type', 'tar', ['dest']),
|
||||
('type', 'oci', ['dest']),
|
||||
("type", "local", ["dest"]),
|
||||
("type", "tar", ["dest"]),
|
||||
("type", "oci", ["dest"]),
|
||||
],
|
||||
mutually_exclusive=[
|
||||
('dest', 'name'),
|
||||
('dest', 'push'),
|
||||
('context', 'name'),
|
||||
('context', 'push'),
|
||||
("dest", "name"),
|
||||
("dest", "push"),
|
||||
("context", "name"),
|
||||
("context", "push"),
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -590,8 +621,11 @@ def main():
|
||||
results = ImageBuilder(client).build_image()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -95,29 +95,29 @@ images:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.constants import (
|
||||
DEFAULT_DATA_CHUNK_SIZE,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.image_archive import (
|
||||
load_archived_image_manifest,
|
||||
api_image_id,
|
||||
ImageArchiveInvalidException,
|
||||
api_image_id,
|
||||
load_archived_image_manifest,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
is_image_name_id,
|
||||
is_valid_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.constants import (
|
||||
DEFAULT_DATA_CHUNK_SIZE,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
|
||||
|
||||
class ImageExportManager(DockerBaseClass):
|
||||
@@ -128,47 +128,52 @@ class ImageExportManager(DockerBaseClass):
|
||||
parameters = self.client.module.params
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
self.path = parameters['path']
|
||||
self.force = parameters['force']
|
||||
self.tag = parameters['tag']
|
||||
self.path = parameters["path"]
|
||||
self.force = parameters["force"]
|
||||
self.tag = parameters["tag"]
|
||||
|
||||
if not is_valid_tag(self.tag, allow_empty=True):
|
||||
self.fail(f'"{self.tag}" is not a valid docker tag')
|
||||
|
||||
# If name contains a tag, it takes precedence over tag parameter.
|
||||
self.names = []
|
||||
for name in parameters['names']:
|
||||
for name in parameters["names"]:
|
||||
if is_image_name_id(name):
|
||||
self.names.append({'id': name, 'joined': name})
|
||||
self.names.append({"id": name, "joined": name})
|
||||
else:
|
||||
repo, repo_tag = parse_repository_tag(name)
|
||||
if not repo_tag:
|
||||
repo_tag = self.tag
|
||||
self.names.append({'name': repo, 'tag': repo_tag, 'joined': f'{repo}:{repo_tag}'})
|
||||
self.names.append(
|
||||
{"name": repo, "tag": repo_tag, "joined": f"{repo}:{repo_tag}"}
|
||||
)
|
||||
|
||||
if not self.names:
|
||||
self.fail('At least one image name must be specified')
|
||||
self.fail("At least one image name must be specified")
|
||||
|
||||
def fail(self, msg):
|
||||
self.client.fail(msg)
|
||||
|
||||
def get_export_reason(self):
|
||||
if self.force:
|
||||
return 'Exporting since force=true'
|
||||
return "Exporting since force=true"
|
||||
|
||||
try:
|
||||
archived_images = load_archived_image_manifest(self.path)
|
||||
if archived_images is None:
|
||||
return 'Overwriting since no image is present in archive'
|
||||
return "Overwriting since no image is present in archive"
|
||||
except ImageArchiveInvalidException as exc:
|
||||
self.log(f'Unable to extract manifest summary from archive: {exc}')
|
||||
return 'Overwriting an unreadable archive file'
|
||||
self.log(f"Unable to extract manifest summary from archive: {exc}")
|
||||
return "Overwriting an unreadable archive file"
|
||||
|
||||
left_names = list(self.names)
|
||||
for archived_image in archived_images:
|
||||
found = False
|
||||
for i, name in enumerate(left_names):
|
||||
if name['id'] == api_image_id(archived_image.image_id) and [name['joined']] == archived_image.repo_tags:
|
||||
if (
|
||||
name["id"] == api_image_id(archived_image.image_id)
|
||||
and [name["joined"]] == archived_image.repo_tags
|
||||
):
|
||||
del left_names[i]
|
||||
found = True
|
||||
break
|
||||
@@ -181,20 +186,22 @@ class ImageExportManager(DockerBaseClass):
|
||||
|
||||
def write_chunks(self, chunks):
|
||||
try:
|
||||
with open(self.path, 'wb') as fd:
|
||||
with open(self.path, "wb") as fd:
|
||||
for chunk in chunks:
|
||||
fd.write(chunk)
|
||||
except Exception as exc:
|
||||
self.fail(f"Error writing image archive {self.path} - {exc}")
|
||||
|
||||
def export_images(self):
|
||||
image_names = [name['joined'] for name in self.names]
|
||||
image_names_str = ', '.join(image_names)
|
||||
image_names = [name["joined"] for name in self.names]
|
||||
image_names_str = ", ".join(image_names)
|
||||
if len(image_names) == 1:
|
||||
self.log(f"Getting archive of image {image_names[0]}")
|
||||
try:
|
||||
chunks = self.client._stream_raw_result(
|
||||
self.client._get(self.client._url('/images/{0}/get', image_names[0]), stream=True),
|
||||
self.client._get(
|
||||
self.client._url("/images/{0}/get", image_names[0]), stream=True
|
||||
),
|
||||
DEFAULT_DATA_CHUNK_SIZE,
|
||||
False,
|
||||
)
|
||||
@@ -205,9 +212,9 @@ class ImageExportManager(DockerBaseClass):
|
||||
try:
|
||||
chunks = self.client._stream_raw_result(
|
||||
self.client._get(
|
||||
self.client._url('/images/get'),
|
||||
self.client._url("/images/get"),
|
||||
stream=True,
|
||||
params={'names': image_names},
|
||||
params={"names": image_names},
|
||||
),
|
||||
DEFAULT_DATA_CHUNK_SIZE,
|
||||
False,
|
||||
@@ -224,26 +231,28 @@ class ImageExportManager(DockerBaseClass):
|
||||
|
||||
images = []
|
||||
for name in self.names:
|
||||
if 'id' in name:
|
||||
image = self.client.find_image_by_id(name['id'], accept_missing_image=True)
|
||||
if "id" in name:
|
||||
image = self.client.find_image_by_id(
|
||||
name["id"], accept_missing_image=True
|
||||
)
|
||||
else:
|
||||
image = self.client.find_image(name=name['name'], tag=name['tag'])
|
||||
image = self.client.find_image(name=name["name"], tag=name["tag"])
|
||||
if not image:
|
||||
self.fail(f"Image {name['joined']} not found")
|
||||
images.append(image)
|
||||
|
||||
# Will have a 'sha256:' prefix
|
||||
name['id'] = image['Id']
|
||||
name["id"] = image["Id"]
|
||||
|
||||
results = {
|
||||
'changed': False,
|
||||
'images': images,
|
||||
"changed": False,
|
||||
"images": images,
|
||||
}
|
||||
|
||||
reason = self.get_export_reason()
|
||||
if reason is not None:
|
||||
results['msg'] = reason
|
||||
results['changed'] = True
|
||||
results["msg"] = reason
|
||||
results["changed"] = True
|
||||
|
||||
if not self.check_mode:
|
||||
self.export_images()
|
||||
@@ -253,10 +262,10 @@ class ImageExportManager(DockerBaseClass):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
path=dict(type='path'),
|
||||
force=dict(type='bool', default=False),
|
||||
names=dict(type='list', elements='str', required=True, aliases=['name']),
|
||||
tag=dict(type='str', default='latest'),
|
||||
path=dict(type="path"),
|
||||
force=dict(type="bool", default=False),
|
||||
names=dict(type="list", elements="str", required=True, aliases=["name"]),
|
||||
tag=dict(type="str", default="latest"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -268,12 +277,16 @@ def main():
|
||||
results = ImageExportManager(client).run()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -137,6 +137,13 @@ images:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
@@ -145,8 +152,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
is_image_name_id,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import parse_repository_tag
|
||||
|
||||
|
||||
class ImageManager(DockerBaseClass):
|
||||
@@ -157,23 +162,23 @@ class ImageManager(DockerBaseClass):
|
||||
|
||||
self.client = client
|
||||
self.results = results
|
||||
self.name = self.client.module.params.get('name')
|
||||
self.name = self.client.module.params.get("name")
|
||||
self.log(f"Gathering facts for images: {self.name}")
|
||||
|
||||
if self.name:
|
||||
self.results['images'] = self.get_facts()
|
||||
self.results["images"] = self.get_facts()
|
||||
else:
|
||||
self.results['images'] = self.get_all_images()
|
||||
self.results["images"] = self.get_all_images()
|
||||
|
||||
def fail(self, msg):
|
||||
self.client.fail(msg)
|
||||
|
||||
def get_facts(self):
|
||||
'''
|
||||
"""
|
||||
Lookup and inspect each image name found in the names parameter.
|
||||
|
||||
:returns array of image dictionaries
|
||||
'''
|
||||
"""
|
||||
|
||||
results = []
|
||||
|
||||
@@ -183,13 +188,13 @@ class ImageManager(DockerBaseClass):
|
||||
|
||||
for name in names:
|
||||
if is_image_name_id(name):
|
||||
self.log(f'Fetching image {name} (ID)')
|
||||
self.log(f"Fetching image {name} (ID)")
|
||||
image = self.client.find_image_by_id(name, accept_missing_image=True)
|
||||
else:
|
||||
repository, tag = parse_repository_tag(name)
|
||||
if not tag:
|
||||
tag = 'latest'
|
||||
self.log(f'Fetching image {repository}:{tag}')
|
||||
tag = "latest"
|
||||
self.log(f"Fetching image {repository}:{tag}")
|
||||
image = self.client.find_image(name=repository, tag=tag)
|
||||
if image:
|
||||
results.append(image)
|
||||
@@ -198,13 +203,13 @@ class ImageManager(DockerBaseClass):
|
||||
def get_all_images(self):
|
||||
results = []
|
||||
params = {
|
||||
'only_ids': 0,
|
||||
'all': 0,
|
||||
"only_ids": 0,
|
||||
"all": 0,
|
||||
}
|
||||
images = self.client.get_json("/images/json", params=params)
|
||||
for image in images:
|
||||
try:
|
||||
inspection = self.client.get_json('/images/{0}/json', image['Id'])
|
||||
inspection = self.client.get_json("/images/{0}/json", image["Id"])
|
||||
except NotFound:
|
||||
inspection = None
|
||||
except Exception as exc:
|
||||
@@ -215,7 +220,7 @@ class ImageManager(DockerBaseClass):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='list', elements='str'),
|
||||
name=dict(type="list", elements="str"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -224,20 +229,21 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
changed=False,
|
||||
images=[]
|
||||
)
|
||||
results = dict(changed=False, images=[])
|
||||
|
||||
ImageManager(client, results)
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -82,6 +82,9 @@ images:
|
||||
import errno
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
@@ -91,8 +94,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
is_image_name_id,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
|
||||
|
||||
class ImageManager(DockerBaseClass):
|
||||
def __init__(self, client, results):
|
||||
@@ -103,73 +104,86 @@ class ImageManager(DockerBaseClass):
|
||||
parameters = self.client.module.params
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
self.path = parameters['path']
|
||||
self.path = parameters["path"]
|
||||
|
||||
self.load_images()
|
||||
|
||||
@staticmethod
|
||||
def _extract_output_line(line, output):
|
||||
'''
|
||||
"""
|
||||
Extract text line from stream output and, if found, adds it to output.
|
||||
'''
|
||||
if 'stream' in line or 'status' in line:
|
||||
"""
|
||||
if "stream" in line or "status" in line:
|
||||
# Make sure we have a string (assuming that line['stream'] and
|
||||
# line['status'] are either not defined, falsish, or a string)
|
||||
text_line = line.get('stream') or line.get('status') or ''
|
||||
text_line = line.get("stream") or line.get("status") or ""
|
||||
output.extend(text_line.splitlines())
|
||||
|
||||
def load_images(self):
|
||||
'''
|
||||
"""
|
||||
Load images from a .tar archive
|
||||
'''
|
||||
"""
|
||||
# Load image(s) from file
|
||||
load_output = []
|
||||
try:
|
||||
self.log(f"Opening image {self.path}")
|
||||
with open(self.path, 'rb') as image_tar:
|
||||
with open(self.path, "rb") as image_tar:
|
||||
self.log(f"Loading images from {self.path}")
|
||||
res = self.client._post(self.client._url("/images/load"), data=image_tar, stream=True)
|
||||
res = self.client._post(
|
||||
self.client._url("/images/load"), data=image_tar, stream=True
|
||||
)
|
||||
for line in self.client._stream_helper(res, decode=True):
|
||||
self.log(line, pretty_print=True)
|
||||
self._extract_output_line(line, load_output)
|
||||
except EnvironmentError as exc:
|
||||
if exc.errno == errno.ENOENT:
|
||||
self.client.fail(f"Error opening archive {self.path} - {exc}")
|
||||
self.client.fail(f"Error loading archive {self.path} - {exc}", stdout='\n'.join(load_output))
|
||||
self.client.fail(
|
||||
f"Error loading archive {self.path} - {exc}",
|
||||
stdout="\n".join(load_output),
|
||||
)
|
||||
except Exception as exc:
|
||||
self.client.fail(f"Error loading archive {self.path} - {exc}", stdout='\n'.join(load_output))
|
||||
self.client.fail(
|
||||
f"Error loading archive {self.path} - {exc}",
|
||||
stdout="\n".join(load_output),
|
||||
)
|
||||
|
||||
# Collect loaded images
|
||||
loaded_images = []
|
||||
for line in load_output:
|
||||
if line.startswith('Loaded image:'):
|
||||
loaded_images.append(line[len('Loaded image:'):].strip())
|
||||
if line.startswith('Loaded image ID:'):
|
||||
loaded_images.append(line[len('Loaded image ID:'):].strip())
|
||||
if line.startswith("Loaded image:"):
|
||||
loaded_images.append(line[len("Loaded image:") :].strip())
|
||||
if line.startswith("Loaded image ID:"):
|
||||
loaded_images.append(line[len("Loaded image ID:") :].strip())
|
||||
|
||||
if not loaded_images:
|
||||
self.client.fail("Detected no loaded images. Archive potentially corrupt?", stdout='\n'.join(load_output))
|
||||
self.client.fail(
|
||||
"Detected no loaded images. Archive potentially corrupt?",
|
||||
stdout="\n".join(load_output),
|
||||
)
|
||||
|
||||
images = []
|
||||
for image_name in loaded_images:
|
||||
if is_image_name_id(image_name):
|
||||
images.append(self.client.find_image_by_id(image_name))
|
||||
elif ':' in image_name:
|
||||
image_name, tag = image_name.rsplit(':', 1)
|
||||
elif ":" in image_name:
|
||||
image_name, tag = image_name.rsplit(":", 1)
|
||||
images.append(self.client.find_image(image_name, tag))
|
||||
else:
|
||||
self.client.module.warn(f'Image name "{image_name}" is neither ID nor has a tag')
|
||||
self.client.module.warn(
|
||||
f'Image name "{image_name}" is neither ID nor has a tag'
|
||||
)
|
||||
|
||||
self.results['image_names'] = loaded_images
|
||||
self.results['images'] = images
|
||||
self.results['changed'] = True
|
||||
self.results['stdout'] = '\n'.join(load_output)
|
||||
self.results["image_names"] = loaded_images
|
||||
self.results["images"] = images
|
||||
self.results["changed"] = True
|
||||
self.results["stdout"] = "\n".join(load_output)
|
||||
|
||||
|
||||
def main():
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=dict(
|
||||
path=dict(type='path', required=True),
|
||||
path=dict(type="path", required=True),
|
||||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
@@ -183,12 +197,16 @@ def main():
|
||||
ImageManager(client, results)
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -92,35 +92,34 @@ image:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._platform import (
|
||||
compare_platform_strings,
|
||||
compose_platform_string,
|
||||
normalize_platform_string,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
is_image_name_id,
|
||||
is_valid_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._platform import (
|
||||
normalize_platform_string,
|
||||
compare_platform_strings,
|
||||
compose_platform_string,
|
||||
)
|
||||
|
||||
|
||||
def image_info(image):
|
||||
result = {}
|
||||
if image:
|
||||
result['id'] = image['Id']
|
||||
result["id"] = image["Id"]
|
||||
else:
|
||||
result['exists'] = False
|
||||
result["exists"] = False
|
||||
return result
|
||||
|
||||
|
||||
@@ -132,10 +131,10 @@ class ImagePuller(DockerBaseClass):
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
parameters = self.client.module.params
|
||||
self.name = parameters['name']
|
||||
self.tag = parameters['tag']
|
||||
self.platform = parameters['platform']
|
||||
self.pull_mode = parameters['pull']
|
||||
self.name = parameters["name"]
|
||||
self.tag = parameters["tag"]
|
||||
self.platform = parameters["platform"]
|
||||
self.pull_mode = parameters["pull"]
|
||||
|
||||
if is_image_name_id(self.name):
|
||||
self.client.fail("Cannot pull an image by ID")
|
||||
@@ -157,47 +156,49 @@ class ImagePuller(DockerBaseClass):
|
||||
diff=dict(before=image_info(image), after=image_info(image)),
|
||||
)
|
||||
|
||||
if image and self.pull_mode == 'not_present':
|
||||
if image and self.pull_mode == "not_present":
|
||||
if self.platform is None:
|
||||
return results
|
||||
host_info = self.client.info()
|
||||
wanted_platform = normalize_platform_string(
|
||||
self.platform,
|
||||
daemon_os=host_info.get('OSType'),
|
||||
daemon_arch=host_info.get('Architecture'),
|
||||
daemon_os=host_info.get("OSType"),
|
||||
daemon_arch=host_info.get("Architecture"),
|
||||
)
|
||||
image_platform = compose_platform_string(
|
||||
os=image.get('Os'),
|
||||
arch=image.get('Architecture'),
|
||||
variant=image.get('Variant'),
|
||||
daemon_os=host_info.get('OSType'),
|
||||
daemon_arch=host_info.get('Architecture'),
|
||||
os=image.get("Os"),
|
||||
arch=image.get("Architecture"),
|
||||
variant=image.get("Variant"),
|
||||
daemon_os=host_info.get("OSType"),
|
||||
daemon_arch=host_info.get("Architecture"),
|
||||
)
|
||||
if compare_platform_strings(wanted_platform, image_platform):
|
||||
return results
|
||||
|
||||
results['actions'].append(f'Pulled image {self.name}:{self.tag}')
|
||||
results["actions"].append(f"Pulled image {self.name}:{self.tag}")
|
||||
if self.check_mode:
|
||||
results['changed'] = True
|
||||
results['diff']['after'] = image_info(dict(Id='unknown'))
|
||||
results["changed"] = True
|
||||
results["diff"]["after"] = image_info(dict(Id="unknown"))
|
||||
else:
|
||||
results['image'], not_changed = self.client.pull_image(self.name, tag=self.tag, platform=self.platform)
|
||||
results['changed'] = not not_changed
|
||||
results['diff']['after'] = image_info(results['image'])
|
||||
results["image"], not_changed = self.client.pull_image(
|
||||
self.name, tag=self.tag, platform=self.platform
|
||||
)
|
||||
results["changed"] = not not_changed
|
||||
results["diff"]["after"] = image_info(results["image"])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
tag=dict(type='str', default='latest'),
|
||||
platform=dict(type='str'),
|
||||
pull=dict(type='str', choices=['always', 'not_present'], default='always'),
|
||||
name=dict(type="str", required=True),
|
||||
tag=dict(type="str", default="latest"),
|
||||
platform=dict(type="str"),
|
||||
pull=dict(type="str", choices=["always", "not_present"], default="always"),
|
||||
)
|
||||
|
||||
option_minimal_versions = dict(
|
||||
platform=dict(docker_api_version='1.32'),
|
||||
platform=dict(docker_api_version="1.32"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -210,12 +211,16 @@ def main():
|
||||
results = ImagePuller(client).pull()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -74,27 +74,26 @@ image:
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
|
||||
get_config_header,
|
||||
resolve_repository_name,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
is_image_name_id,
|
||||
is_valid_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
|
||||
get_config_header,
|
||||
resolve_repository_name,
|
||||
)
|
||||
|
||||
|
||||
class ImagePusher(DockerBaseClass):
|
||||
def __init__(self, client):
|
||||
@@ -104,8 +103,8 @@ class ImagePusher(DockerBaseClass):
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
parameters = self.client.module.params
|
||||
self.name = parameters['name']
|
||||
self.tag = parameters['tag']
|
||||
self.name = parameters["name"]
|
||||
self.tag = parameters["tag"]
|
||||
|
||||
if is_image_name_id(self.name):
|
||||
self.client.fail("Cannot push an image by ID")
|
||||
@@ -126,7 +125,7 @@ class ImagePusher(DockerBaseClass):
|
||||
def push(self):
|
||||
image = self.client.find_image(name=self.name, tag=self.tag)
|
||||
if not image:
|
||||
self.client.fail(f'Cannot find image {self.name}:{self.tag}')
|
||||
self.client.fail(f"Cannot find image {self.name}:{self.tag}")
|
||||
|
||||
results = dict(
|
||||
changed=False,
|
||||
@@ -136,7 +135,7 @@ class ImagePusher(DockerBaseClass):
|
||||
|
||||
push_registry, push_repo = resolve_repository_name(self.name)
|
||||
try:
|
||||
results['actions'].append(f'Pushed image {self.name}:{self.tag}')
|
||||
results["actions"].append(f"Pushed image {self.name}:{self.tag}")
|
||||
|
||||
headers = {}
|
||||
header = get_config_header(self.client, push_registry)
|
||||
@@ -144,28 +143,32 @@ class ImagePusher(DockerBaseClass):
|
||||
# For some reason, from Docker 28.3.3 on not specifying X-Registry-Auth seems to be invalid.
|
||||
# See https://github.com/moby/moby/issues/50614.
|
||||
header = base64.urlsafe_b64encode(b"{}")
|
||||
headers['X-Registry-Auth'] = header
|
||||
headers["X-Registry-Auth"] = header
|
||||
response = self.client._post_json(
|
||||
self.client._url("/images/{0}/push", self.name),
|
||||
data=None,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
params={'tag': self.tag},
|
||||
params={"tag": self.tag},
|
||||
)
|
||||
self.client._raise_for_status(response)
|
||||
for line in self.client._stream_helper(response, decode=True):
|
||||
self.log(line, pretty_print=True)
|
||||
if line.get('errorDetail'):
|
||||
raise Exception(line['errorDetail']['message'])
|
||||
status = line.get('status')
|
||||
if status == 'Pushing':
|
||||
results['changed'] = True
|
||||
if line.get("errorDetail"):
|
||||
raise Exception(line["errorDetail"]["message"])
|
||||
status = line.get("status")
|
||||
if status == "Pushing":
|
||||
results["changed"] = True
|
||||
except Exception as exc:
|
||||
if 'unauthorized' in str(exc):
|
||||
if 'authentication required' in str(exc):
|
||||
self.client.fail(f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Try logging into {push_registry} first.")
|
||||
if "unauthorized" in str(exc):
|
||||
if "authentication required" in str(exc):
|
||||
self.client.fail(
|
||||
f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Try logging into {push_registry} first."
|
||||
)
|
||||
else:
|
||||
self.client.fail(f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Does the repository exist?")
|
||||
self.client.fail(
|
||||
f"Error pushing image {push_registry}/{push_repo}:{self.tag} - {exc}. Does the repository exist?"
|
||||
)
|
||||
self.client.fail(f"Error pushing image {self.name}:{self.tag}: {exc}")
|
||||
|
||||
return results
|
||||
@@ -173,8 +176,8 @@ class ImagePusher(DockerBaseClass):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
tag=dict(type='str', default='latest'),
|
||||
name=dict(type="str", required=True),
|
||||
tag=dict(type="str", default="latest"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -186,12 +189,16 @@ def main():
|
||||
results = ImagePusher(client).push()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -99,22 +99,23 @@ untagged:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
is_image_name_id,
|
||||
is_valid_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
|
||||
|
||||
class ImageRemover(DockerBaseClass):
|
||||
|
||||
@@ -126,10 +127,10 @@ class ImageRemover(DockerBaseClass):
|
||||
self.diff = self.client.module._diff
|
||||
|
||||
parameters = self.client.module.params
|
||||
self.name = parameters['name']
|
||||
self.tag = parameters['tag']
|
||||
self.force = parameters['force']
|
||||
self.prune = parameters['prune']
|
||||
self.name = parameters["name"]
|
||||
self.tag = parameters["tag"]
|
||||
self.force = parameters["force"]
|
||||
self.prune = parameters["prune"]
|
||||
|
||||
if not is_valid_tag(self.tag, allow_empty=True):
|
||||
self.fail(f'"{self.tag}" is not a valid docker tag')
|
||||
@@ -149,9 +150,9 @@ class ImageRemover(DockerBaseClass):
|
||||
return dict(exists=False)
|
||||
return dict(
|
||||
exists=True,
|
||||
id=image['Id'],
|
||||
tags=sorted(image.get('RepoTags') or []),
|
||||
digests=sorted(image.get('RepoDigests') or []),
|
||||
id=image["Id"],
|
||||
tags=sorted(image.get("RepoTags") or []),
|
||||
digests=sorted(image.get("RepoDigests") or []),
|
||||
)
|
||||
|
||||
def absent(self):
|
||||
@@ -172,20 +173,24 @@ class ImageRemover(DockerBaseClass):
|
||||
name = f"{self.name}:{self.tag}"
|
||||
|
||||
if self.diff:
|
||||
results['diff'] = dict(before=self.get_diff_state(image))
|
||||
results["diff"] = dict(before=self.get_diff_state(image))
|
||||
|
||||
if not image:
|
||||
if self.diff:
|
||||
results['diff']['after'] = self.get_diff_state(image)
|
||||
results["diff"]["after"] = self.get_diff_state(image)
|
||||
return results
|
||||
|
||||
results['changed'] = True
|
||||
results['actions'].append(f"Removed image {name}")
|
||||
results['image'] = image
|
||||
results["changed"] = True
|
||||
results["actions"].append(f"Removed image {name}")
|
||||
results["image"] = image
|
||||
|
||||
if not self.check_mode:
|
||||
try:
|
||||
res = self.client.delete_json('/images/{0}', name, params={'force': self.force, 'noprune': not self.prune})
|
||||
res = self.client.delete_json(
|
||||
"/images/{0}",
|
||||
name,
|
||||
params={"force": self.force, "noprune": not self.prune},
|
||||
)
|
||||
except NotFound:
|
||||
# If the image vanished while we were trying to remove it, do not fail
|
||||
res = []
|
||||
@@ -193,45 +198,57 @@ class ImageRemover(DockerBaseClass):
|
||||
self.fail(f"Error removing image {name} - {exc}")
|
||||
|
||||
for entry in res:
|
||||
if entry.get('Untagged'):
|
||||
results['untagged'].append(entry['Untagged'])
|
||||
if entry.get('Deleted'):
|
||||
results['deleted'].append(entry['Deleted'])
|
||||
if entry.get("Untagged"):
|
||||
results["untagged"].append(entry["Untagged"])
|
||||
if entry.get("Deleted"):
|
||||
results["deleted"].append(entry["Deleted"])
|
||||
|
||||
results['untagged'] = sorted(results['untagged'])
|
||||
results['deleted'] = sorted(results['deleted'])
|
||||
results["untagged"] = sorted(results["untagged"])
|
||||
results["deleted"] = sorted(results["deleted"])
|
||||
|
||||
if self.diff:
|
||||
image_after = self.client.find_image_by_id(image['Id'], accept_missing_image=True)
|
||||
results['diff']['after'] = self.get_diff_state(image_after)
|
||||
image_after = self.client.find_image_by_id(
|
||||
image["Id"], accept_missing_image=True
|
||||
)
|
||||
results["diff"]["after"] = self.get_diff_state(image_after)
|
||||
|
||||
elif is_image_name_id(name):
|
||||
results['deleted'].append(image['Id'])
|
||||
results['untagged'] = sorted((image.get('RepoTags') or []) + (image.get('RepoDigests') or []))
|
||||
if not self.force and results['untagged']:
|
||||
self.fail('Cannot delete image by ID that is still in use - use force=true')
|
||||
results["deleted"].append(image["Id"])
|
||||
results["untagged"] = sorted(
|
||||
(image.get("RepoTags") or []) + (image.get("RepoDigests") or [])
|
||||
)
|
||||
if not self.force and results["untagged"]:
|
||||
self.fail(
|
||||
"Cannot delete image by ID that is still in use - use force=true"
|
||||
)
|
||||
if self.diff:
|
||||
results['diff']['after'] = self.get_diff_state({})
|
||||
results["diff"]["after"] = self.get_diff_state({})
|
||||
|
||||
elif is_image_name_id(self.tag):
|
||||
results['untagged'].append(name)
|
||||
if len(image.get('RepoTags') or []) < 1 and len(image.get('RepoDigests') or []) < 2:
|
||||
results['deleted'].append(image['Id'])
|
||||
results["untagged"].append(name)
|
||||
if (
|
||||
len(image.get("RepoTags") or []) < 1
|
||||
and len(image.get("RepoDigests") or []) < 2
|
||||
):
|
||||
results["deleted"].append(image["Id"])
|
||||
if self.diff:
|
||||
results['diff']['after'] = self.get_diff_state(image)
|
||||
results["diff"]["after"] = self.get_diff_state(image)
|
||||
try:
|
||||
results['diff']['after']['digests'].remove(name)
|
||||
results["diff"]["after"]["digests"].remove(name)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
else:
|
||||
results['untagged'].append(name)
|
||||
if len(image.get('RepoTags') or []) < 2 and len(image.get('RepoDigests') or []) < 1:
|
||||
results['deleted'].append(image['Id'])
|
||||
results["untagged"].append(name)
|
||||
if (
|
||||
len(image.get("RepoTags") or []) < 2
|
||||
and len(image.get("RepoDigests") or []) < 1
|
||||
):
|
||||
results["deleted"].append(image["Id"])
|
||||
if self.diff:
|
||||
results['diff']['after'] = self.get_diff_state(image)
|
||||
results["diff"]["after"] = self.get_diff_state(image)
|
||||
try:
|
||||
results['diff']['after']['tags'].remove(name)
|
||||
results["diff"]["after"]["tags"].remove(name)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -240,10 +257,10 @@ class ImageRemover(DockerBaseClass):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
tag=dict(type='str', default='latest'),
|
||||
force=dict(type='bool', default=False),
|
||||
prune=dict(type='bool', default=True),
|
||||
name=dict(type="str", required=True),
|
||||
tag=dict(type="str", default="latest"),
|
||||
force=dict(type="bool", default=False),
|
||||
prune=dict(type="bool", default=True),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -255,12 +272,16 @@ def main():
|
||||
results = ImageRemover(client).absent()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -103,41 +103,40 @@ tagged_images:
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.formatters import human_to_bytes
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
is_image_name_id,
|
||||
is_valid_tag,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
parse_repository_tag,
|
||||
)
|
||||
|
||||
|
||||
def convert_to_bytes(value, module, name, unlimited_value=None):
|
||||
if value is None:
|
||||
return value
|
||||
try:
|
||||
if unlimited_value is not None and value in ('unlimited', str(unlimited_value)):
|
||||
if unlimited_value is not None and value in ("unlimited", str(unlimited_value)):
|
||||
return unlimited_value
|
||||
return human_to_bytes(value)
|
||||
except ValueError as exc:
|
||||
module.fail_json(msg=f'Failed to convert {name} to bytes: {exc}')
|
||||
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
|
||||
|
||||
|
||||
def image_info(name, tag, image):
|
||||
result = dict(name=name, tag=tag)
|
||||
if image:
|
||||
result['id'] = image['Id']
|
||||
result["id"] = image["Id"]
|
||||
else:
|
||||
result['exists'] = False
|
||||
result["exists"] = False
|
||||
return result
|
||||
|
||||
|
||||
@@ -149,8 +148,8 @@ class ImageTagger(DockerBaseClass):
|
||||
parameters = self.client.module.params
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
self.name = parameters['name']
|
||||
self.tag = parameters['tag']
|
||||
self.name = parameters["name"]
|
||||
self.tag = parameters["tag"]
|
||||
if not is_valid_tag(self.tag, allow_empty=True):
|
||||
self.fail(f'"{self.tag}" is not a valid docker tag')
|
||||
|
||||
@@ -161,18 +160,22 @@ class ImageTagger(DockerBaseClass):
|
||||
self.name = repo
|
||||
self.tag = repo_tag
|
||||
|
||||
self.keep_existing_images = parameters['existing_images'] == 'keep'
|
||||
self.keep_existing_images = parameters["existing_images"] == "keep"
|
||||
|
||||
# Make sure names in repository are valid images, and add tag if needed
|
||||
self.repositories = []
|
||||
for i, repository in enumerate(parameters['repository']):
|
||||
for i, repository in enumerate(parameters["repository"]):
|
||||
if is_image_name_id(repository):
|
||||
self.fail(f"repository[{i + 1}] must not be an image ID; got: {repository}")
|
||||
self.fail(
|
||||
f"repository[{i + 1}] must not be an image ID; got: {repository}"
|
||||
)
|
||||
repo, repo_tag = parse_repository_tag(repository)
|
||||
if not repo_tag:
|
||||
repo_tag = parameters['tag']
|
||||
repo_tag = parameters["tag"]
|
||||
elif not is_valid_tag(repo_tag, allow_empty=False):
|
||||
self.fail(f"repository[{i + 1}] must not have a digest; got: {repository}")
|
||||
self.fail(
|
||||
f"repository[{i + 1}] must not have a digest; got: {repository}"
|
||||
)
|
||||
self.repositories.append((repo, repo_tag))
|
||||
|
||||
def fail(self, msg):
|
||||
@@ -182,7 +185,7 @@ class ImageTagger(DockerBaseClass):
|
||||
tagged_image = self.client.find_image(name=name, tag=tag)
|
||||
if tagged_image:
|
||||
# Idempotency checks
|
||||
if tagged_image['Id'] == image['Id']:
|
||||
if tagged_image["Id"] == image["Id"]:
|
||||
return (
|
||||
False,
|
||||
f"target image already exists ({tagged_image['Id']}) and is as expected",
|
||||
@@ -201,11 +204,13 @@ class ImageTagger(DockerBaseClass):
|
||||
if not self.check_mode:
|
||||
try:
|
||||
params = {
|
||||
'tag': tag,
|
||||
'repo': name,
|
||||
'force': True,
|
||||
"tag": tag,
|
||||
"repo": name,
|
||||
"force": True,
|
||||
}
|
||||
res = self.client._post(self.client._url('/images/{0}/tag', image['Id']), params=params)
|
||||
res = self.client._post(
|
||||
self.client._url("/images/{0}/tag", image["Id"]), params=params
|
||||
)
|
||||
self.client._raise_for_status(res)
|
||||
if res.status_code != 201:
|
||||
raise Exception("Tag operation failed.")
|
||||
@@ -237,21 +242,27 @@ class ImageTagger(DockerBaseClass):
|
||||
before.append(image_info(repository, tag, old_image))
|
||||
after.append(image_info(repository, tag, image if tagged else old_image))
|
||||
if tagged:
|
||||
results['changed'] = True
|
||||
results['actions'].append(f"Tagged image {image['Id']} as {repository}:{tag}: {msg}")
|
||||
tagged_images.append(f'{repository}:{tag}')
|
||||
results["changed"] = True
|
||||
results["actions"].append(
|
||||
f"Tagged image {image['Id']} as {repository}:{tag}: {msg}"
|
||||
)
|
||||
tagged_images.append(f"{repository}:{tag}")
|
||||
else:
|
||||
results['actions'].append(f"Not tagged image {image['Id']} as {repository}:{tag}: {msg}")
|
||||
results["actions"].append(
|
||||
f"Not tagged image {image['Id']} as {repository}:{tag}: {msg}"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
tag=dict(type='str', default='latest'),
|
||||
repository=dict(type='list', elements='str', required=True),
|
||||
existing_images=dict(type='str', choices=['keep', 'overwrite'], default='overwrite'),
|
||||
name=dict(type="str", required=True),
|
||||
tag=dict(type="str", default="latest"),
|
||||
repository=dict(type="list", elements="str", required=True),
|
||||
existing_images=dict(
|
||||
type="str", choices=["keep", "overwrite"], default="overwrite"
|
||||
),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -263,12 +274,16 @@ def main():
|
||||
results = ImageTagger(client).tag_images()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+111
-99
@@ -122,7 +122,19 @@ import os
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api import auth
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
|
||||
decode_auth,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.credentials.errors import (
|
||||
CredentialsNotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.credentials.store import (
|
||||
Store,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
@@ -132,18 +144,12 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api import auth
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.auth import decode_auth
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.credentials.errors import CredentialsNotFound
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.credentials.store import Store
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
|
||||
|
||||
class DockerFileStore(object):
|
||||
'''
|
||||
"""
|
||||
A custom credential store class that implements only the functionality we need to
|
||||
update the docker config file when no credential helpers is provided.
|
||||
'''
|
||||
"""
|
||||
|
||||
program = "<legacy config>"
|
||||
|
||||
@@ -151,9 +157,7 @@ class DockerFileStore(object):
|
||||
self._config_path = config_path
|
||||
|
||||
# Make sure we have a minimal config if none is available.
|
||||
self._config = dict(
|
||||
auths=dict()
|
||||
)
|
||||
self._config = dict(auths=dict())
|
||||
|
||||
try:
|
||||
# Attempt to read the existing config.
|
||||
@@ -168,39 +172,36 @@ class DockerFileStore(object):
|
||||
|
||||
@property
|
||||
def config_path(self):
|
||||
'''
|
||||
"""
|
||||
Return the config path configured in this DockerFileStore instance.
|
||||
'''
|
||||
"""
|
||||
|
||||
return self._config_path
|
||||
|
||||
def get(self, server):
|
||||
'''
|
||||
"""
|
||||
Retrieve credentials for `server` if there are any in the config file.
|
||||
Otherwise raise a `StoreError`
|
||||
'''
|
||||
"""
|
||||
|
||||
server_creds = self._config['auths'].get(server)
|
||||
server_creds = self._config["auths"].get(server)
|
||||
if not server_creds:
|
||||
raise CredentialsNotFound('No matching credentials')
|
||||
raise CredentialsNotFound("No matching credentials")
|
||||
|
||||
(username, password) = decode_auth(server_creds['auth'])
|
||||
(username, password) = decode_auth(server_creds["auth"])
|
||||
|
||||
return dict(
|
||||
Username=username,
|
||||
Secret=password
|
||||
)
|
||||
return dict(Username=username, Secret=password)
|
||||
|
||||
def _write(self):
|
||||
'''
|
||||
"""
|
||||
Write config back out to disk.
|
||||
'''
|
||||
"""
|
||||
# Make sure directory exists
|
||||
dir = os.path.dirname(self._config_path)
|
||||
if not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
# Write config; make sure it has permissions 0x600
|
||||
content = json.dumps(self._config, indent=4, sort_keys=True).encode('utf-8')
|
||||
content = json.dumps(self._config, indent=4, sort_keys=True).encode("utf-8")
|
||||
f = os.open(self._config_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
try:
|
||||
os.write(f, content)
|
||||
@@ -208,32 +209,28 @@ class DockerFileStore(object):
|
||||
os.close(f)
|
||||
|
||||
def store(self, server, username, password):
|
||||
'''
|
||||
"""
|
||||
Add a credentials for `server` to the current configuration.
|
||||
'''
|
||||
"""
|
||||
|
||||
b64auth = base64.b64encode(
|
||||
to_bytes(username) + b':' + to_bytes(password)
|
||||
)
|
||||
b64auth = base64.b64encode(to_bytes(username) + b":" + to_bytes(password))
|
||||
auth = to_text(b64auth)
|
||||
|
||||
# build up the auth structure
|
||||
if 'auths' not in self._config:
|
||||
self._config['auths'] = dict()
|
||||
if "auths" not in self._config:
|
||||
self._config["auths"] = dict()
|
||||
|
||||
self._config['auths'][server] = dict(
|
||||
auth=auth
|
||||
)
|
||||
self._config["auths"][server] = dict(auth=auth)
|
||||
|
||||
self._write()
|
||||
|
||||
def erase(self, server):
|
||||
'''
|
||||
"""
|
||||
Remove credentials for the given server from the configuration.
|
||||
'''
|
||||
"""
|
||||
|
||||
if 'auths' in self._config and server in self._config['auths']:
|
||||
self._config['auths'].pop(server)
|
||||
if "auths" in self._config and server in self._config["auths"]:
|
||||
self._config["auths"].pop(server)
|
||||
self._write()
|
||||
|
||||
|
||||
@@ -248,20 +245,20 @@ class LoginManager(DockerBaseClass):
|
||||
parameters = self.client.module.params
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
self.registry_url = parameters.get('registry_url')
|
||||
self.username = parameters.get('username')
|
||||
self.password = parameters.get('password')
|
||||
self.reauthorize = parameters.get('reauthorize')
|
||||
self.config_path = parameters.get('config_path')
|
||||
self.state = parameters.get('state')
|
||||
self.registry_url = parameters.get("registry_url")
|
||||
self.username = parameters.get("username")
|
||||
self.password = parameters.get("password")
|
||||
self.reauthorize = parameters.get("reauthorize")
|
||||
self.config_path = parameters.get("config_path")
|
||||
self.state = parameters.get("state")
|
||||
|
||||
def run(self):
|
||||
'''
|
||||
"""
|
||||
Do the actual work of this task here. This allows instantiation for partial
|
||||
testing.
|
||||
'''
|
||||
"""
|
||||
|
||||
if self.state == 'present':
|
||||
if self.state == "present":
|
||||
self.login()
|
||||
else:
|
||||
self.logout()
|
||||
@@ -282,57 +279,63 @@ class LoginManager(DockerBaseClass):
|
||||
authcfg = self.client._auth_configs.resolve_authconfig(self.registry_url)
|
||||
# If we found an existing auth config for this registry and username
|
||||
# combination, we can return it immediately unless reauth is requested.
|
||||
if authcfg and authcfg.get('username') == self.username and not reauth:
|
||||
if authcfg and authcfg.get("username") == self.username and not reauth:
|
||||
return authcfg
|
||||
|
||||
req_data = {
|
||||
'username': self.username,
|
||||
'password': self.password,
|
||||
'email': None,
|
||||
'serveraddress': self.registry_url,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"email": None,
|
||||
"serveraddress": self.registry_url,
|
||||
}
|
||||
|
||||
response = self.client._post_json(self.client._url('/auth'), data=req_data)
|
||||
response = self.client._post_json(self.client._url("/auth"), data=req_data)
|
||||
if response.status_code == 200:
|
||||
self.client._auth_configs.add_auth(self.registry_url or auth.INDEX_NAME, req_data)
|
||||
self.client._auth_configs.add_auth(
|
||||
self.registry_url or auth.INDEX_NAME, req_data
|
||||
)
|
||||
return self.client._result(response, json=True)
|
||||
|
||||
def login(self):
|
||||
'''
|
||||
"""
|
||||
Log into the registry with provided username/password. On success update the config
|
||||
file with the new authorization.
|
||||
|
||||
:return: None
|
||||
'''
|
||||
"""
|
||||
|
||||
self.results['actions'].append(f"Logged into {self.registry_url}")
|
||||
self.results["actions"].append(f"Logged into {self.registry_url}")
|
||||
self.log(f"Log into {self.registry_url} with username {self.username}")
|
||||
try:
|
||||
response = self._login(self.reauthorize)
|
||||
except Exception as exc:
|
||||
self.fail(f"Logging into {self.registry_url} for user {self.username} failed - {exc}")
|
||||
self.fail(
|
||||
f"Logging into {self.registry_url} for user {self.username} failed - {exc}"
|
||||
)
|
||||
|
||||
# If user is already logged in, then response contains password for user
|
||||
if 'password' in response:
|
||||
if "password" in response:
|
||||
# This returns correct password if user is logged in and wrong password is given.
|
||||
# So if it returns another password as we passed, and the user did not request to
|
||||
# reauthorize, still do it.
|
||||
if not self.reauthorize and response['password'] != self.password:
|
||||
if not self.reauthorize and response["password"] != self.password:
|
||||
try:
|
||||
response = self._login(True)
|
||||
except Exception as exc:
|
||||
self.fail(f"Logging into {self.registry_url} for user {self.username} failed - {exc}")
|
||||
response.pop('password', None)
|
||||
self.results['login_result'] = response
|
||||
self.fail(
|
||||
f"Logging into {self.registry_url} for user {self.username} failed - {exc}"
|
||||
)
|
||||
response.pop("password", None)
|
||||
self.results["login_result"] = response
|
||||
|
||||
self.update_credentials()
|
||||
|
||||
def logout(self):
|
||||
'''
|
||||
"""
|
||||
Log out of the registry. On success update the config file.
|
||||
|
||||
:return: None
|
||||
'''
|
||||
"""
|
||||
|
||||
# Get the configuration store.
|
||||
store = self.get_credential_store_instance(self.registry_url, self.config_path)
|
||||
@@ -342,20 +345,20 @@ class LoginManager(DockerBaseClass):
|
||||
except CredentialsNotFound:
|
||||
# get raises an exception on not found.
|
||||
self.log(f"Credentials for {self.registry_url} not present, doing nothing.")
|
||||
self.results['changed'] = False
|
||||
self.results["changed"] = False
|
||||
return
|
||||
|
||||
if not self.check_mode:
|
||||
store.erase(self.registry_url)
|
||||
self.results['changed'] = True
|
||||
self.results["changed"] = True
|
||||
|
||||
def update_credentials(self):
|
||||
'''
|
||||
"""
|
||||
If the authorization is not stored attempt to store authorization values via
|
||||
the appropriate credential helper or to the config file.
|
||||
|
||||
:return: None
|
||||
'''
|
||||
"""
|
||||
|
||||
# Check to see if credentials already exist.
|
||||
store = self.get_credential_store_instance(self.registry_url, self.config_path)
|
||||
@@ -364,25 +367,30 @@ class LoginManager(DockerBaseClass):
|
||||
current = store.get(self.registry_url)
|
||||
except CredentialsNotFound:
|
||||
# get raises an exception on not found.
|
||||
current = dict(
|
||||
Username='',
|
||||
Secret=''
|
||||
)
|
||||
current = dict(Username="", Secret="")
|
||||
|
||||
if current['Username'] != self.username or current['Secret'] != self.password or self.reauthorize:
|
||||
if (
|
||||
current["Username"] != self.username
|
||||
or current["Secret"] != self.password
|
||||
or self.reauthorize
|
||||
):
|
||||
if not self.check_mode:
|
||||
store.store(self.registry_url, self.username, self.password)
|
||||
self.log(f"Writing credentials to configured helper {store.program} for {self.registry_url}")
|
||||
self.results['actions'].append(f"Wrote credentials to configured helper {store.program} for {self.registry_url}")
|
||||
self.results['changed'] = True
|
||||
self.log(
|
||||
f"Writing credentials to configured helper {store.program} for {self.registry_url}"
|
||||
)
|
||||
self.results["actions"].append(
|
||||
f"Wrote credentials to configured helper {store.program} for {self.registry_url}"
|
||||
)
|
||||
self.results["changed"] = True
|
||||
|
||||
def get_credential_store_instance(self, registry, dockercfg_path):
|
||||
'''
|
||||
"""
|
||||
Return an instance of docker.credentials.Store used by the given registry.
|
||||
|
||||
:return: A Store or None
|
||||
:rtype: Union[docker.credentials.Store, NoneType]
|
||||
'''
|
||||
"""
|
||||
|
||||
credstore_env = self.client.credstore_env
|
||||
|
||||
@@ -402,16 +410,20 @@ class LoginManager(DockerBaseClass):
|
||||
def main():
|
||||
|
||||
argument_spec = dict(
|
||||
registry_url=dict(type='str', default=DEFAULT_DOCKER_REGISTRY, aliases=['registry', 'url']),
|
||||
username=dict(type='str'),
|
||||
password=dict(type='str', no_log=True),
|
||||
reauthorize=dict(type='bool', default=False, aliases=['reauth']),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
config_path=dict(type='path', default='~/.docker/config.json', aliases=['dockercfg_path']),
|
||||
registry_url=dict(
|
||||
type="str", default=DEFAULT_DOCKER_REGISTRY, aliases=["registry", "url"]
|
||||
),
|
||||
username=dict(type="str"),
|
||||
password=dict(type="str", no_log=True),
|
||||
reauthorize=dict(type="bool", default=False, aliases=["reauth"]),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
config_path=dict(
|
||||
type="path", default="~/.docker/config.json", aliases=["dockercfg_path"]
|
||||
),
|
||||
)
|
||||
|
||||
required_if = [
|
||||
('state', 'present', ['username', 'password']),
|
||||
("state", "present", ["username", "password"]),
|
||||
]
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -421,25 +433,25 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
changed=False,
|
||||
actions=[],
|
||||
login_result={}
|
||||
)
|
||||
results = dict(changed=False, actions=[], login_result={})
|
||||
|
||||
manager = LoginManager(client, results)
|
||||
manager.run()
|
||||
|
||||
if 'actions' in results:
|
||||
del results['actions']
|
||||
if "actions" in results:
|
||||
del results["actions"]
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+270
-184
@@ -282,22 +282,23 @@ network:
|
||||
"""
|
||||
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
DifferenceTracker,
|
||||
DockerBaseClass,
|
||||
clean_dict_booleans_for_docker_api,
|
||||
sanitize_labels,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
|
||||
|
||||
class TaskParameters(DockerBaseClass):
|
||||
@@ -331,15 +332,19 @@ class TaskParameters(DockerBaseClass):
|
||||
# config_only sets driver to 'null' (and scope to 'local') so force that here. Otherwise we get
|
||||
# diffs of 'null' --> 'bridge' given that the driver option defaults to 'bridge'.
|
||||
if self.config_only:
|
||||
self.driver = 'null'
|
||||
self.driver = "null"
|
||||
|
||||
|
||||
def container_names_in_network(network):
|
||||
return [c['Name'] for c in network['Containers'].values()] if network['Containers'] else []
|
||||
return (
|
||||
[c["Name"] for c in network["Containers"].values()]
|
||||
if network["Containers"]
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
CIDR_IPV4 = re.compile(r'^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$')
|
||||
CIDR_IPV6 = re.compile(r'^[0-9a-fA-F:]+/([0-9]|[1-9][0-9]|1[0-2][0-9])$')
|
||||
CIDR_IPV4 = re.compile(r"^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$")
|
||||
CIDR_IPV6 = re.compile(r"^[0-9a-fA-F:]+/([0-9]|[1-9][0-9]|1[0-2][0-9])$")
|
||||
|
||||
|
||||
def validate_cidr(cidr):
|
||||
@@ -352,9 +357,9 @@ def validate_cidr(cidr):
|
||||
:raises ValueError: If ``cidr`` is not a valid CIDR
|
||||
"""
|
||||
if CIDR_IPV4.match(cidr):
|
||||
return 'ipv4'
|
||||
return "ipv4"
|
||||
elif CIDR_IPV6.match(cidr):
|
||||
return 'ipv6'
|
||||
return "ipv6"
|
||||
raise ValueError(f'"{cidr}" is not a valid CIDR')
|
||||
|
||||
|
||||
@@ -366,9 +371,7 @@ def normalize_ipam_config_key(key):
|
||||
:return Ansible module key
|
||||
:rtype str
|
||||
"""
|
||||
special_cases = {
|
||||
'AuxiliaryAddresses': 'aux_addresses'
|
||||
}
|
||||
special_cases = {"AuxiliaryAddresses": "aux_addresses"}
|
||||
return special_cases.get(key, key.lower())
|
||||
|
||||
|
||||
@@ -388,10 +391,7 @@ class DockerNetworkManager(object):
|
||||
self.client = client
|
||||
self.parameters = TaskParameters(client)
|
||||
self.check_mode = self.client.check_mode
|
||||
self.results = {
|
||||
'changed': False,
|
||||
'actions': []
|
||||
}
|
||||
self.results = {"changed": False, "actions": []}
|
||||
self.diff = self.client.module._diff
|
||||
self.diff_tracker = DifferenceTracker()
|
||||
self.diff_result = dict()
|
||||
@@ -399,87 +399,117 @@ class DockerNetworkManager(object):
|
||||
self.existing_network = self.get_existing_network()
|
||||
|
||||
if not self.parameters.connected and self.existing_network:
|
||||
self.parameters.connected = container_names_in_network(self.existing_network)
|
||||
self.parameters.connected = container_names_in_network(
|
||||
self.existing_network
|
||||
)
|
||||
|
||||
if self.parameters.ipam_config:
|
||||
try:
|
||||
for ipam_config in self.parameters.ipam_config:
|
||||
validate_cidr(ipam_config['subnet'])
|
||||
validate_cidr(ipam_config["subnet"])
|
||||
except ValueError as e:
|
||||
self.client.fail(to_native(e))
|
||||
|
||||
if self.parameters.driver_options:
|
||||
self.parameters.driver_options = clean_dict_booleans_for_docker_api(self.parameters.driver_options)
|
||||
self.parameters.driver_options = clean_dict_booleans_for_docker_api(
|
||||
self.parameters.driver_options
|
||||
)
|
||||
|
||||
state = self.parameters.state
|
||||
if state == 'present':
|
||||
if state == "present":
|
||||
self.present()
|
||||
elif state == 'absent':
|
||||
elif state == "absent":
|
||||
self.absent()
|
||||
|
||||
if self.diff or self.check_mode or self.parameters.debug:
|
||||
if self.diff:
|
||||
self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after()
|
||||
self.results['diff'] = self.diff_result
|
||||
self.diff_result["before"], self.diff_result["after"] = (
|
||||
self.diff_tracker.get_before_after()
|
||||
)
|
||||
self.results["diff"] = self.diff_result
|
||||
|
||||
def get_existing_network(self):
|
||||
return self.client.get_network(name=self.parameters.name)
|
||||
|
||||
def has_different_config(self, net):
|
||||
'''
|
||||
"""
|
||||
Evaluates an existing network and returns a tuple containing a boolean
|
||||
indicating if the configuration is different and a list of differences.
|
||||
|
||||
:param net: the inspection output for an existing network
|
||||
:return: (bool, list)
|
||||
'''
|
||||
"""
|
||||
differences = DifferenceTracker()
|
||||
if self.parameters.config_only is not None and self.parameters.config_only != net.get('ConfigOnly', False):
|
||||
differences.add('config_only',
|
||||
parameter=self.parameters.config_only,
|
||||
active=net.get('ConfigOnly', False))
|
||||
if self.parameters.config_from is not None and self.parameters.config_from != net.get('ConfigFrom', {}).get('Network', ''):
|
||||
differences.add('config_from',
|
||||
parameter=self.parameters.config_from,
|
||||
active=net.get('ConfigFrom', {}).get('Network', ''))
|
||||
if self.parameters.driver and self.parameters.driver != net['Driver']:
|
||||
differences.add('driver',
|
||||
parameter=self.parameters.driver,
|
||||
active=net['Driver'])
|
||||
if (
|
||||
self.parameters.config_only is not None
|
||||
and self.parameters.config_only != net.get("ConfigOnly", False)
|
||||
):
|
||||
differences.add(
|
||||
"config_only",
|
||||
parameter=self.parameters.config_only,
|
||||
active=net.get("ConfigOnly", False),
|
||||
)
|
||||
if (
|
||||
self.parameters.config_from is not None
|
||||
and self.parameters.config_from
|
||||
!= net.get("ConfigFrom", {}).get("Network", "")
|
||||
):
|
||||
differences.add(
|
||||
"config_from",
|
||||
parameter=self.parameters.config_from,
|
||||
active=net.get("ConfigFrom", {}).get("Network", ""),
|
||||
)
|
||||
if self.parameters.driver and self.parameters.driver != net["Driver"]:
|
||||
differences.add(
|
||||
"driver", parameter=self.parameters.driver, active=net["Driver"]
|
||||
)
|
||||
if self.parameters.driver_options:
|
||||
if not net.get('Options'):
|
||||
differences.add('driver_options',
|
||||
parameter=self.parameters.driver_options,
|
||||
active=net.get('Options'))
|
||||
if not net.get("Options"):
|
||||
differences.add(
|
||||
"driver_options",
|
||||
parameter=self.parameters.driver_options,
|
||||
active=net.get("Options"),
|
||||
)
|
||||
else:
|
||||
for key, value in self.parameters.driver_options.items():
|
||||
if not (key in net['Options']) or value != net['Options'][key]:
|
||||
differences.add(f'driver_options.{key}',
|
||||
parameter=value,
|
||||
active=net['Options'].get(key))
|
||||
if not (key in net["Options"]) or value != net["Options"][key]:
|
||||
differences.add(
|
||||
f"driver_options.{key}",
|
||||
parameter=value,
|
||||
active=net["Options"].get(key),
|
||||
)
|
||||
|
||||
if self.parameters.ipam_driver:
|
||||
if not net.get('IPAM') or net['IPAM']['Driver'] != self.parameters.ipam_driver:
|
||||
differences.add('ipam_driver',
|
||||
parameter=self.parameters.ipam_driver,
|
||||
active=net.get('IPAM'))
|
||||
if (
|
||||
not net.get("IPAM")
|
||||
or net["IPAM"]["Driver"] != self.parameters.ipam_driver
|
||||
):
|
||||
differences.add(
|
||||
"ipam_driver",
|
||||
parameter=self.parameters.ipam_driver,
|
||||
active=net.get("IPAM"),
|
||||
)
|
||||
|
||||
if self.parameters.ipam_driver_options is not None:
|
||||
ipam_driver_options = net['IPAM'].get('Options') or {}
|
||||
ipam_driver_options = net["IPAM"].get("Options") or {}
|
||||
if ipam_driver_options != self.parameters.ipam_driver_options:
|
||||
differences.add('ipam_driver_options',
|
||||
parameter=self.parameters.ipam_driver_options,
|
||||
active=ipam_driver_options)
|
||||
differences.add(
|
||||
"ipam_driver_options",
|
||||
parameter=self.parameters.ipam_driver_options,
|
||||
active=ipam_driver_options,
|
||||
)
|
||||
|
||||
if self.parameters.ipam_config is not None and self.parameters.ipam_config:
|
||||
if not net.get('IPAM') or not net['IPAM']['Config']:
|
||||
differences.add('ipam_config',
|
||||
parameter=self.parameters.ipam_config,
|
||||
active=net.get('IPAM', {}).get('Config'))
|
||||
if not net.get("IPAM") or not net["IPAM"]["Config"]:
|
||||
differences.add(
|
||||
"ipam_config",
|
||||
parameter=self.parameters.ipam_config,
|
||||
active=net.get("IPAM", {}).get("Config"),
|
||||
)
|
||||
else:
|
||||
# Put network's IPAM config into the same format as module's IPAM config
|
||||
net_ipam_configs = []
|
||||
for net_ipam_config in net['IPAM']['Config']:
|
||||
for net_ipam_config in net["IPAM"]["Config"]:
|
||||
config = dict()
|
||||
for k, v in net_ipam_config.items():
|
||||
config[normalize_ipam_config_key(k)] = v
|
||||
@@ -497,118 +527,154 @@ class DockerNetworkManager(object):
|
||||
# (but have default value None if not specified)
|
||||
continue
|
||||
if value != net_config.get(key):
|
||||
differences.add(f'ipam_config[{idx}].{key}',
|
||||
parameter=value,
|
||||
active=net_config.get(key))
|
||||
differences.add(
|
||||
f"ipam_config[{idx}].{key}",
|
||||
parameter=value,
|
||||
active=net_config.get(key),
|
||||
)
|
||||
|
||||
if self.parameters.enable_ipv4 is not None and self.parameters.enable_ipv4 != net.get('EnableIPv4', False):
|
||||
differences.add('enable_ipv4',
|
||||
parameter=self.parameters.enable_ipv4,
|
||||
active=net.get('EnableIPv4', False))
|
||||
if self.parameters.enable_ipv6 is not None and self.parameters.enable_ipv6 != net.get('EnableIPv6', False):
|
||||
differences.add('enable_ipv6',
|
||||
parameter=self.parameters.enable_ipv6,
|
||||
active=net.get('EnableIPv6', False))
|
||||
if (
|
||||
self.parameters.enable_ipv4 is not None
|
||||
and self.parameters.enable_ipv4 != net.get("EnableIPv4", False)
|
||||
):
|
||||
differences.add(
|
||||
"enable_ipv4",
|
||||
parameter=self.parameters.enable_ipv4,
|
||||
active=net.get("EnableIPv4", False),
|
||||
)
|
||||
if (
|
||||
self.parameters.enable_ipv6 is not None
|
||||
and self.parameters.enable_ipv6 != net.get("EnableIPv6", False)
|
||||
):
|
||||
differences.add(
|
||||
"enable_ipv6",
|
||||
parameter=self.parameters.enable_ipv6,
|
||||
active=net.get("EnableIPv6", False),
|
||||
)
|
||||
|
||||
if self.parameters.internal is not None and self.parameters.internal != net.get('Internal', False):
|
||||
differences.add('internal',
|
||||
parameter=self.parameters.internal,
|
||||
active=net.get('Internal'))
|
||||
if (
|
||||
self.parameters.internal is not None
|
||||
and self.parameters.internal != net.get("Internal", False)
|
||||
):
|
||||
differences.add(
|
||||
"internal",
|
||||
parameter=self.parameters.internal,
|
||||
active=net.get("Internal"),
|
||||
)
|
||||
|
||||
if self.parameters.scope is not None and self.parameters.scope != net.get('Scope'):
|
||||
differences.add('scope',
|
||||
parameter=self.parameters.scope,
|
||||
active=net.get('Scope'))
|
||||
if self.parameters.scope is not None and self.parameters.scope != net.get(
|
||||
"Scope"
|
||||
):
|
||||
differences.add(
|
||||
"scope", parameter=self.parameters.scope, active=net.get("Scope")
|
||||
)
|
||||
|
||||
if self.parameters.attachable is not None and self.parameters.attachable != net.get('Attachable', False):
|
||||
differences.add('attachable',
|
||||
parameter=self.parameters.attachable,
|
||||
active=net.get('Attachable'))
|
||||
if self.parameters.ingress is not None and self.parameters.ingress != net.get('Ingress', False):
|
||||
differences.add('ingress',
|
||||
parameter=self.parameters.ingress,
|
||||
active=net.get('Ingress'))
|
||||
if (
|
||||
self.parameters.attachable is not None
|
||||
and self.parameters.attachable != net.get("Attachable", False)
|
||||
):
|
||||
differences.add(
|
||||
"attachable",
|
||||
parameter=self.parameters.attachable,
|
||||
active=net.get("Attachable"),
|
||||
)
|
||||
if self.parameters.ingress is not None and self.parameters.ingress != net.get(
|
||||
"Ingress", False
|
||||
):
|
||||
differences.add(
|
||||
"ingress", parameter=self.parameters.ingress, active=net.get("Ingress")
|
||||
)
|
||||
if self.parameters.labels:
|
||||
if not net.get('Labels'):
|
||||
differences.add('labels',
|
||||
parameter=self.parameters.labels,
|
||||
active=net.get('Labels'))
|
||||
if not net.get("Labels"):
|
||||
differences.add(
|
||||
"labels", parameter=self.parameters.labels, active=net.get("Labels")
|
||||
)
|
||||
else:
|
||||
for key, value in self.parameters.labels.items():
|
||||
if not (key in net['Labels']) or value != net['Labels'][key]:
|
||||
differences.add(f'labels.{key}',
|
||||
parameter=value,
|
||||
active=net['Labels'].get(key))
|
||||
if not (key in net["Labels"]) or value != net["Labels"][key]:
|
||||
differences.add(
|
||||
f"labels.{key}",
|
||||
parameter=value,
|
||||
active=net["Labels"].get(key),
|
||||
)
|
||||
|
||||
return not differences.empty, differences
|
||||
|
||||
def create_network(self):
|
||||
if not self.existing_network:
|
||||
data = {
|
||||
'Name': self.parameters.name,
|
||||
'Driver': self.parameters.driver,
|
||||
'Options': self.parameters.driver_options,
|
||||
'IPAM': None,
|
||||
'CheckDuplicate': None,
|
||||
"Name": self.parameters.name,
|
||||
"Driver": self.parameters.driver,
|
||||
"Options": self.parameters.driver_options,
|
||||
"IPAM": None,
|
||||
"CheckDuplicate": None,
|
||||
}
|
||||
|
||||
if self.parameters.config_only is not None:
|
||||
data['ConfigOnly'] = self.parameters.config_only
|
||||
data["ConfigOnly"] = self.parameters.config_only
|
||||
if self.parameters.config_from:
|
||||
data['ConfigFrom'] = {'Network': self.parameters.config_from}
|
||||
data["ConfigFrom"] = {"Network": self.parameters.config_from}
|
||||
if self.parameters.enable_ipv6 is not None:
|
||||
data['EnableIPv6'] = self.parameters.enable_ipv6
|
||||
data["EnableIPv6"] = self.parameters.enable_ipv6
|
||||
if self.parameters.enable_ipv4 is not None:
|
||||
data['EnableIPv4'] = self.parameters.enable_ipv4
|
||||
data["EnableIPv4"] = self.parameters.enable_ipv4
|
||||
if self.parameters.internal:
|
||||
data['Internal'] = True
|
||||
data["Internal"] = True
|
||||
if self.parameters.scope is not None:
|
||||
data['Scope'] = self.parameters.scope
|
||||
data["Scope"] = self.parameters.scope
|
||||
if self.parameters.attachable is not None:
|
||||
data['Attachable'] = self.parameters.attachable
|
||||
data["Attachable"] = self.parameters.attachable
|
||||
if self.parameters.ingress is not None:
|
||||
data['Ingress'] = self.parameters.ingress
|
||||
data["Ingress"] = self.parameters.ingress
|
||||
if self.parameters.labels is not None:
|
||||
data["Labels"] = self.parameters.labels
|
||||
|
||||
ipam_pools = []
|
||||
if self.parameters.ipam_config:
|
||||
for ipam_pool in self.parameters.ipam_config:
|
||||
ipam_pools.append({
|
||||
'Subnet': ipam_pool['subnet'],
|
||||
'IPRange': ipam_pool['iprange'],
|
||||
'Gateway': ipam_pool['gateway'],
|
||||
'AuxiliaryAddresses': ipam_pool['aux_addresses'],
|
||||
})
|
||||
ipam_pools.append(
|
||||
{
|
||||
"Subnet": ipam_pool["subnet"],
|
||||
"IPRange": ipam_pool["iprange"],
|
||||
"Gateway": ipam_pool["gateway"],
|
||||
"AuxiliaryAddresses": ipam_pool["aux_addresses"],
|
||||
}
|
||||
)
|
||||
|
||||
if self.parameters.ipam_driver or self.parameters.ipam_driver_options or ipam_pools:
|
||||
if (
|
||||
self.parameters.ipam_driver
|
||||
or self.parameters.ipam_driver_options
|
||||
or ipam_pools
|
||||
):
|
||||
# Only add IPAM if a driver was specified or if IPAM parameters were
|
||||
# specified. Leaving this parameter out can significantly speed up
|
||||
# creation; on my machine creation with this option needs ~15 seconds,
|
||||
# and without just a few seconds.
|
||||
data['IPAM'] = {
|
||||
'Driver': self.parameters.ipam_driver,
|
||||
'Config': ipam_pools or [],
|
||||
'Options': self.parameters.ipam_driver_options,
|
||||
data["IPAM"] = {
|
||||
"Driver": self.parameters.ipam_driver,
|
||||
"Config": ipam_pools or [],
|
||||
"Options": self.parameters.ipam_driver_options,
|
||||
}
|
||||
|
||||
if not self.check_mode:
|
||||
resp = self.client.post_json_to_json('/networks/create', data=data)
|
||||
self.client.report_warnings(resp, ['Warning'])
|
||||
self.existing_network = self.client.get_network(network_id=resp['Id'])
|
||||
self.results['actions'].append(f"Created network {self.parameters.name} with driver {self.parameters.driver}")
|
||||
self.results['changed'] = True
|
||||
resp = self.client.post_json_to_json("/networks/create", data=data)
|
||||
self.client.report_warnings(resp, ["Warning"])
|
||||
self.existing_network = self.client.get_network(network_id=resp["Id"])
|
||||
self.results["actions"].append(
|
||||
f"Created network {self.parameters.name} with driver {self.parameters.driver}"
|
||||
)
|
||||
self.results["changed"] = True
|
||||
|
||||
def remove_network(self):
|
||||
if self.existing_network:
|
||||
self.disconnect_all_containers()
|
||||
if not self.check_mode:
|
||||
self.client.delete_call('/networks/{0}', self.parameters.name)
|
||||
if self.existing_network.get('Scope', 'local') == 'swarm':
|
||||
self.client.delete_call("/networks/{0}", self.parameters.name)
|
||||
if self.existing_network.get("Scope", "local") == "swarm":
|
||||
while self.get_existing_network():
|
||||
time.sleep(0.1)
|
||||
self.results['actions'].append(f"Removed network {self.parameters.name}")
|
||||
self.results['changed'] = True
|
||||
self.results["actions"].append(f"Removed network {self.parameters.name}")
|
||||
self.results["changed"] = True
|
||||
|
||||
def is_container_connected(self, container_name):
|
||||
if not self.existing_network:
|
||||
@@ -621,11 +687,15 @@ class DockerNetworkManager(object):
|
||||
return bool(container)
|
||||
|
||||
except DockerException as e:
|
||||
self.client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
self.client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
self.client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
def connect_containers(self):
|
||||
for name in self.parameters.connected:
|
||||
@@ -635,38 +705,42 @@ class DockerNetworkManager(object):
|
||||
"Container": name,
|
||||
"EndpointConfig": None,
|
||||
}
|
||||
self.client.post_json('/networks/{0}/connect', self.parameters.name, data=data)
|
||||
self.results['actions'].append(f"Connected container {name}")
|
||||
self.results['changed'] = True
|
||||
self.diff_tracker.add(f'connected.{name}', parameter=True, active=False)
|
||||
self.client.post_json(
|
||||
"/networks/{0}/connect", self.parameters.name, data=data
|
||||
)
|
||||
self.results["actions"].append(f"Connected container {name}")
|
||||
self.results["changed"] = True
|
||||
self.diff_tracker.add(f"connected.{name}", parameter=True, active=False)
|
||||
|
||||
def disconnect_missing(self):
|
||||
if not self.existing_network:
|
||||
return
|
||||
containers = self.existing_network['Containers']
|
||||
containers = self.existing_network["Containers"]
|
||||
if not containers:
|
||||
return
|
||||
for c in containers.values():
|
||||
name = c['Name']
|
||||
name = c["Name"]
|
||||
if name not in self.parameters.connected:
|
||||
self.disconnect_container(name)
|
||||
|
||||
def disconnect_all_containers(self):
|
||||
containers = self.client.get_network(name=self.parameters.name)['Containers']
|
||||
containers = self.client.get_network(name=self.parameters.name)["Containers"]
|
||||
if not containers:
|
||||
return
|
||||
for cont in containers.values():
|
||||
self.disconnect_container(cont['Name'])
|
||||
self.disconnect_container(cont["Name"])
|
||||
|
||||
def disconnect_container(self, container_name):
|
||||
if not self.check_mode:
|
||||
data = {"Container": container_name, "Force": True}
|
||||
self.client.post_json('/networks/{0}/disconnect', self.parameters.name, data=data)
|
||||
self.results['actions'].append(f"Disconnected container {container_name}")
|
||||
self.results['changed'] = True
|
||||
self.diff_tracker.add(f'connected.{container_name}',
|
||||
parameter=False,
|
||||
active=True)
|
||||
self.client.post_json(
|
||||
"/networks/{0}/disconnect", self.parameters.name, data=data
|
||||
)
|
||||
self.results["actions"].append(f"Disconnected container {container_name}")
|
||||
self.results["changed"] = True
|
||||
self.diff_tracker.add(
|
||||
f"connected.{container_name}", parameter=False, active=True
|
||||
)
|
||||
|
||||
def present(self):
|
||||
different = False
|
||||
@@ -674,7 +748,9 @@ class DockerNetworkManager(object):
|
||||
if self.existing_network:
|
||||
different, differences = self.has_different_config(self.existing_network)
|
||||
|
||||
self.diff_tracker.add('exists', parameter=True, active=self.existing_network is not None)
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=True, active=self.existing_network is not None
|
||||
)
|
||||
if self.parameters.force or different:
|
||||
self.remove_network()
|
||||
self.existing_network = None
|
||||
@@ -685,55 +761,61 @@ class DockerNetworkManager(object):
|
||||
self.disconnect_missing()
|
||||
|
||||
if self.diff or self.check_mode or self.parameters.debug:
|
||||
self.diff_result['differences'] = differences.get_legacy_docker_diffs()
|
||||
self.diff_result["differences"] = differences.get_legacy_docker_diffs()
|
||||
self.diff_tracker.merge(differences)
|
||||
|
||||
if not self.check_mode and not self.parameters.debug:
|
||||
self.results.pop('actions')
|
||||
self.results.pop("actions")
|
||||
|
||||
network_facts = self.get_existing_network()
|
||||
self.results['network'] = network_facts
|
||||
self.results["network"] = network_facts
|
||||
|
||||
def absent(self):
|
||||
self.diff_tracker.add('exists', parameter=False, active=self.existing_network is not None)
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=False, active=self.existing_network is not None
|
||||
)
|
||||
self.remove_network()
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True, aliases=['network_name']),
|
||||
config_from=dict(type='str'),
|
||||
config_only=dict(type='bool'),
|
||||
connected=dict(type='list', default=[], elements='str', aliases=['containers']),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
driver=dict(type='str', default='bridge'),
|
||||
driver_options=dict(type='dict', default={}),
|
||||
force=dict(type='bool', default=False),
|
||||
appends=dict(type='bool', default=False, aliases=['incremental']),
|
||||
ipam_driver=dict(type='str'),
|
||||
ipam_driver_options=dict(type='dict'),
|
||||
ipam_config=dict(type='list', elements='dict', options=dict(
|
||||
subnet=dict(type='str'),
|
||||
iprange=dict(type='str'),
|
||||
gateway=dict(type='str'),
|
||||
aux_addresses=dict(type='dict'),
|
||||
)),
|
||||
enable_ipv4=dict(type='bool'),
|
||||
enable_ipv6=dict(type='bool'),
|
||||
internal=dict(type='bool'),
|
||||
labels=dict(type='dict', default={}),
|
||||
debug=dict(type='bool', default=False),
|
||||
scope=dict(type='str', choices=['local', 'global', 'swarm']),
|
||||
attachable=dict(type='bool'),
|
||||
ingress=dict(type='bool'),
|
||||
name=dict(type="str", required=True, aliases=["network_name"]),
|
||||
config_from=dict(type="str"),
|
||||
config_only=dict(type="bool"),
|
||||
connected=dict(type="list", default=[], elements="str", aliases=["containers"]),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
driver=dict(type="str", default="bridge"),
|
||||
driver_options=dict(type="dict", default={}),
|
||||
force=dict(type="bool", default=False),
|
||||
appends=dict(type="bool", default=False, aliases=["incremental"]),
|
||||
ipam_driver=dict(type="str"),
|
||||
ipam_driver_options=dict(type="dict"),
|
||||
ipam_config=dict(
|
||||
type="list",
|
||||
elements="dict",
|
||||
options=dict(
|
||||
subnet=dict(type="str"),
|
||||
iprange=dict(type="str"),
|
||||
gateway=dict(type="str"),
|
||||
aux_addresses=dict(type="dict"),
|
||||
),
|
||||
),
|
||||
enable_ipv4=dict(type="bool"),
|
||||
enable_ipv6=dict(type="bool"),
|
||||
internal=dict(type="bool"),
|
||||
labels=dict(type="dict", default={}),
|
||||
debug=dict(type="bool", default=False),
|
||||
scope=dict(type="str", choices=["local", "global", "swarm"]),
|
||||
attachable=dict(type="bool"),
|
||||
ingress=dict(type="bool"),
|
||||
)
|
||||
|
||||
option_minimal_versions = dict(
|
||||
config_from=dict(docker_api_version='1.30'),
|
||||
config_only=dict(docker_api_version='1.30'),
|
||||
scope=dict(docker_api_version='1.30'),
|
||||
attachable=dict(docker_api_version='1.26'),
|
||||
enable_ipv4=dict(docker_api_version='1.47'),
|
||||
config_from=dict(docker_api_version="1.30"),
|
||||
config_only=dict(docker_api_version="1.30"),
|
||||
scope=dict(docker_api_version="1.30"),
|
||||
attachable=dict(docker_api_version="1.26"),
|
||||
enable_ipv4=dict(docker_api_version="1.47"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -742,17 +824,21 @@ def main():
|
||||
# "The docker server >= 1.10.0"
|
||||
option_minimal_versions=option_minimal_versions,
|
||||
)
|
||||
sanitize_labels(client.module.params['labels'], 'labels', client)
|
||||
sanitize_labels(client.module.params["labels"], "labels", client)
|
||||
try:
|
||||
cm = DockerNetworkManager(client)
|
||||
client.module.exit_json(**cm.results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -98,16 +98,18 @@ network:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
name=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -116,7 +118,7 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
network = client.get_network(client.module.params['name'])
|
||||
network = client.get_network(client.module.params["name"])
|
||||
|
||||
client.module.exit_json(
|
||||
changed=False,
|
||||
@@ -124,12 +126,16 @@ def main():
|
||||
network=network,
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: docker_node
|
||||
short_description: Manage Docker Swarm node
|
||||
@@ -134,21 +135,24 @@ node:
|
||||
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException, APIError
|
||||
from docker.errors import APIError, DockerException
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
|
||||
pass
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
DockerBaseClass,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import sanitize_labels
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import (
|
||||
AnsibleDockerSwarmClient,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
sanitize_labels,
|
||||
)
|
||||
|
||||
|
||||
class TaskParameters(DockerBaseClass):
|
||||
@@ -208,80 +212,84 @@ class SwarmNodeManager(DockerBaseClass):
|
||||
)
|
||||
|
||||
if self.parameters.role is None:
|
||||
node_spec['Role'] = node_info['Spec']['Role']
|
||||
node_spec["Role"] = node_info["Spec"]["Role"]
|
||||
else:
|
||||
if not node_info['Spec']['Role'] == self.parameters.role:
|
||||
node_spec['Role'] = self.parameters.role
|
||||
if not node_info["Spec"]["Role"] == self.parameters.role:
|
||||
node_spec["Role"] = self.parameters.role
|
||||
changed = True
|
||||
|
||||
if self.parameters.availability is None:
|
||||
node_spec['Availability'] = node_info['Spec']['Availability']
|
||||
node_spec["Availability"] = node_info["Spec"]["Availability"]
|
||||
else:
|
||||
if not node_info['Spec']['Availability'] == self.parameters.availability:
|
||||
node_info['Spec']['Availability'] = self.parameters.availability
|
||||
if not node_info["Spec"]["Availability"] == self.parameters.availability:
|
||||
node_info["Spec"]["Availability"] = self.parameters.availability
|
||||
changed = True
|
||||
|
||||
if self.parameters.labels_state == 'replace':
|
||||
if self.parameters.labels_state == "replace":
|
||||
if self.parameters.labels is None:
|
||||
node_spec['Labels'] = {}
|
||||
if node_info['Spec']['Labels']:
|
||||
node_spec["Labels"] = {}
|
||||
if node_info["Spec"]["Labels"]:
|
||||
changed = True
|
||||
else:
|
||||
if (node_info['Spec']['Labels'] or {}) != self.parameters.labels:
|
||||
node_spec['Labels'] = self.parameters.labels
|
||||
if (node_info["Spec"]["Labels"] or {}) != self.parameters.labels:
|
||||
node_spec["Labels"] = self.parameters.labels
|
||||
changed = True
|
||||
elif self.parameters.labels_state == 'merge':
|
||||
node_spec['Labels'] = dict(node_info['Spec']['Labels'] or {})
|
||||
elif self.parameters.labels_state == "merge":
|
||||
node_spec["Labels"] = dict(node_info["Spec"]["Labels"] or {})
|
||||
if self.parameters.labels is not None:
|
||||
for key, value in self.parameters.labels.items():
|
||||
if node_spec['Labels'].get(key) != value:
|
||||
node_spec['Labels'][key] = value
|
||||
if node_spec["Labels"].get(key) != value:
|
||||
node_spec["Labels"][key] = value
|
||||
changed = True
|
||||
|
||||
if self.parameters.labels_to_remove is not None:
|
||||
for key in self.parameters.labels_to_remove:
|
||||
if self.parameters.labels is not None:
|
||||
if not self.parameters.labels.get(key):
|
||||
if node_spec['Labels'].get(key):
|
||||
node_spec['Labels'].pop(key)
|
||||
if node_spec["Labels"].get(key):
|
||||
node_spec["Labels"].pop(key)
|
||||
changed = True
|
||||
else:
|
||||
self.client.module.warn(
|
||||
f"Label '{to_native(key)}' listed both in 'labels' and 'labels_to_remove'. "
|
||||
"Keeping the assigned label value.")
|
||||
"Keeping the assigned label value."
|
||||
)
|
||||
else:
|
||||
if node_spec['Labels'].get(key):
|
||||
node_spec['Labels'].pop(key)
|
||||
if node_spec["Labels"].get(key):
|
||||
node_spec["Labels"].pop(key)
|
||||
changed = True
|
||||
|
||||
if changed is True:
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.update_node(node_id=node_info['ID'], version=node_info['Version']['Index'],
|
||||
node_spec=node_spec)
|
||||
self.client.update_node(
|
||||
node_id=node_info["ID"],
|
||||
version=node_info["Version"]["Index"],
|
||||
node_spec=node_spec,
|
||||
)
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Failed to update node : {exc}")
|
||||
self.results['node'] = self.client.get_node_inspect(node_id=node_info['ID'])
|
||||
self.results['changed'] = changed
|
||||
self.results["node"] = self.client.get_node_inspect(node_id=node_info["ID"])
|
||||
self.results["changed"] = changed
|
||||
else:
|
||||
self.results['node'] = node_info
|
||||
self.results['changed'] = changed
|
||||
self.results["node"] = node_info
|
||||
self.results["changed"] = changed
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
hostname=dict(type='str', required=True),
|
||||
labels=dict(type='dict'),
|
||||
labels_state=dict(type='str', default='merge', choices=['merge', 'replace']),
|
||||
labels_to_remove=dict(type='list', elements='str'),
|
||||
availability=dict(type='str', choices=['active', 'pause', 'drain']),
|
||||
role=dict(type='str', choices=['worker', 'manager']),
|
||||
hostname=dict(type="str", required=True),
|
||||
labels=dict(type="dict"),
|
||||
labels_state=dict(type="str", default="merge", choices=["merge", "replace"]),
|
||||
labels_to_remove=dict(type="list", elements="str"),
|
||||
availability=dict(type="str", choices=["active", "pause", "drain"]),
|
||||
role=dict(type="str", choices=["worker", "manager"]),
|
||||
)
|
||||
|
||||
client = AnsibleDockerSwarmClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
min_docker_version='2.4.0',
|
||||
min_docker_version="2.4.0",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -292,12 +300,16 @@ def main():
|
||||
SwarmNodeManager(client, results)
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -91,7 +91,10 @@ import traceback
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import (
|
||||
AnsibleDockerSwarmClient,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException
|
||||
@@ -104,22 +107,24 @@ def get_node_facts(client):
|
||||
|
||||
results = []
|
||||
|
||||
if client.module.params['self'] is True:
|
||||
if client.module.params["self"] is True:
|
||||
self_node_id = client.get_swarm_node_id()
|
||||
node_info = client.get_node_inspect(node_id=self_node_id)
|
||||
results.append(node_info)
|
||||
return results
|
||||
|
||||
if client.module.params['name'] is None:
|
||||
if client.module.params["name"] is None:
|
||||
node_info = client.get_all_nodes_inspect()
|
||||
return node_info
|
||||
|
||||
nodes = client.module.params['name']
|
||||
nodes = client.module.params["name"]
|
||||
if not isinstance(nodes, list):
|
||||
nodes = [nodes]
|
||||
|
||||
for next_node_name in nodes:
|
||||
next_node_info = client.get_node_inspect(node_id=next_node_name, skip_missing=True)
|
||||
next_node_info = client.get_node_inspect(
|
||||
node_id=next_node_name, skip_missing=True
|
||||
)
|
||||
if next_node_info:
|
||||
results.append(next_node_info)
|
||||
return results
|
||||
@@ -127,14 +132,14 @@ def get_node_facts(client):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='list', elements='str'),
|
||||
self=dict(type='bool', default=False),
|
||||
name=dict(type="list", elements="str"),
|
||||
self=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
client = AnsibleDockerSwarmClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
min_docker_version='2.4.0',
|
||||
min_docker_version="2.4.0",
|
||||
)
|
||||
|
||||
client.fail_task_if_not_swarm_manager()
|
||||
@@ -147,12 +152,16 @@ def main():
|
||||
nodes=nodes,
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -131,20 +131,21 @@ actions:
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api import auth
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
DifferenceTracker,
|
||||
DockerBaseClass,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api import auth
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
|
||||
|
||||
|
||||
class TaskParameters(DockerBaseClass):
|
||||
def __init__(self, client):
|
||||
@@ -162,11 +163,15 @@ class TaskParameters(DockerBaseClass):
|
||||
|
||||
|
||||
def prepare_options(options):
|
||||
return [f'{k}={v if v is not None else ""}' for k, v in options.items()] if options else []
|
||||
return (
|
||||
[f'{k}={v if v is not None else ""}' for k, v in options.items()]
|
||||
if options
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
def parse_options(options_list):
|
||||
return dict(x.split('=', 1) for x in options_list) if options_list else {}
|
||||
return dict(x.split("=", 1) for x in options_list) if options_list else {}
|
||||
|
||||
|
||||
class DockerPluginManager(object):
|
||||
@@ -187,23 +192,25 @@ class DockerPluginManager(object):
|
||||
self.existing_plugin = self.get_existing_plugin()
|
||||
|
||||
state = self.parameters.state
|
||||
if state == 'present':
|
||||
if state == "present":
|
||||
self.present()
|
||||
elif state == 'absent':
|
||||
elif state == "absent":
|
||||
self.absent()
|
||||
elif state == 'enable':
|
||||
elif state == "enable":
|
||||
self.enable()
|
||||
elif state == 'disable':
|
||||
elif state == "disable":
|
||||
self.disable()
|
||||
|
||||
if self.diff or self.check_mode or self.parameters.debug:
|
||||
if self.diff:
|
||||
self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after()
|
||||
self.diff_result["before"], self.diff_result["after"] = (
|
||||
self.diff_tracker.get_before_after()
|
||||
)
|
||||
self.diff = self.diff_result
|
||||
|
||||
def get_existing_plugin(self):
|
||||
try:
|
||||
return self.client.get_json('/plugins/{0}/json', self.preferred_name)
|
||||
return self.client.get_json("/plugins/{0}/json", self.preferred_name)
|
||||
except NotFound:
|
||||
return None
|
||||
except APIError as e:
|
||||
@@ -217,19 +224,27 @@ class DockerPluginManager(object):
|
||||
"""
|
||||
differences = DifferenceTracker()
|
||||
if self.parameters.plugin_options:
|
||||
settings = self.existing_plugin.get('Settings')
|
||||
settings = self.existing_plugin.get("Settings")
|
||||
if not settings:
|
||||
differences.add('plugin_options', parameters=self.parameters.plugin_options, active=settings)
|
||||
differences.add(
|
||||
"plugin_options",
|
||||
parameters=self.parameters.plugin_options,
|
||||
active=settings,
|
||||
)
|
||||
else:
|
||||
existing_options = parse_options(settings.get('Env'))
|
||||
existing_options = parse_options(settings.get("Env"))
|
||||
|
||||
for key, value in self.parameters.plugin_options.items():
|
||||
if ((not existing_options.get(key) and value) or
|
||||
not value or
|
||||
value != existing_options[key]):
|
||||
differences.add(f'plugin_options.{key}',
|
||||
parameter=value,
|
||||
active=existing_options.get(key))
|
||||
if (
|
||||
(not existing_options.get(key) and value)
|
||||
or not value
|
||||
or value != existing_options[key]
|
||||
):
|
||||
differences.add(
|
||||
f"plugin_options.{key}",
|
||||
parameter=value,
|
||||
active=existing_options.get(key),
|
||||
)
|
||||
|
||||
return differences
|
||||
|
||||
@@ -239,26 +254,42 @@ class DockerPluginManager(object):
|
||||
try:
|
||||
# Get privileges
|
||||
headers = {}
|
||||
registry, repo_name = auth.resolve_repository_name(self.parameters.plugin_name)
|
||||
registry, repo_name = auth.resolve_repository_name(
|
||||
self.parameters.plugin_name
|
||||
)
|
||||
header = auth.get_config_header(self.client, registry)
|
||||
if header:
|
||||
headers['X-Registry-Auth'] = header
|
||||
privileges = self.client.get_json('/plugins/privileges', params={'remote': self.parameters.plugin_name}, headers=headers)
|
||||
headers["X-Registry-Auth"] = header
|
||||
privileges = self.client.get_json(
|
||||
"/plugins/privileges",
|
||||
params={"remote": self.parameters.plugin_name},
|
||||
headers=headers,
|
||||
)
|
||||
# Pull plugin
|
||||
params = {
|
||||
'remote': self.parameters.plugin_name,
|
||||
"remote": self.parameters.plugin_name,
|
||||
}
|
||||
if self.parameters.alias:
|
||||
params['name'] = self.parameters.alias
|
||||
response = self.client._post_json(self.client._url('/plugins/pull'), params=params, headers=headers, data=privileges, stream=True)
|
||||
params["name"] = self.parameters.alias
|
||||
response = self.client._post_json(
|
||||
self.client._url("/plugins/pull"),
|
||||
params=params,
|
||||
headers=headers,
|
||||
data=privileges,
|
||||
stream=True,
|
||||
)
|
||||
self.client._raise_for_status(response)
|
||||
for data in self.client._stream_helper(response, decode=True):
|
||||
pass
|
||||
# Inspect and configure plugin
|
||||
self.existing_plugin = self.client.get_json('/plugins/{0}/json', self.preferred_name)
|
||||
self.existing_plugin = self.client.get_json(
|
||||
"/plugins/{0}/json", self.preferred_name
|
||||
)
|
||||
if self.parameters.plugin_options:
|
||||
data = prepare_options(self.parameters.plugin_options)
|
||||
self.client.post_json('/plugins/{0}/set', self.preferred_name, data=data)
|
||||
self.client.post_json(
|
||||
"/plugins/{0}/set", self.preferred_name, data=data
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
|
||||
@@ -270,7 +301,9 @@ class DockerPluginManager(object):
|
||||
if self.existing_plugin:
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.delete_call('/plugins/{0}', self.preferred_name, params={'force': force})
|
||||
self.client.delete_call(
|
||||
"/plugins/{0}", self.preferred_name, params={"force": force}
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
|
||||
@@ -284,7 +317,9 @@ class DockerPluginManager(object):
|
||||
if not self.check_mode:
|
||||
try:
|
||||
data = prepare_options(self.parameters.plugin_options)
|
||||
self.client.post_json('/plugins/{0}/set', self.preferred_name, data=data)
|
||||
self.client.post_json(
|
||||
"/plugins/{0}/set", self.preferred_name, data=data
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
self.actions.append(f"Updated plugin {self.preferred_name} settings")
|
||||
@@ -297,7 +332,9 @@ class DockerPluginManager(object):
|
||||
if self.existing_plugin:
|
||||
differences = self.has_different_config()
|
||||
|
||||
self.diff_tracker.add('exists', parameter=True, active=self.existing_plugin is not None)
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=True, active=self.existing_plugin is not None
|
||||
)
|
||||
|
||||
if self.existing_plugin:
|
||||
self.update_plugin()
|
||||
@@ -316,10 +353,14 @@ class DockerPluginManager(object):
|
||||
def enable(self):
|
||||
timeout = self.parameters.enable_timeout
|
||||
if self.existing_plugin:
|
||||
if not self.existing_plugin.get('Enabled'):
|
||||
if not self.existing_plugin.get("Enabled"):
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.post_json('/plugins/{0}/enable', self.preferred_name, params={'timeout': timeout})
|
||||
self.client.post_json(
|
||||
"/plugins/{0}/enable",
|
||||
self.preferred_name,
|
||||
params={"timeout": timeout},
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
self.actions.append(f"Enabled plugin {self.preferred_name}")
|
||||
@@ -328,7 +369,11 @@ class DockerPluginManager(object):
|
||||
self.install_plugin()
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.post_json('/plugins/{0}/enable', self.preferred_name, params={'timeout': timeout})
|
||||
self.client.post_json(
|
||||
"/plugins/{0}/enable",
|
||||
self.preferred_name,
|
||||
params={"timeout": timeout},
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
self.actions.append(f"Enabled plugin {self.preferred_name}")
|
||||
@@ -336,10 +381,12 @@ class DockerPluginManager(object):
|
||||
|
||||
def disable(self):
|
||||
if self.existing_plugin:
|
||||
if self.existing_plugin.get('Enabled'):
|
||||
if self.existing_plugin.get("Enabled"):
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.post_json('/plugins/{0}/disable', self.preferred_name)
|
||||
self.client.post_json(
|
||||
"/plugins/{0}/disable", self.preferred_name
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
self.actions.append(f"Disable plugin {self.preferred_name}")
|
||||
@@ -350,30 +397,36 @@ class DockerPluginManager(object):
|
||||
@property
|
||||
def result(self):
|
||||
plugin_data = {}
|
||||
if self.parameters.state != 'absent':
|
||||
if self.parameters.state != "absent":
|
||||
try:
|
||||
plugin_data = self.client.get_json('/plugins/{0}/json', self.preferred_name)
|
||||
plugin_data = self.client.get_json(
|
||||
"/plugins/{0}/json", self.preferred_name
|
||||
)
|
||||
except NotFound:
|
||||
# This can happen in check mode
|
||||
pass
|
||||
result = {
|
||||
'actions': self.actions,
|
||||
'changed': self.changed,
|
||||
'diff': self.diff,
|
||||
'plugin': plugin_data,
|
||||
"actions": self.actions,
|
||||
"changed": self.changed,
|
||||
"diff": self.diff,
|
||||
"plugin": plugin_data,
|
||||
}
|
||||
return dict((k, v) for k, v in result.items() if v is not None)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
alias=dict(type='str'),
|
||||
plugin_name=dict(type='str', required=True),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent', 'enable', 'disable']),
|
||||
plugin_options=dict(type='dict', default={}),
|
||||
debug=dict(type='bool', default=False),
|
||||
force_remove=dict(type='bool', default=False),
|
||||
enable_timeout=dict(type='int', default=0),
|
||||
alias=dict(type="str"),
|
||||
plugin_name=dict(type="str", required=True),
|
||||
state=dict(
|
||||
type="str",
|
||||
default="present",
|
||||
choices=["present", "absent", "enable", "disable"],
|
||||
),
|
||||
plugin_options=dict(type="dict", default={}),
|
||||
debug=dict(type="bool", default=False),
|
||||
force_remove=dict(type="bool", default=False),
|
||||
enable_timeout=dict(type="int", default=0),
|
||||
)
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
@@ -384,12 +437,16 @@ def main():
|
||||
cm = DockerPluginManager(client)
|
||||
client.module.exit_json(**cm.result)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -231,117 +231,138 @@ builder_cache_caches_deleted:
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.formatters import human_to_bytes
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
|
||||
convert_filters,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import clean_dict_booleans_for_docker_api
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import convert_filters
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
clean_dict_booleans_for_docker_api,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
containers=dict(type='bool', default=False),
|
||||
containers_filters=dict(type='dict'),
|
||||
images=dict(type='bool', default=False),
|
||||
images_filters=dict(type='dict'),
|
||||
networks=dict(type='bool', default=False),
|
||||
networks_filters=dict(type='dict'),
|
||||
volumes=dict(type='bool', default=False),
|
||||
volumes_filters=dict(type='dict'),
|
||||
builder_cache=dict(type='bool', default=False),
|
||||
builder_cache_all=dict(type='bool', default=False),
|
||||
builder_cache_filters=dict(type='dict'),
|
||||
builder_cache_keep_storage=dict(type='str'), # convert to bytes
|
||||
containers=dict(type="bool", default=False),
|
||||
containers_filters=dict(type="dict"),
|
||||
images=dict(type="bool", default=False),
|
||||
images_filters=dict(type="dict"),
|
||||
networks=dict(type="bool", default=False),
|
||||
networks_filters=dict(type="dict"),
|
||||
volumes=dict(type="bool", default=False),
|
||||
volumes_filters=dict(type="dict"),
|
||||
builder_cache=dict(type="bool", default=False),
|
||||
builder_cache_all=dict(type="bool", default=False),
|
||||
builder_cache_filters=dict(type="dict"),
|
||||
builder_cache_keep_storage=dict(type="str"), # convert to bytes
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
option_minimal_versions=dict(
|
||||
builder_cache=dict(docker_py_version='1.31'),
|
||||
builder_cache_all=dict(docker_py_version='1.39'),
|
||||
builder_cache_filters=dict(docker_py_version='1.31'),
|
||||
builder_cache_keep_storage=dict(docker_py_version='1.39'),
|
||||
builder_cache=dict(docker_py_version="1.31"),
|
||||
builder_cache_all=dict(docker_py_version="1.39"),
|
||||
builder_cache_filters=dict(docker_py_version="1.31"),
|
||||
builder_cache_keep_storage=dict(docker_py_version="1.39"),
|
||||
),
|
||||
# supports_check_mode=True,
|
||||
)
|
||||
|
||||
builder_cache_keep_storage = None
|
||||
if client.module.params.get('builder_cache_keep_storage') is not None:
|
||||
if client.module.params.get("builder_cache_keep_storage") is not None:
|
||||
try:
|
||||
builder_cache_keep_storage = human_to_bytes(client.module.params.get('builder_cache_keep_storage'))
|
||||
builder_cache_keep_storage = human_to_bytes(
|
||||
client.module.params.get("builder_cache_keep_storage")
|
||||
)
|
||||
except ValueError as exc:
|
||||
client.module.fail_json(msg=f'Error while parsing value of builder_cache_keep_storage: {exc}')
|
||||
client.module.fail_json(
|
||||
msg=f"Error while parsing value of builder_cache_keep_storage: {exc}"
|
||||
)
|
||||
|
||||
try:
|
||||
result = dict()
|
||||
changed = False
|
||||
|
||||
if client.module.params['containers']:
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get('containers_filters'), allow_sequences=True)
|
||||
params = {'filters': convert_filters(filters)}
|
||||
res = client.post_to_json('/containers/prune', params=params)
|
||||
result['containers'] = res.get('ContainersDeleted') or []
|
||||
result['containers_space_reclaimed'] = res['SpaceReclaimed']
|
||||
if result['containers'] or result['containers_space_reclaimed']:
|
||||
if client.module.params["containers"]:
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get("containers_filters"), allow_sequences=True
|
||||
)
|
||||
params = {"filters": convert_filters(filters)}
|
||||
res = client.post_to_json("/containers/prune", params=params)
|
||||
result["containers"] = res.get("ContainersDeleted") or []
|
||||
result["containers_space_reclaimed"] = res["SpaceReclaimed"]
|
||||
if result["containers"] or result["containers_space_reclaimed"]:
|
||||
changed = True
|
||||
|
||||
if client.module.params['images']:
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get('images_filters'), allow_sequences=True)
|
||||
params = {'filters': convert_filters(filters)}
|
||||
res = client.post_to_json('/images/prune', params=params)
|
||||
result['images'] = res.get('ImagesDeleted') or []
|
||||
result['images_space_reclaimed'] = res['SpaceReclaimed']
|
||||
if result['images'] or result['images_space_reclaimed']:
|
||||
if client.module.params["images"]:
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get("images_filters"), allow_sequences=True
|
||||
)
|
||||
params = {"filters": convert_filters(filters)}
|
||||
res = client.post_to_json("/images/prune", params=params)
|
||||
result["images"] = res.get("ImagesDeleted") or []
|
||||
result["images_space_reclaimed"] = res["SpaceReclaimed"]
|
||||
if result["images"] or result["images_space_reclaimed"]:
|
||||
changed = True
|
||||
|
||||
if client.module.params['networks']:
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get('networks_filters'), allow_sequences=True)
|
||||
params = {'filters': convert_filters(filters)}
|
||||
res = client.post_to_json('/networks/prune', params=params)
|
||||
result['networks'] = res.get('NetworksDeleted') or []
|
||||
if result['networks']:
|
||||
if client.module.params["networks"]:
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get("networks_filters"), allow_sequences=True
|
||||
)
|
||||
params = {"filters": convert_filters(filters)}
|
||||
res = client.post_to_json("/networks/prune", params=params)
|
||||
result["networks"] = res.get("NetworksDeleted") or []
|
||||
if result["networks"]:
|
||||
changed = True
|
||||
|
||||
if client.module.params['volumes']:
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get('volumes_filters'), allow_sequences=True)
|
||||
params = {'filters': convert_filters(filters)}
|
||||
res = client.post_to_json('/volumes/prune', params=params)
|
||||
result['volumes'] = res.get('VolumesDeleted') or []
|
||||
result['volumes_space_reclaimed'] = res['SpaceReclaimed']
|
||||
if result['volumes'] or result['volumes_space_reclaimed']:
|
||||
if client.module.params["volumes"]:
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get("volumes_filters"), allow_sequences=True
|
||||
)
|
||||
params = {"filters": convert_filters(filters)}
|
||||
res = client.post_to_json("/volumes/prune", params=params)
|
||||
result["volumes"] = res.get("VolumesDeleted") or []
|
||||
result["volumes_space_reclaimed"] = res["SpaceReclaimed"]
|
||||
if result["volumes"] or result["volumes_space_reclaimed"]:
|
||||
changed = True
|
||||
|
||||
if client.module.params['builder_cache']:
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get('builder_cache_filters'), allow_sequences=True)
|
||||
params = {'filters': convert_filters(filters)}
|
||||
if client.module.params.get('builder_cache_all'):
|
||||
params['all'] = 'true'
|
||||
if client.module.params["builder_cache"]:
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get("builder_cache_filters"), allow_sequences=True
|
||||
)
|
||||
params = {"filters": convert_filters(filters)}
|
||||
if client.module.params.get("builder_cache_all"):
|
||||
params["all"] = "true"
|
||||
if builder_cache_keep_storage is not None:
|
||||
params['keep-storage'] = builder_cache_keep_storage
|
||||
res = client.post_to_json('/build/prune', params=params)
|
||||
result['builder_cache_space_reclaimed'] = res['SpaceReclaimed']
|
||||
if result['builder_cache_space_reclaimed']:
|
||||
params["keep-storage"] = builder_cache_keep_storage
|
||||
res = client.post_to_json("/build/prune", params=params)
|
||||
result["builder_cache_space_reclaimed"] = res["SpaceReclaimed"]
|
||||
if result["builder_cache_space_reclaimed"]:
|
||||
changed = True
|
||||
if 'CachesDeleted' in res:
|
||||
if "CachesDeleted" in res:
|
||||
# API version 1.39+: return value CachesDeleted (list of str)
|
||||
result['builder_cache_caches_deleted'] = res['CachesDeleted']
|
||||
if result['builder_cache_caches_deleted']:
|
||||
result["builder_cache_caches_deleted"] = res["CachesDeleted"]
|
||||
if result["builder_cache_caches_deleted"]:
|
||||
changed = True
|
||||
|
||||
result['changed'] = changed
|
||||
result["changed"] = changed
|
||||
client.module.exit_json(**result)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -191,12 +191,14 @@ import base64
|
||||
import hashlib
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException, APIError
|
||||
from docker.errors import APIError, DockerException
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
|
||||
pass
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
@@ -206,7 +208,6 @@ from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
compare_generic,
|
||||
sanitize_labels,
|
||||
)
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
|
||||
|
||||
class SecretManager(DockerBaseClass):
|
||||
@@ -220,25 +221,25 @@ class SecretManager(DockerBaseClass):
|
||||
self.check_mode = self.client.check_mode
|
||||
|
||||
parameters = self.client.module.params
|
||||
self.name = parameters.get('name')
|
||||
self.state = parameters.get('state')
|
||||
self.data = parameters.get('data')
|
||||
self.name = parameters.get("name")
|
||||
self.state = parameters.get("state")
|
||||
self.data = parameters.get("data")
|
||||
if self.data is not None:
|
||||
if parameters.get('data_is_b64'):
|
||||
if parameters.get("data_is_b64"):
|
||||
self.data = base64.b64decode(self.data)
|
||||
else:
|
||||
self.data = to_bytes(self.data)
|
||||
data_src = parameters.get('data_src')
|
||||
data_src = parameters.get("data_src")
|
||||
if data_src is not None:
|
||||
try:
|
||||
with open(data_src, 'rb') as f:
|
||||
with open(data_src, "rb") as f:
|
||||
self.data = f.read()
|
||||
except Exception as exc:
|
||||
self.client.fail(f'Error while reading {data_src}: {exc}')
|
||||
self.labels = parameters.get('labels')
|
||||
self.force = parameters.get('force')
|
||||
self.rolling_versions = parameters.get('rolling_versions')
|
||||
self.versions_to_keep = parameters.get('versions_to_keep')
|
||||
self.client.fail(f"Error while reading {data_src}: {exc}")
|
||||
self.labels = parameters.get("labels")
|
||||
self.force = parameters.get("force")
|
||||
self.rolling_versions = parameters.get("rolling_versions")
|
||||
self.versions_to_keep = parameters.get("versions_to_keep")
|
||||
|
||||
if self.rolling_versions:
|
||||
self.version = 0
|
||||
@@ -247,16 +248,18 @@ class SecretManager(DockerBaseClass):
|
||||
|
||||
def __call__(self):
|
||||
self.get_secret()
|
||||
if self.state == 'present':
|
||||
if self.state == "present":
|
||||
self.data_key = hashlib.sha224(self.data).hexdigest()
|
||||
self.present()
|
||||
self.remove_old_versions()
|
||||
elif self.state == 'absent':
|
||||
elif self.state == "absent":
|
||||
self.absent()
|
||||
|
||||
def get_version(self, secret):
|
||||
try:
|
||||
return int(secret.get('Spec', {}).get('Labels', {}).get('ansible_version', 0))
|
||||
return int(
|
||||
secret.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
|
||||
)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
@@ -268,9 +271,9 @@ class SecretManager(DockerBaseClass):
|
||||
self.remove_secret(self.secrets.pop(0))
|
||||
|
||||
def get_secret(self):
|
||||
''' Find an existing secret. '''
|
||||
"""Find an existing secret."""
|
||||
try:
|
||||
secrets = self.client.secrets(filters={'name': self.name})
|
||||
secrets = self.client.secrets(filters={"name": self.name})
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error accessing secret {self.name}: {exc}")
|
||||
|
||||
@@ -278,62 +281,66 @@ class SecretManager(DockerBaseClass):
|
||||
self.secrets = [
|
||||
secret
|
||||
for secret in secrets
|
||||
if secret['Spec']['Name'].startswith(f'{self.name}_v')
|
||||
if secret["Spec"]["Name"].startswith(f"{self.name}_v")
|
||||
]
|
||||
self.secrets.sort(key=self.get_version)
|
||||
else:
|
||||
self.secrets = [
|
||||
secret for secret in secrets if secret['Spec']['Name'] == self.name
|
||||
secret for secret in secrets if secret["Spec"]["Name"] == self.name
|
||||
]
|
||||
|
||||
def create_secret(self):
|
||||
''' Create a new secret '''
|
||||
"""Create a new secret"""
|
||||
secret_id = None
|
||||
# We cannot see the data after creation, so adding a label we can use for idempotency check
|
||||
labels = {
|
||||
'ansible_key': self.data_key
|
||||
}
|
||||
labels = {"ansible_key": self.data_key}
|
||||
if self.rolling_versions:
|
||||
self.version += 1
|
||||
labels['ansible_version'] = str(self.version)
|
||||
self.name = f'{self.name}_v{self.version}'
|
||||
labels["ansible_version"] = str(self.version)
|
||||
self.name = f"{self.name}_v{self.version}"
|
||||
if self.labels:
|
||||
labels.update(self.labels)
|
||||
|
||||
try:
|
||||
if not self.check_mode:
|
||||
secret_id = self.client.create_secret(self.name, self.data, labels=labels)
|
||||
self.secrets += self.client.secrets(filters={'id': secret_id})
|
||||
secret_id = self.client.create_secret(
|
||||
self.name, self.data, labels=labels
|
||||
)
|
||||
self.secrets += self.client.secrets(filters={"id": secret_id})
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error creating secret: {exc}")
|
||||
|
||||
if isinstance(secret_id, dict):
|
||||
secret_id = secret_id['ID']
|
||||
secret_id = secret_id["ID"]
|
||||
|
||||
return secret_id
|
||||
|
||||
def remove_secret(self, secret):
|
||||
try:
|
||||
if not self.check_mode:
|
||||
self.client.remove_secret(secret['ID'])
|
||||
self.client.remove_secret(secret["ID"])
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error removing secret {secret['Spec']['Name']}: {exc}")
|
||||
|
||||
def present(self):
|
||||
''' Handles state == 'present', creating or updating the secret '''
|
||||
"""Handles state == 'present', creating or updating the secret"""
|
||||
if self.secrets:
|
||||
secret = self.secrets[-1]
|
||||
self.results['secret_id'] = secret['ID']
|
||||
self.results['secret_name'] = secret['Spec']['Name']
|
||||
self.results["secret_id"] = secret["ID"]
|
||||
self.results["secret_name"] = secret["Spec"]["Name"]
|
||||
data_changed = False
|
||||
attrs = secret.get('Spec', {})
|
||||
if attrs.get('Labels', {}).get('ansible_key'):
|
||||
if attrs['Labels']['ansible_key'] != self.data_key:
|
||||
attrs = secret.get("Spec", {})
|
||||
if attrs.get("Labels", {}).get("ansible_key"):
|
||||
if attrs["Labels"]["ansible_key"] != self.data_key:
|
||||
data_changed = True
|
||||
else:
|
||||
if not self.force:
|
||||
self.client.module.warn("'ansible_key' label not found. Secret will not be changed unless the force parameter is set to 'true'")
|
||||
labels_changed = not compare_generic(self.labels, attrs.get('Labels'), 'allow_more_present', 'dict')
|
||||
self.client.module.warn(
|
||||
"'ansible_key' label not found. Secret will not be changed unless the force parameter is set to 'true'"
|
||||
)
|
||||
labels_changed = not compare_generic(
|
||||
self.labels, attrs.get("Labels"), "allow_more_present", "dict"
|
||||
)
|
||||
if self.rolling_versions:
|
||||
self.version = self.get_version(secret)
|
||||
if data_changed or labels_changed or self.force:
|
||||
@@ -341,41 +348,41 @@ class SecretManager(DockerBaseClass):
|
||||
if not self.rolling_versions:
|
||||
self.absent()
|
||||
secret_id = self.create_secret()
|
||||
self.results['changed'] = True
|
||||
self.results['secret_id'] = secret_id
|
||||
self.results['secret_name'] = self.name
|
||||
self.results["changed"] = True
|
||||
self.results["secret_id"] = secret_id
|
||||
self.results["secret_name"] = self.name
|
||||
else:
|
||||
self.results['changed'] = True
|
||||
self.results['secret_id'] = self.create_secret()
|
||||
self.results['secret_name'] = self.name
|
||||
self.results["changed"] = True
|
||||
self.results["secret_id"] = self.create_secret()
|
||||
self.results["secret_name"] = self.name
|
||||
|
||||
def absent(self):
|
||||
''' Handles state == 'absent', removing the secret '''
|
||||
"""Handles state == 'absent', removing the secret"""
|
||||
if self.secrets:
|
||||
for secret in self.secrets:
|
||||
self.remove_secret(secret)
|
||||
self.results['changed'] = True
|
||||
self.results["changed"] = True
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
state=dict(type='str', default='present', choices=['absent', 'present']),
|
||||
data=dict(type='str', no_log=True),
|
||||
data_is_b64=dict(type='bool', default=False),
|
||||
data_src=dict(type='path'),
|
||||
labels=dict(type='dict'),
|
||||
force=dict(type='bool', default=False),
|
||||
rolling_versions=dict(type='bool', default=False),
|
||||
versions_to_keep=dict(type='int', default=5),
|
||||
name=dict(type="str", required=True),
|
||||
state=dict(type="str", default="present", choices=["absent", "present"]),
|
||||
data=dict(type="str", no_log=True),
|
||||
data_is_b64=dict(type="bool", default=False),
|
||||
data_src=dict(type="path"),
|
||||
labels=dict(type="dict"),
|
||||
force=dict(type="bool", default=False),
|
||||
rolling_versions=dict(type="bool", default=False),
|
||||
versions_to_keep=dict(type="int", default=5),
|
||||
)
|
||||
|
||||
required_if = [
|
||||
('state', 'present', ['data', 'data_src'], True),
|
||||
("state", "present", ["data", "data_src"], True),
|
||||
]
|
||||
|
||||
mutually_exclusive = [
|
||||
('data', 'data_src'),
|
||||
("data", "data_src"),
|
||||
]
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -383,26 +390,26 @@ def main():
|
||||
supports_check_mode=True,
|
||||
required_if=required_if,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
min_docker_version='2.1.0',
|
||||
min_docker_version="2.1.0",
|
||||
)
|
||||
sanitize_labels(client.module.params['labels'], 'labels', client)
|
||||
sanitize_labels(client.module.params["labels"], "labels", client)
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
changed=False,
|
||||
secret_id='',
|
||||
secret_name=''
|
||||
)
|
||||
results = dict(changed=False, secret_id="", secret_name="")
|
||||
|
||||
SecretManager(client, results)()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -159,34 +159,37 @@ import json
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from time import sleep
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
from jsondiff import diff as json_diff
|
||||
|
||||
HAS_JSONDIFF = True
|
||||
except ImportError:
|
||||
HAS_JSONDIFF = False
|
||||
|
||||
try:
|
||||
from yaml import dump as yaml_dump
|
||||
|
||||
HAS_YAML = True
|
||||
except ImportError:
|
||||
HAS_YAML = False
|
||||
|
||||
|
||||
def docker_stack_services(client, stack_name):
|
||||
rc, out, err = client.call_cli("stack", "services", stack_name, "--format", "{{.Name}}")
|
||||
rc, out, err = client.call_cli(
|
||||
"stack", "services", stack_name, "--format", "{{.Name}}"
|
||||
)
|
||||
if to_native(err) == f"Nothing found in stack: {stack_name}\n":
|
||||
return []
|
||||
return to_native(out).strip().split('\n')
|
||||
return to_native(out).strip().split("\n")
|
||||
|
||||
|
||||
def docker_service_inspect(client, service_name):
|
||||
@@ -194,7 +197,7 @@ def docker_service_inspect(client, service_name):
|
||||
if rc != 0:
|
||||
return None
|
||||
else:
|
||||
ret = json.loads(out)[0]['Spec']
|
||||
ret = json.loads(out)[0]["Spec"]
|
||||
return ret
|
||||
|
||||
|
||||
@@ -207,11 +210,9 @@ def docker_stack_deploy(client, stack_name, compose_files):
|
||||
if client.module.params["with_registry_auth"]:
|
||||
command += ["--with-registry-auth"]
|
||||
if client.module.params["resolve_image"]:
|
||||
command += ["--resolve-image",
|
||||
client.module.params["resolve_image"]]
|
||||
command += ["--resolve-image", client.module.params["resolve_image"]]
|
||||
for compose_file in compose_files:
|
||||
command += ["--compose-file",
|
||||
compose_file]
|
||||
command += ["--compose-file", compose_file]
|
||||
command += [stack_name]
|
||||
rc, out, err = client.call_cli(*command)
|
||||
return rc, to_native(out), to_native(err)
|
||||
@@ -240,15 +241,15 @@ def docker_stack_rm(client, stack_name, retries, interval):
|
||||
def main():
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec={
|
||||
'name': dict(type='str', required=True),
|
||||
'compose': dict(type='list', elements='raw', default=[]),
|
||||
'prune': dict(type='bool', default=False),
|
||||
'detach': dict(type='bool', default=True),
|
||||
'with_registry_auth': dict(type='bool', default=False),
|
||||
'resolve_image': dict(type='str', choices=['always', 'changed', 'never']),
|
||||
'state': dict(type='str', default='present', choices=['present', 'absent']),
|
||||
'absent_retries': dict(type='int', default=0),
|
||||
'absent_retries_interval': dict(type='int', default=1)
|
||||
"name": dict(type="str", required=True),
|
||||
"compose": dict(type="list", elements="raw", default=[]),
|
||||
"prune": dict(type="bool", default=False),
|
||||
"detach": dict(type="bool", default=True),
|
||||
"with_registry_auth": dict(type="bool", default=False),
|
||||
"resolve_image": dict(type="str", choices=["always", "changed", "never"]),
|
||||
"state": dict(type="str", default="present", choices=["present", "absent"]),
|
||||
"absent_retries": dict(type="int", default=0),
|
||||
"absent_retries_interval": dict(type="int", default=1),
|
||||
},
|
||||
supports_check_mode=False,
|
||||
)
|
||||
@@ -260,28 +261,32 @@ def main():
|
||||
return client.fail("yaml is not installed, try 'pip install pyyaml'")
|
||||
|
||||
try:
|
||||
state = client.module.params['state']
|
||||
compose = client.module.params['compose']
|
||||
name = client.module.params['name']
|
||||
absent_retries = client.module.params['absent_retries']
|
||||
absent_retries_interval = client.module.params['absent_retries_interval']
|
||||
state = client.module.params["state"]
|
||||
compose = client.module.params["compose"]
|
||||
name = client.module.params["name"]
|
||||
absent_retries = client.module.params["absent_retries"]
|
||||
absent_retries_interval = client.module.params["absent_retries_interval"]
|
||||
|
||||
if state == 'present':
|
||||
if state == "present":
|
||||
if not compose:
|
||||
client.fail("compose parameter must be a list containing at least one element")
|
||||
client.fail(
|
||||
"compose parameter must be a list containing at least one element"
|
||||
)
|
||||
|
||||
compose_files = []
|
||||
for i, compose_def in enumerate(compose):
|
||||
if isinstance(compose_def, dict):
|
||||
compose_file_fd, compose_file = tempfile.mkstemp()
|
||||
client.module.add_cleanup_file(compose_file)
|
||||
with os.fdopen(compose_file_fd, 'w') as stack_file:
|
||||
with os.fdopen(compose_file_fd, "w") as stack_file:
|
||||
compose_files.append(compose_file)
|
||||
stack_file.write(yaml_dump(compose_def))
|
||||
elif isinstance(compose_def, str):
|
||||
compose_files.append(compose_def)
|
||||
else:
|
||||
client.fail(f"compose element '{compose_def}' must be a string or a dictionary")
|
||||
client.fail(
|
||||
f"compose element '{compose_def}' must be a string or a dictionary"
|
||||
)
|
||||
|
||||
before_stack_services = docker_stack_inspect(client, name)
|
||||
|
||||
@@ -290,13 +295,20 @@ def main():
|
||||
after_stack_services = docker_stack_inspect(client, name)
|
||||
|
||||
if rc != 0:
|
||||
client.fail("docker stack up deploy command failed", rc=rc, stdout=out, stderr=err)
|
||||
client.fail(
|
||||
"docker stack up deploy command failed",
|
||||
rc=rc,
|
||||
stdout=out,
|
||||
stderr=err,
|
||||
)
|
||||
|
||||
before_after_differences = json_diff(before_stack_services, after_stack_services)
|
||||
before_after_differences = json_diff(
|
||||
before_stack_services, after_stack_services
|
||||
)
|
||||
for k in before_after_differences.keys():
|
||||
if isinstance(before_after_differences[k], dict):
|
||||
before_after_differences[k].pop('UpdatedAt', None)
|
||||
before_after_differences[k].pop('Version', None)
|
||||
before_after_differences[k].pop("UpdatedAt", None)
|
||||
before_after_differences[k].pop("Version", None)
|
||||
if not list(before_after_differences[k].keys()):
|
||||
before_after_differences.pop(k)
|
||||
|
||||
@@ -322,7 +334,9 @@ def main():
|
||||
|
||||
else:
|
||||
if docker_stack_services(client, name):
|
||||
rc, out, err = docker_stack_rm(client, name, absent_retries, absent_retries_interval)
|
||||
rc, out, err = docker_stack_rm(
|
||||
client, name, absent_retries, absent_retries_interval
|
||||
)
|
||||
if rc != 0:
|
||||
client.module.fail_json(
|
||||
msg="'docker stack down' command failed",
|
||||
@@ -340,7 +354,10 @@ def main():
|
||||
)
|
||||
client.module.exit_json(changed=False)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: docker_stack_info
|
||||
author: "Jose Angel Munoz (@imjoseangel)"
|
||||
@@ -78,7 +79,6 @@ import json
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
@@ -86,31 +86,36 @@ from ansible_collections.community.docker.plugins.module_utils.common_cli import
|
||||
|
||||
|
||||
def docker_stack_list(module):
|
||||
docker_bin = module.get_bin_path('docker', required=True)
|
||||
docker_bin = module.get_bin_path("docker", required=True)
|
||||
rc, out, err = module.run_command(
|
||||
[docker_bin, "stack", "ls", "--format={{json .}}"])
|
||||
[docker_bin, "stack", "ls", "--format={{json .}}"]
|
||||
)
|
||||
|
||||
return rc, out.strip(), err.strip()
|
||||
|
||||
|
||||
def main():
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec={
|
||||
},
|
||||
argument_spec={},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
try:
|
||||
rc, ret, stderr = client.call_cli_json_stream('stack', 'ls', '--format={{json .}}', check_rc=True)
|
||||
rc, ret, stderr = client.call_cli_json_stream(
|
||||
"stack", "ls", "--format={{json .}}", check_rc=True
|
||||
)
|
||||
client.module.exit_json(
|
||||
changed=False,
|
||||
rc=rc,
|
||||
stdout='\n'.join([json.dumps(entry) for entry in ret]),
|
||||
stdout="\n".join([json.dumps(entry) for entry in ret]),
|
||||
stderr=to_native(stderr).strip(),
|
||||
results=ret,
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: docker_stack_task_info
|
||||
author: "Jose Angel Munoz (@imjoseangel)"
|
||||
@@ -86,7 +87,6 @@ import json
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_cli import (
|
||||
AnsibleModuleDockerClient,
|
||||
DockerException,
|
||||
@@ -94,33 +94,37 @@ from ansible_collections.community.docker.plugins.module_utils.common_cli import
|
||||
|
||||
|
||||
def docker_stack_task(module, stack_name):
|
||||
docker_bin = module.get_bin_path('docker', required=True)
|
||||
docker_bin = module.get_bin_path("docker", required=True)
|
||||
rc, out, err = module.run_command(
|
||||
[docker_bin, "stack", "ps", stack_name, "--format={{json .}}"])
|
||||
[docker_bin, "stack", "ps", stack_name, "--format={{json .}}"]
|
||||
)
|
||||
|
||||
return rc, out.strip(), err.strip()
|
||||
|
||||
|
||||
def main():
|
||||
client = AnsibleModuleDockerClient(
|
||||
argument_spec={
|
||||
'name': dict(type='str', required=True)
|
||||
},
|
||||
argument_spec={"name": dict(type="str", required=True)},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
try:
|
||||
name = client.module.params['name']
|
||||
rc, ret, stderr = client.call_cli_json_stream('stack', 'ps', name, '--format={{json .}}', check_rc=True)
|
||||
name = client.module.params["name"]
|
||||
rc, ret, stderr = client.call_cli_json_stream(
|
||||
"stack", "ps", name, "--format={{json .}}", check_rc=True
|
||||
)
|
||||
client.module.exit_json(
|
||||
changed=False,
|
||||
rc=rc,
|
||||
stdout='\n'.join([json.dumps(entry) for entry in ret]),
|
||||
stdout="\n".join([json.dumps(entry) for entry in ret]),
|
||||
stderr=to_native(stderr).strip(),
|
||||
results=ret,
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+179
-149
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: docker_swarm
|
||||
short_description: Manage Swarm cluster
|
||||
@@ -292,8 +293,9 @@ actions:
|
||||
import json
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException, APIError
|
||||
from docker.errors import APIError, DockerException
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
|
||||
pass
|
||||
@@ -302,13 +304,14 @@ from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
DockerBaseClass,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import (
|
||||
AnsibleDockerSwarmClient,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DifferenceTracker,
|
||||
sanitize_labels,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
|
||||
|
||||
|
||||
class TaskParameters(DockerBaseClass):
|
||||
def __init__(self):
|
||||
@@ -353,68 +356,70 @@ class TaskParameters(DockerBaseClass):
|
||||
return result
|
||||
|
||||
def update_from_swarm_info(self, swarm_info):
|
||||
spec = swarm_info['Spec']
|
||||
spec = swarm_info["Spec"]
|
||||
|
||||
ca_config = spec.get('CAConfig') or dict()
|
||||
ca_config = spec.get("CAConfig") or dict()
|
||||
if self.node_cert_expiry is None:
|
||||
self.node_cert_expiry = ca_config.get('NodeCertExpiry')
|
||||
self.node_cert_expiry = ca_config.get("NodeCertExpiry")
|
||||
if self.ca_force_rotate is None:
|
||||
self.ca_force_rotate = ca_config.get('ForceRotate')
|
||||
self.ca_force_rotate = ca_config.get("ForceRotate")
|
||||
|
||||
dispatcher = spec.get('Dispatcher') or dict()
|
||||
dispatcher = spec.get("Dispatcher") or dict()
|
||||
if self.dispatcher_heartbeat_period is None:
|
||||
self.dispatcher_heartbeat_period = dispatcher.get('HeartbeatPeriod')
|
||||
self.dispatcher_heartbeat_period = dispatcher.get("HeartbeatPeriod")
|
||||
|
||||
raft = spec.get('Raft') or dict()
|
||||
raft = spec.get("Raft") or dict()
|
||||
if self.snapshot_interval is None:
|
||||
self.snapshot_interval = raft.get('SnapshotInterval')
|
||||
self.snapshot_interval = raft.get("SnapshotInterval")
|
||||
if self.keep_old_snapshots is None:
|
||||
self.keep_old_snapshots = raft.get('KeepOldSnapshots')
|
||||
self.keep_old_snapshots = raft.get("KeepOldSnapshots")
|
||||
if self.heartbeat_tick is None:
|
||||
self.heartbeat_tick = raft.get('HeartbeatTick')
|
||||
self.heartbeat_tick = raft.get("HeartbeatTick")
|
||||
if self.log_entries_for_slow_followers is None:
|
||||
self.log_entries_for_slow_followers = raft.get('LogEntriesForSlowFollowers')
|
||||
self.log_entries_for_slow_followers = raft.get("LogEntriesForSlowFollowers")
|
||||
if self.election_tick is None:
|
||||
self.election_tick = raft.get('ElectionTick')
|
||||
self.election_tick = raft.get("ElectionTick")
|
||||
|
||||
orchestration = spec.get('Orchestration') or dict()
|
||||
orchestration = spec.get("Orchestration") or dict()
|
||||
if self.task_history_retention_limit is None:
|
||||
self.task_history_retention_limit = orchestration.get('TaskHistoryRetentionLimit')
|
||||
self.task_history_retention_limit = orchestration.get(
|
||||
"TaskHistoryRetentionLimit"
|
||||
)
|
||||
|
||||
encryption_config = spec.get('EncryptionConfig') or dict()
|
||||
encryption_config = spec.get("EncryptionConfig") or dict()
|
||||
if self.autolock_managers is None:
|
||||
self.autolock_managers = encryption_config.get('AutoLockManagers')
|
||||
self.autolock_managers = encryption_config.get("AutoLockManagers")
|
||||
|
||||
if self.name is None:
|
||||
self.name = spec['Name']
|
||||
self.name = spec["Name"]
|
||||
|
||||
if self.labels is None:
|
||||
self.labels = spec.get('Labels') or {}
|
||||
self.labels = spec.get("Labels") or {}
|
||||
|
||||
if 'LogDriver' in spec['TaskDefaults']:
|
||||
self.log_driver = spec['TaskDefaults']['LogDriver']
|
||||
if "LogDriver" in spec["TaskDefaults"]:
|
||||
self.log_driver = spec["TaskDefaults"]["LogDriver"]
|
||||
|
||||
def update_parameters(self, client):
|
||||
assign = dict(
|
||||
snapshot_interval='snapshot_interval',
|
||||
task_history_retention_limit='task_history_retention_limit',
|
||||
keep_old_snapshots='keep_old_snapshots',
|
||||
log_entries_for_slow_followers='log_entries_for_slow_followers',
|
||||
heartbeat_tick='heartbeat_tick',
|
||||
election_tick='election_tick',
|
||||
dispatcher_heartbeat_period='dispatcher_heartbeat_period',
|
||||
node_cert_expiry='node_cert_expiry',
|
||||
name='name',
|
||||
labels='labels',
|
||||
signing_ca_cert='signing_ca_cert',
|
||||
signing_ca_key='signing_ca_key',
|
||||
ca_force_rotate='ca_force_rotate',
|
||||
autolock_managers='autolock_managers',
|
||||
log_driver='log_driver',
|
||||
snapshot_interval="snapshot_interval",
|
||||
task_history_retention_limit="task_history_retention_limit",
|
||||
keep_old_snapshots="keep_old_snapshots",
|
||||
log_entries_for_slow_followers="log_entries_for_slow_followers",
|
||||
heartbeat_tick="heartbeat_tick",
|
||||
election_tick="election_tick",
|
||||
dispatcher_heartbeat_period="dispatcher_heartbeat_period",
|
||||
node_cert_expiry="node_cert_expiry",
|
||||
name="name",
|
||||
labels="labels",
|
||||
signing_ca_cert="signing_ca_cert",
|
||||
signing_ca_key="signing_ca_key",
|
||||
ca_force_rotate="ca_force_rotate",
|
||||
autolock_managers="autolock_managers",
|
||||
log_driver="log_driver",
|
||||
)
|
||||
params = dict()
|
||||
for dest, source in assign.items():
|
||||
if not client.option_minimal_versions[source]['supported']:
|
||||
if not client.option_minimal_versions[source]["supported"]:
|
||||
continue
|
||||
value = getattr(self, source)
|
||||
if value is not None:
|
||||
@@ -423,12 +428,21 @@ class TaskParameters(DockerBaseClass):
|
||||
|
||||
def compare_to_active(self, other, client, differences):
|
||||
for k in self.__dict__:
|
||||
if k in ('advertise_addr', 'listen_addr', 'remote_addrs', 'join_token',
|
||||
'rotate_worker_token', 'rotate_manager_token', 'spec',
|
||||
'default_addr_pool', 'subnet_size', 'data_path_addr',
|
||||
'data_path_port'):
|
||||
if k in (
|
||||
"advertise_addr",
|
||||
"listen_addr",
|
||||
"remote_addrs",
|
||||
"join_token",
|
||||
"rotate_worker_token",
|
||||
"rotate_manager_token",
|
||||
"spec",
|
||||
"default_addr_pool",
|
||||
"subnet_size",
|
||||
"data_path_addr",
|
||||
"data_path_port",
|
||||
):
|
||||
continue
|
||||
if not client.option_minimal_versions[k]['supported']:
|
||||
if not client.option_minimal_versions[k]["supported"]:
|
||||
continue
|
||||
value = getattr(self, k)
|
||||
if value is None:
|
||||
@@ -437,9 +451,9 @@ class TaskParameters(DockerBaseClass):
|
||||
if value != other_value:
|
||||
differences.add(k, parameter=value, active=other_value)
|
||||
if self.rotate_worker_token:
|
||||
differences.add('rotate_worker_token', parameter=True, active=False)
|
||||
differences.add("rotate_worker_token", parameter=True, active=False)
|
||||
if self.rotate_manager_token:
|
||||
differences.add('rotate_manager_token', parameter=True, active=False)
|
||||
differences.add("rotate_manager_token", parameter=True, active=False)
|
||||
return differences
|
||||
|
||||
|
||||
@@ -454,9 +468,9 @@ class SwarmManager(DockerBaseClass):
|
||||
self.check_mode = self.client.check_mode
|
||||
self.swarm_info = {}
|
||||
|
||||
self.state = client.module.params['state']
|
||||
self.force = client.module.params['force']
|
||||
self.node_id = client.module.params['node_id']
|
||||
self.state = client.module.params["state"]
|
||||
self.force = client.module.params["force"]
|
||||
self.node_id = client.module.params["node_id"]
|
||||
|
||||
self.differences = DifferenceTracker()
|
||||
self.parameters = TaskParameters.from_ansible_params(client)
|
||||
@@ -475,8 +489,8 @@ class SwarmManager(DockerBaseClass):
|
||||
|
||||
if self.client.module._diff or self.parameters.debug:
|
||||
diff = dict()
|
||||
diff['before'], diff['after'] = self.differences.get_before_after()
|
||||
self.results['diff'] = diff
|
||||
diff["before"], diff["after"] = self.differences.get_before_after()
|
||||
self.results["diff"] = diff
|
||||
|
||||
def inspect_swarm(self):
|
||||
try:
|
||||
@@ -484,8 +498,8 @@ class SwarmManager(DockerBaseClass):
|
||||
json_str = json.dumps(data, ensure_ascii=False)
|
||||
self.swarm_info = json.loads(json_str)
|
||||
|
||||
self.results['changed'] = False
|
||||
self.results['swarm_facts'] = self.swarm_info
|
||||
self.results["changed"] = False
|
||||
self.results["swarm_facts"] = self.swarm_info
|
||||
|
||||
unlock_key = self.get_unlock_key()
|
||||
self.swarm_info.update(unlock_key)
|
||||
@@ -493,7 +507,7 @@ class SwarmManager(DockerBaseClass):
|
||||
return
|
||||
|
||||
def get_unlock_key(self):
|
||||
default = {'UnlockKey': None}
|
||||
default = {"UnlockKey": None}
|
||||
if not self.has_swarm_lock_changed():
|
||||
return default
|
||||
try:
|
||||
@@ -503,7 +517,7 @@ class SwarmManager(DockerBaseClass):
|
||||
|
||||
def has_swarm_lock_changed(self):
|
||||
return self.parameters.autolock_managers and (
|
||||
self.created or self.differences.has_difference_for('autolock_managers')
|
||||
self.created or self.differences.has_difference_for("autolock_managers")
|
||||
)
|
||||
|
||||
def init_swarm(self):
|
||||
@@ -513,19 +527,19 @@ class SwarmManager(DockerBaseClass):
|
||||
|
||||
if not self.check_mode:
|
||||
init_arguments = {
|
||||
'advertise_addr': self.parameters.advertise_addr,
|
||||
'listen_addr': self.parameters.listen_addr,
|
||||
'force_new_cluster': self.force,
|
||||
'swarm_spec': self.parameters.spec,
|
||||
"advertise_addr": self.parameters.advertise_addr,
|
||||
"listen_addr": self.parameters.listen_addr,
|
||||
"force_new_cluster": self.force,
|
||||
"swarm_spec": self.parameters.spec,
|
||||
}
|
||||
if self.parameters.default_addr_pool is not None:
|
||||
init_arguments['default_addr_pool'] = self.parameters.default_addr_pool
|
||||
init_arguments["default_addr_pool"] = self.parameters.default_addr_pool
|
||||
if self.parameters.subnet_size is not None:
|
||||
init_arguments['subnet_size'] = self.parameters.subnet_size
|
||||
init_arguments["subnet_size"] = self.parameters.subnet_size
|
||||
if self.parameters.data_path_addr is not None:
|
||||
init_arguments['data_path_addr'] = self.parameters.data_path_addr
|
||||
init_arguments["data_path_addr"] = self.parameters.data_path_addr
|
||||
if self.parameters.data_path_port is not None:
|
||||
init_arguments['data_path_port'] = self.parameters.data_path_port
|
||||
init_arguments["data_path_port"] = self.parameters.data_path_port
|
||||
try:
|
||||
self.client.init_swarm(**init_arguments)
|
||||
except APIError as exc:
|
||||
@@ -537,180 +551,196 @@ class SwarmManager(DockerBaseClass):
|
||||
|
||||
self.created = True
|
||||
self.inspect_swarm()
|
||||
self.results['actions'].append(f"New Swarm cluster created: {self.swarm_info.get('ID')}")
|
||||
self.differences.add('state', parameter='present', active='absent')
|
||||
self.results['changed'] = True
|
||||
self.results['swarm_facts'] = {
|
||||
'JoinTokens': self.swarm_info.get('JoinTokens'),
|
||||
'UnlockKey': self.swarm_info.get('UnlockKey')
|
||||
self.results["actions"].append(
|
||||
f"New Swarm cluster created: {self.swarm_info.get('ID')}"
|
||||
)
|
||||
self.differences.add("state", parameter="present", active="absent")
|
||||
self.results["changed"] = True
|
||||
self.results["swarm_facts"] = {
|
||||
"JoinTokens": self.swarm_info.get("JoinTokens"),
|
||||
"UnlockKey": self.swarm_info.get("UnlockKey"),
|
||||
}
|
||||
|
||||
def __update_swarm(self):
|
||||
try:
|
||||
self.inspect_swarm()
|
||||
version = self.swarm_info['Version']['Index']
|
||||
version = self.swarm_info["Version"]["Index"]
|
||||
self.parameters.update_from_swarm_info(self.swarm_info)
|
||||
old_parameters = TaskParameters()
|
||||
old_parameters.update_from_swarm_info(self.swarm_info)
|
||||
self.parameters.compare_to_active(old_parameters, self.client, self.differences)
|
||||
self.parameters.compare_to_active(
|
||||
old_parameters, self.client, self.differences
|
||||
)
|
||||
if self.differences.empty:
|
||||
self.results['actions'].append("No modification")
|
||||
self.results['changed'] = False
|
||||
self.results["actions"].append("No modification")
|
||||
self.results["changed"] = False
|
||||
return
|
||||
update_parameters = TaskParameters.from_ansible_params(self.client)
|
||||
update_parameters.update_parameters(self.client)
|
||||
if not self.check_mode:
|
||||
self.client.update_swarm(
|
||||
version=version, swarm_spec=update_parameters.spec,
|
||||
version=version,
|
||||
swarm_spec=update_parameters.spec,
|
||||
rotate_worker_token=self.parameters.rotate_worker_token,
|
||||
rotate_manager_token=self.parameters.rotate_manager_token)
|
||||
rotate_manager_token=self.parameters.rotate_manager_token,
|
||||
)
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Can not update a Swarm Cluster: {exc}")
|
||||
return
|
||||
|
||||
self.inspect_swarm()
|
||||
self.results['actions'].append("Swarm cluster updated")
|
||||
self.results['changed'] = True
|
||||
self.results["actions"].append("Swarm cluster updated")
|
||||
self.results["changed"] = True
|
||||
|
||||
def join(self):
|
||||
if self.client.check_if_swarm_node():
|
||||
self.results['actions'].append("This node is already part of a swarm.")
|
||||
self.results["actions"].append("This node is already part of a swarm.")
|
||||
return
|
||||
if not self.check_mode:
|
||||
join_arguments = {
|
||||
'remote_addrs': self.parameters.remote_addrs,
|
||||
'join_token': self.parameters.join_token,
|
||||
'listen_addr': self.parameters.listen_addr,
|
||||
'advertise_addr': self.parameters.advertise_addr,
|
||||
"remote_addrs": self.parameters.remote_addrs,
|
||||
"join_token": self.parameters.join_token,
|
||||
"listen_addr": self.parameters.listen_addr,
|
||||
"advertise_addr": self.parameters.advertise_addr,
|
||||
}
|
||||
if self.parameters.data_path_addr is not None:
|
||||
join_arguments['data_path_addr'] = self.parameters.data_path_addr
|
||||
join_arguments["data_path_addr"] = self.parameters.data_path_addr
|
||||
try:
|
||||
self.client.join_swarm(**join_arguments)
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Can not join the Swarm Cluster: {exc}")
|
||||
self.results['actions'].append("New node is added to swarm cluster")
|
||||
self.differences.add('joined', parameter=True, active=False)
|
||||
self.results['changed'] = True
|
||||
self.results["actions"].append("New node is added to swarm cluster")
|
||||
self.differences.add("joined", parameter=True, active=False)
|
||||
self.results["changed"] = True
|
||||
|
||||
def leave(self):
|
||||
if not self.client.check_if_swarm_node():
|
||||
self.results['actions'].append("This node is not part of a swarm.")
|
||||
self.results["actions"].append("This node is not part of a swarm.")
|
||||
return
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.leave_swarm(force=self.force)
|
||||
except APIError as exc:
|
||||
self.client.fail(f"This node can not leave the Swarm Cluster: {exc}")
|
||||
self.results['actions'].append("Node has left the swarm cluster")
|
||||
self.differences.add('joined', parameter='absent', active='present')
|
||||
self.results['changed'] = True
|
||||
self.results["actions"].append("Node has left the swarm cluster")
|
||||
self.differences.add("joined", parameter="absent", active="present")
|
||||
self.results["changed"] = True
|
||||
|
||||
def remove(self):
|
||||
if not self.client.check_if_swarm_manager():
|
||||
self.client.fail("This node is not a manager.")
|
||||
|
||||
try:
|
||||
status_down = self.client.check_if_swarm_node_is_down(node_id=self.node_id, repeat_check=5)
|
||||
status_down = self.client.check_if_swarm_node_is_down(
|
||||
node_id=self.node_id, repeat_check=5
|
||||
)
|
||||
except APIError:
|
||||
return
|
||||
|
||||
if not status_down:
|
||||
self.client.fail("Can not remove the node. The status node is ready and not down.")
|
||||
self.client.fail(
|
||||
"Can not remove the node. The status node is ready and not down."
|
||||
)
|
||||
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.remove_node(node_id=self.node_id, force=self.force)
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Can not remove the node from the Swarm Cluster: {exc}")
|
||||
self.results['actions'].append("Node is removed from swarm cluster.")
|
||||
self.differences.add('joined', parameter=False, active=True)
|
||||
self.results['changed'] = True
|
||||
self.client.fail(
|
||||
f"Can not remove the node from the Swarm Cluster: {exc}"
|
||||
)
|
||||
self.results["actions"].append("Node is removed from swarm cluster.")
|
||||
self.differences.add("joined", parameter=False, active=True)
|
||||
self.results["changed"] = True
|
||||
|
||||
|
||||
def _detect_remove_operation(client):
|
||||
return client.module.params['state'] == 'remove'
|
||||
return client.module.params["state"] == "remove"
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
advertise_addr=dict(type='str'),
|
||||
data_path_addr=dict(type='str'),
|
||||
data_path_port=dict(type='int'),
|
||||
state=dict(type='str', default='present', choices=['present', 'join', 'absent', 'remove']),
|
||||
force=dict(type='bool', default=False),
|
||||
listen_addr=dict(type='str', default='0.0.0.0:2377'),
|
||||
remote_addrs=dict(type='list', elements='str'),
|
||||
join_token=dict(type='str', no_log=True),
|
||||
snapshot_interval=dict(type='int'),
|
||||
task_history_retention_limit=dict(type='int'),
|
||||
keep_old_snapshots=dict(type='int'),
|
||||
log_entries_for_slow_followers=dict(type='int'),
|
||||
heartbeat_tick=dict(type='int'),
|
||||
election_tick=dict(type='int'),
|
||||
dispatcher_heartbeat_period=dict(type='int'),
|
||||
node_cert_expiry=dict(type='int'),
|
||||
name=dict(type='str'),
|
||||
labels=dict(type='dict'),
|
||||
signing_ca_cert=dict(type='str'),
|
||||
signing_ca_key=dict(type='str', no_log=True),
|
||||
ca_force_rotate=dict(type='int'),
|
||||
autolock_managers=dict(type='bool'),
|
||||
node_id=dict(type='str'),
|
||||
rotate_worker_token=dict(type='bool', default=False),
|
||||
rotate_manager_token=dict(type='bool', default=False),
|
||||
default_addr_pool=dict(type='list', elements='str'),
|
||||
subnet_size=dict(type='int'),
|
||||
advertise_addr=dict(type="str"),
|
||||
data_path_addr=dict(type="str"),
|
||||
data_path_port=dict(type="int"),
|
||||
state=dict(
|
||||
type="str",
|
||||
default="present",
|
||||
choices=["present", "join", "absent", "remove"],
|
||||
),
|
||||
force=dict(type="bool", default=False),
|
||||
listen_addr=dict(type="str", default="0.0.0.0:2377"),
|
||||
remote_addrs=dict(type="list", elements="str"),
|
||||
join_token=dict(type="str", no_log=True),
|
||||
snapshot_interval=dict(type="int"),
|
||||
task_history_retention_limit=dict(type="int"),
|
||||
keep_old_snapshots=dict(type="int"),
|
||||
log_entries_for_slow_followers=dict(type="int"),
|
||||
heartbeat_tick=dict(type="int"),
|
||||
election_tick=dict(type="int"),
|
||||
dispatcher_heartbeat_period=dict(type="int"),
|
||||
node_cert_expiry=dict(type="int"),
|
||||
name=dict(type="str"),
|
||||
labels=dict(type="dict"),
|
||||
signing_ca_cert=dict(type="str"),
|
||||
signing_ca_key=dict(type="str", no_log=True),
|
||||
ca_force_rotate=dict(type="int"),
|
||||
autolock_managers=dict(type="bool"),
|
||||
node_id=dict(type="str"),
|
||||
rotate_worker_token=dict(type="bool", default=False),
|
||||
rotate_manager_token=dict(type="bool", default=False),
|
||||
default_addr_pool=dict(type="list", elements="str"),
|
||||
subnet_size=dict(type="int"),
|
||||
)
|
||||
|
||||
required_if = [
|
||||
('state', 'join', ['remote_addrs', 'join_token']),
|
||||
('state', 'remove', ['node_id'])
|
||||
("state", "join", ["remote_addrs", "join_token"]),
|
||||
("state", "remove", ["node_id"]),
|
||||
]
|
||||
|
||||
option_minimal_versions = dict(
|
||||
labels=dict(docker_py_version='2.6.0', docker_api_version='1.32'),
|
||||
signing_ca_cert=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
|
||||
signing_ca_key=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
|
||||
ca_force_rotate=dict(docker_py_version='2.6.0', docker_api_version='1.30'),
|
||||
autolock_managers=dict(docker_py_version='2.6.0'),
|
||||
log_driver=dict(docker_py_version='2.6.0'),
|
||||
labels=dict(docker_py_version="2.6.0", docker_api_version="1.32"),
|
||||
signing_ca_cert=dict(docker_py_version="2.6.0", docker_api_version="1.30"),
|
||||
signing_ca_key=dict(docker_py_version="2.6.0", docker_api_version="1.30"),
|
||||
ca_force_rotate=dict(docker_py_version="2.6.0", docker_api_version="1.30"),
|
||||
autolock_managers=dict(docker_py_version="2.6.0"),
|
||||
log_driver=dict(docker_py_version="2.6.0"),
|
||||
remove_operation=dict(
|
||||
docker_py_version='2.4.0',
|
||||
docker_py_version="2.4.0",
|
||||
detect_usage=_detect_remove_operation,
|
||||
usage_msg='remove swarm nodes'
|
||||
usage_msg="remove swarm nodes",
|
||||
),
|
||||
default_addr_pool=dict(docker_py_version='4.0.0', docker_api_version='1.39'),
|
||||
subnet_size=dict(docker_py_version='4.0.0', docker_api_version='1.39'),
|
||||
data_path_addr=dict(docker_py_version='4.0.0', docker_api_version='1.30'),
|
||||
data_path_port=dict(docker_py_version='6.0.0', docker_api_version='1.40'),
|
||||
default_addr_pool=dict(docker_py_version="4.0.0", docker_api_version="1.39"),
|
||||
subnet_size=dict(docker_py_version="4.0.0", docker_api_version="1.39"),
|
||||
data_path_addr=dict(docker_py_version="4.0.0", docker_api_version="1.30"),
|
||||
data_path_port=dict(docker_py_version="6.0.0", docker_api_version="1.40"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerSwarmClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
required_if=required_if,
|
||||
min_docker_version='1.10.0',
|
||||
min_docker_version="1.10.0",
|
||||
option_minimal_versions=option_minimal_versions,
|
||||
)
|
||||
sanitize_labels(client.module.params['labels'], 'labels', client)
|
||||
sanitize_labels(client.module.params["labels"], "labels", client)
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
changed=False,
|
||||
result='',
|
||||
actions=[]
|
||||
)
|
||||
results = dict(changed=False, result="", actions=[])
|
||||
|
||||
SwarmManager(client, results)()
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
DOCUMENTATION = r"""
|
||||
module: docker_swarm_info
|
||||
|
||||
@@ -186,14 +187,19 @@ tasks:
|
||||
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException, APIError
|
||||
from docker.errors import APIError, DockerException
|
||||
except ImportError:
|
||||
# missing Docker SDK for Python handled in ansible.module_utils.docker_common
|
||||
pass
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import RequestException
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import (
|
||||
AnsibleDockerSwarmClient,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
clean_dict_booleans_for_docker_api,
|
||||
@@ -208,22 +214,26 @@ class DockerSwarmManager(DockerBaseClass):
|
||||
|
||||
self.client = client
|
||||
self.results = results
|
||||
self.verbose_output = self.client.module.params['verbose_output']
|
||||
self.verbose_output = self.client.module.params["verbose_output"]
|
||||
|
||||
listed_objects = ['tasks', 'services', 'nodes']
|
||||
listed_objects = ["tasks", "services", "nodes"]
|
||||
|
||||
self.client.fail_task_if_not_swarm_manager()
|
||||
|
||||
self.results['swarm_facts'] = self.get_docker_swarm_facts()
|
||||
self.results["swarm_facts"] = self.get_docker_swarm_facts()
|
||||
|
||||
for docker_object in listed_objects:
|
||||
if self.client.module.params[docker_object]:
|
||||
returned_name = docker_object
|
||||
filter_name = docker_object + "_filters"
|
||||
filters = clean_dict_booleans_for_docker_api(client.module.params.get(filter_name))
|
||||
self.results[returned_name] = self.get_docker_items_list(docker_object, filters)
|
||||
if self.client.module.params['unlock_key']:
|
||||
self.results['swarm_unlock_key'] = self.get_docker_swarm_unlock_key()
|
||||
filters = clean_dict_booleans_for_docker_api(
|
||||
client.module.params.get(filter_name)
|
||||
)
|
||||
self.results[returned_name] = self.get_docker_items_list(
|
||||
docker_object, filters
|
||||
)
|
||||
if self.client.module.params["unlock_key"]:
|
||||
self.results["swarm_unlock_key"] = self.get_docker_swarm_unlock_key()
|
||||
|
||||
def get_docker_swarm_facts(self):
|
||||
try:
|
||||
@@ -236,14 +246,16 @@ class DockerSwarmManager(DockerBaseClass):
|
||||
items_list = []
|
||||
|
||||
try:
|
||||
if docker_object == 'nodes':
|
||||
if docker_object == "nodes":
|
||||
items = self.client.nodes(filters=filters)
|
||||
elif docker_object == 'tasks':
|
||||
elif docker_object == "tasks":
|
||||
items = self.client.tasks(filters=filters)
|
||||
elif docker_object == 'services':
|
||||
elif docker_object == "services":
|
||||
items = self.client.services(filters=filters)
|
||||
except APIError as exc:
|
||||
self.client.fail(f"Error inspecting docker swarm for object '{docker_object}': {exc}")
|
||||
self.client.fail(
|
||||
f"Error inspecting docker swarm for object '{docker_object}': {exc}"
|
||||
)
|
||||
|
||||
if self.verbose_output:
|
||||
return items
|
||||
@@ -251,14 +263,14 @@ class DockerSwarmManager(DockerBaseClass):
|
||||
for item in items:
|
||||
item_record = dict()
|
||||
|
||||
if docker_object == 'nodes':
|
||||
if docker_object == "nodes":
|
||||
item_record = self.get_essential_facts_nodes(item)
|
||||
elif docker_object == 'tasks':
|
||||
elif docker_object == "tasks":
|
||||
item_record = self.get_essential_facts_tasks(item)
|
||||
elif docker_object == 'services':
|
||||
elif docker_object == "services":
|
||||
item_record = self.get_essential_facts_services(item)
|
||||
if item_record.get('Mode') == 'Global':
|
||||
item_record['Replicas'] = len(items)
|
||||
if item_record.get("Mode") == "Global":
|
||||
item_record["Replicas"] = len(items)
|
||||
items_list.append(item_record)
|
||||
|
||||
return items_list
|
||||
@@ -267,35 +279,42 @@ class DockerSwarmManager(DockerBaseClass):
|
||||
def get_essential_facts_nodes(item):
|
||||
object_essentials = dict()
|
||||
|
||||
object_essentials['ID'] = item.get('ID')
|
||||
object_essentials['Hostname'] = item['Description']['Hostname']
|
||||
object_essentials['Status'] = item['Status']['State']
|
||||
object_essentials['Availability'] = item['Spec']['Availability']
|
||||
if 'ManagerStatus' in item:
|
||||
object_essentials['ManagerStatus'] = item['ManagerStatus']['Reachability']
|
||||
if 'Leader' in item['ManagerStatus'] and item['ManagerStatus']['Leader'] is True:
|
||||
object_essentials['ManagerStatus'] = "Leader"
|
||||
object_essentials["ID"] = item.get("ID")
|
||||
object_essentials["Hostname"] = item["Description"]["Hostname"]
|
||||
object_essentials["Status"] = item["Status"]["State"]
|
||||
object_essentials["Availability"] = item["Spec"]["Availability"]
|
||||
if "ManagerStatus" in item:
|
||||
object_essentials["ManagerStatus"] = item["ManagerStatus"]["Reachability"]
|
||||
if (
|
||||
"Leader" in item["ManagerStatus"]
|
||||
and item["ManagerStatus"]["Leader"] is True
|
||||
):
|
||||
object_essentials["ManagerStatus"] = "Leader"
|
||||
else:
|
||||
object_essentials['ManagerStatus'] = None
|
||||
object_essentials['EngineVersion'] = item['Description']['Engine']['EngineVersion']
|
||||
object_essentials["ManagerStatus"] = None
|
||||
object_essentials["EngineVersion"] = item["Description"]["Engine"][
|
||||
"EngineVersion"
|
||||
]
|
||||
|
||||
return object_essentials
|
||||
|
||||
def get_essential_facts_tasks(self, item):
|
||||
object_essentials = dict()
|
||||
|
||||
object_essentials['ID'] = item['ID']
|
||||
object_essentials["ID"] = item["ID"]
|
||||
# Returning container ID to not trigger another connection to host
|
||||
# Container ID is sufficient to get extended info in other tasks
|
||||
object_essentials['ContainerID'] = item['Status']['ContainerStatus']['ContainerID']
|
||||
object_essentials['Image'] = item['Spec']['ContainerSpec']['Image']
|
||||
object_essentials['Node'] = self.client.get_node_name_by_id(item['NodeID'])
|
||||
object_essentials['DesiredState'] = item['DesiredState']
|
||||
object_essentials['CurrentState'] = item['Status']['State']
|
||||
if 'Err' in item['Status']:
|
||||
object_essentials['Error'] = item['Status']['Err']
|
||||
object_essentials["ContainerID"] = item["Status"]["ContainerStatus"][
|
||||
"ContainerID"
|
||||
]
|
||||
object_essentials["Image"] = item["Spec"]["ContainerSpec"]["Image"]
|
||||
object_essentials["Node"] = self.client.get_node_name_by_id(item["NodeID"])
|
||||
object_essentials["DesiredState"] = item["DesiredState"]
|
||||
object_essentials["CurrentState"] = item["Status"]["State"]
|
||||
if "Err" in item["Status"]:
|
||||
object_essentials["Error"] = item["Status"]["Err"]
|
||||
else:
|
||||
object_essentials['Error'] = None
|
||||
object_essentials["Error"] = None
|
||||
|
||||
return object_essentials
|
||||
|
||||
@@ -303,47 +322,51 @@ class DockerSwarmManager(DockerBaseClass):
|
||||
def get_essential_facts_services(item):
|
||||
object_essentials = dict()
|
||||
|
||||
object_essentials['ID'] = item['ID']
|
||||
object_essentials['Name'] = item['Spec']['Name']
|
||||
if 'Replicated' in item['Spec']['Mode']:
|
||||
object_essentials['Mode'] = "Replicated"
|
||||
object_essentials['Replicas'] = item['Spec']['Mode']['Replicated']['Replicas']
|
||||
elif 'Global' in item['Spec']['Mode']:
|
||||
object_essentials['Mode'] = "Global"
|
||||
object_essentials["ID"] = item["ID"]
|
||||
object_essentials["Name"] = item["Spec"]["Name"]
|
||||
if "Replicated" in item["Spec"]["Mode"]:
|
||||
object_essentials["Mode"] = "Replicated"
|
||||
object_essentials["Replicas"] = item["Spec"]["Mode"]["Replicated"][
|
||||
"Replicas"
|
||||
]
|
||||
elif "Global" in item["Spec"]["Mode"]:
|
||||
object_essentials["Mode"] = "Global"
|
||||
# Number of replicas have to be updated in calling method or may be left as None
|
||||
object_essentials['Replicas'] = None
|
||||
object_essentials['Image'] = item['Spec']['TaskTemplate']['ContainerSpec']['Image']
|
||||
if item['Spec'].get('EndpointSpec') and 'Ports' in item['Spec']['EndpointSpec']:
|
||||
object_essentials['Ports'] = item['Spec']['EndpointSpec']['Ports']
|
||||
object_essentials["Replicas"] = None
|
||||
object_essentials["Image"] = item["Spec"]["TaskTemplate"]["ContainerSpec"][
|
||||
"Image"
|
||||
]
|
||||
if item["Spec"].get("EndpointSpec") and "Ports" in item["Spec"]["EndpointSpec"]:
|
||||
object_essentials["Ports"] = item["Spec"]["EndpointSpec"]["Ports"]
|
||||
else:
|
||||
object_essentials['Ports'] = []
|
||||
object_essentials["Ports"] = []
|
||||
|
||||
return object_essentials
|
||||
|
||||
def get_docker_swarm_unlock_key(self):
|
||||
unlock_key = self.client.get_unlock_key() or {}
|
||||
return unlock_key.get('UnlockKey') or None
|
||||
return unlock_key.get("UnlockKey") or None
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
nodes=dict(type='bool', default=False),
|
||||
nodes_filters=dict(type='dict'),
|
||||
tasks=dict(type='bool', default=False),
|
||||
tasks_filters=dict(type='dict'),
|
||||
services=dict(type='bool', default=False),
|
||||
services_filters=dict(type='dict'),
|
||||
unlock_key=dict(type='bool', default=False),
|
||||
verbose_output=dict(type='bool', default=False),
|
||||
nodes=dict(type="bool", default=False),
|
||||
nodes_filters=dict(type="dict"),
|
||||
tasks=dict(type="bool", default=False),
|
||||
tasks_filters=dict(type="dict"),
|
||||
services=dict(type="bool", default=False),
|
||||
services_filters=dict(type="dict"),
|
||||
unlock_key=dict(type="bool", default=False),
|
||||
verbose_output=dict(type="bool", default=False),
|
||||
)
|
||||
option_minimal_versions = dict(
|
||||
unlock_key=dict(docker_py_version='2.7.0'),
|
||||
unlock_key=dict(docker_py_version="2.7.0"),
|
||||
)
|
||||
|
||||
client = AnsibleDockerSwarmClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
min_docker_version='1.10.0',
|
||||
min_docker_version="1.10.0",
|
||||
option_minimal_versions=option_minimal_versions,
|
||||
fail_results=dict(
|
||||
can_talk_to_docker=False,
|
||||
@@ -351,9 +374,9 @@ def main():
|
||||
docker_swarm_manager=False,
|
||||
),
|
||||
)
|
||||
client.fail_results['can_talk_to_docker'] = True
|
||||
client.fail_results['docker_swarm_active'] = client.check_if_swarm_node()
|
||||
client.fail_results['docker_swarm_manager'] = client.check_if_swarm_manager()
|
||||
client.fail_results["can_talk_to_docker"] = True
|
||||
client.fail_results["docker_swarm_active"] = client.check_if_swarm_node()
|
||||
client.fail_results["docker_swarm_manager"] = client.check_if_swarm_manager()
|
||||
|
||||
try:
|
||||
results = dict(
|
||||
@@ -364,12 +387,16 @@ def main():
|
||||
results.update(client.fail_results)
|
||||
client.module.exit_json(**results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+1032
-873
File diff suppressed because it is too large
Load Diff
@@ -64,6 +64,7 @@ service:
|
||||
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
from docker.errors import DockerException
|
||||
except ImportError:
|
||||
@@ -73,27 +74,25 @@ except ImportError:
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import AnsibleDockerSwarmClient
|
||||
from ansible_collections.community.docker.plugins.module_utils.swarm import (
|
||||
AnsibleDockerSwarmClient,
|
||||
)
|
||||
|
||||
|
||||
def get_service_info(client):
|
||||
service = client.module.params['name']
|
||||
return client.get_service_inspect(
|
||||
service_id=service,
|
||||
skip_missing=True
|
||||
)
|
||||
service = client.module.params["name"]
|
||||
return client.get_service_inspect(service_id=service, skip_missing=True)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True),
|
||||
name=dict(type="str", required=True),
|
||||
)
|
||||
|
||||
client = AnsibleDockerSwarmClient(
|
||||
argument_spec=argument_spec,
|
||||
supports_check_mode=True,
|
||||
min_docker_version='2.0.0',
|
||||
min_docker_version="2.0.0",
|
||||
)
|
||||
|
||||
client.fail_task_if_not_swarm_manager()
|
||||
@@ -101,18 +100,18 @@ def main():
|
||||
try:
|
||||
service = get_service_info(client)
|
||||
|
||||
client.module.exit_json(
|
||||
changed=False,
|
||||
service=service,
|
||||
exists=bool(service)
|
||||
)
|
||||
client.module.exit_json(changed=False, service=service, exists=bool(service))
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when Docker SDK for Python tried to talk to the docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -120,20 +120,19 @@ volume:
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DockerBaseClass,
|
||||
DifferenceTracker,
|
||||
DockerBaseClass,
|
||||
sanitize_labels,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
APIError,
|
||||
DockerException,
|
||||
)
|
||||
|
||||
|
||||
class TaskParameters(DockerBaseClass):
|
||||
@@ -158,10 +157,7 @@ class DockerVolumeManager(object):
|
||||
self.client = client
|
||||
self.parameters = TaskParameters(client)
|
||||
self.check_mode = self.client.check_mode
|
||||
self.results = {
|
||||
'changed': False,
|
||||
'actions': []
|
||||
}
|
||||
self.results = {"changed": False, "actions": []}
|
||||
self.diff = self.client.module._diff
|
||||
self.diff_tracker = DifferenceTracker()
|
||||
self.diff_result = dict()
|
||||
@@ -169,27 +165,29 @@ class DockerVolumeManager(object):
|
||||
self.existing_volume = self.get_existing_volume()
|
||||
|
||||
state = self.parameters.state
|
||||
if state == 'present':
|
||||
if state == "present":
|
||||
self.present()
|
||||
elif state == 'absent':
|
||||
elif state == "absent":
|
||||
self.absent()
|
||||
|
||||
if self.diff or self.check_mode or self.parameters.debug:
|
||||
if self.diff:
|
||||
self.diff_result['before'], self.diff_result['after'] = self.diff_tracker.get_before_after()
|
||||
self.results['diff'] = self.diff_result
|
||||
self.diff_result["before"], self.diff_result["after"] = (
|
||||
self.diff_tracker.get_before_after()
|
||||
)
|
||||
self.results["diff"] = self.diff_result
|
||||
|
||||
def get_existing_volume(self):
|
||||
try:
|
||||
volumes = self.client.get_json('/volumes')
|
||||
volumes = self.client.get_json("/volumes")
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
|
||||
if volumes['Volumes'] is None:
|
||||
if volumes["Volumes"] is None:
|
||||
return None
|
||||
|
||||
for volume in volumes['Volumes']:
|
||||
if volume['Name'] == self.parameters.volume_name:
|
||||
for volume in volumes["Volumes"]:
|
||||
if volume["Name"] == self.parameters.volume_name:
|
||||
return volume
|
||||
|
||||
return None
|
||||
@@ -201,27 +199,42 @@ class DockerVolumeManager(object):
|
||||
:return: list of options that differ
|
||||
"""
|
||||
differences = DifferenceTracker()
|
||||
if self.parameters.driver and self.parameters.driver != self.existing_volume['Driver']:
|
||||
differences.add('driver', parameter=self.parameters.driver, active=self.existing_volume['Driver'])
|
||||
if (
|
||||
self.parameters.driver
|
||||
and self.parameters.driver != self.existing_volume["Driver"]
|
||||
):
|
||||
differences.add(
|
||||
"driver",
|
||||
parameter=self.parameters.driver,
|
||||
active=self.existing_volume["Driver"],
|
||||
)
|
||||
if self.parameters.driver_options:
|
||||
if not self.existing_volume.get('Options'):
|
||||
differences.add('driver_options',
|
||||
parameter=self.parameters.driver_options,
|
||||
active=self.existing_volume.get('Options'))
|
||||
if not self.existing_volume.get("Options"):
|
||||
differences.add(
|
||||
"driver_options",
|
||||
parameter=self.parameters.driver_options,
|
||||
active=self.existing_volume.get("Options"),
|
||||
)
|
||||
else:
|
||||
for key, value in self.parameters.driver_options.items():
|
||||
if (not self.existing_volume['Options'].get(key) or
|
||||
value != self.existing_volume['Options'][key]):
|
||||
differences.add(f'driver_options.{key}',
|
||||
parameter=value,
|
||||
active=self.existing_volume['Options'].get(key))
|
||||
if (
|
||||
not self.existing_volume["Options"].get(key)
|
||||
or value != self.existing_volume["Options"][key]
|
||||
):
|
||||
differences.add(
|
||||
f"driver_options.{key}",
|
||||
parameter=value,
|
||||
active=self.existing_volume["Options"].get(key),
|
||||
)
|
||||
if self.parameters.labels:
|
||||
existing_labels = self.existing_volume.get('Labels') or {}
|
||||
existing_labels = self.existing_volume.get("Labels") or {}
|
||||
for label in self.parameters.labels:
|
||||
if existing_labels.get(label) != self.parameters.labels.get(label):
|
||||
differences.add(f'labels.{label}',
|
||||
parameter=self.parameters.labels.get(label),
|
||||
active=existing_labels.get(label))
|
||||
differences.add(
|
||||
f"labels.{label}",
|
||||
parameter=self.parameters.labels.get(label),
|
||||
active=existing_labels.get(label),
|
||||
)
|
||||
|
||||
return differences
|
||||
|
||||
@@ -230,67 +243,81 @@ class DockerVolumeManager(object):
|
||||
if not self.check_mode:
|
||||
try:
|
||||
data = {
|
||||
'Name': self.parameters.volume_name,
|
||||
'Driver': self.parameters.driver,
|
||||
'DriverOpts': self.parameters.driver_options,
|
||||
"Name": self.parameters.volume_name,
|
||||
"Driver": self.parameters.driver,
|
||||
"DriverOpts": self.parameters.driver_options,
|
||||
}
|
||||
if self.parameters.labels is not None:
|
||||
data['Labels'] = self.parameters.labels
|
||||
resp = self.client.post_json_to_json('/volumes/create', data=data)
|
||||
self.existing_volume = self.client.get_json('/volumes/{0}', resp['Name'])
|
||||
data["Labels"] = self.parameters.labels
|
||||
resp = self.client.post_json_to_json("/volumes/create", data=data)
|
||||
self.existing_volume = self.client.get_json(
|
||||
"/volumes/{0}", resp["Name"]
|
||||
)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
|
||||
self.results['actions'].append(f"Created volume {self.parameters.volume_name} with driver {self.parameters.driver}")
|
||||
self.results['changed'] = True
|
||||
self.results["actions"].append(
|
||||
f"Created volume {self.parameters.volume_name} with driver {self.parameters.driver}"
|
||||
)
|
||||
self.results["changed"] = True
|
||||
|
||||
def remove_volume(self):
|
||||
if self.existing_volume:
|
||||
if not self.check_mode:
|
||||
try:
|
||||
self.client.delete_call('/volumes/{0}', self.parameters.volume_name)
|
||||
self.client.delete_call("/volumes/{0}", self.parameters.volume_name)
|
||||
except APIError as e:
|
||||
self.client.fail(to_native(e))
|
||||
|
||||
self.results['actions'].append(f"Removed volume {self.parameters.volume_name}")
|
||||
self.results['changed'] = True
|
||||
self.results["actions"].append(
|
||||
f"Removed volume {self.parameters.volume_name}"
|
||||
)
|
||||
self.results["changed"] = True
|
||||
|
||||
def present(self):
|
||||
differences = DifferenceTracker()
|
||||
if self.existing_volume:
|
||||
differences = self.has_different_config()
|
||||
|
||||
self.diff_tracker.add('exists', parameter=True, active=self.existing_volume is not None)
|
||||
if (not differences.empty and self.parameters.recreate == 'options-changed') or self.parameters.recreate == 'always':
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=True, active=self.existing_volume is not None
|
||||
)
|
||||
if (
|
||||
not differences.empty and self.parameters.recreate == "options-changed"
|
||||
) or self.parameters.recreate == "always":
|
||||
self.remove_volume()
|
||||
self.existing_volume = None
|
||||
|
||||
self.create_volume()
|
||||
|
||||
if self.diff or self.check_mode or self.parameters.debug:
|
||||
self.diff_result['differences'] = differences.get_legacy_docker_diffs()
|
||||
self.diff_result["differences"] = differences.get_legacy_docker_diffs()
|
||||
self.diff_tracker.merge(differences)
|
||||
|
||||
if not self.check_mode and not self.parameters.debug:
|
||||
self.results.pop('actions')
|
||||
self.results.pop("actions")
|
||||
|
||||
volume_facts = self.get_existing_volume()
|
||||
self.results['volume'] = volume_facts
|
||||
self.results["volume"] = volume_facts
|
||||
|
||||
def absent(self):
|
||||
self.diff_tracker.add('exists', parameter=False, active=self.existing_volume is not None)
|
||||
self.diff_tracker.add(
|
||||
"exists", parameter=False, active=self.existing_volume is not None
|
||||
)
|
||||
self.remove_volume()
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
volume_name=dict(type='str', required=True, aliases=['name']),
|
||||
state=dict(type='str', default='present', choices=['present', 'absent']),
|
||||
driver=dict(type='str', default='local'),
|
||||
driver_options=dict(type='dict', default={}),
|
||||
labels=dict(type='dict'),
|
||||
recreate=dict(type='str', default='never', choices=['always', 'never', 'options-changed']),
|
||||
debug=dict(type='bool', default=False)
|
||||
volume_name=dict(type="str", required=True, aliases=["name"]),
|
||||
state=dict(type="str", default="present", choices=["present", "absent"]),
|
||||
driver=dict(type="str", default="local"),
|
||||
driver_options=dict(type="dict", default={}),
|
||||
labels=dict(type="dict"),
|
||||
recreate=dict(
|
||||
type="str", default="never", choices=["always", "never", "options-changed"]
|
||||
),
|
||||
debug=dict(type="bool", default=False),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -298,18 +325,22 @@ def main():
|
||||
supports_check_mode=True,
|
||||
# "The docker server >= 1.9.0"
|
||||
)
|
||||
sanitize_labels(client.module.params['labels'], 'labels', client)
|
||||
sanitize_labels(client.module.params["labels"], "labels", client)
|
||||
|
||||
try:
|
||||
cm = DockerVolumeManager(client)
|
||||
client.module.exit_json(**cm.results)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -72,16 +72,19 @@ volume:
|
||||
|
||||
import traceback
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
|
||||
DockerException,
|
||||
NotFound,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import DockerException, NotFound
|
||||
|
||||
|
||||
def get_existing_volume(client, volume_name):
|
||||
try:
|
||||
return client.get_json('/volumes/{0}', volume_name)
|
||||
return client.get_json("/volumes/{0}", volume_name)
|
||||
except NotFound as dummy:
|
||||
return None
|
||||
except Exception as exc:
|
||||
@@ -90,7 +93,7 @@ def get_existing_volume(client, volume_name):
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
name=dict(type='str', required=True, aliases=['volume_name']),
|
||||
name=dict(type="str", required=True, aliases=["volume_name"]),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
@@ -99,7 +102,7 @@ def main():
|
||||
)
|
||||
|
||||
try:
|
||||
volume = get_existing_volume(client, client.module.params['name'])
|
||||
volume = get_existing_volume(client, client.module.params["name"])
|
||||
|
||||
client.module.exit_json(
|
||||
changed=False,
|
||||
@@ -107,12 +110,16 @@ def main():
|
||||
volume=volume,
|
||||
)
|
||||
except DockerException as e:
|
||||
client.fail(f'An unexpected Docker error occurred: {e}', exception=traceback.format_exc())
|
||||
client.fail(
|
||||
f"An unexpected Docker error occurred: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
except RequestException as e:
|
||||
client.fail(
|
||||
f'An unexpected requests error occurred when trying to talk to the Docker daemon: {e}',
|
||||
exception=traceback.format_exc())
|
||||
f"An unexpected requests error occurred when trying to talk to the Docker daemon: {e}",
|
||||
exception=traceback.format_exc(),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common import (
|
||||
AnsibleDockerClientBase,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DOCKER_COMMON_ARGS,
|
||||
)
|
||||
@@ -24,18 +24,22 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
self.display = Display()
|
||||
super(AnsibleDockerClient, self).__init__(
|
||||
min_docker_version=min_docker_version,
|
||||
min_docker_api_version=min_docker_api_version)
|
||||
min_docker_api_version=min_docker_api_version,
|
||||
)
|
||||
|
||||
def fail(self, msg, **kwargs):
|
||||
if kwargs:
|
||||
msg += '\nContext:\n' + '\n'.join(f' {k} = {v!r}' for (k, v) in kwargs.items())
|
||||
msg += "\nContext:\n" + "\n".join(
|
||||
f" {k} = {v!r}" for (k, v) in kwargs.items()
|
||||
)
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
|
||||
def deprecate(self, msg, version=None, date=None, collection_name=None):
|
||||
self.display.deprecated(msg, version=version, date=date, collection_name=collection_name)
|
||||
self.display.deprecated(
|
||||
msg, version=version, date=date, collection_name=collection_name
|
||||
)
|
||||
|
||||
def _get_params(self):
|
||||
return dict([
|
||||
(option, self.plugin.get_option(option))
|
||||
for option in DOCKER_COMMON_ARGS
|
||||
])
|
||||
return dict(
|
||||
[(option, self.plugin.get_option(option)) for option in DOCKER_COMMON_ARGS]
|
||||
)
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.utils.display import Display
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClientBase,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.util import (
|
||||
DOCKER_COMMON_ARGS,
|
||||
)
|
||||
@@ -22,18 +19,22 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
|
||||
self.plugin = plugin
|
||||
self.display = Display()
|
||||
super(AnsibleDockerClient, self).__init__(
|
||||
min_docker_api_version=min_docker_api_version)
|
||||
min_docker_api_version=min_docker_api_version
|
||||
)
|
||||
|
||||
def fail(self, msg, **kwargs):
|
||||
if kwargs:
|
||||
msg += '\nContext:\n' + '\n'.join(f' {k} = {v!r}' for (k, v) in kwargs.items())
|
||||
msg += "\nContext:\n" + "\n".join(
|
||||
f" {k} = {v!r}" for (k, v) in kwargs.items()
|
||||
)
|
||||
raise AnsibleConnectionFailure(msg)
|
||||
|
||||
def deprecate(self, msg, version=None, date=None, collection_name=None):
|
||||
self.display.deprecated(msg, version=version, date=date, collection_name=collection_name)
|
||||
self.display.deprecated(
|
||||
msg, version=version, date=date, collection_name=collection_name
|
||||
)
|
||||
|
||||
def _get_params(self):
|
||||
return dict([
|
||||
(option, self.plugin.get_option(option))
|
||||
for option in DOCKER_COMMON_ARGS
|
||||
])
|
||||
return dict(
|
||||
[(option, self.plugin.get_option(option)) for option in DOCKER_COMMON_ARGS]
|
||||
)
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
@@ -15,4 +17,6 @@ from ansible_collections.community.docker.plugins.module_utils.socket_handler im
|
||||
|
||||
class DockerSocketHandler(DockerSocketHandlerBase):
|
||||
def __init__(self, display, sock, log=None, container=None):
|
||||
super(DockerSocketHandler, self).__init__(sock, selectors, log=lambda msg: display.vvvv(msg, host=container))
|
||||
super(DockerSocketHandler, self).__init__(
|
||||
sock, selectors, log=lambda msg: display.vvvv(msg, host=container)
|
||||
)
|
||||
|
||||
@@ -5,16 +5,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from collections.abc import Mapping, Set
|
||||
|
||||
from ansible.module_utils.common.collections import is_sequence
|
||||
from ansible.utils.unsafe_proxy import (
|
||||
AnsibleUnsafe,
|
||||
wrap_var as _make_unsafe,
|
||||
)
|
||||
from ansible.utils.unsafe_proxy import wrap_var as _make_unsafe
|
||||
|
||||
_RE_TEMPLATE_CHARS = re.compile('[{}]')
|
||||
_RE_TEMPLATE_CHARS_BYTES = re.compile(b'[{}]')
|
||||
|
||||
_RE_TEMPLATE_CHARS = re.compile("[{}]")
|
||||
_RE_TEMPLATE_CHARS_BYTES = re.compile(b"[{}]")
|
||||
|
||||
|
||||
def make_unsafe(value):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user