Exemple #1
0
def reload(
    services: List[str] = typer.Argument(
        None,
        help="Services to be reloaded",
        shell_complete=Application.autocomplete_service,
    ),
) -> None:

    Application.print_command(Application.serialize_parameter("", services))

    Application.get_controller().controller_init(services)

    docker = Docker()
    running_services = docker.get_running_services()

    if "frontend" in services and len(services) > 1:
        print_and_exit("Can't reload frontend and other services at once")

    reloaded = 0
    for service in Application.data.services:

        # Special case: frontend in production mode
        if Configuration.production and service == "frontend":
            # Only consider it if explicitly requested in input
            if "frontend" not in services:
                log.debug(
                    "Can't reload the frontend if not explicitly requested")
            else:
                log.info("Reloading frontend...")
                # The frontend build stucks in swarm mode... let's start the container
                # always in compose mode when using the reload comand
                Configuration.FORCE_COMPOSE_ENGINE = True
                Application.get_controller().controller_init([service])
                docker = Docker()
                docker.compose.start_containers([service], force=True)
                reloaded += 1
            continue

        if service not in running_services:
            continue

        containers = docker.get_containers(service)
        if not containers:
            log.warning("Can't find any container for {}", service)
            continue

        try:
            # get the first container from the containers dict
            container = containers.get(list(containers.keys())[0])

            # Just added for typing purpose
            if not container:  # pragma: no conver
                log.warning("Can't find any container for {}", service)
                continue

            output = docker.exec_command(
                container,
                user="******",
                command="ls /usr/local/bin/reload",
                force_output_return=True,
            )

            # this is to consume the iterator and raise the exception with exit code
            if output:
                [_ for _ in output]

        except DockerException as e:
            # fail2ban fails with code 1
            if "It returned with code 1" in str(e):
                log.warning("Service {} does not support the reload command",
                            service)
                continue

            # backend fails with code 2
            if "It returned with code 2" in str(e):
                log.warning("Service {} does not support the reload command",
                            service)
                continue
            raise

        docker.exec_command(containers,
                            user="******",
                            command="/usr/local/bin/reload")
        reloaded += 1

    if reloaded == 0:
        log.info("No service reloaded")
    else:
        log.info("Services reloaded")
Exemple #2
0
def init(
    create_projectrc: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Overwrite initialization files if already exist",
        show_default=False,
    ),
    submodules_path: Path = typer.Option(
        None,
        "--submodules-path",
        help=
        "Link all submodules in an existing folder instead of download them",
    ),
) -> None:

    Application.print_command(
        Application.serialize_parameter("--force",
                                        create_projectrc,
                                        IF=create_projectrc),
        Application.serialize_parameter("--submodules-path",
                                        submodules_path,
                                        IF=submodules_path),
    )
    Application.get_controller().controller_init()

    for p in Application.project_scaffold.data_folders:
        if not p.exists():
            p.mkdir(parents=True, exist_ok=True)

    for p in Application.project_scaffold.data_files:
        if not p.exists():
            p.touch()

    if not Configuration.projectrc and not Configuration.host_configuration:
        create_projectrc = True

    # We have to create the .projectrc twice
    # One generic here with main options and another after the complete
    # conf reading to set services variables
    if create_projectrc:
        Application.get_controller().create_projectrc()
        Application.get_controller().read_specs(read_extended=False)

    if submodules_path is not None:
        if not submodules_path.exists():
            print_and_exit("Local path not found: {}", submodules_path)

    Application.git_submodules(from_path=submodules_path)

    Application.get_controller().read_specs(read_extended=True)
    Application.get_controller().make_env()

    # Compose services and variables
    Application.get_controller().get_compose_configuration()
    # We have to create the .projectrc twice
    # One generic with main options and another here
    # when services are available to set specific configurations
    if create_projectrc:
        Application.get_controller().create_projectrc()
        Application.get_controller().read_specs(read_extended=True)
        Application.get_controller().make_env()

    if Configuration.swarm_mode:
        docker = Docker(verify_swarm=False)
        if not docker.swarm.get_token():
            docker.swarm.init()
            log.info("Swarm is now initialized")
        else:
            log.debug("Swarm is already initialized")

    if Configuration.frontend == ANGULAR:
        yarn_lock = DATA_DIR.joinpath(Configuration.project, "frontend",
                                      "yarn.lock")
        if yarn_lock.exists():
            yarn_lock.unlink()
            log.info("Yarn lock file deleted")

    log.info("Project initialized")
