Repository: JonathonReinhart/scuba
Pull request #66 (2016-07-12 02:53:57)

Execute user command via shell
See #65

Note: User command will no longer be running as PID 1


Patch:
diff --git a/scuba/__main__.py b/scuba/__main__.py
index 7650efd..a1e2c7a 100644
--- a/scuba/__main__.py
+++ b/scuba/__main__.py
@@ -246,7 +246,14 @@ class ScubaDive(object):
                 raise ScubaError(str(e))
             verbose_msg('{0} Cmd: "{1}"'.format(self.config.image, cmd))
 
-        self.docker_cmd = cmd
+        # The user command is executed via a generated shell script
+        with self.open_scubadir_file('command.sh', 'wt') as f:
+            self.docker_cmd = ['/bin/sh', f.container_path]
+            writeln(f, '#!/bin/sh')
+            writeln(f, '# Auto-generated from scuba')
+            writeln(f, 'set -e')
+            writeln(f, shell_quote_cmd(cmd))
+
 
     def open_scubadir_file(self, name, mode):
         '''Opens a file in the 'scubadir'
@@ -257,6 +264,9 @@ class ScubaDive(object):
         path = os.path.join(self.__scubadir_hostpath, name)
         assert not os.path.exists(path)
 
+        # Make any directories required
+        mkdir_p(os.path.dirname(path))
+
         try:
             # Python 2
             # open() returns builtin file object which has no __dict__
@@ -279,7 +289,7 @@ class ScubaDive(object):
             return
 
         # Generate the hook script, mount it into the container, and tell scubainit
-        with self.open_scubadir_file('{0}.sh'.format(name), 'wt') as f:
+        with self.open_scubadir_file('hooks/{0}.sh'.format(name), 'wt') as f:
 
             self.add_env('SCUBAINIT_HOOK_{0}'.format(name.upper()), f.container_path)
 
diff --git a/scuba/utils.py b/scuba/utils.py
index 0bd2e0e..4dac376 100644
--- a/scuba/utils.py
+++ b/scuba/utils.py
@@ -1,9 +1,15 @@
+import errno
+import os
 try:
     from shlex import quote as shell_quote
 except ImportError:
     from pipes import quote as shell_quote
 
 
+def shell_quote_cmd(cmdlist):
+    return ' '.join(map(shell_quote, cmdlist))
+
+
 def format_cmdline(args, maxwidth=80):
     '''Format args into a shell-quoted command line.
 
@@ -33,3 +39,12 @@ def format_cmdline(args, maxwidth=80):
         yield line
 
     return ' \\\n'.join(lines())
+
+
+def mkdir_p(path):
+    # http://stackoverflow.com/a/600612/119527
+    try:
+        os.makedirs(path)
+    except OSError as exc:
+        if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
+            raise


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ff9ad97..104fc92 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -33,7 +33,7 @@ class TestUtils(TestCase):
         assert_seq_equal(out_args, args)
 
 
-    def test_basic(self):
+    def test_format_cmdline(self):
         '''format_cmdline works as expected'''
 
         self._test_format_cmdline([
@@ -47,3 +47,27 @@ class TestUtils(TestCase):
             'some', 'more', 'stuff',
             'and even more stuff',
         ])
+
+
+    def test_shell_quote_cmd(self):
+        args = ['foo', 'bar pop', '"tee ball"']
+
+        result = scuba.utils.shell_quote_cmd(args)
+
+        out_args = shlex.split(result)
+
+        assert_seq_equal(out_args, args)
+
+
+    def test_mkdir_p(self):
+        '''mkdir_p creates directories as expected'''
+        relpath = 'one/two/three'
+        with InTempDir(prefix='scubatest'):
+            scuba.utils.mkdir_p(relpath)
+
+            assert(os.path.exists(relpath))
+            assert(os.path.isdir(relpath))
+
+    def test_mkdir_p_fail_exist(self):
+        '''mkdir_p fails when expected'''
+        assert_raises(OSError, scuba.utils.mkdir_p, '/dev/null')
diff --git a/tests/utils.py b/tests/utils.py
index b7d07e0..d20faa5 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,6 +1,8 @@
 import os
 from nose.tools import *
 from os.path import normpath
+import tempfile
+import shutil
 
 def assert_set_equal(a, b):
     assert_equal(set(a), set(b))
@@ -53,3 +55,20 @@ class PseudoTTY(object):
         return getattr(self.__underlying, name)
     def isatty(self):
         return True
+
+
+class InTempDir(object):
+    def __init__(self, suffix='', prefix='tmp', delete=True):
+        self.delete = delete
+        self.temp_path = tempfile.mkdtemp(suffix=suffix, prefix=prefix)
+
+    def __enter__(self):
+        self.orig_path = os.getcwd()
+        os.chdir(self.temp_path)
+        return self
+
+    def __exit__(self, *exc_info):
+        # Restore the working dir and cleanup the temp one
+        os.chdir(self.orig_path)
+        if self.delete:
+            shutil.rmtree(self.temp_path)