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

Add support for setting environment variable in .scuba.yml #120

Merged
merged 12 commits into from
Sep 10, 2018
Merged
Show file tree
Hide file tree
Changes from 8 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
34 changes: 34 additions & 0 deletions doc/yaml-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ Example:
image: debian:8.2
```

### `environment`

The optional `environment` node allows environment variables to be specified.
This can be either a mapping (dictionary), or a list of `KEY=VALUE` pairs.
If a value is not specified, the value is taken from the external environment.

Examples:
```yaml
environment:
FOO: "This is foo"
SECRET:
```
```yaml
environment:
- FOO=This is foo
- SECRET
```


### `aliases`

The optional `aliases` node is a mapping (dictionary) of bash-like aliases,
Expand Down Expand Up @@ -53,6 +72,21 @@ aliases:
- cat /etc/os-release
```

Aliases can add to the top-level `environment` and override its values using
the same syntax:
```yaml
environment:
FOO: "Top-level"
aliases:
example:
environment:
FOO: "Override"
BAR: "New"
script:
- echo $FOO $BAR
```


### `hooks`

The optional `hooks` node is a mapping (dictionary) of "hook" scripts that run
Expand Down
21 changes: 21 additions & 0 deletions example/env_vars/.scuba.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
image: !from_yaml ../common.yml image

environment:
FOO: Top-level
BAR: 42
EMPTY: ""
EXTERNAL_1:

aliases:
example:
environment:
FOO: Overridden by alias
EXTERNAL_2:
script:
- echo "FOO=\"$FOO\""
- echo "BAR=\"$BAR\""
- echo "EMPTY=\"$EMPTY\""
- echo "EXTERNAL_1=\"$EXTERNAL_1\""
- echo "EXTERNAL_2=\"$EXTERNAL_2\""
- echo "CMDLINE=\"$CMDLINE\""

7 changes: 7 additions & 0 deletions example/env_vars/run_example.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
cd $(dirname $0)

export EXTERNAL_1="Value 1 taken from external environment"
export EXTERNAL_2="Value 2 taken from external environment"

scuba -e CMDLINE="This comes from the cmdline" example
3 changes: 3 additions & 0 deletions scuba/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ def prepare(self):
# Docker is running natively
self.__setup_native_run()

# Apply environment vars from .scuba.yml
self.env_vars.update(self.context.environment)

def __str__(self):
s = StringIO()
writeln(s, 'ScubaDive')
Expand Down
54 changes: 50 additions & 4 deletions scuba/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,25 +129,58 @@ def _process_script_node(node, name):
raise ConfigError("{0}: must be string or dict".format(name))


def _process_environment(node, name):
# Environment can be either a list of strings ("KEY=VALUE") or a mapping
# Environment keys and values are always strings
result = {}

if not node:
pass
elif isinstance(node, dict):
for k, v in node.items():
if v is None:
v = os.getenv(k)
JonathonReinhart marked this conversation as resolved.
Show resolved Hide resolved
result[k] = str(v)
elif isinstance(node, list):
for e in node:
k, v = parse_env_var(e)
result[k] = v
else:
raise ConfigError("'{0}' must be list or mapping, not {1}".format(
name, type(node).__name__))

return result



class ScubaAlias(object):
def __init__(self, name, script, image):
def __init__(self, name, script, image, environment):
self.name = name
self.script = script
self.image = image
self.environment = environment

@classmethod
def from_dict(cls, name, node):
script = _process_script_node(node, name)
image = node.get('image') if isinstance(node, dict) else None
return cls(name, script, image)
image = None
environment = None

if isinstance(node, dict): # Rich alias
image = node.get('image')
environment = _process_environment(
node.get('environment'),
'{0}.{1}'.format(name, 'environment'))

return cls(name, script, image, environment)

class ScubaContext(object):
pass

class ScubaConfig(object):
def __init__(self, **data):
required_nodes = ('image',)
optional_nodes = ('aliases','hooks',)
optional_nodes = ('aliases','hooks','environment')

