import math
import queue
import threading


def excepthook(args):
    errors.put(args)


errors = queue.Queue()
threading.excepthook = excepthook


import functools
import logging
import queue
import random
import sys
import threading
import time
from collections import defaultdict, deque
from contextlib import contextmanager
from datetime import datetime

import docker

import restarter.config as config
import restarter.docker_utils as docker_utils

logging.basicConfig(format="[%(threadName)s] %(message)s", level=logging.INFO)


# Keeps only the most recent element
class CoalescingQueue(queue.Queue):
    def __init__(self):
        self._lock = RWLock()
        super().__init__()

    def put(self, item):
        try:
            self.get(block=False)
        except queue.Empty:
            pass
        super().put(item)


# Source: https://gist.github.com/tylerneylon/a7ff6017b7a1f9a506cf75aa23eacfd6
class RWLock:
    def __init__(self):
        self.w_lock = threading.Lock()
        self.num_r_lock = threading.Lock()
        self.num_r = 0

    def r_acquire(self):
        self.num_r_lock.acquire()
        self.num_r += 1
        if self.num_r == 1:
            self.w_lock.acquire()
        self.num_r_lock.release()

    def r_release(self):
        self.num_r_lock.acquire()
        self.num_r -= 1
        if self.num_r == 0:
            self.w_lock.release()
        self.num_r_lock.release()

    @contextmanager
    def r_locked(self):
        try:
            self.r_acquire()
            yield
        finally:
            self.r_release()

    def w_acquire(self):
        self.w_lock.acquire()

    def w_release(self):
        self.w_lock.release()

    @contextmanager
    def w_locked(self):
        try:
            self.w_acquire()
            yield
        finally:
            self.w_release()


class CannotRestartError(Exception):
    pass


class Worker:
    def __init__(self, name):
        self.name = name
        self.lock = threading.Lock()
        self.work = CoalescingQueue()
        self.done = threading.Event()
        self.recent_status = deque([None, None], maxlen=2)
        threading.Thread(name=f"worker-{name}", target=self._work, daemon=True).start()
        logging.info(f"Worker created for container {name}")

    def _work(self):
        while True:
            # Don't block on the work queue to allow GC to (reliably) assert whether
            # (1) the work queue is empty (the read-write lock shared with the containers poller and the events handler)
            # (2) and there is no work in progress (the lock below and the `done` Event)
            while True:
                time.sleep(1)
                with self.lock:
                    try:
                        work_timestamp = self.work.get_nowait()
                    except queue.Empty:
                        continue
                    else:
                        self.done.clear()
                break
            if work_timestamp is None:
                logging.info(f"Worker for container {self.name} is shutting down.")
                self.done.set()
                return

            wait = max(math.ceil(work_timestamp + 10 - time.time()), 0)
            if wait:
                logging.info(f"Waiting {wait} seconds before taking any action.")
                time.sleep(wait)

            try:
                try:
                    container = docker_utils.client.containers.get(self.name)
                except docker.errors.NotFound:
                    raise CannotRestartError(
                        f"Container {self.name} doesn't exist anymore."
                    )

                settings = config.from_labels(container.labels)
                started_at = datetime.fromisoformat(
                    container.attrs["State"]["StartedAt"]
                ).timestamp()
                if started_at > work_timestamp:
                    raise CannotRestartError(
                        f"Container {self.name} has already been restarted."
                    )

                network_mode = container.attrs["HostConfig"].get("NetworkMode", "")
                if not network_mode.startswith("container:"):
                    try:
                        logging.info(f"Restarting container {self.name}.")
                        container.restart()
                    except Exception as err:
                        raise CannotRestartError(
                            f"Failed to restart container {self.name}. Error: {err}"
                        )
                else:
                    dependency_id = network_mode.split(":")[1]
                    dependency = None
                    try:
                        dependency = docker_utils.client.containers.get(dependency_id)
                    except docker.errors.NotFound:
                        pass
                    if dependency:
                        logging.info(f"Restarting container {self.name}.")
                        try:
                            container.restart()
                        except Exception as err:
                            raise CannotRestartError(
                                f"Failed to restart container {self.name}. Error: {err}"
                            )
                    else:
                        parent = None
                        restarter_network_mode = settings[config.Setting.NETWORK_MODE]
                        if not restarter_network_mode:
                            raise CannotRestartError(
                                f"Label {RESTARTER_NETWORK_MODE} is required in order to recreate component {self.name}."
                            )
                        if restarter_network_mode.lower().startswith("container:"):
                            dependency_name = restarter_network_mode.split(":")[1]
                            try:
                                parent = docker_utils.client.containers.get(
                                    dependency_name
                                )
                            except docker.errors.NotFound:
                                pass
                        elif restarter_network_mode.lower().startswith("service:"):
                            service = restarter_network_mode.split(":")[1]
                            for p in docker_utils.list_with_retry():
                                if p.labels.get(COMPOSE_SERVICE, "") == service:
                                    parent = p
                                    break
                        elif container.labels.get(COMPOSE_SERVICE, ""):
                            service = restarter_network_mode
                            for p in docker_utils.list_with_retry():
                                if p.labels.get(COMPOSE_SERVICE, "") == service:
                                    parent = p
                                    break
                        else:
                            dependency_name = restarter_network_mode
                            try:
                                parent = docker_utils.client.containers.get(
                                    dependency_name
                                )
                            except docker.errors.NotFound:
                                pass

                        if not parent:
                            raise CannotRestartError(
                                f"Could not find any container matching {RESTARTER_NETWORK_MODE}={restarter_network_mode}."
                            )

                        run_args = docker_utils.get_container_run_args(
                            container, parent.id
                        )

                        try:
                            logging.info(f"Removing container {self.name}.")
                            container.remove(force=True)
                        except docker.errors.NotFound:
                            raise CannotRestartError(
                                f"Container {self.name} doesn't exist anymore."
                            )

                        logging.info(f"Recreating container {self.name}.")
                        docker_utils.client.containers.run(**run_args)
            except CannotRestartError as err:
                logging.info(
                    f"Can't/won't restart container {self.name}. Reason: {err}"
                )
                continue
            finally:
                self.done.set()


