~charles/bem

a34665ca24b2de8b70fa504bd5fca10f757493c3 — Charles Daniels 4 years ago 485f8a6
fix configuration system
4 files changed, 68 insertions(+), 80 deletions(-)

M bem/bem_cli.py
M bem/bem_config.py
M bem/bem_engine.py
M bem/bem_util.py
M bem/bem_cli.py => bem/bem_cli.py +9 -12
@@ 54,7 54,6 @@ def bem_cli():
        bem_util.setup_logging("ERROR")
    elif args.debug:
        bem_util.setup_logging("DEBUG")
    else:
        bem_util.setup_logging("INFO")

    logging.debug("Starting BEM version {}".format(bem_constants.version))


@@ 75,28 74,26 @@ def bem_cli():

    logging.debug("using bemfile: '{}'".format(bemfile))

    config = configparser.ConfigParser()
    config.read(bemfile)
    configreader = configparser.ConfigParser()
    configreader.read(bemfile)

    config = bem_config.load_config(configreader, args, bemfile.parent)

    min_version = bem_config.get_key(config, args, bemfile.parent,
            "minimum_version")
    if not bem_util.compare_versions(bem_constants.version, min_version):
    if not bem_util.compare_versions(bem_constants.version, config["minimum_version"]):
        logging.error("BEM version '{}' too old, at least version '{}' is required"
                .format(bem_constants.version, min_version))
        sys.exit(1)

    logging.debug("project dir: {}".format(
        bem_config.get_key(config, args, bemfile.parent, "projectdir")))
    logging.debug("dockerfile: {}".format(
        bem_config.get_key(config, args, bemfile.parent, "dockerfile")))
    logging.debug("project dir: {}".format(config["projectdir"]))
    logging.debug("dockerfile: {}".format(config["dockerfile"]))

    try:
        bem_engine.validate_config(config, args, bemfile.parent)
        bem_engine.validate_config(config)
    except Exception as e:
        logging.error("Configuration error: {}".format(e))
        sys.exit(1)

    bem_engine.execute_command(config, args, bemfile.parent, args.command)
    bem_engine.execute_command(config, args.command)

if __name__ == "__main__":
    bem_cli()

M bem/bem_config.py => bem/bem_config.py +44 -52
@@ 1,24 1,15 @@
import logging
import configparser
import copy

from . import bem_constants
from . import bem_util

def apply_indirection(inifile, args, ini_dir, val):
def load_config(inifile, args, ini_dir):
    """load_config

    # there isn't a good way to enumerate what keys are available, so we
    # make a dummy that portends to be a dict, but really just calls get_key()
    class DummyDict:
        def __getitem__(this, key):
            return get_key(inifile, args, ini_dir, key, True)

        def __missing__(this, key):
            logging.error("unknown indirect key '{}'".format(key))

    dummy = DummyDict()
    return str(val).format_map(dummy)

def get_key(inifile, args, ini_dir, key, indirection=True):
    """get_key
    Load a configuration dictionary from the INI file, CLI arguments, and so
    on, resolving any indirect values.

    :param inifile: INI file as loaded by configparser.ConfigParser.read
    :param args: args file from argparse


