# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 CERN.
#
# Docker-Services-CLI is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.
"""CLI module."""
from functools import update_wrapper
from pathlib import Path
import click
from .config import SERVICE_TYPES
from .env import (
normalize_service_name,
override_default_env,
print_setup_env_config,
set_env,
)
from .services import services_down, services_up
def _get_module_path():
"""Gets the path in which the module is installed."""
parent_path = Path(__file__).parent
return str(parent_path)
[docs]def env_output(env_set_command):
"""Decorate command to print exportable environment settings."""
if env_set_command not in ["export", "unset"]:
click.secho("Wrong environment set command.", fg="red")
exit(1)
def print_env_output(func):
@click.option(
"--env",
is_flag=True,
default=False,
help="Print export statements to set environment.",
)
def _print_env_output(*args, **kwargs):
env = kwargs.pop("env", False)
services = kwargs.get("services") or SERVICE_TYPES
if env:
# comment command output until env export
click.echo(": '")
click.get_current_context().invoke(func, *args, **kwargs)
if env:
# end of multiline comment, start of export statements
click.echo("'")
print_setup_env_config(
services,
click.get_current_context().info_name,
env_set_command=env_set_command,
)
return update_wrapper(_print_env_output, func)
return print_env_output
[docs]def services_by_type(func):
"""Decorate command adding all service types as options.
:param func: The function that implements the Click command to which the
service types options will be added.
:return: A wrapped function around the passed Click command which exposes
all ``config.SERVICES_TYPES`` as Click options. The list of services
by type is injected as ``services`` keyword argument.
"""
def collect_services_by_type(*args, **kwargs):
services = {}
for service_type in SERVICE_TYPES:
service = kwargs.pop(service_type)
if service:
services.setdefault(service_type, []).extend(
service if isinstance(service, list) else [service]
)
kwargs["services"] = services
click.get_current_context().invoke(func, *args, **kwargs)
def validate_service_name(ctx, service_type, services_list):
available_services = SERVICE_TYPES.get(service_type.name, [])
for service in services_list:
if not (
normalize_service_name(service)
or normalize_service_name(service) in available_services
):
raise click.BadParameter(
"{service} is not a valid service of type {type}. "
"Try one of: \n{available_services}".format(
service=service,
type=service_type.name,
available_services=available_services,
)
)
return list(services_list)
for service_type in SERVICE_TYPES:
click.option(
"--{}".format(service_type),
callback=validate_service_name,
multiple=True,
help="Specify which service should run as {0}. "
"Available {0} services: {1}.".format(
service_type, ", ".join(SERVICE_TYPES.get(service_type))
),
)(func)
return update_wrapper(collect_services_by_type, func)
[docs]class ServicesCtx(object):
"""Context class for docker services cli."""
def __init__(self, filepath, verbose):
"""Constructor."""
self.filepath = filepath
self.verbose = verbose
@click.group()
@click.version_option()
@click.option(
"--filepath",
"-f",
required=False,
default=f"{_get_module_path()}/docker-services.yml",
type=click.Path(exists=True),
help="Path to a docker compose file with the desired services definition.",
)
@click.option(
"--verbose",
is_flag=True,
default=False,
help="Verbose output.",
)
@click.pass_context
def cli(ctx, filepath, verbose):
"""Initialize CLI context."""
set_env()
ctx.obj = ServicesCtx(filepath=filepath, verbose=verbose)
@cli.command()
@click.option(
"--no-wait",
is_flag=True,
help="Wait for services to be up (use healthchecks).",
)
@click.option(
"--retries",
default=6,
type=int,
help="Number of times to retry a service's healthcheck.",
)
@services_by_type
@env_output(env_set_command="export")
@click.pass_obj
def up(services_ctx, services, no_wait, retries):
r"""Boots up the required services.
Example:
$ docker-services-cli up --db postgresql11
Note: All services will be boot up if no service is specified.
"""
_services = [s for services_list in services.values() for s in services_list]
# NOTE: docker-compose boots up all if none is provided
if len(_services) == 1 and _services[0].lower() == "all":
_services = []
override_default_env(services_to_override=_services)
click.secho("Environment setup", fg="green")
normalized_services = [normalize_service_name(s) for s in _services]
services_up(
services=normalized_services,
filepath=services_ctx.filepath,
wait=(not no_wait),
retries=retries,
verbose=services_ctx.verbose,
)
click.secho("Services up!", fg="green")
@cli.command()
@env_output(env_set_command="unset")
@click.pass_obj
def down(services_ctx):
"""Shuts down the required services."""
services_down(filepath=services_ctx.filepath)
click.secho("Services down!", fg="green")