mirror of
https://github.com/ansible-collections/community.docker.git
synced 2026-07-29 11:55:04 +00:00
Add docker_container_copy_into module (#545)
* Move copying functionality to module_utils. * Add docker_container_copy_into module. * Use new module in other tests. * Fix copyright and attributes. * Improve idempotency, improve stat code. * Document and test when a stopped container works. * Improve owner/group detection error handling when container is stopped. * Fix formulation. Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com> * Improve file comparison. * Avoid reading whole file at once. * Stream when fetching files from daemon. * Fix comment. * Use read() instead of read1(). * Stream files when copying into container. * Linting. * Add force parameter. * Simplify library code. * Linting. * Add content and content_is_b64 options. * Make force=false work as for copy module: only copy if the destination does not exist. * Improve docs. * content should be no_log. * Implement diff mode. * Improve error handling. * Lint and improve. * Set owner/group ID to avoid ID lookup (which fails in paused containers). * Apply suggestions from code review Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com> Co-authored-by: Brian Scholer <1260690+briantist@users.noreply.github.com>
This commit is contained in:
@@ -68,11 +68,8 @@ options:
|
||||
type: integer
|
||||
'''
|
||||
|
||||
import io
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import tarfile
|
||||
|
||||
from ansible.errors import AnsibleFileNotFound, AnsibleConnectionFailure
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
@@ -82,6 +79,13 @@ from ansible.utils.display import Display
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
RequestException,
|
||||
)
|
||||
from ansible_collections.community.docker.plugins.module_utils.copy import (
|
||||
DockerFileCopyError,
|
||||
DockerFileNotFound,
|
||||
fetch_file,
|
||||
put_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.plugin_utils.socket_handler import (
|
||||
DockerSocketHandler,
|
||||
)
|
||||
@@ -89,7 +93,6 @@ from ansible_collections.community.docker.plugins.plugin_utils.common_api import
|
||||
AnsibleDockerClient,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.constants import DEFAULT_DATA_CHUNK_SIZE
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
|
||||
|
||||
MIN_DOCKER_API = None
|
||||
@@ -260,24 +263,12 @@ class Connection(ConnectionBase):
|
||||
remote_path = os.path.join(os.path.sep, remote_path)
|
||||
return os.path.normpath(remote_path)
|
||||
|
||||
def _put_archive(self, container, path, data):
|
||||
# data can also be file object for streaming. This is because _put uses requests's put().
|
||||
# See https://2.python-requests.org/en/master/user/advanced/#streaming-uploads
|
||||
# WARNING: might not work with all transports!
|
||||
url = self.client._url('/containers/{0}/archive', container)
|
||||
res = self.client._put(url, params={'path': path}, data=data)
|
||||
self.client._raise_for_status(res)
|
||||
return res.status_code == 200
|
||||
|
||||
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.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')):
|
||||
raise AnsibleFileNotFound(
|
||||
"file or module does not exist: %s" % to_native(in_path))
|
||||
|
||||
if self.actual_user not in self.ids:
|
||||
dummy, ids, dummy = self.exec_command(b'id -u && id -g')
|
||||
@@ -294,43 +285,25 @@ class Connection(ConnectionBase):
|
||||
.format(e, self.get_option('remote_addr'), ids)
|
||||
)
|
||||
|
||||
b_in_path = to_bytes(in_path, errors='surrogate_or_strict')
|
||||
|
||||
out_dir, out_file = os.path.split(out_path)
|
||||
|
||||
# TODO: stream tar file, instead of creating it in-memory into a BytesIO
|
||||
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode='w|', dereference=True, encoding='utf-8') as tar:
|
||||
# Note that without both name (bytes) and arcname (unicode), this either fails for
|
||||
# Python 2.7, Python 3.5/3.6, or Python 3.7+. Only when passing both (in this
|
||||
# form) it works with Python 2.7, 3.5, 3.6, and 3.7 up to 3.11
|
||||
tarinfo = tar.gettarinfo(b_in_path, arcname=to_text(out_file))
|
||||
user_id, group_id = self.ids[self.actual_user]
|
||||
tarinfo.uid = user_id
|
||||
tarinfo.uname = ''
|
||||
if self.actual_user:
|
||||
tarinfo.uname = self.actual_user
|
||||
tarinfo.gid = group_id
|
||||
tarinfo.gname = ''
|
||||
tarinfo.mode &= 0o700
|
||||
with open(b_in_path, 'rb') as f:
|
||||
tar.addfile(tarinfo, fileobj=f)
|
||||
data = bio.getvalue()
|
||||
|
||||
ok = self._call_client(
|
||||
lambda: self._put_archive(
|
||||
self.get_option('remote_addr'),
|
||||
out_dir,
|
||||
data,
|
||||
),
|
||||
not_found_can_be_resource=True,
|
||||
)
|
||||
if not ok:
|
||||
raise AnsibleConnectionFailure(
|
||||
'Unknown error while creating file "{0}" in container "{1}".'
|
||||
.format(out_path, self.get_option('remote_addr'))
|
||||
user_id, group_id = self.ids[self.actual_user]
|
||||
try:
|
||||
self._call_client(
|
||||
lambda: put_file(
|
||||
self.client,
|
||||
container=self.get_option('remote_addr'),
|
||||
in_path=in_path,
|
||||
out_path=out_path,
|
||||
user_id=user_id,
|
||||
group_id=group_id,
|
||||
user_name=self.actual_user,
|
||||
follow_links=True,
|
||||
),
|
||||
not_found_can_be_resource=True,
|
||||
)
|
||||
except DockerFileNotFound as exc:
|
||||
raise AnsibleFileNotFound(to_native(exc))
|
||||
except DockerFileCopyError as exc:
|
||||
raise AnsibleConnectionFailure(to_native(exc))
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
""" Fetch a file from container to local. """
|
||||
@@ -338,55 +311,23 @@ class Connection(ConnectionBase):
|
||||
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')
|
||||
|
||||
considered_in_paths = set()
|
||||
|
||||
while True:
|
||||
if in_path in considered_in_paths:
|
||||
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.get_option('remote_addr'))
|
||||
stream = self._call_client(
|
||||
lambda: self.client.get_raw_stream(
|
||||
'/containers/{0}/archive', self.get_option('remote_addr'),
|
||||
params={'path': in_path},
|
||||
headers={'Accept-Encoding': 'identity'},
|
||||
try:
|
||||
self._call_client(
|
||||
lambda: fetch_file(
|
||||
self.client,
|
||||
container=self.get_option('remote_addr'),
|
||||
in_path=in_path,
|
||||
out_path=out_path,
|
||||
follow_links=True,
|
||||
log=lambda msg: display.vvvv(msg, host=self.get_option('remote_addr')),
|
||||
),
|
||||
not_found_can_be_resource=True,
|
||||
)
|
||||
|
||||
# TODO: stream tar file instead of downloading it into a BytesIO
|
||||
|
||||
bio = io.BytesIO()
|
||||
for chunk in stream:
|
||||
bio.write(chunk)
|
||||
bio.seek(0)
|
||||
|
||||
with tarfile.open(fileobj=bio, mode='r|') as tar:
|
||||
symlink_member = None
|
||||
first = True
|
||||
for member in tar:
|
||||
if not first:
|
||||
raise AnsibleConnectionFailure('Received tarfile contains more than one file!')
|
||||
first = False
|
||||
if member.issym():
|
||||
symlink_member = member
|
||||
continue
|
||||
if not member.isfile():
|
||||
raise AnsibleConnectionFailure('Remote file "%s" is not a regular file or a symbolic link' % in_path)
|
||||
in_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
|
||||
with open(b_out_path, 'wb') as out_f:
|
||||
shutil.copyfileobj(in_f, out_f, member.size)
|
||||
if first:
|
||||
raise AnsibleConnectionFailure('Received tarfile is empty!')
|
||||
# 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.get_option('remote_addr'))
|
||||
continue
|
||||
return
|
||||
except DockerFileNotFound as exc:
|
||||
raise AnsibleFileNotFound(to_native(exc))
|
||||
except DockerFileCopyError as exc:
|
||||
raise AnsibleConnectionFailure(to_native(exc))
|
||||
|
||||
def close(self):
|
||||
""" Terminate the connection. Nothing to do for Docker"""
|
||||
|
||||
Reference in New Issue
Block a user