1#!/usr/local/bin/python3.8
2#
3# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9
10DOCUMENTATION = '''
11---
12module: docker_container_exec
13
14short_description: Execute command in a docker container
15
16version_added: 1.5.0
17
18description:
19  - Executes a command in a Docker container.
20
21options:
22  container:
23    type: str
24    required: true
25    description:
26      - The name of the container to execute the command in.
27  argv:
28    type: list
29    elements: str
30    description:
31      - The command to execute.
32      - Since this is a list of arguments, no quoting is needed.
33      - Exactly one of I(argv) and I(command) must be specified.
34  command:
35    type: str
36    description:
37      - The command to execute.
38      - Exactly one of I(argv) and I(command) must be specified.
39  chdir:
40    type: str
41    description:
42      - The directory to run the command in.
43  user:
44    type: str
45    description:
46      - If specified, the user to execute this command with.
47  stdin:
48    type: str
49    description:
50      - Set the stdin of the command directly to the specified value.
51  stdin_add_newline:
52    type: bool
53    default: true
54    description:
55      - If set to C(true), appends a newline to I(stdin).
56  strip_empty_ends:
57    type: bool
58    default: true
59    description:
60      - Strip empty lines from the end of stdout/stderr in result.
61  tty:
62    type: bool
63    default: false
64    description:
65      - Whether to allocate a TTY.
66
67extends_documentation_fragment:
68  - community.docker.docker
69  - community.docker.docker.docker_py_1_documentation
70notes:
71  - Does not support C(check_mode).
72author:
73  - "Felix Fontein (@felixfontein)"
74
75requirements:
76  - "L(Docker SDK for Python,https://docker-py.readthedocs.io/en/stable/) >= 1.8.0 (use L(docker-py,https://pypi.org/project/docker-py/) for Python 2.6)"
77  - "Docker API >= 1.20"
78'''
79
80EXAMPLES = '''
81- name: Run a simple command (command)
82  community.docker.docker_container_exec:
83    container: foo
84    command: /bin/bash -c "ls -lah"
85    chdir: /root
86  register: result
87
88- name: Print stdout
89  debug:
90    var: result.stdout
91
92- name: Run a simple command (argv)
93  community.docker.docker_container_exec:
94    container: foo
95    argv:
96      - /bin/bash
97      - "-c"
98      - "ls -lah > /dev/stderr"
99    chdir: /root
100  register: result
101
102- name: Print stderr lines
103  debug:
104    var: result.stderr_lines
105'''
106
107RETURN = '''
108stdout:
109    type: str
110    returned: success
111    description:
112      - The standard output of the container command.
113stderr:
114    type: str
115    returned: success
116    description:
117      - The standard error output of the container command.
118rc:
119    type: int
120    returned: success
121    sample: 0
122    description:
123      - The exit code of the command.
124'''
125
126import shlex
127import traceback
128
129from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
130
131from ansible_collections.community.docker.plugins.module_utils.common import (
132    AnsibleDockerClient,
133    RequestException,
134)
135
136from ansible_collections.community.docker.plugins.module_utils.socket_helper import (
137    shutdown_writing,
138    write_to_socket,
139)
140
141from ansible_collections.community.docker.plugins.module_utils.socket_handler import (
142    find_selectors,
143    DockerSocketHandlerModule,
144)
145
146try:
147    from docker.errors import DockerException, APIError, NotFound
148except Exception:
149    # missing Docker SDK for Python handled in ansible.module_utils.docker.common
150    pass
151
152
153def main():
154    argument_spec = dict(
155        container=dict(type='str', required=True),
156        argv=dict(type='list', elements='str'),
157        command=dict(type='str'),
158        chdir=dict(type='str'),
159        user=dict(type='str'),
160        stdin=dict(type='str'),
161        stdin_add_newline=dict(type='bool', default=True),
162        strip_empty_ends=dict(type='bool', default=True),
163        tty=dict(type='bool', default=False),
164    )
165
166    client = AnsibleDockerClient(
167        argument_spec=argument_spec,
168        min_docker_api_version='1.20',
169        mutually_exclusive=[('argv', 'command')],
170        required_one_of=[('argv', 'command')],
171    )
172
173    container = client.module.params['container']
174    argv = client.module.params['argv']
175    command = client.module.params['command']
176    chdir = client.module.params['chdir']
177    user = client.module.params['user']
178    stdin = client.module.params['stdin']
179    strip_empty_ends = client.module.params['strip_empty_ends']
180    tty = client.module.params['tty']
181
182    if command is not None:
183        argv = shlex.split(command)
184
185    if stdin is not None and client.module.params['stdin_add_newline']:
186        stdin += '\n'
187
188    selectors = None
189    if stdin:
190        selectors = find_selectors(client.module)
191
192    try:
193        exec_data = client.exec_create(
194            container,
195            argv,
196            stdout=True,
197            stderr=True,
198            stdin=bool(stdin),
199            user=user or '',
200            workdir=chdir,
201        )
202        exec_id = exec_data['Id']
203
204        if selectors:
205            exec_socket = client.exec_start(
206                exec_id,
207                tty=tty,
208                detach=False,
209                socket=True,
210            )
211            try:
212                with DockerSocketHandlerModule(exec_socket, client.module, selectors) as exec_socket_handler:
213                    if stdin:
214                        exec_socket_handler.write(to_bytes(stdin))
215
216                    stdout, stderr = exec_socket_handler.consume()
217            finally:
218                exec_socket.close()
219        else:
220            stdout, stderr = client.exec_start(
221                exec_id,
222                tty=tty,
223                detach=False,
224                stream=False,
225                socket=False,
226                demux=True,
227            )
228
229        result = client.exec_inspect(exec_id)
230
231        stdout = to_text(stdout or b'')
232        stderr = to_text(stderr or b'')
233        if strip_empty_ends:
234            stdout = stdout.rstrip('\r\n')
235            stderr = stderr.rstrip('\r\n')
236
237        client.module.exit_json(
238            changed=True,
239            stdout=stdout,
240            stderr=stderr,
241            rc=result.get('ExitCode') or 0,
242        )
243    except NotFound:
244        client.fail('Could not find container "{0}"'.format(container))
245    except APIError as e:
246        if e.response and e.response.status_code == 409:
247            client.fail('The container "{0}" has been paused ({1})'.format(container, to_native(e)))
248        client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
249    except DockerException as e:
250        client.fail('An unexpected docker error occurred: {0}'.format(to_native(e)), exception=traceback.format_exc())
251    except RequestException as e:
252        client.fail(
253            'An unexpected requests error occurred when docker-py tried to talk to the docker daemon: {0}'.format(to_native(e)),
254            exception=traceback.format_exc())
255
256
257if __name__ == '__main__':
258    main()
259