Exemple #3
0
def remove(
    services: List[str] = typer.Argument(
        None,
        help="Services to be removed",
        shell_complete=Application.autocomplete_service,
    ),
    rm_all: bool = typer.Option(
        False,
        "--all",
        help="Also remove persistent data stored in docker volumes",
        show_default=False,
    ),
) -> None:
    Application.print_command(
        Application.serialize_parameter("--all", rm_all, IF=rm_all),
        Application.serialize_parameter("", services),
    )

    remove_extras: List[str] = []
    for extra in (
            REGISTRY,
            "adminer",
            "swaggerui",
    ):
        if services and extra in services:
            # services is a tuple, even if defined as List[str] ...
            services = list(services)
            services.pop(services.index(extra))
            remove_extras.append(extra)

    Application.get_controller().controller_init(services)

    docker = Docker()

    if remove_extras:
        for extra_service in remove_extras:
            if not docker.client.container.exists(extra_service):
                log.error("Service {} is not running", extra_service)
                continue

            docker.client.container.remove(extra_service, force=True)
            log.info("Service {} removed", extra_service)

        # Nothing more to do
        if not services:
            return

    all_services = Application.data.services == Application.data.active_services

    if all_services and rm_all:
        # Networks are not removed, but based on docker compose down --help they should
        # Also docker-compose down removes network from what I remember
        # Should be reported as bug? If corrected a specific check in test_remove.py
        # will start to fail
        docker.client.compose.down(
            remove_orphans=False,
            remove_images="local",
            # Remove named volumes declared in the volumes section of the
            # Compose file and anonymous volumes attached to containers.
            volumes=rm_all,
        )
    else:
        # Important note: volumes=True only destroy anonymous volumes,
        # not named volumes like down should do
        docker.client.compose.rm(Application.data.services,
                                 stop=True,
                                 volumes=rm_all)

    log.info("Stack removed")
Exemple #4
0
def shell(
    service: str = typer.Argument(
        ...,
        help="Service name",
        shell_complete=Application.autocomplete_service),
    command: str = typer.Argument(
        "bash",
        help="UNIX command to be executed on selected running service"),
    user: Optional[str] = typer.Option(
        None,
        "--user",
        "-u",
        help="User existing in selected service",
        show_default=False,
    ),
    default_command: bool = typer.Option(
        False,
        "--default-command",
        "--default",
        help="Execute the default command configured for the container",
        show_default=False,
    ),
    no_tty: bool = typer.Option(
        False,
        "--no-tty",
        help=
        "Disable pseudo-tty allocation (useful for non-interactive script)",
        show_default=False,
    ),
    replica: int = typer.Option(
        1,
        "--replica",
        "--slot",
        help="Execute the command on the specified replica",
        show_default=False,
    ),
    broadcast: bool = typer.Option(
        False,
        "--broadcast",
        help="Execute the command on all the replicas",
        show_default=False,
    ),
) -> None:

    Application.print_command(
        Application.serialize_parameter("--user", user, IF=user),
        Application.serialize_parameter("--default",
                                        default_command,
                                        IF=default_command),
        Application.serialize_parameter("", service),
        Application.serialize_parameter("", command),
    )

    if no_tty:
        log.warning("--no-tty option is deprecated, you can stop using it")

    if replica > 1 and broadcast:
        print_and_exit("--replica and --broadcast options are not compatible")
    Application.get_controller().controller_init()

    docker = Docker()

    if not user:
        user = services.get_default_user(service)

    if default_command:
        command = services.get_default_command(service)

    log.debug("Requested command: {} with user: {}", command, user
              or "default")
    if broadcast:
        containers = docker.get_containers(service)
        if not containers:
            print_and_exit("No running container found for {} service",
                           service)

        docker.exec_command(containers, user=user, command=command)
    else:
        container = docker.get_container(service, slot=replica)

        if not container:
            if replica != 1:
                print_and_exit("Replica number {} not found for {} service",
                               str(replica), service)
            print_and_exit("No running container found for {} service",
                           service)

        docker.exec_command(container, user=user, command=command)
