Skip to content

Commit

Permalink
Adjust docker_api plugin to ansible-core 2.13. (ansible-collections#308)
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein authored Mar 22, 2022
1 parent 421b712 commit b353a39
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 30 deletions.
4 changes: 4 additions & 0 deletions changelogs/fragments/308-docker_api-connection-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bugfixes:
- "docker_api connection plugin - fix option handling to be compatible with ansible-core 2.13 (https://github.com/ansible-collections/community.docker/pull/308)."
minor_changes:
- "docker_api connection plugin - the plugin supports new ways to define the timeout. These are the ``ANSIBLE_DOCKER_TIMEOUT`` environment variable, the ``timeout`` setting in the ``docker_connection`` section of ``ansible.cfg``, and the ``ansible_docker_timeout`` variable (https://github.com/ansible-collections/community.docker/pull/308)."
91 changes: 61 additions & 30 deletions plugins/connection/docker_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,44 @@
vars:
- name: ansible_user
- name: ansible_docker_user
ini:
- section: defaults
key: remote_user
env:
- name: ANSIBLE_REMOTE_USER
cli:
- name: user
keyword:
- name: remote_user
remote_addr:
type: str
description:
- The name of the container you want to access.
default: inventory_hostname
vars:
- name: inventory_hostname
- name: ansible_host
- name: ansible_docker_host
container_timeout:
default: 10
description:
- Controls how long we can wait to access reading output from the container once execution started.
env:
- name: ANSIBLE_TIMEOUT
- name: ANSIBLE_DOCKER_TIMEOUT
version_added: 2.2.0
ini:
- key: timeout
section: defaults
- key: timeout
section: docker_connection
version_added: 2.2.0
vars:
- name: ansible_docker_timeout
version_added: 2.2.0
cli:
- name: timeout
type: integer
extends_documentation_fragment:
- community.docker.docker
Expand Down Expand Up @@ -79,28 +109,28 @@ class Connection(ConnectionBase):
transport = 'community.docker.docker_api'
has_pipelining = True

def _call_client(self, play_context, callable, not_found_can_be_resource=False):
def _call_client(self, callable, not_found_can_be_resource=False):
try:
return callable()
except NotFound as e:
if not_found_can_be_resource:
raise AnsibleConnectionFailure('Could not find container "{1}" or resource in it ({0})'.format(e, play_context.remote_addr))
raise AnsibleConnectionFailure('Could not find container "{1}" or resource in it ({0})'.format(e, self.get_option('remote_addr')))
else:
raise AnsibleConnectionFailure('Could not find container "{1}" ({0})'.format(e, play_context.remote_addr))
raise AnsibleConnectionFailure('Could not find container "{1}" ({0})'.format(e, self.get_option('remote_addr')))
except APIError as e:
if e.response and e.response.status_code == 409:
raise AnsibleConnectionFailure('The container "{1}" has been paused ({0})'.format(e, play_context.remote_addr))
raise AnsibleConnectionFailure('The container "{1}" has been paused ({0})'.format(e, self.get_option('remote_addr')))
self.client.fail(
'An unexpected docker error occurred for container "{1}": {0}'.format(e, play_context.remote_addr)
'An unexpected docker error occurred for container "{1}": {0}'.format(e, self.get_option('remote_addr'))
)
except DockerException as e:
self.client.fail(
'An unexpected docker error occurred for container "{1}": {0}'.format(e, play_context.remote_addr)
'An unexpected docker error occurred for container "{1}": {0}'.format(e, self.get_option('remote_addr'))
)
except RequestException as e:
self.client.fail(
'An unexpected requests error occurred for container "{1}" when docker-py tried to talk to the docker daemon: {0}'
.format(e, play_context.remote_addr)
.format(e, self.get_option('remote_addr'))
)

def __init__(self, play_context, new_stdin, *args, **kwargs):
Expand All @@ -113,14 +143,15 @@ def __init__(self, play_context, new_stdin, *args, **kwargs):
if getattr(self._shell, "_IS_WINDOWS", False):
self.module_implementation_preferences = ('.ps1', '.exe', '')

self.actual_user = play_context.remote_user
self.actual_user = None

def _connect(self, port=None):
""" Connect to the container. Nothing to do """
super(Connection, self)._connect()
if not self._connected:
self.actual_user = self.get_option('remote_user')
display.vvv(u"ESTABLISH DOCKER CONNECTION FOR USER: {0}".format(
self.actual_user or u'?'), host=self._play_context.remote_addr
self.actual_user or u'?'), host=self.get_option('remote_addr')
)
if self.client is None:
self.client = AnsibleDockerClient(self, min_docker_version=MIN_DOCKER_PY, min_docker_api_version=MIN_DOCKER_API)
Expand All @@ -131,7 +162,7 @@ def _connect(self, port=None):
# Only do this if display verbosity is high enough that we'll need the value
# This saves overhead from calling into docker when we don't need to
display.vvv(u"Trying to determine actual user")
result = self._call_client(self._play_context, lambda: self.client.inspect_container(self._play_context.remote_addr))
result = self._call_client(lambda: self.client.inspect_container(self.get_option('remote_addr')))
if result.get('Config'):
self.actual_user = result['Config'].get('User')
if self.actual_user is not None:
Expand All @@ -152,30 +183,30 @@ def exec_command(self, cmd, in_data=None, sudoable=False):
', with stdin ({0} bytes)'.format(len(in_data)) if in_data is not None else '',
', with become prompt' if do_become else '',
),
host=self._play_context.remote_addr
host=self.get_option('remote_addr')
)

