Use docker exec -u when needed and if supported.
If remote_user is given and cannot be set in docker, a warning will be displayed unless the default container user matches remote_user.
This commit is contained in:
parent
60c943997b
commit
ea1a6c56b9
1 changed files with 69 additions and 33 deletions
|
@ -74,15 +74,31 @@ class Connection(ConnectionBase):
|
||||||
if not self.docker_cmd:
|
if not self.docker_cmd:
|
||||||
raise AnsibleError("docker command not found in PATH")
|
raise AnsibleError("docker command not found in PATH")
|
||||||
|
|
||||||
self.can_copy_bothways = False
|
|
||||||
|
|
||||||
docker_version = self._get_docker_version()
|
docker_version = self._get_docker_version()
|
||||||
if LooseVersion(docker_version) < LooseVersion('1.3'):
|
if LooseVersion(docker_version) < LooseVersion('1.3'):
|
||||||
raise AnsibleError('docker connection type requires docker 1.3 or higher')
|
raise AnsibleError('docker connection type requires docker 1.3 or higher')
|
||||||
# Docker cp in 1.8.0 sets the owner and group to root rather than the
|
|
||||||
# user that the docker container is set to use by default.
|
# The remote user we will request from docker (if supported)
|
||||||
#if LooseVersion(docker_version) >= LooseVersion('1.8.0'):
|
self.remote_user = None
|
||||||
# self.can_copy_bothways = True
|
# The actual user which will execute commands in docker (if known)
|
||||||
|
self.actual_user = None
|
||||||
|
|
||||||
|
if self._play_context.remote_user is not None:
|
||||||
|
if LooseVersion(docker_version) >= LooseVersion('1.7'):
|
||||||
|
# Support for specifying the exec user was added in docker 1.7
|
||||||
|
self.remote_user = self._play_context.remote_user
|
||||||
|
self.actual_user = self.remote_user
|
||||||
|
else:
|
||||||
|
self.actual_user = self._get_docker_remote_user()
|
||||||
|
|
||||||
|
if self.actual_user != self._play_context.remote_user:
|
||||||
|
display.warning('docker {0} does not support remote_user, using container default: {1}'
|
||||||
|
.format(docker_version, self.actual_user or '?'))
|
||||||
|
elif self._display.verbosity > 2:
|
||||||
|
# Since we're not setting the actual_user, look it up so we have it for logging later
|
||||||
|
# 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
|
||||||
|
self.actual_user = self._get_docker_remote_user()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanitize_version(version):
|
def _sanitize_version(version):
|
||||||
|
@ -107,12 +123,43 @@ class Connection(ConnectionBase):
|
||||||
|
|
||||||
return self._sanitize_version(cmd_output)
|
return self._sanitize_version(cmd_output)
|
||||||
|
|
||||||
|
def _get_docker_remote_user(self):
|
||||||
|
""" Get the default user configured in the docker container """
|
||||||
|
p = subprocess.Popen([self.docker_cmd, 'inspect', '--format', '{{.Config.User}}', self._play_context.remote_addr],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
|
||||||
|
out, err = p.communicate()
|
||||||
|
|
||||||
|
if p.returncode != 0:
|
||||||
|
display.warning('unable to retrieve default user from docker container: %s' % out + err)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# The default exec user is root, unless it was changed in the Dockerfile with USER
|
||||||
|
return out.strip() or 'root'
|
||||||
|
|
||||||
|
def _build_exec_cmd(self, cmd):
|
||||||
|
""" Build the local docker exec command to run cmd on remote_host
|
||||||
|
|
||||||
|
If remote_user is available and is supported by the docker
|
||||||
|
version we are using, it will be provided to docker exec.
|
||||||
|
"""
|
||||||
|
|
||||||
|
local_cmd = [self.docker_cmd, 'exec']
|
||||||
|
|
||||||
|
if self.remote_user is not None:
|
||||||
|
local_cmd += ['-u', self.remote_user]
|
||||||
|
|
||||||
|
# -i is needed to keep stdin open which allows pipelining to work
|
||||||
|
local_cmd += ['-i', self._play_context.remote_addr] + cmd
|
||||||
|
|
||||||
|
return local_cmd
|
||||||
|
|
||||||
def _connect(self, port=None):
|
def _connect(self, port=None):
|
||||||
""" Connect to the container. Nothing to do """
|
""" Connect to the container. Nothing to do """
|
||||||
super(Connection, self)._connect()
|
super(Connection, self)._connect()
|
||||||
if not self._connected:
|
if not self._connected:
|
||||||
display.vvv(u"ESTABLISH DOCKER CONNECTION FOR USER: {0}".format(
|
display.vvv(u"ESTABLISH DOCKER CONNECTION FOR USER: {0}".format(
|
||||||
self._play_context.remote_user, host=self._play_context.remote_addr)
|
self.actual_user or '?', host=self._play_context.remote_addr)
|
||||||
)
|
)
|
||||||
self._connected = True
|
self._connected = True
|
||||||
|
|
||||||
|
@ -120,9 +167,7 @@ class Connection(ConnectionBase):
|
||||||
""" Run a command on the docker host """
|
""" Run a command on the docker host """
|
||||||
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||||||
|
|
||||||
executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else '/bin/sh'
|
local_cmd = self._build_exec_cmd([self._play_context.executable, '-c', cmd])
|
||||||
# -i is needed to keep stdin open which allows pipelining to work
|
|
||||||
local_cmd = [self.docker_cmd, "exec", '-u', self._play_context.remote_user, '-i', self._play_context.remote_addr, executable, '-c', cmd]
|
|
||||||
|
|
||||||
display.vvv("EXEC %s" % (local_cmd,), host=self._play_context.remote_addr)
|
display.vvv("EXEC %s" % (local_cmd,), host=self._play_context.remote_addr)
|
||||||
local_cmd = [to_bytes(i, errors='strict') for i in local_cmd]
|
local_cmd = [to_bytes(i, errors='strict') for i in local_cmd]
|
||||||
|
@ -156,32 +201,23 @@ class Connection(ConnectionBase):
|
||||||
raise AnsibleFileNotFound(
|
raise AnsibleFileNotFound(
|
||||||
"file or module does not exist: %s" % in_path)
|
"file or module does not exist: %s" % in_path)
|
||||||
|
|
||||||
if self.can_copy_bothways:
|
out_path = pipes.quote(out_path)
|
||||||
# only docker >= 1.8.1 can do this natively
|
# Older docker doesn't have native support for copying files into
|
||||||
args = [ self.docker_cmd, "cp", in_path, "%s:%s" % (self._play_context.remote_addr, out_path) ]
|
# running containers, so we use docker exec to implement this
|
||||||
args = [to_bytes(i, errors='strict') for i in args]
|
# Although docker version 1.8 and later provide support, the
|
||||||
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
# owner and group of the files are always set to root
|
||||||
|
args = self._build_exec_cmd([self._play_context.executable, "-c", "dd of=%s bs=%s" % (out_path, BUFSIZE)])
|
||||||
|
args = [to_bytes(i, errors='strict') for i in args]
|
||||||
|
with open(to_bytes(in_path, errors='strict'), 'rb') as in_file:
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(args, stdin=in_file,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
except OSError:
|
||||||
|
raise AnsibleError("docker connection requires dd command in the container to put files")
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
||||||
else:
|
|
||||||
out_path = pipes.quote(out_path)
|
|
||||||
# Older docker doesn't have native support for copying files into
|
|
||||||
# running containers, so we use docker exec to implement this
|
|
||||||
executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else '/bin/sh'
|
|
||||||
args = [self.docker_cmd, "exec", '-u', self._play_context.remote_user, "-i", self._play_context.remote_addr, executable, "-c",
|
|
||||||
"dd of=%s bs=%s" % (out_path, BUFSIZE)]
|
|
||||||
args = [to_bytes(i, errors='strict') for i in args]
|
|
||||||
with open(to_bytes(in_path, errors='strict'), 'rb') as in_file:
|
|
||||||
try:
|
|
||||||
p = subprocess.Popen(args, stdin=in_file,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
except OSError:
|
|
||||||
raise AnsibleError("docker connection with docker < 1.8.1 requires dd command in the chroot")
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
|
||||||
raise AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
|
|
||||||
|
|
||||||
def fetch_file(self, in_path, out_path):
|
def fetch_file(self, in_path, out_path):
|
||||||
""" Fetch a file from container to local. """
|
""" Fetch a file from container to local. """
|
||||||
|
|
Loading…
Reference in a new issue