class Workers(dict[str, Worker], RWLock):
    def __init__(self):
        self._lock = RWLock()
        dict.__init__(self)
        RWLock.__init__(self)

    def __getitem__(self, name):
        if name not in self:
            with self.w_locked():
                if name not in self:
                    self[name] = Worker(name)
        return super().__getitem__(name)


# CONSTANTS
RESTARTER_DEPENDS_ON = "restarter.depends_on"
RESTARTER_NETWORK_MODE = "restarter.network_mode"
COMPOSE_CONFIG_FILES = "com.docker.compose.project.config_files"
COMPOSE_DEPENDS_ON = "com.docker.compose.depends_on"
COMPOSE_PROJECT = "com.docker.compose.project"
COMPOSE_SERVICE = "com.docker.compose.service"
COMPOSE_WORKING_DIR = "com.docker.compose.project.working_dir"

# globals
workers_lock = RWLock()
workers = Workers()


def timed(*, message):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            logging.info(f"{message}... Starting")
            result = func(*args, **kwargs)
            duration = time.time() - start
            logging.info(f"{message}... Done ({round(duration, 1)}s)")
            return result

        return wrapper

    return decorator


def repeat(*, every_seconds):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            while True:
                func(*args, **kwargs)
                time.sleep(every_seconds)

        return wrapper

    return decorator


