Repository: JonathonReinhart/scuba

Issue:
Setting environment variables inside the container from the command line is hard
Currently the only way I have found to accomplish this is with the -d argument like this:
`scuba -d "'-e=woot=1'" /bin/bash`

I would recommended either additional documentation or adding a special -e option to scuba specifically for environment variables.


Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index cdf06d2..f7d115b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -53,7 +53,7 @@ deploy:
 
   # Test PyPI (on any push to master)
   - provider: pypi
-    server: https://test.pypi.org/legacy/
+    server: https://testpypi.python.org/pypi
     user: jreinhart
     password:
       secure: "BYbUHbV3G2lsvDDR9LrYCRFz/g+LAI/pmfa+d+KMlXbY+s7v+pYsENRmwVbhpc1oDSn0UnKgJyc3ZyGAEkmlV0Cog9YxWnoVQgmsT321jIuQur1SjeQ70mgkwS3fRh1hdzrfKkJ3SghFfFyrXzK1kyn8TzDBg7sSyymep2MgO+kRsYxIKP2e8GCTPLmXhifxAPDQZ6rDtRCl4fDgSoakLNsaHQPeaEgEN7E4699edce0lWN2g+2w3NGAmO6jdx3Ly64XX6MoM47/Pjdmi/xncqAqVQ37CCTIM2HRNClujy35KVnTOpjN0ZSsPuCYfx0aIOBzR3cCo7II2VBziQhQ09o1gJOEnd/q2/4rUoaa9Plu1SeO0yQxvjtBS+7iSDnZEDzqQvah/XidZzv6efmvNrme5ueTGnFUt+v4bq03Nu661Jat9FbaDb8NCi1qhnUG+ebJ4t3EIXqd4vIdu6M+1RdNqU5Wmf4j6IUutrjnF8qOexG0hh/MkdiSO6o+cEbltiMhr+Z+F6Y7j9snsqqYvnZ42mjf7KNKfX56/hfi8do2Z9Lb3V9xTHqiL9reRPQdq5s+iXmC75/Ca9rZv6UtNRPt56FdBtEEWGuFcbenZRk/av2baX/MFbqGZbToQ0KbiDcDQNKZGNeaH6+6YfSvjhmFUtM/duhmQaCfcXjhoZA="      
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2da0e1..752638a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [Unreleased]
+### Added
+- Add -e/--env command-line option (#111)
+
+### Changed
+- Implemented auto-versioning using Git and Travis (#112)
+
+
 ## [2.2.0] - 2018-03-07
 ### Changed
 - Allow `script` to be a single string value in the "common script schema"
diff --git a/scuba/__main__.py b/scuba/__main__.py
index 19591e5..06e27ef 100644
--- a/scuba/__main__.py
+++ b/scuba/__main__.py
@@ -11,6 +11,7 @@ import itertools
 import argparse
 import tempfile
 import shutil
+import collections
 
 from .cmdlineargs import *
 from .compat import File, StringIO
@@ -49,6 +50,9 @@ def parse_scuba_args(argv):
     ap.add_argument('-d', '--docker-arg', dest='docker_args', action='append',
             type=lambda x: shlex.split(x), default=[],
             help="Pass additional arguments to 'docker run'")
+    ap.add_argument('-e', '--env', dest='env_vars', action='append',
+            type=parse_env_var, default=[],
+            help='Environment variables to pass to docker')
     ap.add_argument('--list-aliases', action='store_true',
             help=argparse.SUPPRESS)
     ap.add_argument('--list-available-options', action=ListOptsAction,
@@ -69,6 +73,14 @@ def parse_scuba_args(argv):
     # Flatten docker arguments into single list
     args.docker_args = list(itertools.chain.from_iterable(args.docker_args))
 
+    # Convert env var tuples into a dict, forbidding duplicates
+    env = dict()
+    for k,v in args.env_vars:
+        if k in env:
+            ap.error("Duplicate env var {}".format(k))
+        env[k] = v
+    args.env_vars = env
+
     global g_verbose
     g_verbose = args.verbose
 
@@ -79,17 +91,22 @@ class ScubaError(Exception):
     pass
 
 class ScubaDive(object):
-    def __init__(self, user_command, docker_args=[], as_root=False, verbose=False,
+    def __init__(self, user_command, docker_args=None, env=None, as_root=False, verbose=False,
             image_override=None):
+
+        env = env or {}
+        if not isinstance(env, collections.Mapping):
+            raise ValueError('Argument env must be dict-like')
+
         self.user_command = user_command
         self.as_root = as_root
         self.verbose = verbose
         self.image_override = image_override
 
         # These will be added to docker run cmdline
-        self.env_vars = {}
+        self.env_vars = env
         self.volumes = []
-        self.options = docker_args
+        self.options = docker_args or []
         self.workdir = None
 
         self.__locate_scubainit()
@@ -359,6 +376,7 @@ def run_scuba(scuba_args):
     dive = ScubaDive(
         scuba_args.command,
         docker_args = scuba_args.docker_args,
+        env = scuba_args.env_vars,
         as_root = scuba_args.root,
         verbose = scuba_args.verbose,
         image_override = scuba_args.image,
diff --git a/scuba/utils.py b/scuba/utils.py
index 299c2a7..f1555fc 100644
--- a/scuba/utils.py
+++ b/scuba/utils.py
@@ -56,3 +56,22 @@ def mkdir_p(path):
     except OSError as exc:
         if not (exc.errno == errno.EEXIST and os.path.isdir(path)):
             raise
+
+
+def parse_env_var(s):
+    """Parse an environment variable string
+
+    Returns a key-value tuple
+
+    Apply the same logic as `docker run -e`:
+    "If the operator names an environment variable without specifying a value,
+    then the current value of the named variable is propagated into the
+    container's environment
+    """
+    parts = s.split('=', 1)
+    if len(parts) == 2:
+        k, v = parts
+        return (k, v)
+
+    k = parts[0]
+    return (k, os.getenv(k))
diff --git a/setup.py b/setup.py
index 145859a..ce5ee35 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,6 @@ from setuptools import setup, Command
 from distutils.command.build import build
 from setuptools.command.sdist import sdist 
 from subprocess import check_call
-import os
 
 
 class build_scubainit(Command):
@@ -26,18 +25,11 @@ class build_hook(build):
         self.run_command('build_scubainit')
         build.run(self)
 
-def read_project_file(path):
-    proj_dir = os.path.dirname(__file__)
-    path = os.path.join(proj_dir, 'README.md')
-    with open(path, 'r') as f:
-        return f.read()
 
 setup(
     name = 'scuba',
     version = scuba.version.__version__,
     description = 'Simplify use of Docker containers for building software',
-    long_description = read_project_file('README.md'),
-    long_description_content_type = 'text/markdown',
     classifiers = [
         'Development Status :: 5 - Production/Stable',
         'Environment :: Console',


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 588d0e9..0c192af 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -356,6 +356,33 @@ class TestMain(TmpDirTestCase):
 
         assert_str_equalish(out, data)
 
+    def test_env_var_keyval(self):
+        '''Verify -e KEY=VAL works'''
+        with open('.scuba.yml', 'w') as f:
+            f.write('image: {0}\n'.format(DOCKER_IMAGE))
+        args = [
+            '-e', 'KEY=VAL',
+            '/bin/sh', '-c', 'echo $KEY',
+        ]
+        out, _ = self.run_scuba(args)
+        assert_str_equalish(out, 'VAL')
+
+    def test_env_var_key_only(self):
+        '''Verify -e KEY works'''
+        with open('.scuba.yml', 'w') as f:
+            f.write('image: {0}\n'.format(DOCKER_IMAGE))
+        args = [
+            '-e', 'KEY',
+            '/bin/sh', '-c', 'echo $KEY',
+        ]
+        def mocked_getenv(key):
+            self.assertEqual(key, 'KEY')
+            return 'mockedvalue'
+        with mock.patch('os.getenv', side_effect=mocked_getenv):
+            out, _ = self.run_scuba(args)
+        assert_str_equalish(out, 'mockedvalue')
+
+
     def test_image_entrypoint(self):
         '''Verify scuba doesn't interfere with the configured image ENTRYPOINT'''
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 104fc92..0e6c130 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,6 +2,10 @@ from __future__ import print_function
 
 from nose.tools import *
 from unittest import TestCase
+try:
+    from unittest import mock
+except ImportError:
+    import mock
 
 import logging
 import shlex
@@ -71,3 +75,24 @@ class TestUtils(TestCase):
     def test_mkdir_p_fail_exist(self):
         '''mkdir_p fails when expected'''
         assert_raises(OSError, scuba.utils.mkdir_p, '/dev/null')
+
+
+    def test_parse_env_var(self):
+        '''parse_env_var returns a key, value pair'''
+        result = scuba.utils.parse_env_var('KEY=value')
+        self.assertEqual(result, ('KEY', 'value'))
+
+    def test_parse_env_var_more_equals(self):
+        '''parse_env_var handles multiple equals signs'''
+        result = scuba.utils.parse_env_var('KEY=anotherkey=value')
+        self.assertEqual(result, ('KEY', 'anotherkey=value'))
+
+    def test_parse_env_var_no_equals(self):
+        '''parse_env_var handles no equals and gets value from environment'''
+        def mocked_getenv(key):
+            self.assertEqual(key, 'KEY')
+            return 'mockedvalue'
+
+        with mock.patch('os.getenv', side_effect=mocked_getenv):
+            result = scuba.utils.parse_env_var('KEY')
+        self.assertEqual(result, ('KEY', 'mockedvalue'))