# Check for missing required nodes
missing = [n for n in required_nodes if not n in data]
Expand All @@ -165,6 +198,7 @@ def __init__(self, **data):

self._load_aliases(data)
self._load_hooks(data)
self._environment = self._load_environment(data)



Expand All @@ -187,6 +221,9 @@ def _load_hooks(self, data):
hook = _process_script_node(node, name)
self._hooks[name] = hook

def _load_environment(self, data):
return _process_environment(data.get('environment'), 'environment')


@property
def image(self):
Expand All @@ -200,6 +237,10 @@ def aliases(self):
def hooks(self):
return self._hooks

@property
def environment(self):
return self._environment


def process_command(self, command):
'''Processes a user command using aliases
Expand All @@ -214,6 +255,7 @@ def process_command(self, command):
result = ScubaContext()
result.script = None
result.image = self.image
result.environment = self.environment.copy()

if command:
alias = self.aliases.get(command[0])
Expand All @@ -226,6 +268,10 @@ def process_command(self, command):
if alias.image:
result.image = alias.image

# Merge/override the environment
if alias.environment:
result.environment.update(alias.environment)

if len(alias.script) > 1:
# Alias is a multiline script; no additional
# arguments are allowed in the scuba invocation.
Expand Down
103 changes: 103 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,106 @@ def test_hooks_missing_script(self):
''')

assert_raises(scuba.config.ConfigError, scuba.config.load_config, '.scuba.yml')


############################################################################
# Env

def test_env_top_dict(self):
'''Top-level environment can be loaded (dict)'''
with open('.scuba.yml', 'w') as f:
f.write(r'''
image: na
environment:
FOO: This is foo
FOO_WITH_QUOTES: "\"Quoted foo\"" # Quotes included in value
BAR: "This is bar"
MAGIC: 42
SWITCH_1: true # YAML boolean
SWITCH_2: "true" # YAML string
EMPTY: ""
EXTERNAL: # Comes from os env
''')

with mocked_os_env(EXTERNAL='Outside world'):
config = scuba.config.load_config('.scuba.yml')

expect = dict(
FOO = "This is foo",
FOO_WITH_QUOTES = "\"Quoted foo\"",
BAR = "This is bar",
MAGIC = "42", # N.B. string
SWITCH_1 = "True", # Unfortunately this is due to str(bool(1))
SWITCH_2 = "true",
EMPTY = "",
EXTERNAL = "Outside world",
)
self.assertEqual(expect, config.environment)


def test_env_top_list(self):
'''Top-level environment can be loaded (list)'''
with open('.scuba.yml', 'w') as f:
f.write(r'''
image: na
environment:
- FOO=This is foo # No quotes
- FOO_WITH_QUOTES="Quoted foo" # Quotes included in value
- BAR=This is bar
- MAGIC=42
- SWITCH_2=true
- EMPTY=
- EXTERNAL # Comes from os env
''')

with mocked_os_env(EXTERNAL='Outside world'):
config = scuba.config.load_config('.scuba.yml')

expect = dict(
FOO = "This is foo",
FOO_WITH_QUOTES = "\"Quoted foo\"",
BAR = "This is bar",
MAGIC = "42", # N.B. string
SWITCH_2 = "true",
EMPTY = "",
EXTERNAL = "Outside world",
)
self.assertEqual(expect, config.environment)


def test_env_alias(self):
'''Alias can have environment which overrides top-level'''
with open('.scuba.yml', 'w') as f:
f.write(r'''
image: na
environment:
FOO: Top-level
BAR: 42
aliases:
al:
script: Don't care
environment:
FOO: Overridden
MORE: Hello world
''')

config = scuba.config.load_config('.scuba.yml')

self.assertEqual(config.environment, dict(
FOO = "Top-level",
BAR = "42",
))

self.assertEqual(config.aliases['al'].environment, dict(
FOO = "Overridden",
MORE = "Hello world",
))

# Does the environment get overridden / merged?
ctx = config.process_command(['al'])

self.assertEqual(ctx.environment, dict(
FOO = "Overridden",
BAR = "42",
MORE = "Hello world",
))
Loading