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

Inject /etc/passwd and friends instead of .scubainit #24

Merged
merged 5 commits into from
Jan 8, 2016
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Added `--verbose` option

### Removed
- umask is no longer set in the container. (See [#24])

### Fixed
- Problems with Ctrl+C in images are fixed. The user command now runs
as PID 1 again, as there is no more `.scubainit` script.


## [1.3.0] - 2016-01-07
### Added
- Set umask in container to the same as the host (local Docker only)
Expand Down Expand Up @@ -68,3 +76,4 @@ First versioned release
[1.0.0]: https://github.com/JonathonReinhart/scuba/compare/v0.1.0...v1.0.0

[issue 11]: https://github.com/JonathonReinhart/scuba/issues/11
[#24]: https://github.com/JonathonReinhart/scuba/pull/24
167 changes: 85 additions & 82 deletions src/scuba
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ def verbose_msg(fmt, *args):
if g_verbose:
appmsg(fmt, *args)

class FileCleanup(object):
def __init__(self, skip=False):
self.files = []
if not skip:
atexit.register(self.__cleanup)

def add(self, path):
self.files.append(path)

def __cleanup(self):
for f in self.files:
try:
os.remove(f)
except OSError:
pass


# http://stackoverflow.com/a/9577670
class Loader(yaml.Loader):
Expand Down Expand Up @@ -172,94 +188,85 @@ def group_entry(groupname, password, gid, users=[]):
gid = gid,
users = ','.join(users))

def shell_quote(s):
# http://stackoverflow.com/a/847800/119527
return pipes.quote(s)
def shadow_entry(username, **kw):
return '{username}:{password}:{lstchg}:{minchg}:{maxchg}:{warn}:{inact}:{expire}:{flag}'.format(
username = username,
password = kw.get('password', '*'),
lstchg = kw.get('lstchg', ''),
minchg = kw.get('minchg', ''),
maxchg = kw.get('maxchg', ''),
warn = kw.get('warn', ''),
inact = kw.get('inact', ''),
expire = kw.get('expire', ''),
flag = '',
)

def get_image_command(image):
'''Gets the default command for an image'''
args = ['docker', 'inspect', image]
try:
p = subprocess.Popen(args, stdout = subprocess.PIPE)
except OSError as e:
if e.errno == errno.ENOENT:
appmsg('Failed to execute docker. Is it installed?')
sys.exit(2)

stdout, _ = p.communicate()
if not p.returncode == 0:
appmsg('Failed to inspect image')
sys.exit(2)
def get_native_opts():
opts = []

info = json.loads(stdout)[0]
return info['Config']['Cmd']
uid = os.getuid()
gid = os.getgid()

def get_umask():
# Same logic as bash/builtins/umask.def
val = os.umask(022)
os.umask(val)
return val
opts.append('--user={uid}:{gid}'.format(uid=uid, gid=gid))

def generate_scubainit(config, command):
'''Generate a .scubainit script
def writeln(f, line):
f.write(line + '\n')

This script is executed by /bin/sh, as passed to "docker run".
It is responsible for setting up the environment, and running the
user-specified command. Normally, if the user provides no command to
"docker run", the image's default CMD is run. Because we force
"/bin/sh .scubainit" to be run, scuba must emulate the default behavior
itself.
'''
if len(command) == 0:
# No user-provided command; we want to run the image's default command
verbose_msg('No user command; getting command from image')
command = get_image_command(config['image'])
verbose_msg('{0} Cmd: "{1}"'.format(config['image'], command))

# Turn command into a string which you would enter in a shell
command = ' '.join(shell_quote(c) for c in command)

# Open a temporary file
with NamedTemporaryFile(prefix='scubainit', delete=False) as f:
# Delete this file when scuba exits
atexit.register(os.remove, f.name)

def write_cmd(*args):
f.write(' '.join(c for c in args) + '\n')

write_cmd('#!/bin/sh')

# Add scubauser with current uid/gid
# BusyBox only has 'adduser', with arguments incompatible with
# that of the standard 'useradd'.
# Instead, we'll just write the entry ourselves.
entry = passwd_entry(
# /etc/passwd
with NamedTemporaryFile(prefix='scuba', delete=False) as f:
cleanup.add(f.name)
opts.append(make_vol_opt(f.name, '/etc/passwd', 'z'))

writeln(f, passwd_entry(
username = 'root',
password = 'x',
uid = 0,
gid = 0,
gecos = 'root',
homedir = '/root',
shell = '/bin/sh',
))

writeln(f, passwd_entry(
username = SCUBA_USER,
password = 'x',
uid = os.getuid(),
gid = os.getgid(),
uid = uid,
gid = gid,
gecos = 'Scuba User',
homedir = '/', # Docker sets $HOME=/
shell = '/bin/sh',
)
write_cmd('echo', shell_quote(entry), '>>', '/etc/passwd')
))

# /etc/group
with NamedTemporaryFile(prefix='scuba', delete=False) as f:
cleanup.add(f.name)
opts.append(make_vol_opt(f.name, '/etc/group', 'z'))

# Add scubauser group
entry = group_entry(
writeln(f, group_entry(
groupname = 'root',
password = 'x',
gid = 0,
))

writeln(f, group_entry(
groupname = SCUBA_GROUP,
password = 'x',
gid = os.getgid(),
users = [SCUBA_USER],
)
write_cmd('echo', shell_quote(entry), '>>', '/etc/group')
gid = gid,
))

# Set the umask
write_cmd('umask', oct(get_umask()))
# /etc/shadow
with NamedTemporaryFile(prefix='scuba', delete=False) as f:
cleanup.add(f.name)
opts.append(make_vol_opt(f.name, '/etc/shadow', 'z'))

# Execute the command indicated by the user, as the scuba user
write_cmd('su', SCUBA_USER, '-c', shell_quote(command))
writeln(f, shadow_entry(
username = 'root',
))
writeln(f, shadow_entry(
username = SCUBA_USER,
))

return f.name
return opts

def parse_args():
ap = argparse.ArgumentParser(description='Simple Container-Utilizing Build Apparatus')
Expand All @@ -276,6 +283,9 @@ def parse_args():
def main():
args = parse_args()

global cleanup
cleanup = FileCleanup()

# top_path is where .scuba.yml is found, and becomes the top of our bind mount.
# top_rel is the relative path from top_path to the current working directory,
# and is where we'll set the working directory in the container (relative to
Expand Down Expand Up @@ -309,19 +319,12 @@ def main():

We want files created inside the container (in scubaroot) to appear to the
host as if they were created there (owned by the same uid/gid, with same
umask, etc.) So, we use a .scubainit script to create a user with the same
uid/gid, su(1) to that user, and run the usercommand.
umask, etc.)
'''
verbose_msg('Docker running natively')

# Generate a .scubainit script
scubainit_path = generate_scubainit(config, usercmd)

# Mount scubainit script
docker_opts = [make_vol_opt(scubainit_path, '/.scubainit', 'z')]

# Run the .scubainit at startup
docker_cmd = ['/bin/sh', '/.scubainit']
docker_opts = get_native_opts()
docker_cmd = usercmd

# NOTE: This tells Docker to re-label the directory for compatibility
# with SELinux. See `man docker-run` for more information.
Expand Down