~charles/bem

e1e2ff86b1230d9eea39e0c11395912973dff68c — Charles Daniels 4 years ago a34665c
add environment handling
6 files changed, 154 insertions(+), 20 deletions(-)

M BEMfile
M README.md
M bem/bem_config.py
M bem/bem_constants.py
M bem/bem_engine.py
M bem/bem_util.py
M BEMfile => BEMfile +6 -0
@@ 1,2 1,8 @@
[BEM]
minimum_version = 0.0.1

[environment]
TERM = XTERM

[indirectenvironment]
BEM_VERSION = {version}

M README.md => README.md +52 -0
@@ 114,3 114,55 @@ that could be used instead.
Not really. It definitely adds to start-up time, especially if the image needs
to be rebuilt. However, starting cold from an image that has already been built
and cached, BEM only adds 1-2 seconds of additional start up time.

## Configuration

### Configuration Keys

The following keys may be specified in the `[BEM]` section of the `BEMfile`.
Values are strings if not otherwise noted. Values may include other
configuration keys for substitution using the syntax `{key}`. This makes it
easy to, for example, specify a file relative to the project root.

* `minimum_version` -- minimum BEM version required to use this `BEMfile`
  (default: current version)
* `dockerfile` -- path to Dockerfile BEM should use (default:
  `{bem_file_dir}/Dockerfile`)
* `projectdir` -- path to the directory BEM should treat as the project's
  top-level (default: `{bem_file_dir}`)
* `nonineractive` -- boolean indicating BEM should run in non-interactive mode
  (default: `False`) -- **non-interactive mode is WiP and has several problems
  at the moment**.
* `logfile` -- when running in non-interactive mode, output should be written
  here; ignored in interactive mode (default: `/dev/null`)
* `flush_interval` -- number of seconds between flushes to the logfile
* `noenvtamper` -- set to `True` to prevent BEM from "tampering" with your
  environment, such as setting `BEM_PROJECT` and overriding `PS1`. Environment
  overrides are still honored. (default: `False`)
* `noenvpassthrough` -- set to `True` to prevent BEM from passing through the
  host environment into the container. (default: `False`).
* `x11` -- set to `True` to bind mount in `/tmp/.X11-unix`. (default: `True`)
* `squashmethod` -- specify how the user account should be squashed. (default:
  `bind`)
	* `bind` -- bind-mount in `/etc/passwd`, `/etc/shadow`, and
	  `/etc/group` read-only, and set the UID within the container to the
	  UID running BEM.
	* `none` -- do nothing and set the UID within the container to 0 (root).

The following config keys should not be changed, but may be used via for
substitution/indirection.

* `version` -- the running BEM version
* `bem_file_dir` -- parent directory of the current `BEMfile`

Configuration keys beginning with a `#` are reserved for internal use and
should not be used for substitutions, nor overridden.

### Environment Overrides

The section `[environment]` in the `BEMfile` may be used to specify environment
variables that should be defined within the container. Such values will still
be set even if `noenvtamper` or `noenvpassthrough` are asserted. Values placed
in `[indirectenvironment]` will be expanded using the substitution syntax used
for config values. If the same name exists in both `[environment]` and
`[indirectenvironment]`, the latter takes precedence.

M bem/bem_config.py => bem/bem_config.py +60 -1
@@ 1,6 1,8 @@
import logging
import configparser
import copy
import pathlib
import os

from . import bem_constants
from . import bem_util