def check_containers():
    containers = docker_utils.list_with_retry(all=True)

    containers_idx = defaultdict(dict)
    for container in containers:
        containers_idx["id"][container.id] = container
        containers_idx["name"][container.name] = container
        if service := container.labels.get(COMPOSE_SERVICE, None):
            containers_idx["service"][service] = container

    to_be_restarted = set()
    for container in containers:
        settings = config.from_labels(container.labels)
        if not settings[config.Setting.ENABLE]:
            continue

        if (
            config.Policy.UNHEALTHY in settings[config.Setting.POLICY]
            and container.attrs["State"].get("Health", {}).get("Status", "")
            == "unhealthy"
        ):
            logging.info(f"Container {container.name} is in unhealthy state.")
            to_be_restarted.add(container.name)

        if config.Policy.DEPENDENCY in settings[config.Setting.POLICY]:
            dependencies = set()

            if (
                network_mode := container.attrs["HostConfig"].get("NetworkMode", "")
            ).startswith("container:"):
                dependency_id = network_mode.split(":")[1]
                if dependency_id in containers_idx["id"]:
                    dependencies.add(containers_idx["id"][dependency_id])

            for depends_on in container.labels.get(COMPOSE_DEPENDS_ON, "").split(","):
                if not depends_on:
                    continue
                service = depends_on.split(":")[0]
                if service in containers_idx["service"]:
                    dependencies.add(containers_idx["service"][service])

            for depends_on in container.labels.get(RESTARTER_DEPENDS_ON, "").split(
                ","
            ) + [container.labels.get(RESTARTER_NETWORK_MODE, "")]:
                if not depends_on:
                    continue
                if depends_on.startswith("container:"):
                    dependency_name = depends_on.split(":")[1]
                    if dependency_name in containers_idx["name"]:
                        dependencies.add(containers_idx["name"][dependency_name])
                elif depends_on.startswith("service:"):
                    service = depends_on.split(":")[1]
                    if service in containers_idx["service"]:
                        dependencies.add(containers_idx["service"][service])
                elif container.labels.get(COMPOSE_SERVICE, ""):
                    if depends_on in containers_idx["service"]:
                        dependencies.add(containers_idx["service"][depends_on])
                else:
                    if depends_on in containers_idx["name"]:
                        dependencies.add(containers_idx["name"][depends_on])

            started_at = datetime.fromisoformat(
                container.attrs["State"]["StartedAt"]
            ).timestamp()
            for dependency in dependencies:
                if (
                    dependency.attrs["State"].get("Health", {}).get("Status", "")
                    == "unhealthy"
                    or dependency.attrs["State"]["Status"] != "running"
                ):
                    logging.info(
                        f"Container {dependency.name} is in unhealthy state or not running and container {container.name} depends on it."
                    )
                    to_be_restarted.add(dependency.name)
                    to_be_restarted.add(container.name)

                dependency_started_at = datetime.fromisoformat(
                    dependency.attrs["State"]["StartedAt"]
                ).timestamp()
                if started_at <= dependency_started_at:
                    logging.info(
                        f"Container {container.name} has been started before its dependency {dependency.name}."
                    )
                    to_be_restarted.add(container.name)

    timestamp = time.time()
    for container_name in to_be_restarted:
        with workers_lock.r_locked():
            workers[container_name].work.put(timestamp)


@repeat(
    every_seconds=config.global_settings[config.GlobalSetting.GC_EVERY_SECONDS],
)
@timed(message="Periodic garbage collection")
def gc():
    with workers_lock.w_locked():
        for name in list(workers.keys()):
            with workers[name].lock:
                if workers[name].work.empty() and workers[name].done.is_set():
                    logging.info(
                        f"Worker for container {name} is not required anymore."
                    )
                    workers[name].work.put(None)
                    del workers[name]


MONITORED_EVENTS = ("start", "health_status: unhealthy", "die")


def events():
    for event in docker_utils.client.events(
        decode=True,
        filters={"type": "container"},
    ):
        status = event["status"]
        if status not in MONITORED_EVENTS:
            continue
        with workers_lock.r_locked():
            name = event["Actor"]["Attributes"]["name"]
            workers[name].recent_status.append(status)

            logging.info(
                f'Received a "{status}" event for container {name}. Triggering a full check.'
            )
            timed(message="Ad-hoc containers check")(check_containers)()


logging.info("docker-restarter https://github.com/cascandaliato/docker-restarter")
config.dump_env_variables()
config.dump(config.global_settings, "Global settings:")
config.dump(config.defaults, "Defaults:")

threading.Thread(name="events", target=events, daemon=True).start()
threading.Thread(
    name="poller",
    target=repeat(
        every_seconds=config.global_settings[config.GlobalSetting.CHECK_EVERY_SECONDS]
    )(timed(message="Periodic containers check")(check_containers)),
).start()
time.sleep(random.randrange(10))
threading.Thread(name="gc", target=gc, daemon=True).start()

error = errors.get()

if error.thread:
    logging.info(f"Thread: {error.thread}")
logging.info(f"Exception Type: {error.exc_type}")
if error.exc_value:
    logging.info(f"Exception Value: {error.exc_value}")
if error.exc_traceback:
    import traceback

    traceback.print_tb(error.exc_traceback)

logging.info("Exiting...")

main.py
cascandaliato/docker-restarter
import logging
import os
import sys
from collections import ChainMap
from enum import Enum

_PREFIX = "restarter"


def _env(setting, default):
    return os.environ.get(f"{_PREFIX.upper()}_{setting.upper()}", default)


def _parse_policy(policy):
    return sorted(
        [Policy(p.strip().lower()) for p in policy.split(",")], key=lambda p: p.value
    )


def _to_bool(s):
    return s.strip().lower() in ["yes", "true"]


class Policy(Enum):
    DEPENDENCY = "dependency"
    UNHEALTHY = "unhealthy"


class GlobalSetting(Enum):
    CHECK_EVERY_SECONDS = (int, 60)
    GC_EVERY_SECONDS = (int, 300)


