From 423a9bbf6134933d793e49da38093162209f3b58 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 27 Sep 2024 13:00:48 +0300 Subject: [PATCH] Add Docker Compose v2 exec and run modules. (#969) --- meta/runtime.yml | 2 + plugins/module_utils/compose_v2.py | 4 +- plugins/modules/docker_compose_v2_exec.py | 299 ++++++++++++ plugins/modules/docker_compose_v2_run.py | 430 ++++++++++++++++++ .../targets/docker_compose_v2_exec/aliases | 6 + .../docker_compose_v2_exec/meta/main.yml | 10 + .../docker_compose_v2_exec/tasks/main.yml | 49 ++ .../docker_compose_v2_exec/tasks/run-test.yml | 7 + .../tasks/tests/basic.yml | 97 ++++ .../targets/docker_compose_v2_run/aliases | 6 + .../docker_compose_v2_run/meta/main.yml | 10 + .../docker_compose_v2_run/tasks/main.yml | 49 ++ .../docker_compose_v2_run/tasks/run-test.yml | 7 + .../tasks/tests/basic.yml | 104 +++++ 14 files changed, 1078 insertions(+), 2 deletions(-) create mode 100644 plugins/modules/docker_compose_v2_exec.py create mode 100644 plugins/modules/docker_compose_v2_run.py create mode 100644 tests/integration/targets/docker_compose_v2_exec/aliases create mode 100644 tests/integration/targets/docker_compose_v2_exec/meta/main.yml create mode 100644 tests/integration/targets/docker_compose_v2_exec/tasks/main.yml create mode 100644 tests/integration/targets/docker_compose_v2_exec/tasks/run-test.yml create mode 100644 tests/integration/targets/docker_compose_v2_exec/tasks/tests/basic.yml create mode 100644 tests/integration/targets/docker_compose_v2_run/aliases create mode 100644 tests/integration/targets/docker_compose_v2_run/meta/main.yml create mode 100644 tests/integration/targets/docker_compose_v2_run/tasks/main.yml create mode 100644 tests/integration/targets/docker_compose_v2_run/tasks/run-test.yml create mode 100644 tests/integration/targets/docker_compose_v2_run/tasks/tests/basic.yml diff --git a/meta/runtime.yml b/meta/runtime.yml index e29f84be1..150d93f19 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -8,7 +8,9 @@ action_groups: docker: - docker_compose - docker_compose_v2 + - docker_compose_v2_exec - docker_compose_v2_pull + - docker_compose_v2_run - docker_config - docker_container - docker_container_copy_into diff --git a/plugins/module_utils/compose_v2.py b/plugins/module_utils/compose_v2.py index a6ee094a5..b96f921ee 100644 --- a/plugins/module_utils/compose_v2.py +++ b/plugins/module_utils/compose_v2.py @@ -718,9 +718,9 @@ def fail(self, msg, **kwargs): self.cleanup() self.client.fail(msg, **kwargs) - def get_base_args(self): + def get_base_args(self, plain_progress=False): args = ['compose', '--ansi', 'never'] - if self.use_json_events: + if self.use_json_events and not plain_progress: args.extend(['--progress', 'json']) elif self.compose_version >= LooseVersion('2.19.0'): # https://github.com/docker/compose/pull/10690 diff --git a/plugins/modules/docker_compose_v2_exec.py b/plugins/modules/docker_compose_v2_exec.py new file mode 100644 index 000000000..5590f77e1 --- /dev/null +++ b/plugins/modules/docker_compose_v2_exec.py @@ -0,0 +1,299 @@ +#!/usr/bin/python +# +# Copyright (c) 2023, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' + +module: docker_compose_v2_exec + +short_description: Run command in a container of a Compose service + +version_added: 3.13.0 + +description: + - Uses Docker Compose to run a command in a service's container. + - This can be used to run one-off commands in an existing service's container, + and encapsulates C(docker compose exec). + +extends_documentation_fragment: + - community.docker.compose_v2 + - community.docker.compose_v2.minimum_version + - community.docker.docker.cli_documentation + - community.docker.attributes + - community.docker.attributes.actiongroup_docker + +attributes: + check_mode: + support: none + diff_mode: + support: none + +options: + service: + description: + - The service to run the command in. + type: str + required: true + index: + description: + - The index of the container to run the command in if the service has multiple replicas. + type: int + argv: + type: list + elements: str + description: + - The command to execute. + - Since this is a list of arguments, no quoting is needed. + - Exactly one of O(argv) or O(command) must be specified. + command: + type: str + description: + - The command to execute. + - Exactly one of O(argv) or O(command) must be specified. + chdir: + type: str + description: + - The directory to run the command in. + detach: + description: + - Whether to run the command synchronously (O(detach=false), default) or asynchronously (O(detach=true)). + - If set to V(true), O(stdin) cannot be provided, and the return values RV(stdout), RV(stderr), and + RV(rc) are not returned. + type: bool + default: false + user: + type: str + description: + - If specified, the user to execute this command with. + stdin: + type: str + description: + - Set the stdin of the command directly to the specified value. + - Can only be used if O(detach=false). + stdin_add_newline: + type: bool + default: true + description: + - If set to V(true), appends a newline to O(stdin). + strip_empty_ends: + type: bool + default: true + description: + - Strip empty lines from the end of stdout/stderr in result. + privileged: + type: bool + default: false + description: + - Whether to give extended privileges to the process. + tty: + type: bool + default: true + description: + - Whether to allocate a TTY. + env: + description: + - Dictionary of environment variables with their respective values to be passed to the command ran inside the container. + - Values which might be parsed as numbers, booleans or other types by the YAML parser must be quoted (for example V("true")) in order to avoid data loss. + - Please note that if you are passing values in with Jinja2 templates, like V("{{ value }}"), you need to add V(| string) to prevent Ansible to + convert strings such as V("true") back to booleans. The correct way is to use V("{{ value | string }}"). + type: dict + +author: + - Felix Fontein (@felixfontein) + +seealso: + - module: community.docker.docker_compose_v2 + +notes: + - If you need to evaluate environment variables of the container in O(command) or O(argv), you need to pass the command through a shell, + like O(command=/bin/sh -c "echo $ENV_VARIABLE"). +''' + +EXAMPLES = ''' +- name: Run a simple command (command) + community.docker.docker_compose_v2_exec: + service: foo + command: /bin/bash -c "ls -lah" + chdir: /root + register: result + +- name: Print stdout + ansible.builtin.debug: + var: result.stdout + +- name: Run a simple command (argv) + community.docker.docker_compose_v2_exec: + service: foo + argv: + - /bin/bash + - "-c" + - "ls -lah > /dev/stderr" + chdir: /root + register: result + +- name: Print stderr lines + ansible.builtin.debug: + var: result.stderr_lines +''' + +RETURN = ''' +stdout: + type: str + returned: success and O(detach=false) + description: + - The standard output of the container command. +stderr: + type: str + returned: success and O(detach=false) + description: + - The standard error output of the container command. +rc: + type: int + returned: success and O(detach=false) + sample: 0 + description: + - The exit code of the command. +''' + +import shlex +import traceback + +from ansible.module_utils.common.text.converters import to_text, to_native +from ansible.module_utils.six import string_types + +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, +) + + +class ExecManager(BaseComposeManager): + def __init__(self, client): + 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.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.') + + 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()): + if not isinstance(value, string_types): + self.fail( + "Non-string value found for env option. Ambiguous env options must be " + "wrapped in quotes to avoid them being interpreted. Key: %s" % (name, ) + ) + 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'] + if self.index is not None: + args.extend(['--index', str(self.index)]) + if self.chdir is not None: + args.extend(['--workdir', self.chdir]) + if self.detach: + args.extend(['--detach']) + if self.user is not None: + args.extend(['--user', self.user]) + if self.privileged: + args.append('--privileged') + if not self.tty: + args.append('--no-TTY') + if self.env: + for name, value in list(self.env.items()): + args.append('{0}={1}'.format(name, value)) + args.append('--') + args.append(self.service) + args.extend(self.argv) + return args + + def run(self): + args = self.get_exec_cmd(self.check_mode) + kwargs = { + 'cwd': self.project_src, + } + if self.stdin is not None: + kwargs['data'] = self.stdin.encode('utf-8') + if self.detach: + 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') + return { + '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'), + ) + argspec_ex = common_compose_argspec_ex() + argument_spec.update(argspec_ex.pop('argspec')) + + client = AnsibleModuleDockerClient( + argument_spec=argument_spec, + supports_check_mode=False, + needs_api_version=False, + **argspec_ex + ) + + try: + manager = ExecManager(client) + result = manager.run() + manager.cleanup() + client.module.exit_json(**result) + except DockerException as e: + client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/docker_compose_v2_run.py b/plugins/modules/docker_compose_v2_run.py new file mode 100644 index 000000000..9dbaf789c --- /dev/null +++ b/plugins/modules/docker_compose_v2_run.py @@ -0,0 +1,430 @@ +#!/usr/bin/python +# +# Copyright (c) 2023, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' + +module: docker_compose_v2_run + +short_description: Run command in a new container of a Compose service + +version_added: 3.13.0 + +description: + - Uses Docker Compose to run a command in a new container for a service. + - This encapsulates C(docker compose run). + +extends_documentation_fragment: + - community.docker.compose_v2 + - community.docker.compose_v2.minimum_version + - community.docker.docker.cli_documentation + - community.docker.attributes + - community.docker.attributes.actiongroup_docker + +attributes: + check_mode: + support: none + diff_mode: + support: none + +options: + service: + description: + - The service to run the command in. + type: str + required: true + argv: + type: list + elements: str + description: + - The command to execute. + - Since this is a list of arguments, no quoting is needed. + - O(argv) or O(command) are mutually exclusive. + command: + type: str + description: + - The command to execute. + - O(argv) or O(command) are mutually exclusive. + build: + description: + - Build image before starting container. + - Note that building can insert information into RV(stdout) or RV(stderr). + type: bool + default: false + cap_add: + description: + - Linux capabilities to add to the container. + type: list + elements: str + cap_drop: + description: + - Linux capabilities to drop from the container. + type: list + elements: str + entrypoint: + description: + - Override the entrypoint of the container image. + type: str + interactive: + description: + - Whether to keep STDIN open even if not attached. + type: bool + default: true + labels: + description: + - Add or override labels to the container. + type: list + elements: str + name: + description: + - Assign a name to the container. + type: str + no_deps: + description: + - Do not start linked services. + type: bool + default: false + publish: + description: + - Publish a container's port(s) to the host. + type: list + elements: str + quiet_pull: + description: + - Pull without printing progress information. + - Note that pulling can insert information into RV(stdout) or RV(stderr). + type: bool + default: false + remove_orphans: + description: + - Remove containers for services not defined in the Compose file. + type: bool + default: false + cleanup: + description: + - Automatically remove th econtainer when it exits. + - Corresponds to the C(--rm) option of C(docker compose run). + type: bool + default: false + service_ports: + description: + - Run command with all service's ports enabled and mapped to the host. + type: bool + default: false + use_aliases: + description: + - Use the service's network C(useAliases) in the network(s) the container connects to. + type: bool + default: false + volumes: + description: + - Bind mount one or more volumes. + type: list + elements: str + chdir: + type: str + description: + - The directory to run the command in. + detach: + description: + - Whether to run the command synchronously (O(detach=false), default) or asynchronously (O(detach=true)). + - If set to V(true), O(stdin) cannot be provided, and the return values RV(stdout), RV(stderr), and + RV(rc) are not returned. Instead, the return value RV(container_id) is provided. + type: bool + default: false + user: + type: str + description: + - If specified, the user to execute this command with. + stdin: + type: str + description: + - Set the stdin of the command directly to the specified value. + - Can only be used if O(detach=false). + stdin_add_newline: + type: bool + default: true + description: + - If set to V(true), appends a newline to O(stdin). + strip_empty_ends: + type: bool + default: true + description: + - Strip empty lines from the end of stdout/stderr in result. + tty: + type: bool + default: true + description: + - Whether to allocate a TTY. + env: + description: + - Dictionary of environment variables with their respective values to be passed to the command ran inside the container. + - Values which might be parsed as numbers, booleans or other types by the YAML parser must be quoted (for example V("true")) in order to avoid data loss. + - Please note that if you are passing values in with Jinja2 templates, like V("{{ value }}"), you need to add V(| string) to prevent Ansible to + convert strings such as V("true") back to booleans. The correct way is to use V("{{ value | string }}"). + type: dict + +author: + - Felix Fontein (@felixfontein) + +seealso: + - module: community.docker.docker_compose_v2 + +notes: + - If you need to evaluate environment variables of the container in O(command) or O(argv), you need to pass the command through a shell, + like O(command=/bin/sh -c "echo $ENV_VARIABLE"). +''' + +EXAMPLES = ''' +- name: Run a simple command (command) + community.docker.docker_compose_v2_run: + service: foo + command: /bin/bash -c "ls -lah" + chdir: /root + register: result + +- name: Print stdout + ansible.builtin.debug: + var: result.stdout + +- name: Run a simple command (argv) + community.docker.docker_compose_v2_run: + service: foo + argv: + - /bin/bash + - "-c" + - "ls -lah > /dev/stderr" + chdir: /root + register: result + +- name: Print stderr lines + ansible.builtin.debug: + var: result.stderr_lines +''' + +RETURN = ''' +container_id: + type: str + returned: success and O(detach=true) + description: + - The ID of the created container. +stdout: + type: str + returned: success and O(detach=false) + description: + - The standard output of the container command. +stderr: + type: str + returned: success and O(detach=false) + description: + - The standard error output of the container command. +rc: + type: int + returned: success and O(detach=false) + sample: 0 + description: + - The exit code of the command. +''' + +import shlex +import traceback + +from ansible.module_utils.common.text.converters import to_text, to_native +from ansible.module_utils.six import string_types + +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, +) + + +class ExecManager(BaseComposeManager): + def __init__(self, client): + 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.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.') + + 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()): + if not isinstance(value, string_types): + self.fail( + "Non-string value found for env option. Ambiguous env options must be " + "wrapped in quotes to avoid them being interpreted. Key: %s" % (name, ) + ) + 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'] + if self.build: + args.append('--build') + if self.cap_add: + for cap in self.cap_add: + args.extend(['--cap-add', cap]) + if self.cap_drop: + for cap in self.cap_drop: + args.extend(['--cap-drop', cap]) + if self.entrypoint is not None: + args.extend(['--entrypoint', self.entrypoint]) + if not self.interactive: + args.append('--no-interactive') + if self.labels: + for label in self.labels: + args.extend(['--label', label]) + if self.name is not None: + args.extend(['--name', self.name]) + if self.no_deps: + args.append('--no-deps') + if self.publish: + for publish in self.publish: + args.extend(['--publish', publish]) + if self.quiet_pull: + args.append('--quiet-pull') + if self.remove_orphans: + args.append('--remove-orphans') + if self.do_cleanup: + args.append('--rm') + if self.service_ports: + args.append('--service-ports') + if self.use_aliases: + args.append('--use-aliases') + if self.volumes: + for volume in self.volumes: + args.extend(['--volume', volume]) + if self.chdir is not None: + args.extend(['--workdir', self.chdir]) + if self.detach: + args.extend(['--detach']) + if self.user is not None: + args.extend(['--user', self.user]) + if not self.tty: + args.append('--no-TTY') + if self.env: + for name, value in list(self.env.items()): + args.append('{0}={1}'.format(name, value)) + args.append('--') + args.append(self.service) + if self.argv: + args.extend(self.argv) + return args + + def run(self): + args = self.get_run_cmd(self.check_mode) + kwargs = { + 'cwd': self.project_src, + } + if self.stdin is not None: + kwargs['data'] = self.stdin.encode('utf-8') + if self.detach: + kwargs['check_rc'] = True + rc, stdout, stderr = self.client.call_cli(*args, **kwargs) + if self.detach: + return { + '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') + return { + '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'), + ) + argspec_ex = common_compose_argspec_ex() + argument_spec.update(argspec_ex.pop('argspec')) + + client = AnsibleModuleDockerClient( + argument_spec=argument_spec, + supports_check_mode=False, + needs_api_version=False, + **argspec_ex + ) + + try: + manager = ExecManager(client) + result = manager.run() + manager.cleanup() + client.module.exit_json(**result) + except DockerException as e: + client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc()) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/docker_compose_v2_exec/aliases b/tests/integration/targets/docker_compose_v2_exec/aliases new file mode 100644 index 000000000..2e1acc0ad --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_exec/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/4 +destructive diff --git a/tests/integration/targets/docker_compose_v2_exec/meta/main.yml b/tests/integration/targets/docker_compose_v2_exec/meta/main.yml new file mode 100644 index 000000000..aefcf50f2 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_exec/meta/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_docker_cli_compose + # The Python dependencies are needed for the other modules + - setup_docker_python_deps + - setup_remote_tmp_dir diff --git a/tests/integration/targets/docker_compose_v2_exec/tasks/main.yml b/tests/integration/targets/docker_compose_v2_exec/tasks/main.yml new file mode 100644 index 000000000..dbb2ece71 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_exec/tasks/main.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Create random name prefix (for services, ...) +- name: Create random container name prefix + set_fact: + name_prefix: "{{ 'ansible-docker-test-%0x' % ((2**32) | random) }}" + cnames: [] + dnetworks: [] + +- debug: + msg: "Using name prefix {{ name_prefix }}" + +# Run the tests +- block: + - name: Show docker compose --help output + command: docker compose --help + + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + loop_control: + loop_var: test_name + + always: + - name: "Make sure all containers are removed" + docker_container: + name: "{{ item }}" + state: absent + force_kill: true + with_items: "{{ cnames }}" + diff: false + + - name: "Make sure all networks are removed" + docker_network: + name: "{{ item }}" + state: absent + force: true + with_items: "{{ dnetworks }}" + diff: false + + when: docker_has_compose and docker_compose_version is version('2.18.0', '>=') diff --git a/tests/integration/targets/docker_compose_v2_exec/tasks/run-test.yml b/tests/integration/targets/docker_compose_v2_exec/tasks/run-test.yml new file mode 100644 index 000000000..72a58962d --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_exec/tasks/run-test.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "Loading tasks from {{ test_name }}" + include_tasks: "{{ test_name }}" diff --git a/tests/integration/targets/docker_compose_v2_exec/tasks/tests/basic.yml b/tests/integration/targets/docker_compose_v2_exec/tasks/tests/basic.yml new file mode 100644 index 000000000..730026b91 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_exec/tasks/tests/basic.yml @@ -0,0 +1,97 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- vars: + pname: "{{ name_prefix }}-start-stop" + cname: "{{ name_prefix }}-container" + project_src: "{{ remote_tmp_dir }}/{{ pname }}" + test_service: | + services: + {{ cname }}: + image: "{{ docker_test_image_alpine }}" + command: '/bin/sh -c "sleep 10m"' + stop_grace_period: 1s + + block: + - name: Registering container name + set_fact: + cnames: "{{ cnames + [pname ~ '-' ~ cname ~ '-1'] }}" + dnetworks: "{{ dnetworks + [pname ~ '_default'] }}" + + - name: Create project directory + file: + path: '{{ project_src }}' + state: directory + + - name: Template default project file + copy: + dest: '{{ project_src }}/docker-compose.yml' + content: '{{ test_service }}' + + - block: + - name: Start services + docker_compose_v2: + project_src: '{{ project_src }}' + state: present + + - name: Run command with command + docker_compose_v2_exec: + project_src: '{{ project_src }}' + service: '{{ cname }}' + command: /bin/sh -c "ls /" + register: result_1 + + - name: Run command with argv + docker_compose_v2_exec: + project_src: '{{ project_src }}' + service: '{{ cname }}' + argv: + - /bin/sh + - "-c" + - whoami + user: "1234" + register: result_2 + failed_when: result_2.rc != 1 + + - name: Run detached command + docker_compose_v2_exec: + project_src: '{{ project_src }}' + service: '{{ cname }}' + command: /bin/sh -c "sleep 1" + detach: true + register: result_3 + + - name: Run command with input + docker_compose_v2_exec: + project_src: '{{ project_src }}' + service: '{{ cname }}' + command: /bin/sh -c "cat" + stdin: This is a test + register: result_4 + + - assert: + that: + - result_1.rc == 0 + - result_1.stderr == "" + - >- + "usr" in result_1.stdout_lines + and + "etc" in result_1.stdout_lines + - result_2.rc == 1 + - >- + "whoami: unknown uid 1234" in result_2.stderr + - result_2.stdout == "" + - result_3.rc is not defined + - result_3.stdout is not defined + - result_3.stderr is not defined + - result_4.rc == 0 + - result_4.stdout == "This is a test" + - result_4.stderr == "" + + always: + - name: Cleanup + docker_compose_v2: + project_src: '{{ project_src }}' + state: absent diff --git a/tests/integration/targets/docker_compose_v2_run/aliases b/tests/integration/targets/docker_compose_v2_run/aliases new file mode 100644 index 000000000..2e1acc0ad --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_run/aliases @@ -0,0 +1,6 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/4 +destructive diff --git a/tests/integration/targets/docker_compose_v2_run/meta/main.yml b/tests/integration/targets/docker_compose_v2_run/meta/main.yml new file mode 100644 index 000000000..aefcf50f2 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_run/meta/main.yml @@ -0,0 +1,10 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_docker_cli_compose + # The Python dependencies are needed for the other modules + - setup_docker_python_deps + - setup_remote_tmp_dir diff --git a/tests/integration/targets/docker_compose_v2_run/tasks/main.yml b/tests/integration/targets/docker_compose_v2_run/tasks/main.yml new file mode 100644 index 000000000..dbb2ece71 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_run/tasks/main.yml @@ -0,0 +1,49 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Create random name prefix (for services, ...) +- name: Create random container name prefix + set_fact: + name_prefix: "{{ 'ansible-docker-test-%0x' % ((2**32) | random) }}" + cnames: [] + dnetworks: [] + +- debug: + msg: "Using name prefix {{ name_prefix }}" + +# Run the tests +- block: + - name: Show docker compose --help output + command: docker compose --help + + - include_tasks: run-test.yml + with_fileglob: + - "tests/*.yml" + loop_control: + loop_var: test_name + + always: + - name: "Make sure all containers are removed" + docker_container: + name: "{{ item }}" + state: absent + force_kill: true + with_items: "{{ cnames }}" + diff: false + + - name: "Make sure all networks are removed" + docker_network: + name: "{{ item }}" + state: absent + force: true + with_items: "{{ dnetworks }}" + diff: false + + when: docker_has_compose and docker_compose_version is version('2.18.0', '>=') diff --git a/tests/integration/targets/docker_compose_v2_run/tasks/run-test.yml b/tests/integration/targets/docker_compose_v2_run/tasks/run-test.yml new file mode 100644 index 000000000..72a58962d --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_run/tasks/run-test.yml @@ -0,0 +1,7 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: "Loading tasks from {{ test_name }}" + include_tasks: "{{ test_name }}" diff --git a/tests/integration/targets/docker_compose_v2_run/tasks/tests/basic.yml b/tests/integration/targets/docker_compose_v2_run/tasks/tests/basic.yml new file mode 100644 index 000000000..03555d968 --- /dev/null +++ b/tests/integration/targets/docker_compose_v2_run/tasks/tests/basic.yml @@ -0,0 +1,104 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- vars: + pname: "{{ name_prefix }}-start-stop" + cname: "{{ name_prefix }}-container" + project_src: "{{ remote_tmp_dir }}/{{ pname }}" + test_service: | + services: + {{ cname }}: + image: "{{ docker_test_image_alpine }}" + command: '/bin/sh -c "sleep 10m"' + stop_grace_period: 1s + + block: + - name: Registering container name + set_fact: + cnames: "{{ cnames + [pname ~ '-' ~ cname ~ '-1'] }}" + dnetworks: "{{ dnetworks + [pname ~ '_default'] }}" + + - name: Create project directory + file: + path: '{{ project_src }}' + state: directory + + - name: Template default project file + copy: + dest: '{{ project_src }}/docker-compose.yml' + content: '{{ test_service }}' + + - block: + - name: Start services + docker_compose_v2: + project_src: '{{ project_src }}' + state: present + + - name: Run command with command + docker_compose_v2_run: + project_src: '{{ project_src }}' + service: '{{ cname }}' + command: /bin/sh -c "ls /" + cleanup: true + register: result_1 + + - name: Run command with argv + docker_compose_v2_run: + project_src: '{{ project_src }}' + service: '{{ cname }}' + argv: + - /bin/sh + - "-c" + - whoami + user: "1234" + cleanup: true + register: result_2 + failed_when: result_2.rc != 1 + + - name: Run detached command + docker_compose_v2_run: + project_src: '{{ project_src }}' + service: '{{ cname }}' + command: /bin/sh -c "sleep 1" + detach: true + cleanup: true + register: result_3 + + - name: Run command with input + docker_compose_v2_run: + project_src: '{{ project_src }}' + service: '{{ cname }}' + command: /bin/sh -c "cat" + stdin: This is a test + register: result_4 + + - assert: + that: + - result_1.rc == 0 + - result_1.stderr == "" + - >- + "usr" in result_1.stdout_lines + and + "etc" in result_1.stdout_lines + - result_1.container_id is not defined + - result_2.rc == 1 + - >- + "whoami: unknown uid 1234" in result_2.stderr + - result_2.stdout == "" + - result_2.container_id is not defined + - result_3.rc is not defined + - result_3.stdout is not defined + - result_3.stderr is not defined + - result_3.container_id is string + - result_4.rc == 0 + - result_4.stdout == "This is a test" + - result_4.stderr is string + - result_4.container_id is not defined + + always: + - name: Cleanup + docker_compose_v2: + project_src: '{{ project_src }}' + state: absent