@@ 26,65 17,66 @@ def get_key(inifile, args, ini_dir, key, indirection=True):
    :param indirection: set to True to allow indirection
    :param ini_dir: parent directory of the loaded INI file

    Retrieves the specified config key. Order of precedence is:
    Order of precedence is:

    * runtime keys
    * arguments
    * INI file
    * bem_constants.config_defaults

    Returned value will always be a string. Throws a KeyError if there is no
    such config value.
    Returns a dict.
    """

    logging.debug("retrieving config key '{}'".format(key))


    runtime = {
        "bem_file_dir": ini_dir,
        "version": bem_constants.version
    }

    result = None
    # grab all defaults
    config = copy.deepcopy(bem_constants.config_defaults)

    # check values provided by arguments
    arguments = {}
    # load from INI file
    for key in inifile["BEM"]:
        config[key] = inifile["BEM"][key]

    # handle arguments
    if args.dockerfile is not None:
        arguments["dockerfile"] = args.dockerfile
        config["dockerfile"] = args.dockerfile

    if args.projectdir is not None:
        arguments["projectdir"] = args.projectdir
        config["projectdir"] = args.projectdir

    if args.logfile is not None:
        arguments["logfile"] = args.logfile
        config["logfile"] = args.logfile

    if args.noninteractive is not None:
        arguments["noninteractive"] = args.noninteractive
        config["noninteractive"] = args.noninteractive

    if key in runtime:
        logging.debug("\trequested key found in runtime: '{}'".format(runtime[key]))
        result = runtime[key]
    # runtime
    config["bem_file_dir"] = ini_dir
    config["version"] = bem_constants.version

    elif key in arguments:
        result = arguments[key]
        logging.debug("\tkey found in arguments: '{}'".format(result))
    logging.debug("unresolved config... ")
    bem_util.log_pretty(logging.debug, config)

    elif key in inifile["BEM"]:
        logging.debug("\trequested key found in INI file: '{}'".format(inifile["BEM"][key]))
        result = inifile["BEM"][key]
    iterations = 0
    while True:

    elif key in bem_constants.config_defaults:
        logging.debug("\trequested key found in defaults: '{}'".format(bem_constants.config_defaults[key]))
        result = bem_constants.config_defaults[key]
        # check if we're done resolving indirect values
        count = 0
        for key in config:
            if '{' in str(config[key]):
                count += 1

    if result is not None:
        if indirection:
            return apply_indirection(inifile, args, ini_dir, result)
        else:
            return result
        if count == 0:
            return config

    logging.error("No such config key: '{}'".format(key))
    raise KeyError("No such config key: '{}'".format(key))
        if iterations > 1000:
            raise Exception(
                    "Too many levels of indirection, cannot load config. Offending keys: {}"
                        .format([k for k in config if '{' in config[k]]))

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

        iterations += 1

M bem/bem_engine.py => bem/bem_engine.py +14 -16
@@ 9,31 9,29 @@ import os
from . import bem_config
from . import bem_util

def validate_config(config, args, ini_dir):
def validate_config(config):
    """validate_config

    Sanity check a given runtime configuration. Throw an exception if there is
    a problem.

    :param config:
    :param args:
    :param ini_dir:
    """

    logging.debug("validating config... ")

    try:
        dockerfile = pathlib.Path(bem_config.get_key(config, args, ini_dir, "dockerfile"))
        dockerfile = pathlib.Path(config["dockerfile"])
    except Exception as e:
        raise Exception("unable to get key 'dockerfile', this may indicate a problem with your BEMfile; reason: {}".format(e))

    try:
        projectdir = pathlib.Path(bem_config.get_key(config, args, ini_dir, "projectdir"))
        projectdir = pathlib.Path(config["projectdir"])
    except Exception as e:
        raise Exception("unable to get key 'projectdir', this may indicate a problem with your BEMfile; reason: {}".format(e))

    try:
        cachedir = pathlib.Path(bem_config.get_key(config, args, ini_dir, "cachedir"))
        cachedir = pathlib.Path(config["cachedir"])
    except Exception as e:
        raise Exception("unable to get key 'cachedir', this may indicate a problem with your BEMfile; reason: {}".format(e))



@@ 50,14 48,14 @@ def validate_config(config, args, ini_dir):
                .format(cachedir))

    try:
        bool(bem_config.get_key(config, args, ini_dir, "noninteractive"))
        bem_util.parse_bool(config["noninteractive"])
    except Exception as e:
        raise Exception("Invalid value for 'noninteractive'; reason: {}".format(e))

    logging.debug("config looks ok")


def ensure_image_built(config, args, ini_dir):
def ensure_image_built(config):
    """ensure_image_built

    Ensure the docker image for the particular project is built.


@@ 71,8 69,8 @@ def ensure_image_built(config, args, ini_dir):

    logging.info("building image... ")

    dockerfile = pathlib.Path(bem_config.get_key(config, args, ini_dir, "dockerfile"))
    projectdir = pathlib.Path(bem_config.get_key(config, args, ini_dir, "projectdir"))
    dockerfile = pathlib.Path(config["dockerfile"])
    projectdir = pathlib.Path(config["projectdir"])

    client = docker.from_env()
    image = client.images.build(path=str(projectdir), dockerfile=str(dockerfile))


@@ 82,12 80,12 @@ def ensure_image_built(config, args, ini_dir):

    return image[0].id

def execute_command(config, args, ini_dir, command):
    image = ensure_image_built(config, args, ini_dir)
    noninteractive = bem_util.parse_bool(bem_config.get_key(config, args, ini_dir, "noninteractive"))
    logfile = str(bem_config.get_key(config, args, ini_dir, "logfile"))
    flush_interval = int(bem_config.get_key(config, args, ini_dir, "flush_interval"))
    projectdir = pathlib.Path(bem_config.get_key(config, args, ini_dir, "projectdir"))
def execute_command(config, command):
    image = ensure_image_built(config)
    noninteractive = bem_util.parse_bool(config["noninteractive"])
    logfile = str(config["logfile"])
    flush_interval = int(config["flush_interval"])
    projectdir = pathlib.Path(config["projectdir"])

    client = docker.from_env()


M bem/bem_util.py => bem/bem_util.py +1 -0
@@ 1,4 1,5 @@
import logging
import pprint
import pathlib

from . import bem_config