class Setting(Enum):
    ENABLE = (2, _to_bool, "yes")
    DEPENDS_ON = (3, str, "")
    NETWORK_MODE = (6, str, "")
    POLICY = (7, str, "dependency,unhealthy")


global_settings = {}
for setting in GlobalSetting:
    type_, default = setting.value
    global_settings[setting] = type_(_env(setting.name, default))

defaults = {}
for setting in Setting:
    _, type_, default = setting.value
    if setting == Setting.POLICY:
        defaults[setting] = _parse_policy(type_(_env(setting.name, default)))
    else:
        defaults[setting] = type_(_env(setting.name, default))


def from_labels(labels):
    config = {}
    for setting in Setting:
        key = f"{_PREFIX}.{setting.name.lower()}"
        if key in labels:
            config[setting] = setting.value[1](labels[key])
            if setting == Setting.POLICY:
                config[setting] = _parse_policy(config[setting])
    return ChainMap(config, defaults)


_SORTED_SETTINGS = sorted(
    [s for enum_ in [GlobalSetting, Setting] for s in enum_], key=lambda s: s.name
)


def dump(settings, message):
    for setting in _SORTED_SETTINGS:
        if setting in settings:
            if setting == Setting.POLICY:
                message += f"\n  {setting.name.lower()} = {', '.join(p.value for p in settings[setting])}"
            else:
                value = settings[setting]
                if isinstance(value, bool):
                    value = "yes" if value else "no"
                elif isinstance(value, str) and not value:
                    value = "<empty>"
                elif isinstance(value, int) and value == sys.maxsize:
                    value = "unlimited"
                message += f"\n  {setting.name.lower()} = {value}"
    logging.info(message)


def dump_env_variables():
    message = "Environment variables:"
    vars = [var for var in os.environ if var.startswith(f"{_PREFIX.upper()}_")]
    for var in sorted(vars):
        message += f"\n  {var} = {os.environ[var]}"
    if not vars:
        message += "\n  -"
    logging.info(message)

restarter/config.py
cascandaliato/docker-restarter
import logging
import time

import docker
from docker.types import DeviceRequest, LogConfig, Mount, Ulimit