Exemple #5
0
def password(
    service: SupportedServices = typer.Argument(None, help="Service name"),
    show: bool = typer.Option(
        False,
        "--show",
        help="Show the current password(s)",
        show_default=False,
    ),
    random: bool = typer.Option(
        False,
        "--random",
        help="Generate a random password",
        show_default=False,
    ),
    new_password: str = typer.Option(
        None,
        "--password",
        help="Force the given password",
        show_default=False,
    ),
) -> None:

    Application.print_command(
        Application.serialize_parameter("--show", show, IF=show),
        Application.serialize_parameter("--random", random, IF=random),
        Application.serialize_parameter("--password",
                                        new_password,
                                        IF=new_password),
        Application.serialize_parameter("", service),
    )

    Application.get_controller().controller_init()

    # No service specified, only a summary will be reported
    if not service:

        if random:
            print_and_exit("--random flag is not supported without a service")

        if new_password:
            print_and_exit(
                "--password option is not supported without a service")

        MIN_PASSWORD_SCORE = int(
            Application.env.get("MIN_PASSWORD_SCORE", 2)  # type: ignore
        )

        last_updates = parse_projectrc()
        now = datetime.now()

        table: List[List[str]] = []
        for s in PASSWORD_MODULES:
            # This should never happens and can't be (easily) tested
            if s not in Application.data.base_services:  # pragma: no cover
                print_and_exit("Command misconfiguration, unknown {} service",
                               s)

            if s != REGISTRY and s not in Application.data.active_services:
                continue

            if s == REGISTRY and not Configuration.swarm_mode:
                continue

            module = PASSWORD_MODULES.get(s)

            if not module:  # pragma: no cover
                print_and_exit(f"{s} misconfiguration, module not found")

            for variable in module.PASSWORD_VARIABLES:

                password = Application.env.get(variable)

                if password == PLACEHOLDER:
                    score = None
                else:
                    result = zxcvbn(password)
                    score = result["score"]

                if variable in last_updates:
                    change_date = last_updates.get(variable,
                                                   datetime.fromtimestamp(0))
                    expiration_date = change_date + timedelta(
                        days=PASSWORD_EXPIRATION)
                    expired = now > expiration_date
                    last_change = change_date.strftime("%Y-%m-%d")
                else:
                    expired = True
                    last_change = "N/A"

                pass_line: List[str] = []

                pass_line.append(s)
                pass_line.append(variable)

                if expired:
                    pass_line.append(RED(last_change))
                else:
                    pass_line.append(GREEN(last_change))

                if score is None:
                    pass_line.append(RED("NOT SET"))
                elif score < MIN_PASSWORD_SCORE:
                    pass_line.append(RED(score))
                else:
                    pass_line.append(GREEN(score))

                if show:
                    pass_line.append(str(password))

                table.append(pass_line)

        headers = ["SERVICE", "VARIABLE", "LAST CHANGE", "STRENGTH"]
        if show:
            headers.append("PASSWORD")

        print("")
        print(tabulate(
            table,
            tablefmt=TABLE_FORMAT,
            headers=headers,
        ))

    # In this case a service is asked to be updated
    else:

        module = PASSWORD_MODULES.get(service.value)

        if not module:  # pragma: no cover
            print_and_exit(
                f"{service.value} misconfiguration, module not found")

        if random:
            new_password = get_strong_password()
        elif not new_password:
            print_and_exit(
                "Please specify one between --random and --password options")

        docker = Docker()

        variables = module.PASSWORD_VARIABLES
        old_password = Application.env.get(variables[0])
        new_variables = {variable: new_password for variable in variables}

        # Some services can only be updated if already running,
        # others can be updated even if offline,
        # but in every case if the stack is running it has to be restarted

        if service.value == REGISTRY:
            is_running = docker.registry.ping(do_exit=False)
            container: Optional[Tuple[str, str]] = ("registry", "")
        else:
            container = docker.get_container(service.value)
            is_running = container is not None

        is_running_needed = module.IS_RUNNING_NEEDED

        log.info("Changing password for {}...", service.value)

        if is_running_needed and (not is_running or not container):
            print_and_exit(
                "Can't update {} because it is not running. Please start your stack",
                service.value,
            )

        update_projectrc(new_variables)

        if container:
            module.password(container, old_password, new_password)

        if is_running:
            log.info("{} was running, restarting services...", service.value)

            Application.get_controller().check_placeholders_and_passwords(
                Application.data.compose_config, Application.data.services)
            if service.value == REGISTRY:
                port = cast(int, Application.env["REGISTRY_PORT"])

                docker.client.container.remove(REGISTRY, force=True)

                docker.compose.create_volatile_container(REGISTRY,
                                                         detach=True,
                                                         publish=[(port, port)
                                                                  ])
            elif Configuration.swarm_mode:

                docker.compose.dump_config(Application.data.services)
                docker.swarm.deploy()

            else:
                docker.compose.start_containers(Application.data.services)
        else:
            log.info("{} was not running, restart is not needed",
                     service.value)

        log.info(
            "The password of {} has been changed. "
            "Please find the new password into your .projectrc file as {} variable",
            service.value,
            variables[0],
        )