need_stdin = True if (in_data is not None) or do_become else False

exec_data = self._call_client(self._play_context, lambda: self.client.exec_create(
self._play_context.remote_addr,
exec_data = self._call_client(lambda: self.client.exec_create(
self.get_option('remote_addr'),
command,
stdout=True,
stderr=True,
stdin=need_stdin,
user=self._play_context.remote_user or '',
user=self.get_option('remote_user') or '',
# workdir=None, - only works for Docker SDK for Python 3.0.0 and later
))
exec_id = exec_data['Id']

if need_stdin:
exec_socket = self._call_client(self._play_context, lambda: self.client.exec_start(
exec_socket = self._call_client(lambda: self.client.exec_start(
exec_id,
detach=False,
socket=True,
))
try:
with DockerSocketHandler(display, exec_socket, container=self._play_context.remote_addr) as exec_socket_handler:
with DockerSocketHandler(display, exec_socket, container=self.get_option('remote_addr')) as exec_socket_handler:
if do_become:
become_output = [b'']

Expand All @@ -185,7 +216,7 @@ def append_become_output(stream_id, data):
exec_socket_handler.set_block_done_callback(append_become_output)

while not self.become.check_success(become_output[0]) and not self.become.check_password_prompt(become_output[0]):
if not exec_socket_handler.select(self._play_context.timeout):
if not exec_socket_handler.select(self.get_option('container_timeout')):
stdout, stderr = exec_socket_handler.consume()
raise AnsibleConnectionFailure('timeout waiting for privilege escalation password prompt:\n' + to_native(become_output[0]))

Expand All @@ -203,15 +234,15 @@ def append_become_output(stream_id, data):
finally:
exec_socket.close()
else:
stdout, stderr = self._call_client(self._play_context, lambda: self.client.exec_start(
stdout, stderr = self._call_client(lambda: self.client.exec_start(
exec_id,
detach=False,
stream=False,
socket=False,
demux=True,
))

result = self._call_client(self._play_context, lambda: self.client.exec_inspect(exec_id))
result = self._call_client(lambda: self.client.exec_inspect(exec_id))

return result.get('ExitCode') or 0, stdout or b'', stderr or b''

Expand All @@ -236,7 +267,7 @@ def _prefix_login_path(self, remote_path):
def put_file(self, in_path, out_path):
""" Transfer a file from local to docker container """
super(Connection, self).put_file(in_path, out_path)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr'))

out_path = self._prefix_login_path(out_path)
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
Expand All @@ -250,12 +281,12 @@ def put_file(self, in_path, out_path):
self.ids[self.actual_user] = int(user_id), int(group_id)
display.vvvv(
'PUT: Determined uid={0} and gid={1} for user "{2}"'.format(user_id, group_id, self.actual_user),
host=self._play_context.remote_addr
host=self.get_option('remote_addr')
)
except Exception as e:
raise AnsibleConnectionFailure(
'Error while determining user and group ID of current user in container "{1}": {0}\nGot value: {2!r}'
.format(e, self._play_context.remote_addr, ids)
.format(e, self.get_option('remote_addr'), ids)
)

b_in_path = to_bytes(in_path, errors='surrogate_or_strict')
Expand All @@ -282,8 +313,8 @@ def put_file(self, in_path, out_path):
tar.addfile(tarinfo, fileobj=f)
data = bio.getvalue()

ok = self._call_client(self._play_context, lambda: self.client.put_archive(
self._play_context.remote_addr,
ok = self._call_client(lambda: self.client.put_archive(
self.get_option('remote_addr'),
out_dir,
data, # can also be file object for streaming; this is only clear from the
# implementation of put_archive(), which uses requests's put().
Expand All @@ -293,13 +324,13 @@ def put_file(self, in_path, out_path):
if not ok:
raise AnsibleConnectionFailure(
'Unknown error while creating file "{0}" in container "{1}".'
.format(out_path, self._play_context.remote_addr)
.format(out_path, self.get_option('remote_addr'))
)

def fetch_file(self, in_path, out_path):
""" Fetch a file from container to local. """
super(Connection, self).fetch_file(in_path, out_path)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr)
display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.get_option('remote_addr'))

in_path = self._prefix_login_path(in_path)
b_out_path = to_bytes(out_path, errors='surrogate_or_strict')
Expand All @@ -311,9 +342,9 @@ def fetch_file(self, in_path, out_path):
raise AnsibleConnectionFailure('Found infinite symbolic link loop when trying to fetch "{0}"'.format(in_path))
considered_in_paths.add(in_path)

display.vvvv('FETCH: Fetching "%s"' % in_path, host=self._play_context.remote_addr)
stream, stats = self._call_client(self._play_context, lambda: self.client.get_archive(
self._play_context.remote_addr,
display.vvvv('FETCH: Fetching "%s"' % in_path, host=self.get_option('remote_addr'))
stream, stats = self._call_client(lambda: self.client.get_archive(
self.get_option('remote_addr'),
in_path,
), not_found_can_be_resource=True)

Expand Down Expand Up @@ -344,7 +375,7 @@ def fetch_file(self, in_path, out_path):
# If the only member was a file, it's already extracted. If it is a symlink, process it now.
if symlink_member is not None:
in_path = os.path.join(os.path.split(in_path)[0], symlink_member.linkname)
display.vvvv('FETCH: Following symbolic link to "%s"' % in_path, host=self._play_context.remote_addr)
display.vvvv('FETCH: Following symbolic link to "%s"' % in_path, host=self.get_option('remote_addr'))
continue
return

Expand Down

0 comments on commit b353a39

Please sign in to comment.