@@ 54,17 56,21 @@ def load_config(inifile, args, ini_dir):
    logging.debug("unresolved config... ")
    bem_util.log_pretty(logging.debug, config)

    ignore = ["#environment"]
    iterations = 0
    while True:

        # check if we're done resolving indirect values
        count = 0
        for key in config:
            if key in ignore:
                continue

            if '{' in str(config[key]):
                count += 1

        if count == 0:
            return config
            break

        if iterations > 1000:
            raise Exception(


@@ 72,6 78,9 @@ def load_config(inifile, args, ini_dir):
                        .format([k for k in config if '{' in config[k]]))

        for key in config:
            if key in ignore:
                continue

            # perform the next iteration of indirection
            if '{' in str(config[key]):
                logging.debug("resolving indirect value for key '{}': "


@@ 80,3 89,53 @@ def load_config(inifile, args, ini_dir):
                logging.debug("\tresolved to: {}".format(config[key]))

        iterations += 1

    # typecast keys as needed
    keytypes = {
            "noenvtamper": bem_util.parse_bool,
            "noenvpassthrough": bem_util.parse_bool,
            "x11": bem_util.parse_bool,
            "flush_interval": int,
            "dockerfile": pathlib.Path,
            "bem_file_dir": pathlib.Path,
            "projectdir": pathlib.Path,
            "noninteractive": pathlib.Path,
    }

    for key in config:
        if key in keytypes:
            config[key] = keytypes[key](config[key])

    # setup environment
    if not config["noenvpassthrough"]:
        config["#environment"] = dict(os.environ)
    else:
        config["#environment"] = {}

    # environment tampering
    if not config["noenvtamper"]:
        config["#environment"]["PS1"] = bem_constants.default_ps1
        config["#environment"]["BEM_PROJECT"] = pathlib.Path(config["projectdir"]).name

    # load direct environment variables from ini
    if "environment" in inifile:
        for key in inifile["environment"]:
            config["#environment"][key] = inifile["environment"][key]
            logging.debug("loading key {} from environment: {}".format(
                key, inifile["environment"][key]))

    # load indirect environment variables from ini
    if "indirectenvironment" in inifile:
        for key in inifile["indirectenvironment"]:
            config["#environment"][key] = inifile["indirectenvironment"][key]

            while '{' in config["#environment"][key]:
                config["#environment"][key] = config["#environment"][key].format_map(config)

            logging.debug("loading key {} from indirect environment: {}".format(
                key, inifile["indirectenvironment"][key]))

    logging.debug("resolved config... ")
    bem_util.log_pretty(logging.debug, config)

    return config

M bem/bem_constants.py => bem/bem_constants.py +6 -0
@@ 11,5 11,11 @@ config_defaults = {
    "noninteractive": "False",
    "logfile": "/dev/null",
    "flush_interval": "5",
    "noenvtamper": False,
    "x11": True,
    "squashmethod": "bind",
    "noenvpassthrough": False,
}

default_ps1 = "[BEM:$BEM_PROJECT] $(whoami)@$(hostname) $(pwd) $ "


M bem/bem_engine.py => bem/bem_engine.py +29 -18
@@ 105,29 105,40 @@ def execute_command(config, command):
                    "bind": str(projectdir),
                    "mode": "rw"
                },
            "/etc/shadow": {
                    "bind": "/etc/shadow",
                    "mode": "ro",
                },
            "/etc/passwd": {
                    "bind": "/etc/passwd",
                    "mode": "ro",
                },
            "/etc/group": {
                    "bind": "/etc/group",
                    "mode": "ro",
                },
            "/tmp/.X11-unix": {
            }

    if bem_util.parse_bool(config["x11"]):
        volumes["/tmp/.X11-unix"] = {
                    "bind": "/tmp/.X11-unix",
                    "mode": "rw",
                }

    user = 0
    if config["squashmethod"] == "bind":
        volumes["/etc/shadow"] = {
                "bind": "/etc/shadow",
                "mode": "ro",
            }

    logging.debug("volumes: {}".format(volumes))
        volumes["/etc/passwd"] = {
                "bind": "/etc/passwd",
                "mode": "ro",
            }

        volumes["/etc/group"] = {
                "bind": "/etc/group",
                "mode": "ro",
            }

        user = os.getuid()

    environment = dict(os.environ)
    elif config["squashmethod"] == "none":
        pass

    environment["PS1"] = "[BEM:{}] $(whoami)@$(hostname) $(pwd) $ ".format(pathlib.Path(projectdir).name)
    else:
        raise Exception("unknown squashmethod: {}".format(config["squashmethod"]))

    logging.debug("volumes: {}".format(volumes))

    # TODO: if getcwd does not exist inside of the container (not a child of
    # any volume), set to project dir instead


@@ 156,8 167,8 @@ def execute_command(config, command):
            host_config=client.api.create_host_config(
                binds=volumes,
            ),
            user=os.getuid(),
            environment=environment,
            user=user,
            environment=config["#environment"],
            working_dir=os.getcwd()
        )


M bem/bem_util.py => bem/bem_util.py +1 -1
@@ 98,4 98,4 @@ def compare_versions(v1, v2):
    return True

def parse_bool(s):
    return s.lower in ["true", "t", "yes", "1", "on", "one", "yep", "yup"]
    return str(s).lower in ["true", "t", "yes", "1", "on", "one", "yep", "yup"]