amancevice/dip

View on GitHub
dip/main.py

Summary

Maintainability
C
7 hrs
Test Coverage
A
100%
"""
dip CLI tool main entrypoint
"""
import json
import subprocess

import click
import docker
from dip import __version__
from dip import colors
from dip import errors
from dip import options
from dip import settings
from dip import utils


def clickerr(func):
    """ Decorator to catch errors and re-raise as ClickException. """
    # pylint: disable=missing-docstring
    def wrapper(*args, **kwargs):
        try:
            func(*args, **kwargs)
        except errors.DipError as err:
            raise click.ClickException(str(err))
    wrapper.__doc__ = func.__doc__
    return wrapper


def warnsleep(app):
    """ Warn about app divergence and sleep. """
    # Warn about divergence
    warn = '\n'\
        'Local service has diverged from remote or is inaccessible.\n'\
        'Sleeping for {}s\n'\
        'CTRL-C to exit\n'.format(app.repo.sleeptime)
    click.echo(colors.amber(warn), err=True)

    # Give hint to upgrade
    upgrade = 'dip upgrade {}'.format(app.name)
    hint = 'Run `{}` to git-pull updates from remote\n'\
        .format(colors.teal(upgrade))
    click.echo(hint, err=True)

    # Sleep
    app.repo.sleep()


def warnask(app):
    """ Warn about app divergence and ask to upgrade. """
    # Warn about divergence
    warn = '\nLocal service has diverged from remote or is inaccessible.'
    click.echo(colors.amber(warn), err=True)

    # Ask to upgrade
    upgrade = colors.teal('Attempt to upgrade before continuing?')
    if click.confirm(upgrade):
        # Upgrade
        app.repo.pull()
        click.echo(err=True)
    else:
        override = colors.teal('Continue without upgrading?')
        if not click.confirm(override):
            goodbye = 'Please resolve these changes before re-attempting.\n'
            click.echo(goodbye, err=True)
            raise SystemExit(1)


def warnupgrade(app):
    """ Warn about app divergence and do upgrade. """
    # Warn about divergence
    warn = '\nLocal service has diverged from remote or is inaccessible.'
    click.echo(colors.amber(warn), err=True)

    # Ask to upgrade
    click.echo(colors.teal('Attempting to auto-upgrade'), err=True)
    app.repo.pull()


@click.group(context_settings={'help_option_names': ['-h', '--help']})
@click.version_option(__version__, '-v', '--version')
def dip():
    """ Install CLIs using docker-compose.

        The following ENV variables are supported by `dip`:

        \b
        :DIP_HOME: The location of the dip settings.json file
        :DIP_PATH: The default location of installed executables

        See https://github.com/amancevice/dip for more information.
    """


@dip.command('completion')
def dip_completion():
    """ Print bash completion script. """
    pipe = subprocess.Popen('_DIP_COMPLETE=source dip',
                            stdout=subprocess.PIPE,
                            shell=True)
    for line in pipe.communicate():
        # pylint: disable=superfluous-parens
        print(line.decode('utf-8').strip())
        return


@dip.command('config')
@options.EDIT
@options.KEYS
@clickerr
def dip_config(edit, keys):
    """ Show current dip configuration.

        \b
        dip config NAME             # Get NAME config dict
        dip config NAME git remote  # Get name of remote
    """
    with settings.load() as cfg:
        if edit:
            try:
                subprocess.call([utils.editor(), cfg.filepath])
            except KeyError:
                raise click.ClickException('EDITOR value not defined in ENV')
        else:
            working = cfg.data
            for key in keys:
                try:
                    working = working[key]
                except (KeyError, TypeError):
                    raise SystemExit(1)

            if isinstance(working, dict):
                click.echo(json.dumps(working, indent=4, sort_keys=True))
            else:
                click.echo(working)


@dip.command('diff')
@options.NAME
@options.QUIET
def dip_diff(name, quiet):
    """ Run diff against remote. """
    with settings.diffapp(name, quiet=quiet) as app_diff:
        _, diff = app_diff
        if diff:
            raise SystemExit(1)


