Reformat code with black and isort.

This commit is contained in:
Felix Fontein
2025-10-06 18:34:59 +02:00
parent f45232635c
commit d65d37e9e9
132 changed files with 17581 additions and 14729 deletions
+27 -25
View File
@@ -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()
+130 -96
View File
@@ -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()
+56 -55
View File
@@ -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()
+38 -27
View File
@@ -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()
+96 -95
View File
@@ -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()
+81 -72
View File
@@ -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()
+1 -2
View File
@@ -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()
+427 -190
View File
@@ -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()
+90 -70
View File
@@ -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()
+13 -7
View File
@@ -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()
+62 -47
View File
@@ -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()
+81 -58
View File
@@ -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()
File diff suppressed because it is too large Load Diff
+186 -152
View File
@@ -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()
+58 -45
View File
@@ -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()
+28 -22
View File
@@ -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()
+48 -30
View File
@@ -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()
+46 -41
View File
@@ -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()
+39 -32
View File
@@ -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()
+71 -50
View File
@@ -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()
+50 -35
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+13 -7
View File
@@ -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()
+55 -43
View File
@@ -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()
+21 -12
View File
@@ -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()
+114 -57
View File
@@ -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()
+90 -69
View File
@@ -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()
+75 -68
View File
@@ -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()
+50 -33
View File
@@ -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__":
+13 -8
View File
@@ -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__":
+14 -10
View File
@@ -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
View File
@@ -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()
+95 -68
View File
@@ -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()
File diff suppressed because it is too large Load Diff
+17 -18
View File
@@ -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()
+93 -62
View File
@@ -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()
+15 -8
View File
@@ -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()