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:
@@ -0,0 +1,869 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
|
||||
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: docker_container_copy_into
|
||||
|
||||
short_description: Copy a file into a Docker container
|
||||
|
||||
description:
|
||||
- Copy a file into a Docker container.
|
||||
- Similar to C(docker cp).
|
||||
- To copy files in a non-running container, you must provide the I(owner_id) and I(group_id) options.
|
||||
This is also necessary if the container does not contain a C(/bin/sh) shell with an C(id) tool.
|
||||
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: full
|
||||
details:
|
||||
- Additional data will need to be transferred to compute diffs.
|
||||
- The module uses R(the MAX_FILE_SIZE_FOR_DIFF ansible-core configuration,MAX_FILE_SIZE_FOR_DIFF)
|
||||
to determine for how large files diffs should be computed.
|
||||
|
||||
options:
|
||||
container:
|
||||
description:
|
||||
- The name of the container to copy files to.
|
||||
type: str
|
||||
required: true
|
||||
path:
|
||||
description:
|
||||
- Path to a file on the managed node.
|
||||
- Mutually exclusive with I(content). One of I(content) and I(path) is required.
|
||||
type: path
|
||||
content:
|
||||
description:
|
||||
- The file's content.
|
||||
- If you plan to provide binary data, provide it pre-encoded to base64, and set I(content_is_b64=true).
|
||||
- Mutually exclusive with I(path). One of I(content) and I(path) is required.
|
||||
type: str
|
||||
content_is_b64:
|
||||
description:
|
||||
- If set to C(true), the content in I(content) is assumed to be Base64 encoded and
|
||||
will be decoded before being used.
|
||||
- To use binary I(content), it is better to keep it Base64 encoded and let it
|
||||
be decoded by this option. Otherwise you risk the data to be interpreted as
|
||||
UTF-8 and corrupted.
|
||||
type: bool
|
||||
default: false
|
||||
container_path:
|
||||
description:
|
||||
- Path to a file inside the Docker container.
|
||||
- Must be an absolute path.
|
||||
type: str
|
||||
required: true
|
||||
follow:
|
||||
description:
|
||||
- This flag indicates that filesystem links in the Docker container, if they exist, should be followed.
|
||||
type: bool
|
||||
default: false
|
||||
local_follow:
|
||||
description:
|
||||
- This flag indicates that filesystem links in the source tree (where the module is executed), if they exist, should be followed.
|
||||
type: bool
|
||||
default: true
|
||||
owner_id:
|
||||
description:
|
||||
- The owner ID to use when writing the file to disk.
|
||||
- If provided, I(group_id) must also be provided.
|
||||
- If not provided, the module will try to determine the user and group ID for the current user in the container.
|
||||
This will only work if C(/bin/sh) is present in the container and the C(id) binary or shell builtin is available.
|
||||
Also the container must be running.
|
||||
type: int
|
||||
group_id:
|
||||
description:
|
||||
- The group ID to use when writing the file to disk.
|
||||
- If provided, I(owner_id) must also be provided.
|
||||
- If not provided, the module will try to determine the user and group ID for the current user in the container.
|
||||
This will only work if C(/bin/sh) is present in the container and the C(id) binary or shell builtin is available.
|
||||
Also the container must be running.
|
||||
type: int
|
||||
mode:
|
||||
description:
|
||||
- The file mode to use when writing the file to disk.
|
||||
- Will use the file's mode from the source system if this option is not provided.
|
||||
type: int
|
||||
force:
|
||||
description:
|
||||
- If set to C(true), force writing the file (without performing any idempotency checks).
|
||||
- If set to C(false), only write the file if it does not exist on the target. If a filesystem object exists at
|
||||
the destination, the module will not do any change.
|
||||
- If this option is not specified, the module will be idempotent. To verify idempotency, it will try to get information
|
||||
on the filesystem object in the container, and if everything seems to match will download the file from the container
|
||||
to compare it to the file to upload.
|
||||
type: bool
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.docker.docker.api_documentation
|
||||
- community.docker.attributes
|
||||
- community.docker.attributes.actiongroup_docker
|
||||
|
||||
author:
|
||||
- "Felix Fontein (@felixfontein)"
|
||||
|
||||
requirements:
|
||||
- "Docker API >= 1.25"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Copy a file into the container
|
||||
community.docker.docker_container_copy_into:
|
||||
container: mydata
|
||||
path: /home/user/data.txt
|
||||
container_path: /data/input.txt
|
||||
|
||||
- name: Copy a file into the container with owner, group, and mode set
|
||||
community.docker.docker_container_copy_into:
|
||||
container: mydata
|
||||
path: /home/user/bin/runme.o
|
||||
container_path: /bin/runme
|
||||
owner: 0 # root
|
||||
group: 0 # root
|
||||
mode: 0o755 # readable and executable by all users, writable by root
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
container_path:
|
||||
description:
|
||||
- The actual path in the container.
|
||||
- Can only be different from I(container_path) when I(follow=true).
|
||||
type: str
|
||||
returned: success
|
||||
'''
|
||||
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, DockerException, NotFound
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.common_api import (
|
||||
AnsibleDockerClient,
|
||||
RequestException,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils.copy import (
|
||||
DockerFileCopyError,
|
||||
DockerFileNotFound,
|
||||
DockerUnexpectedError,
|
||||
determine_user_group,
|
||||
fetch_file_ex,
|
||||
put_file,
|
||||
put_file_content,
|
||||
stat_file,
|
||||
)
|
||||
|
||||
from ansible_collections.community.docker.plugins.module_utils._scramble import generate_insecure_key, scramble
|
||||
|
||||
|
||||
def are_fileobjs_equal(f1, f2):
|
||||
'''Given two (buffered) file objects, compare their contents.'''
|
||||
blocksize = 65536
|
||||
b1buf = b''
|
||||
b2buf = b''
|
||||
while True:
|
||||
if f1 and len(b1buf) < blocksize:
|
||||
f1b = f1.read(blocksize)
|
||||
if not f1b:
|
||||
# f1 is EOF, so stop reading from it
|
||||
f1 = None
|
||||
b1buf += f1b
|
||||
if f2 and len(b2buf) < blocksize:
|
||||
f2b = f2.read(blocksize)
|
||||
if not f2b:
|
||||
# f2 is EOF, so stop reading from it
|
||||
f2 = None
|
||||
b2buf += f2b
|
||||
if not b1buf or not b2buf:
|
||||
# At least one of f1 and f2 is EOF and all its data has
|
||||
# been processed. If both are EOF and their data has been
|
||||
# processed, the files are equal, otherwise not.
|
||||
return not b1buf and not b2buf
|
||||
# Compare the next chunk of data, and remove it from the buffers
|
||||
buflen = min(len(b1buf), len(b2buf))
|
||||
if b1buf[:buflen] != b2buf[:buflen]:
|
||||
return False
|
||||
b1buf = b1buf[buflen:]
|
||||
b2buf = b2buf[buflen:]
|
||||
|
||||
|
||||
def are_fileobjs_equal_read_first(f1, f2):
|
||||
'''Given two (buffered) file objects, compare their contents.
|
||||
|
||||
Returns a tuple (is_equal, content_of_f1), where the first element indicates
|
||||
whether the two file objects have the same content, and the second element is
|
||||
the content of the first file object.'''
|
||||
blocksize = 65536
|
||||
b1buf = b''
|
||||
b2buf = b''
|
||||
is_equal = True
|
||||
content = []
|
||||
while True:
|
||||
if f1 and len(b1buf) < blocksize:
|
||||
f1b = f1.read(blocksize)
|
||||
if not f1b:
|
||||
# f1 is EOF, so stop reading from it
|
||||
f1 = None
|
||||
b1buf += f1b
|
||||
if f2 and len(b2buf) < blocksize:
|
||||
f2b = f2.read(blocksize)
|
||||
if not f2b:
|
||||
# f2 is EOF, so stop reading from it
|
||||
f2 = None
|
||||
b2buf += f2b
|
||||
if not b1buf or not b2buf:
|
||||
# At least one of f1 and f2 is EOF and all its data has
|
||||
# been processed. If both are EOF and their data has been
|
||||
# processed, the files are equal, otherwise not.
|
||||
is_equal = not b1buf and not b2buf
|
||||
break
|
||||
# Compare the next chunk of data, and remove it from the buffers
|
||||
buflen = min(len(b1buf), len(b2buf))
|
||||
if b1buf[:buflen] != b2buf[:buflen]:
|
||||
is_equal = False
|
||||
break
|
||||
content.append(b1buf[:buflen])
|
||||
b1buf = b1buf[buflen:]
|
||||
b2buf = b2buf[buflen:]
|
||||
|
||||
content.append(b1buf)
|
||||
if f1:
|
||||
content.append(f1.read())
|
||||
|
||||
return is_equal, b''.join(content)
|
||||
|
||||
|
||||
def is_container_file_not_regular_file(container_stat):
|
||||
for bit in (
|
||||
# https://pkg.go.dev/io/fs#FileMode
|
||||
32 - 1, # ModeDir
|
||||
32 - 4, # ModeTemporary
|
||||
32 - 5, # ModeSymlink
|
||||
32 - 6, # ModeDevice
|
||||
32 - 7, # ModeNamedPipe
|
||||
32 - 8, # ModeSocket
|
||||
32 - 11, # ModeCharDevice
|
||||
32 - 13, # ModeIrregular
|
||||
):
|
||||
if container_stat['mode'] & (1 << bit) != 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_container_file_mode(container_stat):
|
||||
mode = container_stat['mode'] & 0xFFF
|
||||
if container_stat['mode'] & (1 << (32 - 9)) != 0: # ModeSetuid
|
||||
mode |= stat.S_ISUID # set UID bit
|
||||
if container_stat['mode'] & (1 << (32 - 10)) != 0: # ModeSetgid
|
||||
mode |= stat.S_ISGID # set GID bit
|
||||
if container_stat['mode'] & (1 << (32 - 12)) != 0: # ModeSticky
|
||||
mode |= stat.S_ISVTX # sticky bit
|
||||
return mode
|
||||
|
||||
|
||||
def add_other_diff(diff, in_path, member):
|
||||
if diff is None:
|
||||
return
|
||||
diff['before_header'] = in_path
|
||||
if member.isdir():
|
||||
diff['before'] = '(directory)'
|
||||
elif member.issym() or member.islnk():
|
||||
diff['before'] = member.linkname
|
||||
elif member.ischr():
|
||||
diff['before'] = '(character device)'
|
||||
elif member.isblk():
|
||||
diff['before'] = '(block device)'
|
||||
elif member.isfifo():
|
||||
diff['before'] = '(fifo)'
|
||||
elif member.isdev():
|
||||
diff['before'] = '(device)'
|
||||
elif member.isfile():
|
||||
raise DockerUnexpectedError('should not be a regular file')
|
||||
else:
|
||||
diff['before'] = '(unknown filesystem object)'
|
||||
|
||||
|
||||
def retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat=None, link_target=None):
|
||||
if diff is None:
|
||||
return
|
||||
if regular_stat is not None:
|
||||
# First handle all filesystem object types that are not regular files
|
||||
if regular_stat['mode'] & (1 << (32 - 1)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(directory)'
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 4)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(temporary file)'
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 5)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = link_target
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 6)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(device)'
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 7)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(named pipe)'
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 8)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(socket)'
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 11)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(character device)'
|
||||
return
|
||||
elif regular_stat['mode'] & (1 << (32 - 13)) != 0:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = '(unknown filesystem object)'
|
||||
return
|
||||
# Check whether file is too large
|
||||
if regular_stat['size'] > max_file_size_for_diff > 0:
|
||||
diff['dst_larger'] = max_file_size_for_diff
|
||||
return
|
||||
|
||||
# We need to get hold of the content
|
||||
def process_none(in_path):
|
||||
diff['before'] = ''
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
|
||||
|
||||
def process_symlink(in_path, member):
|
||||
diff['before_header'] = in_path
|
||||
diff['before'] = member.linkname
|
||||
|
||||
def process_other(in_path, member):
|
||||
add_other_diff(diff, in_path, member)
|
||||
|
||||
fetch_file_ex(
|
||||
client,
|
||||
container,
|
||||
in_path=container_path,
|
||||
process_none=process_none,
|
||||
process_regular=process_regular,
|
||||
process_symlink=process_symlink,
|
||||
process_other=process_other,
|
||||
follow_links=follow_links,
|
||||
)
|
||||
|
||||
|
||||
def is_binary(content):
|
||||
if b'\x00' in content:
|
||||
return True
|
||||
# TODO: better detection
|
||||
# (ansible-core also just checks for 0x00, and even just sticks to the first 8k, so this isn't too bad...)
|
||||
return False
|
||||
|
||||
|
||||
def are_fileobjs_equal_with_diff_of_first(f1, f2, size, diff, max_file_size_for_diff, container_path):
|
||||
if diff is None:
|
||||
return are_fileobjs_equal(f1, f2)
|
||||
if size > max_file_size_for_diff > 0:
|
||||
diff['dst_larger'] = max_file_size_for_diff
|
||||
return are_fileobjs_equal(f1, f2)
|
||||
is_equal, content = are_fileobjs_equal_read_first(f1, f2)
|
||||
if is_binary(content):
|
||||
diff['dst_binary'] = 1
|
||||
else:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = to_text(content)
|
||||
return is_equal
|
||||
|
||||
|
||||
def add_diff_dst_from_regular_member(diff, max_file_size_for_diff, container_path, tar, member):
|
||||
if diff is None:
|
||||
return
|
||||
if member.size > max_file_size_for_diff > 0:
|
||||
diff['dst_larger'] = max_file_size_for_diff
|
||||
return
|
||||
|
||||
tar_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
|
||||
content = tar_f.read()
|
||||
if is_binary(content):
|
||||
diff['dst_binary'] = 1
|
||||
else:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = to_text(content)
|
||||
|
||||
|
||||
def copy_dst_to_src(diff):
|
||||
if diff is None:
|
||||
return
|
||||
for f, t in [
|
||||
('dst_size', 'src_size'),
|
||||
('dst_binary', 'src_binary'),
|
||||
('before_header', 'after_header'),
|
||||
('before', 'after'),
|
||||
]:
|
||||
if f in diff:
|
||||
diff[t] = diff[f]
|
||||
elif t in diff:
|
||||
diff.pop(t)
|
||||
|
||||
|
||||
def is_file_idempotent(client, container, managed_path, container_path, follow_links, local_follow_links, owner_id, group_id, mode,
|
||||
force=False, diff=None, max_file_size_for_diff=1):
|
||||
# Retrieve information of local file
|
||||
try:
|
||||
file_stat = os.stat(managed_path) if local_follow_links else os.lstat(managed_path)
|
||||
except OSError as exc:
|
||||
if exc.errno == 2:
|
||||
raise DockerFileNotFound('Cannot find local file {managed_path}'.format(managed_path=managed_path))
|
||||
raise
|
||||
if mode is None:
|
||||
mode = stat.S_IMODE(file_stat.st_mode)
|
||||
if not stat.S_ISLNK(file_stat.st_mode) and not stat.S_ISREG(file_stat.st_mode):
|
||||
raise DockerFileCopyError('Local path {managed_path} is not a symbolic link or file')
|
||||
|
||||
if diff is not None:
|
||||
if file_stat.st_size > max_file_size_for_diff > 0:
|
||||
diff['src_larger'] = max_file_size_for_diff
|
||||
elif stat.S_ISLNK(file_stat.st_mode):
|
||||
diff['after_header'] = managed_path
|
||||
diff['after'] = os.readlink(managed_path)
|
||||
else:
|
||||
with open(managed_path, 'rb') as f:
|
||||
content = f.read()
|
||||
if is_binary(content):
|
||||
diff['src_binary'] = 1
|
||||
else:
|
||||
diff['after_header'] = managed_path
|
||||
diff['after'] = to_text(content)
|
||||
|
||||
# When forcing and we're not following links in the container, go!
|
||||
if force and not follow_links:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
|
||||
return container_path, mode, False
|
||||
|
||||
# Resolve symlinks in the container (if requested), and get information on container's file
|
||||
real_container_path, regular_stat, link_target = stat_file(
|
||||
client,
|
||||
container,
|
||||
in_path=container_path,
|
||||
follow_links=follow_links,
|
||||
)
|
||||
|
||||
# Follow links in the Docker container?
|
||||
if follow_links:
|
||||
container_path = real_container_path
|
||||
|
||||
# If the file wasn't found, continue
|
||||
if regular_stat is None:
|
||||
if diff is not None:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = ''
|
||||
return container_path, mode, False
|
||||
|
||||
# When forcing, go!
|
||||
if force:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
|
||||
# If force is set to False, and the destination exists, assume there's nothing to do
|
||||
if force is False:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
copy_dst_to_src(diff)
|
||||
return container_path, mode, True
|
||||
|
||||
# Basic idempotency checks
|
||||
if stat.S_ISLNK(file_stat.st_mode):
|
||||
if link_target is None:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
local_link_target = os.readlink(managed_path)
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, local_link_target == link_target
|
||||
if link_target is not None:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
if is_container_file_not_regular_file(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
if file_stat.st_size != regular_stat['size']:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
if mode != get_container_file_mode(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
|
||||
# Fetch file from container
|
||||
def process_none(in_path):
|
||||
return container_path, mode, False
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
# Check things like user/group ID and mode
|
||||
if any([
|
||||
member.mode & 0xFFF != mode,
|
||||
member.uid != owner_id,
|
||||
member.gid != group_id,
|
||||
not stat.S_ISREG(file_stat.st_mode),
|
||||
member.size != file_stat.st_size,
|
||||
]):
|
||||
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
|
||||
return container_path, mode, False
|
||||
|
||||
tar_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
|
||||
with open(managed_path, 'rb') as local_f:
|
||||
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path)
|
||||
return container_path, mode, is_equal
|
||||
|
||||
def process_symlink(in_path, member):
|
||||
if diff is not None:
|
||||
diff['before_header'] = in_path
|
||||
diff['before'] = member.linkname
|
||||
|
||||
# Check things like user/group ID and mode
|
||||
if member.mode & 0xFFF != mode:
|
||||
return container_path, mode, False
|
||||
if member.uid != owner_id:
|
||||
return container_path, mode, False
|
||||
if member.gid != group_id:
|
||||
return container_path, mode, False
|
||||
|
||||
if not stat.S_ISLNK(file_stat.st_mode):
|
||||
return container_path, mode, False
|
||||
|
||||
local_link_target = os.readlink(managed_path)
|
||||
return container_path, mode, member.linkname == local_link_target
|
||||
|
||||
def process_other(in_path, member):
|
||||
add_other_diff(diff, in_path, member)
|
||||
return container_path, mode, False
|
||||
|
||||
return fetch_file_ex(
|
||||
client,
|
||||
container,
|
||||
in_path=container_path,
|
||||
process_none=process_none,
|
||||
process_regular=process_regular,
|
||||
process_symlink=process_symlink,
|
||||
process_other=process_other,
|
||||
follow_links=follow_links,
|
||||
)
|
||||
|
||||
|
||||
def copy_file_into_container(client, container, managed_path, container_path, follow_links, local_follow_links,
|
||||
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
|
||||
if diff:
|
||||
diff = {}
|
||||
else:
|
||||
diff = None
|
||||
|
||||
container_path, mode, idempotent = is_file_idempotent(
|
||||
client,
|
||||
container,
|
||||
managed_path,
|
||||
container_path,
|
||||
follow_links,
|
||||
local_follow_links,
|
||||
owner_id,
|
||||
group_id,
|
||||
mode,
|
||||
force=force,
|
||||
diff=diff,
|
||||
max_file_size_for_diff=max_file_size_for_diff,
|
||||
)
|
||||
changed = not idempotent
|
||||
|
||||
if changed and not client.module.check_mode:
|
||||
put_file(
|
||||
client,
|
||||
container,
|
||||
in_path=managed_path,
|
||||
out_path=container_path,
|
||||
user_id=owner_id,
|
||||
group_id=group_id,
|
||||
mode=mode,
|
||||
follow_links=local_follow_links,
|
||||
)
|
||||
|
||||
result = dict(
|
||||
container_path=container_path,
|
||||
changed=changed,
|
||||
)
|
||||
if diff:
|
||||
result['diff'] = diff
|
||||
client.module.exit_json(**result)
|
||||
|
||||
|
||||
def is_content_idempotent(client, container, content, container_path, follow_links, owner_id, group_id, mode,
|
||||
force=False, diff=None, max_file_size_for_diff=1):
|
||||
if diff is not None:
|
||||
if len(content) > max_file_size_for_diff > 0:
|
||||
diff['src_larger'] = max_file_size_for_diff
|
||||
elif is_binary(content):
|
||||
diff['src_binary'] = 1
|
||||
else:
|
||||
diff['after_header'] = 'dynamically generated'
|
||||
diff['after'] = to_text(content)
|
||||
|
||||
# When forcing and we're not following links in the container, go!
|
||||
if force and not follow_links:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff)
|
||||
return container_path, mode, False
|
||||
|
||||
# Resolve symlinks in the container (if requested), and get information on container's file
|
||||
real_container_path, regular_stat, link_target = stat_file(
|
||||
client,
|
||||
container,
|
||||
in_path=container_path,
|
||||
follow_links=follow_links,
|
||||
)
|
||||
|
||||
# Follow links in the Docker container?
|
||||
if follow_links:
|
||||
container_path = real_container_path
|
||||
|
||||
# If the file wasn't found, continue
|
||||
if regular_stat is None:
|
||||
if diff is not None:
|
||||
diff['before_header'] = container_path
|
||||
diff['before'] = ''
|
||||
return container_path, mode, False
|
||||
|
||||
# When forcing, go!
|
||||
if force:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
|
||||
# If force is set to False, and the destination exists, assume there's nothing to do
|
||||
if force is False:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
copy_dst_to_src(diff)
|
||||
return container_path, mode, True
|
||||
|
||||
# Basic idempotency checks
|
||||
if link_target is not None:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
if is_container_file_not_regular_file(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
if len(content) != regular_stat['size']:
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
if mode != get_container_file_mode(regular_stat):
|
||||
retrieve_diff(client, container, container_path, follow_links, diff, max_file_size_for_diff, regular_stat, link_target)
|
||||
return container_path, mode, False
|
||||
|
||||
# Fetch file from container
|
||||
def process_none(in_path):
|
||||
if diff is not None:
|
||||
diff['before'] = ''
|
||||
return container_path, mode, False
|
||||
|
||||
def process_regular(in_path, tar, member):
|
||||
# Check things like user/group ID and mode
|
||||
if any([
|
||||
member.mode & 0xFFF != mode,
|
||||
member.uid != owner_id,
|
||||
member.gid != group_id,
|
||||
member.size != len(content),
|
||||
]):
|
||||
add_diff_dst_from_regular_member(diff, max_file_size_for_diff, in_path, tar, member)
|
||||
return container_path, mode, False
|
||||
|
||||
tar_f = tar.extractfile(member) # in Python 2, this *cannot* be used in `with`...
|
||||
is_equal = are_fileobjs_equal_with_diff_of_first(tar_f, io.BytesIO(content), member.size, diff, max_file_size_for_diff, in_path)
|
||||
return container_path, mode, is_equal
|
||||
|
||||
def process_symlink(in_path, member):
|
||||
if diff is not None:
|
||||
diff['before_header'] = in_path
|
||||
diff['before'] = member.linkname
|
||||
|
||||
return container_path, mode, False
|
||||
|
||||
def process_other(in_path, member):
|
||||
add_other_diff(diff, in_path, member)
|
||||
return container_path, mode, False
|
||||
|
||||
return fetch_file_ex(
|
||||
client,
|
||||
container,
|
||||
in_path=container_path,
|
||||
process_none=process_none,
|
||||
process_regular=process_regular,
|
||||
process_symlink=process_symlink,
|
||||
process_other=process_other,
|
||||
follow_links=follow_links,
|
||||
)
|
||||
|
||||
|
||||
def copy_content_into_container(client, container, content, container_path, follow_links,
|
||||
owner_id, group_id, mode, force=False, diff=False, max_file_size_for_diff=1):
|
||||
if diff:
|
||||
diff = {}
|
||||
else:
|
||||
diff = None
|
||||
|
||||
container_path, mode, idempotent = is_content_idempotent(
|
||||
client,
|
||||
container,
|
||||
content,
|
||||
container_path,
|
||||
follow_links,
|
||||
owner_id,
|
||||
group_id,
|
||||
mode,
|
||||
force=force,
|
||||
diff=diff,
|
||||
max_file_size_for_diff=max_file_size_for_diff,
|
||||
)
|
||||
changed = not idempotent
|
||||
|
||||
if changed and not client.module.check_mode:
|
||||
put_file_content(
|
||||
client,
|
||||
container,
|
||||
content=content,
|
||||
out_path=container_path,
|
||||
user_id=owner_id,
|
||||
group_id=group_id,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
result = dict(
|
||||
container_path=container_path,
|
||||
changed=changed,
|
||||
)
|
||||
if diff:
|
||||
# Since the content is no_log, make sure that the before/after strings look sufficiently different
|
||||
key = generate_insecure_key()
|
||||
diff['scrambled_diff'] = base64.b64encode(key)
|
||||
for k in ('before', 'after'):
|
||||
if k in diff:
|
||||
diff[k] = scramble(diff[k], key)
|
||||
result['diff'] = diff
|
||||
client.module.exit_json(**result)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
container=dict(type='str', required=True),
|
||||
path=dict(type='path'),
|
||||
container_path=dict(type='str', required=True),
|
||||
follow=dict(type='bool', default=False),
|
||||
local_follow=dict(type='bool', default=True),
|
||||
owner_id=dict(type='int'),
|
||||
group_id=dict(type='int'),
|
||||
mode=dict(type='int'),
|
||||
force=dict(type='bool'),
|
||||
content=dict(type='str', no_log=True),
|
||||
content_is_b64=dict(type='bool', default=False),
|
||||
|
||||
# Undocumented parameters for use by the action plugin
|
||||
_max_file_size_for_diff=dict(type='int'),
|
||||
)
|
||||
|
||||
client = AnsibleDockerClient(
|
||||
argument_spec=argument_spec,
|
||||
min_docker_api_version='1.20',
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[('path', 'content')],
|
||||
required_together=[('owner_id', 'group_id')],
|
||||
required_by={
|
||||
'content': ['mode'],
|
||||
},
|
||||
)
|
||||
|
||||
container = client.module.params['container']
|
||||
managed_path = client.module.params['path']
|
||||
container_path = client.module.params['container_path']
|
||||
follow = client.module.params['follow']
|
||||
local_follow = client.module.params['local_follow']
|
||||
owner_id = client.module.params['owner_id']
|
||||
group_id = client.module.params['group_id']
|
||||
mode = client.module.params['mode']
|
||||
force = client.module.params['force']
|
||||
content = client.module.params['content']
|
||||
max_file_size_for_diff = client.module.params['_max_file_size_for_diff'] or 1
|
||||
|
||||
if content is not None:
|
||||
if client.module.params['content_is_b64']:
|
||||
try:
|
||||
content = base64.b64decode(content)
|
||||
except Exception as e: # depending on Python version and error, multiple different exceptions can be raised
|
||||
client.fail('Cannot Base64 decode the content option: {0}'.format(e))
|
||||
else:
|
||||
content = to_bytes(content)
|
||||
|
||||
if not container_path.startswith(os.path.sep):
|
||||
container_path = os.path.join(os.path.sep, container_path)
|
||||
container_path = os.path.normpath(container_path)
|
||||
|
||||
try:
|
||||
if owner_id is None or group_id is None:
|
||||
owner_id, group_id = determine_user_group(client, container)
|
||||
|
||||
if content is not None:
|
||||
copy_content_into_container(
|
||||
client,
|
||||
container,
|
||||
content,
|
||||
container_path,
|
||||
follow_links=follow,
|
||||
owner_id=owner_id,
|
||||
group_id=group_id,
|
||||
mode=mode,
|
||||
force=force,
|
||||
diff=client.module._diff,
|
||||
max_file_size_for_diff=max_file_size_for_diff,
|
||||
)
|
||||
elif managed_path is not None:
|
||||
copy_file_into_container(
|
||||
client,
|
||||
container,
|
||||
managed_path,
|
||||
container_path,
|
||||
follow_links=follow,
|
||||
local_follow_links=local_follow,
|
||||
owner_id=owner_id,
|
||||
group_id=group_id,
|
||||
mode=mode,
|
||||
force=force,
|
||||
diff=client.module._diff,
|
||||
max_file_size_for_diff=max_file_size_for_diff,
|
||||
)
|
||||
else:
|
||||
# Can happen if a user explicitly passes `content: null` or `path: null`...
|
||||
client.fail('One of path and content must be supplied')
|
||||
except NotFound as exc:
|
||||
client.fail('Could not find container "{1}" or resource in it ({0})'.format(exc, container))
|
||||
except APIError as exc:
|
||||
client.fail('An unexpected Docker error occurred for container "{1}": {0}'.format(exc, container), exception=traceback.format_exc())
|
||||
except DockerException as exc:
|
||||
client.fail('An unexpected Docker error occurred for container "{1}": {0}'.format(exc, container), exception=traceback.format_exc())
|
||||
except RequestException as exc:
|
||||
client.fail(
|
||||
'An unexpected requests error occurred for container "{1}" when trying to talk to the Docker daemon: {0}'.format(exc, container),
|
||||
exception=traceback.format_exc())
|
||||
except DockerUnexpectedError as exc:
|
||||
client.fail('Unexpected error: {exc}'.format(exc=to_native(exc)), exception=traceback.format_exc())
|
||||
except DockerFileCopyError as exc:
|
||||
client.fail(to_native(exc))
|
||||
except OSError as exc:
|
||||
client.fail('Unexpected error: {exc}'.format(exc=to_native(exc)), exception=traceback.format_exc())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user