Loading .pre-commit-config.yaml +45 −0 Original line number Diff line number Diff line Loading @@ -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 images/docker/Dockerfile +6 −1 Original line number Diff line number Diff line 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/* images/docker/compose-health-check 0 → 100755 +113 −0 Original line number Diff line number Diff line #!/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()) Loading
.pre-commit-config.yaml +45 −0 Original line number Diff line number Diff line Loading @@ -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
images/docker/Dockerfile +6 −1 Original line number Diff line number Diff line 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/*
images/docker/compose-health-check 0 → 100755 +113 −0 Original line number Diff line number Diff line #!/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())