def get_container_run_args(container, parent_id):
    image = container.image

    run_args = {
        "image": image.id,
        "command": container.attrs["Config"]["Cmd"],
        "auto_remove": False,
        "blkio_weight_device": container.attrs["HostConfig"]["BlkioWeightDevice"],
        "blkio_weight": container.attrs["HostConfig"]["BlkioWeight"],
        "cap_add": container.attrs["HostConfig"]["CapAdd"],
        "cap_drop": container.attrs["HostConfig"]["CapDrop"],
        "cgroup_parent": container.attrs["HostConfig"]["CgroupParent"],
        "cgroupns": container.attrs["HostConfig"]["CgroupnsMode"],
        "cpu_count": container.attrs["HostConfig"]["CpuCount"],
        "cpu_percent": container.attrs["HostConfig"]["CpuPercent"],
        "cpu_period": container.attrs["HostConfig"]["CpuPeriod"],
        "cpu_quota": container.attrs["HostConfig"]["CpuQuota"],
        "cpu_rt_period": container.attrs["HostConfig"]["CpuRealtimePeriod"],
        "cpu_rt_runtime": container.attrs["HostConfig"]["CpuRealtimeRuntime"],
        "cpu_shares": container.attrs["HostConfig"]["CpuShares"],
        "cpuset_cpus": container.attrs["HostConfig"]["CpusetCpus"],
        "cpuset_mems": container.attrs["HostConfig"]["CpusetMems"],
        "detach": True,
        "device_cgroup_rules": container.attrs["HostConfig"]["DeviceCgroupRules"],
        "device_read_bps": container.attrs["HostConfig"]["BlkioDeviceReadBps"],
        "device_read_iops": container.attrs["HostConfig"]["BlkioDeviceReadIOps"],
        "device_write_bps": container.attrs["HostConfig"]["BlkioDeviceWriteBps"],
        "device_write_iops": container.attrs["HostConfig"]["BlkioDeviceWriteIOps"],
        "devices": [
            f'{d["PathOnHost"]}:{d["PathInContainer"]}:{d["CgroupPermissions"]}'
            for d in container.attrs["HostConfig"]["Devices"] or []
        ],
        "device_requests": container.attrs["HostConfig"]["DeviceRequests"],
        "dns": container.attrs["HostConfig"]["Dns"],
        "dns_opt": container.attrs["HostConfig"]["DnsOptions"],
        "dns_search": container.attrs["HostConfig"]["DnsSearch"],
        "domainname": container.attrs["Config"]["Domainname"],
        "entrypoint": container.attrs["Config"]["Entrypoint"],
        "environment": container.attrs["Config"]["Env"],
        "group_add": container.attrs["HostConfig"]["GroupAdd"],
        "hostname": container.attrs["Config"]["Hostname"],
        "init": container.attrs["HostConfig"].get("Init", None),
        "ipc_mode": container.attrs["HostConfig"]["IpcMode"],
        "isolation": container.attrs["HostConfig"]["Isolation"],
        "labels": container.attrs["Config"]["Labels"],
        "mac_address": container.attrs["NetworkSettings"]["MacAddress"],
        "mem_limit": container.attrs["HostConfig"]["Memory"],
        "mem_reservation": container.attrs["HostConfig"]["MemoryReservation"],
        "mem_swappiness": container.attrs["HostConfig"]["MemorySwappiness"],
        "memswap_limit": container.attrs["HostConfig"]["MemorySwap"],
        "name": container.name,
        "nano_cpus": container.attrs["HostConfig"]["NanoCpus"],
        # "network": None, # incompatible with network_mode
        "network_disabled": container.attrs["Config"].get("NetworkDisabled", False),
        "network_mode": container.attrs["HostConfig"]["NetworkMode"],
        # "network_driver_opt": None, # incompatible with network_mode
        "oom_kill_disable": container.attrs["HostConfig"]["OomKillDisable"],
        "oom_score_adj": container.attrs["HostConfig"]["OomScoreAdj"],
        "pid_mode": container.attrs["HostConfig"]["PidMode"],
        "pids_limit": container.attrs["HostConfig"]["PidsLimit"],
        "platform": container.attrs["Platform"],
        "privileged": container.attrs["HostConfig"]["Privileged"],
        "publish_all_ports": container.attrs["HostConfig"]["PublishAllPorts"],
        "read_only": container.attrs["HostConfig"]["ReadonlyRootfs"],
        "restart_policy": container.attrs["HostConfig"]["RestartPolicy"],
        "security_opt": container.attrs["HostConfig"]["SecurityOpt"],
        "shm_size": container.attrs["HostConfig"]["ShmSize"],
        "stdin_open": container.attrs["Config"]["OpenStdin"],
        "stdout": container.attrs["Config"]["AttachStdout"],
        "stderr": container.attrs["Config"]["AttachStderr"],
        "stop_signal": container.attrs["Config"].get("StopSignal", None),
        "tty": container.attrs["Config"]["Tty"],
        # "use_config_proxy": False,
        "user": container.attrs["Config"]["User"],
        "userns_mode": container.attrs["HostConfig"]["UsernsMode"],
        "uts_mode": container.attrs["HostConfig"]["UTSMode"],
        "version": "auto",
        "volume_driver": container.attrs["HostConfig"]["VolumeDriver"],
        "volumes_from": container.attrs["HostConfig"]["VolumesFrom"],
        "working_dir": container.attrs["Config"]["WorkingDir"],
    }

    if device_requests := container.attrs["HostConfig"].get("DeviceRequests", None):
        run_args["device_requests"] = [DeviceRequest(**dr) for dr in device_requests]

    if extra_hosts := container.attrs["HostConfig"].get("ExtraHosts", None):
        run_args["extra_hosts"] = {
            eh.split(":")[0]: eh.split(":")[1] for eh in extra_hosts
        }

    if healtcheck := container.attrs["Config"].get("Healthcheck", None):
        run_args["healthcheck"] = {}
        for field in ["test", "interval", "timeout", "retries", "start_period"]:
            source = "".join(t.capitalize() for t in field.split("_"))
            if source in healtcheck:
                run_args["healthcheck"][field] = healtcheck[source]

    # [ "/linked:/linker/alias" ] to { "linked": "alias" }
    if links := container.attrs["HostConfig"].get("Links", None):
        run_args = {l.split(":")[0][1:]: l.split(":")[1].split("/")[2] for l in links}

    if container.attrs["HostConfig"].get("LogConfig", None):
        log_config = {}
        for field in ["type", "config"]:
            if (source := field.capitalize()) in container.attrs["HostConfig"][
                "LogConfig"
            ]:
                log_config[field] = container.attrs["HostConfig"]["LogConfig"][source]
        if log_config:
            run_args["log_config"] = LogConfig(**log_config)

    if ports := container.attrs["HostConfig"]["PortBindings"]:
        run_args["ports"] = {
            k: [(b["HostIp"], b["HostPort"]) for b in v] for k, v in ports.items()
        }

    # https://github.com/docker/docker-py/blob/c38656dc7894363f32317affecc3e4279e1163f8/docker/types/services.py#L217
    if mounts := container.attrs["HostConfig"].get("Mounts", None):
        mounts_arg = []
        for mount in mounts:
            mount_args = {}
            for field in ["target", "source", "type", "read_only", "consistency"]:
                source = "".join(w.capitalize() for w in field.split("_"))
                if source in mount:
                    mount_args[field] = mount[source]
            if "source" not in mount_args:
                mount_args["source"] = None

            if "BindOptions" in mount and "Propagation" in mount["BindOptions"]:
                mount_args["propagation"] = mount["BindOptions"]["Propagation"]

            if "VolumeOptions" in mount:
                for field in ["no_copy", "labels", "driver_config"]:
                    source = "".join(w.capitalize() for w in field.split("_"))
                    if source in mount["VolumeOptions"]:
                        mount_args[field] = mount["VolumeOptions"][source]

            if "TmpfsOptions" in mount:
                for source, destination in [
                    ("Mode", "tmpfs_mode"),
                    ("SizeBytes", "tmpfs_size"),
                ]:
                    if source in mount["TmpfsOptions"]:
                        mount_args[destination] = int(mount["TmpfsOptions"][source])

            mounts_arg.append(Mount(**mount_args))
        run_args["mounts"] = mounts_arg

    if sysctls := container.attrs["HostConfig"].get("Sysctls", None):
        run_args["sysctls"] = sysctls

    if tmpfs := container.attrs["HostConfig"].get("Tmpfs", None):
        run_args["tmpfs"] = tmpfs

    if ulimits := container.attrs["HostConfig"].get("Ulimits", None):
        run_args["ulimits"] = [
            Ulimit(**{k.lower(): v for k, v in ulimit.items()}) for ulimit in ulimits
        ]

    # https://github.com/containrrr/watchtower/blob/9b28fbc24ddad49b29146fe48fad78cac02838e4/pkg/container/container.go
    # https://github.com/containrrr/watchtower/blob/9b28fbc24ddad49b29146fe48fad78cac02838e4/internal/util/util.go
    if container.attrs["Config"]["WorkingDir"] == image.attrs["Config"]["WorkingDir"]:
        run_args["working_dir"] = ""

    if container.attrs["HostConfig"]["NetworkMode"].startswith("container:"):
        run_args["hostname"] = ""
        run_args["network_mode"] = f"container:{parent_id}"

    if container.attrs["Config"]["Entrypoint"] == image.attrs["Config"]["Entrypoint"]:
        run_args["entrypoint"] = None
        if container.attrs["Config"]["Cmd"] == image.attrs["Config"]["Cmd"]:
            run_args["command"] = None

    run_args["environment"] = [
        v
        for v in container.attrs["Config"].get("Env", [])
        if v not in image.attrs["Config"].get("Env", [])
    ]

    container_labels = container.attrs["Config"]["Labels"] or {}
    image_labels = image.attrs["Config"]["Labels"] or {}
    run_args["labels"] = {
        k: v
        for k, v in container_labels.items()
        if k not in image_labels or image_labels[k] != v
    }

    if binds := container.attrs["HostConfig"].get("Binds", None):
        run_args["volumes"] = binds

    for v in image.attrs["Config"].get("Volumes", {}) or {}:
        for i in range(len(run_args.get("mounts", []))):
            if run_args["mounts"][i]["Target"] == v:
                del run_args["mounts"][i]
                break

    # // subtract ports exposed in image from container
    # for k := range config.ExposedPorts {
    # 	if _, ok := imageConfig.ExposedPorts[k]; ok {
    # 		delete(config.ExposedPorts, k)
    # 	}
    # }
    # for p := range c.containerInfo.HostConfig.PortBindings {
    # 	config.ExposedPorts[p] = struct{}{}
    # }

    return run_args


client = docker.from_env()


def list_with_retry(*args, **kwargs):
    while True:
        try:
            return client.containers.list(*args, **kwargs)
        except docker.errors.NotFound as err:
            logging.info(
                f"Failed to retrieve containers. Retrying in one second. Error: {err}"
            )
            time.sleep(1)

restarter/docker_utils.py
cascandaliato/docker-restarter