diff --git a/.github/workflows/podman_container_exec.yml b/.github/workflows/podman_container_exec.yml new file mode 100644 index 00000000..4b2115a2 --- /dev/null +++ b/.github/workflows/podman_container_exec.yml @@ -0,0 +1,99 @@ +name: Podman container exec + +on: + push: + paths: + - '.github/workflows/podman_container_exec.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_container_exec.yml' + - 'plugins/modules/podman_container_exec.py' + - 'tests/integration/targets/podman_container_exec/**' + branches: + - master + pull_request: + paths: + - '.github/workflows/podman_container_exec.yml' + - 'ci/*.yml' + - 'ci/run_containers_tests.sh' + - 'ci/playbooks/containers/podman_container_exec.yml' + - 'plugins/modules/podman_container_exec.py' + - 'tests/integration/targets/podman_container_exec/**' + schedule: + - cron: 4 0 * * * # Run daily at 0:03 UTC + +jobs: + + test_podman_container_exec: + name: Podman container exec ${{ matrix.ansible-version }}-${{ matrix.os || 'ubuntu-22.04' }} + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + ansible-version: + - ansible<2.10 + # - git+https://github.com/ansible/ansible.git@stable-2.11 + - git+https://github.com/ansible/ansible.git@devel + os: + - ubuntu-22.04 + python-version: + - "3.10" + + steps: + + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip and display Python and PIP versions + run: | + sudo apt-get update + sudo apt-get install -y python*-wheel python*-yaml + python -m pip install --upgrade pip + python -V + pip --version + - name: Set up pip cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ github.ref }}-units-VMs + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install Ansible ${{ matrix.ansible-version }} + run: python3 -m pip install --user --force-reinstall --upgrade '${{ matrix.ansible-version }}' + + - name: Build and install the collection tarball + run: | + rm -rf /tmp/just_new_collection + ~/.local/bin/ansible-galaxy collection build --output-path /tmp/just_new_collection --force + ~/.local/bin/ansible-galaxy collection install -vvv --force /tmp/just_new_collection/*.tar.gz + - name: Run collection tests for Podman container exec + run: | + export PATH=~/.local/bin:$PATH + echo "Run ansible version" + command -v ansible + ansible --version + export ANSIBLE_CONFIG=$(pwd)/ci/ansible-dev.cfg + if [[ '${{ matrix.ansible-version }}' == 'ansible<2.10' ]]; then + export ANSIBLE_CONFIG=$(pwd)/ci/ansible-2.9.cfg + fi + echo $ANSIBLE_CONFIG + command -v ansible-playbook + pip --version + python --version + ansible-playbook --version + ansible-playbook -vv ci/playbooks/pre.yml \ + -e host=localhost \ + -i localhost, \ + -e ansible_connection=local \ + -e setup_python=false + TEST2RUN=podman_container_exec ./ci/run_containers_tests.sh + shell: bash \ No newline at end of file diff --git a/ci/playbooks/containers/podman_container_exec.yml b/ci/playbooks/containers/podman_container_exec.yml new file mode 100644 index 00000000..057b68eb --- /dev/null +++ b/ci/playbooks/containers/podman_container_exec.yml @@ -0,0 +1,8 @@ +--- +- hosts: all + gather_facts: true + tasks: + - include_role: + name: podman_container_exec + vars: + ansible_python_interpreter: "{{ _ansible_python_interpreter }}" \ No newline at end of file diff --git a/plugins/modules/podman_container_exec.py b/plugins/modules/podman_container_exec.py new file mode 100644 index 00000000..77662302 --- /dev/null +++ b/plugins/modules/podman_container_exec.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2023, Takuya Nishimura <@nishipy> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +module: podman_container_exec +author: + - Takuya Nishimura (@nishipy) +short_description: Executes a command in a running container. +description: + - Executes a command in a running container. +options: + name: + description: + - Name of the container where the command is executed. + type: str + required: true + command: + description: + - The command to run in the container. + - One of the I(command) or I(args) is required. + type: str + argv: + description: + - Passes the command as a list rather than a string. + - One of the I(command) or I(args) is required. + type: list + elements: str + detach: + description: + - If true, the command runs in the background. + - The exec session is automatically removed when it completes. + type: bool + default: false + env: + description: + - Set environment variables. + type: dict + privileged: + description: + - Give extended privileges to the container. + type: bool + default: false + tty: + description: + - Allocate a pseudo-TTY. + type: bool + default: false + user: + description: + - The username or UID used and, optionally, the groupname or GID for the specified command. + - Both user and group may be symbolic or numeric. + type: str + workdir: + description: + - Working directory inside the container. + type: str +requirements: + - podman +notes: + - See L(the Podman documentation,https://docs.podman.io/en/latest/markdown/podman-exec.1.html) for details of podman-exec(1). +''' + +EXAMPLES = r''' +- name: Execute a command with workdir + containers.podman.podman_container_exec: + name: ubi8 + command: "cat redhat-release" + workdir: /etc + +- name: Execute a command with a list of args and enviroment variables + containers.podman.podman_container_exec: + name: test_container + argv: + - /bin/sh + - -c + - echo $HELLO $BYE + env: + HELLO: hello world + BYE: goodbye world + +- name: Execute command in background by using detach + containers.podman.podman_container_exec: + name: detach_container + command: "cat redhat-release" + detach: true +''' + +RETURN = r''' +stdout: + type: str + returned: success + description: + - The standard output of the command executed in the container. +stderr: + type: str + returned: success + description: + - The standard output of the command executed in the container. +rc: + type: int + returned: success + sample: 0 + description: + - The exit code of the command executed in the container. +exec_id: + type: str + returned: success and I(detach=true) + sample: f99002e34c1087fd1aa08d5027e455bf7c2d6b74f019069acf6462a96ddf2a47 + description: + - The ID of the exec session. +''' + + +import shlex +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.containers.podman.plugins.module_utils.podman.common import run_podman_command + + +def run_container_exec(module: AnsibleModule) -> dict: + ''' + Execute podman-container-exec for the given options + ''' + exec_with_args = ['container', 'exec'] + # podman_container_exec always returns changed=true + changed = True + exec_options = [] + + name = module.params['name'] + argv = module.params['argv'] + command = module.params['command'] + detach = module.params['detach'] + env = module.params['env'] + privileged = module.params['privileged'] + tty = module.params['tty'] + user = module.params['user'] + workdir = module.params['workdir'] + + if command is not None: + argv = shlex.split(command) + + if detach: + exec_options.append('--detach') + + if env is not None: + for key, value in env.items(): + if not isinstance(value, string_types): + module.fail_json( + msg="Specify string value %s on the env field" % (value)) + + to_text(value, errors='surrogate_or_strict') + exec_options += ['--env', + '%s="%s"' % (key, value)] + + if privileged: + exec_options.append('--privileged') + + if tty: + exec_options.append('--tty') + + if user is not None: + exec_options += ['--user', + to_text(user, errors='surrogate_or_strict')] + + if workdir is not None: + exec_options += ['--workdir', + to_text(workdir, errors='surrogate_or_strict')] + + exec_options.append(name) + exec_options.extend(argv) + + exec_with_args.extend(exec_options) + + rc, stdout, stderr = run_podman_command( + module=module, executable='podman', args=exec_with_args) + + result = { + 'changed': changed, + 'podman_command': exec_options, + 'rc': rc, + 'stdout': stdout, + 'stderr': stderr, + } + + if detach: + result['exec_id'] = stdout.replace('\n', '') + + return result + + +def main(): + argument_spec = { + 'name': { + 'type': 'str', + 'required': True, + }, + 'command': { + 'type': 'str', + }, + 'argv': { + 'type': 'list', + 'elements': 'str', + }, + 'detach': { + 'type': 'bool', + 'default': False, + }, + 'env': { + 'type': 'dict', + }, + 'privileged': { + 'type': 'bool', + 'default': False, + }, + 'tty': { + 'type': 'bool', + 'default': False, + }, + 'user': { + 'type': 'str', + }, + 'workdir': { + 'type': 'str', + }, + } + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=[('argv', 'command')], + ) + + result = run_container_exec(module) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/podman_container_exec/tasks/main.yml b/tests/integration/targets/podman_container_exec/tasks/main.yml new file mode 100644 index 00000000..2d287432 --- /dev/null +++ b/tests/integration/targets/podman_container_exec/tasks/main.yml @@ -0,0 +1,67 @@ +- name: Test podman_container_exec + block: + - name: Generate random value for container name + set_fact: + container_name: "{{ 'ansible-test-podman-%0x' % ((2**32) | random) }}" + + - name: Make sure container doesn't exist + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: "{{ container_name }}" + state: absent + + - name: Test exec when the container doesn't exist + containers.podman.podman_container_exec: + name: "{{ container_name }}" + command: "cat /etc/redhat-release" + ignore_errors: true + register: no_container + + - name: Create and start a container for testing + containers.podman.podman_container: + name: "{{ container_name }}" + image: registry.access.redhat.com/ubi8 + command: sleep 1d + state: started + + - name: Test exec with command and workdir options + containers.podman.podman_container_exec: + name: "{{ container_name }}" + command: "cat redhat-release" + workdir: /etc + register: exec1 + + - name: Test exec with argv and env options + containers.podman.podman_container_exec: + name: "{{ container_name }}" + argv: + - /bin/sh + - -c + - echo $HELLO $BYE + env: + HELLO: hello world + BYE: goodbye world + register: exec2 + + - name: Test exec with detach option + containers.podman.podman_container_exec: + name: "{{ container_name }}" + command: "cat redhat-release" + detach: true + register: exec3 + + - name: Check if the result is as expected + assert: + that: + - no_container is failed + - "'Red Hat Enterprise Linux' in exec1.stdout" + - "'hello world' in exec2.stdout" + - "'goodbye world' in exec2.stdout" + - exec3.exec_id is defined + + always: + - name: Cleanup + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: "{{ container_name }}" + state: absent