diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0288a39de68039ca01b3488f1cac97c113c29255..b19b7fc2c5b6224bc0ad04fbaf4449f5545ce793 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -80,3 +80,48 @@ repos: args: - "--server" - https://git.shore.co.il + + - repo: https://github.com/ambv/black.git + rev: 21.9b0 + hooks: + - id: black + args: + - | + --line-length=79 + + - repo: https://github.com/PyCQA/prospector.git + rev: 1.5.1 + hooks: + - id: prospector + args: + - |- + --max-line-length=79 + - |- + --with-tool=pyroma + - |- + --with-tool=bandit + - |- + --without-tool=pep257 + - |- + --doc-warnings + - |- + --test-warnings + - |- + --full-pep8 + - |- + --strictness=high + - |- + --no-autodetect + additional_dependencies: + - bandit + - pyroma + + - repo: https://gitlab.com/pycqa/flake8.git + rev: 3.9.2 + hooks: + - id: flake8 + args: + - |- + --doctests + additional_dependencies: + - flake8-bugbear diff --git a/images/docker/Dockerfile b/images/docker/Dockerfile index 4d1e535a648e7dd5f3bdb717ed9c977669be19af..1d0795b3a109d8dc6b4a478082c89ef88db76d9c 100644 --- a/images/docker/Dockerfile +++ b/images/docker/Dockerfile @@ -1,3 +1,8 @@ FROM docker:20.10 # hadolint ignore=DL3018 -RUN apk add --update-cache --no-cache docker-compose +RUN apk add --update-cache --no-cache \ + docker-compose \ + docker-py \ + py3-dotenv \ + && \ + rm -rf /root/.cache /tmp/* /var/tmp/* diff --git a/images/docker/compose-health-check b/images/docker/compose-health-check new file mode 100755 index 0000000000000000000000000000000000000000..f849664791e5c5adc5981834274de8fd2e660ef2 --- /dev/null +++ b/images/docker/compose-health-check @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# pylint: disable=invalid-name +"""Checks the health of a Docker Compose deployment.""" + + +import argparse +import datetime +import os +import sys +import time +import docker # pylint: disable=import-error +import dotenv # pylint: disable=import-error + + +def get_project_containers_health(client, project): + """Get a Docker connection and the Docker Compose project name and return a + dictionary of container objects and their health status (if available, else + their status).""" + return { + i: i.attrs["State"]["Health"]["Status"] + if "Health" in i.attrs["State"] + else i.attrs["State"]["Status"] + for i in client.containers.list( + filters={"label": f"com.docker.compose.project={project}"} + ) + } + + +def check_project_health(project, timeout): + """Checks if a Docker Compose project is healthy. + + If all of the containers are healthy or running (because there's no + healthcheck set), return True. If any of the containers is starting (health + check hasn't run yet or in the grace period), sleep 10 and try again + (ignoring the timeout). Otherwise, if haven't passed the timeout, sleep 10 + and try again, if over the timeout, return if all the containers are + healthy or running. + """ + starttime = datetime.datetime.now() + deadline = starttime + datetime.timedelta(seconds=timeout) + docker_client = docker.from_env() + docker_client.ping() + while True: + healths = get_project_containers_health(docker_client, project) + # pylint: disable=no-else-return + if all(map(lambda x: x in ["healthy", "running"], healths.values())): + return True + elif any(map(lambda x: x == "starting", healths.values())): + time.sleep(10) + continue + elif datetime.datetime.now() > deadline: + break + else: + time.sleep(10) + + if all(map(lambda x: x in ["healthy", "running"], healths.values())): + return True + unhealthy = ", ".join( + [ + f"{x.id}({x.name})" + for x in healths + if healths[x] not in ["healthy", "running"] + ] + ) + print(f"""Containers {unhealthy} are not healthy.""") + return False + + +def main(): + """Main entrypoint.""" + + epilog = ( + "The Docker Compose project name is resolved in the following order:" # noqa: E501 + "\n1. From the command line parameter." + "\n2. From the COMPOSE_PROJECT_NAME variable in the .env file." + "\n3. From the COMPOSE_PROJECT_NAME environment variable." + ) + arg_parser = argparse.ArgumentParser( + description=__doc__, + epilog=epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + arg_parser.add_argument( + "project", help="Name of the Compose Project.", nargs="?" + ) + arg_parser.add_argument( + "-t", + "--timeout", + help="Number of seconds to wait for the deployment to be healthy, defaults to 300.", # noqa: E501 + type=int, + default=300, + ) + args = arg_parser.parse_args() + if args.project: + project = args.project + else: + env = dotenv.dotenv_values(dotenv_path=".env") + if "COMPOSE_PROJECT_NAME" in env: + project = env["COMPOSE_PROJECT_NAME"] + elif "COMPOSE_PROJECT_NAME" in os.environ: + project = os.environ["COMPOSE_PROJECT_NAME"] + else: + arg_parser.error( + "Compose project wasn't specified, the COMPOSE_PROJECT_NAME variable is missing from the environment and from the .env file." # noqa: E501 + ) + + if check_project_health(project, args.timeout): + return 0 + return 1 + + +if __name__ == "__main__": + sys.exit(main())