Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker_api connection plugin: adjust to ansible-core 2.13 #308

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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