@dip.command('install')
@options.NAME
@options.HOME
@options.PATH
@options.REMOTE
@options.DOTENV
@options.ENV
@options.SECRET
@options.SLEEP
@options.AUTO_UPGRADE
@options.NO_EXE
@clickerr
def dip_install(name, home, path, remote, dotenv, env, secret, sleep,
                auto_upgrade, no_exe):
    """ Install CLI by name.

        \b
        dip install fizz .                   # Relative path
        dip install fizz /path/to/dir        # Absolute path
        dip install fizz . -r origin/master  # Tracking git remote/branch
    """
    # pylint: disable=too-many-arguments
    with settings.saveonexit() as cfg:
        # Interactively set ENV
        for sec in secret:
            env[sec] = click.prompt(sec, hide_input=True)  # pragma: no cover

        # Parse git config
        remote, branch = remote
        git = {'remote': remote,
               'branch': branch,
               'sleep': sleep,
               'auto_upgrade': auto_upgrade}

        # Install
        if no_exe:
            app = cfg[name] = settings.Dip(name, home, path, env, git, dotenv)
        else:
            app = cfg.install(name, home, path, env, git, dotenv)

        # Validate configuration
        app.validate()

        # Finish
        click.echo("Installed {name} to {path}".format(
            name=colors.teal(app.name),
            path=colors.blue(app.path)))


@dip.command('list')
@clickerr
def dip_list():
    """ List installed CLIs. """
    with settings.load() as cfg:
        if any(cfg):
            click.echo()
            homes = [utils.contractuser(cfg[x].home) for x in cfg]
            maxname = max(len(x) for x in cfg)
            maxhome = max(len(x) for x in homes)
            for key in sorted(cfg):
                app = cfg[key]
                name = colors.teal(app.name.ljust(maxname))
                home = colors.blue(utils.contractuser(app.home).ljust(maxhome))
                remote = branch = None
                tpl = "{name} {home}"
                if app.repo:
                    try:
                        remote = app.repo.remotename
                        branch = app.repo.branch
                        tpl += " {remote}/{branch}"
                    except Exception:  # pylint:  disable=broad-except
                        tpl += colors.red(' [git error]')
                click.echo(tpl.format(name=name,
                                      home=home,
                                      remote=remote,
                                      branch=branch))
            click.echo()


@dip.command('pull')
@options.NAME
@clickerr
def dip_pull(name):
    """ Pull updates from docker-compose. """
    with settings.diffapp(name) as app_diff:
        app, diff = app_diff
        if diff and app.git.get('sleep'):
            warnsleep(app)
        elif diff:
            warnask(app)
        try:
            return app.service.pull()
        except docker.errors.APIError:
            err = "Could not pull '{}' image".format(name)
            click.echo(colors.red(err), err=True)
        raise SystemExit(1)


@dip.command('reset')
@options.FORCE
@clickerr
def dip_reset(force):
    """ Reset dip configuration to defaults. """
    if force:
        settings.reset()


@dip.command('run')
@options.NAME
@options.QUICK
@options.ARGS
@clickerr
def dip_run(name, quick, args):
    """ Run dip CLI. """
    if quick:
        with settings.getapp(name) as app:
            app.run(*args)
    else:
        with settings.diffapp(name) as app_diff:
            app, diff = app_diff
            if diff and app.sleep:
                warnsleep(app)
            elif diff and app.auto_upgrade:
                warnupgrade(app)
            elif diff:
                warnask(app)
            app.run(*args)


@dip.command('show')
@options.NAME
@clickerr
def dip_show(name):
    """ Show service configuration. """
    with settings.diffapp(name) as app_diff:
        app, diff = app_diff
        if diff and app.git.get('sleep'):
            warnsleep(app)
        elif diff:
            warnask(app)
        for definition in app.definitions:
            click.echo("\n{}\n".format(definition.strip()))


@dip.command('uninstall')
@options.NAMES
@clickerr
def dip_uninstall(names):
    """ Uninstall CLI by name. """
    for name in names:
        with settings.saveonexit() as cfg:
            try:
                cfg.uninstall(name)
                click.echo("Uninstalled {name}".format(name=colors.red(name)))
            except KeyError:
                pass


@dip.command('upgrade')
@options.NAMES
@clickerr
def dip_upgrade(names):
    """ Upgrade CLI by pulling from git remote. """
    for name in names:
        with settings.getapp(name) as app:
            try:
                app.repo.pull()
            except AttributeError:
                pass


if __name__ == '__main__':
    dip()  # pragma: no cover