add garbage colletor run method

This commit is contained in:
Robert Kaussow 2020-03-09 01:05:17 +01:00
parent 371789c258
commit 1561264542
5 changed files with 82 additions and 48 deletions

View File

@ -68,4 +68,8 @@ class AutoStop:
def _get_docker_client(self): def _get_docker_client(self):
config = self.config.config config = self.config.config
return docker.APIClient(version="auto", timeout=config["timeout"]) return docker.APIClient(version="auto", timeout=config["http_timeout"])
def run(self):
"""Autostop main method."""
print("run stop")

View File

@ -5,7 +5,9 @@ import argparse
import dockertidy.Exception import dockertidy.Exception
from dockertidy import __version__ from dockertidy import __version__
from dockertidy.Autostop import AutoStop
from dockertidy.Config import SingleConfig from dockertidy.Config import SingleConfig
from dockertidy.GarbageCollector import GarbageCollector
from dockertidy.Logger import SingleLog from dockertidy.Logger import SingleLog
from dockertidy.Parser import timedelta_validator from dockertidy.Parser import timedelta_validator
@ -18,6 +20,9 @@ class DockerTidy:
self.logger = self.log.logger self.logger = self.log.logger
self.args = self._cli_args() self.args = self._cli_args()
self.config = self._get_config() self.config = self._get_config()
self.gc = GarbageCollector()
self.stop = AutoStop()
self.run()
def _cli_args(self): def _cli_args(self):
""" """
@ -54,6 +59,7 @@ class DockerTidy:
) )
subparsers = parser.add_subparsers(dest="command", help="sub-command help") subparsers = parser.add_subparsers(dest="command", help="sub-command help")
subparsers.required = True
parser_gc = subparsers.add_parser("gc", help="Run docker garbage collector.") parser_gc = subparsers.add_parser("gc", help="Run docker garbage collector.")
parser_gc.add_argument( parser_gc.add_argument(
@ -84,7 +90,7 @@ class DockerTidy:
"--exclude-image", "--exclude-image",
action="append", action="append",
type=str, type=str,
dest="gc.exclude_image", dest="gc.exclude_images",
metavar="EXCLUDE_IMAGE", metavar="EXCLUDE_IMAGE",
help="Never remove images with this tag." help="Never remove images with this tag."
) )
@ -92,7 +98,7 @@ class DockerTidy:
"--exclude-container-label", "--exclude-container-label",
action="append", action="append",
type=str, type=str,
dest="gc.exclude_container_label", dest="gc.exclude_container_labels",
metavar="EXCLUDE_CONTAINER_LABEL", metavar="EXCLUDE_CONTAINER_LABEL",
help="Never remove containers with this label key " help="Never remove containers with this label key "
"or label key=value" "or label key=value"
@ -127,13 +133,19 @@ class DockerTidy:
except dockertidy.Exception.ConfigError as e: except dockertidy.Exception.ConfigError as e:
self.log.sysexit_with_message(e) self.log.sysexit_with_message(e)
print(config.config)
try: try:
self.log.set_level(config.config["logging"]["level"]) self.log.set_level(config.config["logging"]["level"])
except ValueError as e: except ValueError as e:
self.log.sysexit_with_message("Can not set log level.\n{}".format(str(e))) self.log.sysexit_with_message("Can not set log level.\n{}".format(str(e)))
self.logger.info("Using config file {}".format(config.config_file)) self.logger.info("Using config file {}".format(config.config_file))
self.logger.debug("Config dump: {}".format(config.config))
return config return config
def run(self):
"""Cli main method."""
if self.config.config["command"] == "gc":
self.gc.run()
elif self.config.config["command"] == "stop":
self.stop.run()

View File

@ -63,37 +63,37 @@ class Config():
"type": environs.Env().bool "type": environs.Env().bool
}, },
"gc.max_container_age": { "gc.max_container_age": {
"default": "1day", "default": "",
"env": "GC_MAX_CONTAINER_AGE", "env": "GC_MAX_CONTAINER_AGE",
"file": True, "file": True,
"type": env.timedelta_validator "type": env.timedelta_validator
}, },
"gc.max_image_age": { "gc.max_image_age": {
"default": "1day", "default": "",
"env": "GC_MAX_IMAGE_AGE", "env": "GC_MAX_IMAGE_AGE",
"file": True, "file": True,
"type": env.timedelta_validator "type": env.timedelta_validator
}, },
"gc.dangling_volumes": { "gc.dangling_volumes": {
"default": False, "default": False,
"env": "GC_EXCLUDE_IMAGE", "env": "GC_DANGLING_VOLUMES",
"file": True, "file": True,
"type": environs.Env().bool "type": environs.Env().bool
}, },
"gc.exclude_image": { "gc.exclude_images": {
"default": [], "default": [],
"env": "GC_DANGLING_VOLUMES", "env": "GC_EXCLUDE_IMAGES",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list
}, },
"gc.exclude_container_label": { "gc.exclude_container_labels": {
"default": [], "default": [],
"env": "GC_EXCLUDE_CONTAINER_LABEL", "env": "GC_EXCLUDE_CONTAINER_LABELS",
"file": True, "file": True,
"type": environs.Env().list "type": environs.Env().list
}, },
"stop.max_run_time": { "stop.max_run_time": {
"default": "3days", "default": "",
"env": "STOP_MAX_RUN_TIME", "env": "STOP_MAX_RUN_TIME",
"file": True, "file": True,
"type": env.timedelta_validator "type": env.timedelta_validator

View File

@ -11,6 +11,7 @@ import requests.exceptions
from dockertidy.Config import SingleConfig from dockertidy.Config import SingleConfig
from dockertidy.Logger import SingleLog from dockertidy.Logger import SingleLog
from dockertidy.Parser import timedelta
class GarbageCollector: class GarbageCollector:
@ -30,10 +31,14 @@ class GarbageCollector:
"""Identify old containers and remove them.""" """Identify old containers and remove them."""
config = self.config.config config = self.config.config
client = self.docker client = self.docker
all_containers = self._get_all_containers(client) all_containers = self._get_all_containers()
filtered_containers = self._filter_excluded_containers(
all_containers, filtered_containers = self._filter_excluded_containers(all_containers)
config["gc"]["exclude_container_labels"],
self.logger.info(
"Removing containers older than '{}'".format(
timedelta(config["gc"]["max_container_age"], dt_format="%Y-%m-%d, %H:%M:%S")
)
) )
for container_summary in reversed(list(filtered_containers)): for container_summary in reversed(list(filtered_containers)):
@ -43,14 +48,14 @@ class GarbageCollector:
) )
if not container or not self._should_remove_container( if not container or not self._should_remove_container(
container, container,
config["gc"]["max_container_age"], timedelta(config["gc"]["max_container_age"]),
): ):
continue continue
self.logger.info( self.logger.info(
"Removing container %s %s %s" % ( "Removing container {} {} {}".format(
container["Id"][:16], container.get("Name", "").lstrip("/"), container["Id"][:16],
container["State"]["FinishedAt"] container.get("Name", "").lstrip("/"), container["State"]["FinishedAt"]
) )
) )
@ -152,16 +157,19 @@ class GarbageCollector:
images = self._filter_images_in_use_by_id(images, image_ids_in_use) images = self._filter_images_in_use_by_id(images, image_ids_in_use)
images = self._filter_excluded_images(images, exclude_set) images = self._filter_excluded_images(images, exclude_set)
for image_summary in reversed(list(images)): self.logger.info(
self._rmove_image( "Removing images older than '{}'".format(
client, image_summary, config["gc"]["max_image_age"], config["dry_run"] timedelta(config["gc"]["max_image_age"], dt_format="%Y-%m-%d, %H:%M:%S")
) )
)
for image_summary in reversed(list(images)):
self._remove_image(image_summary, timedelta(config["gc"]["max_image_age"]))
def _filter_excluded_images(self, images, exclude_set): def _filter_excluded_images(self, images, exclude_set):
def include_image(image_summary): def include_image(image_summary):
image_tags = image_summary.get("RepoTags") image_tags = image_summary.get("RepoTags")
if self.no_image_tags(image_tags): if self._no_image_tags(image_tags):
return True return True
for exclude_pattern in exclude_set: for exclude_pattern in exclude_set:
if fnmatch.filter(image_tags, exclude_pattern): if fnmatch.filter(image_tags, exclude_pattern):
@ -197,7 +205,7 @@ class GarbageCollector:
def _no_image_tags(self, image_tags): def _no_image_tags(self, image_tags):
return not image_tags or image_tags == ["<none>:<none>"] return not image_tags or image_tags == ["<none>:<none>"]
def _remove_image(self, client, image_summary, min_date): def _remove_image(self, image_summary, min_date):
config = self.config.config config = self.config.config
client = self.docker client = self.docker
image = self._api_call(client.inspect_image, image=image_summary["Id"]) image = self._api_call(client.inspect_image, image=image_summary["Id"])
@ -219,8 +227,9 @@ class GarbageCollector:
for image_tag in image_tags: for image_tag in image_tags:
self._api_call(client.remove_image, image=image_tag) self._api_call(client.remove_image, image=image_tag)
def _remove_volume(self, client, volume): def _remove_volume(self, volume):
config = self.config.config config = self.config.config
client = self.docker
if not volume: if not volume:
return return
@ -230,13 +239,14 @@ class GarbageCollector:
self._api_call(client.remove_volume, name=volume["Name"]) self._api_call(client.remove_volume, name=volume["Name"])
def cleanup_volumes(self, client): def cleanup_volumes(self):
"""Identify old volumes and remove them.""" """Identify old volumes and remove them."""
dangling_volumes = self._get_dangling_volumes(client)
dangling_volumes = self._get_dangling_volumes()
for volume in reversed(dangling_volumes): for volume in reversed(dangling_volumes):
self.logger.info("Removing dangling volume %s", volume["Name"]) self.logger.info("Removing dangling volume %s", volume["Name"])
self._remove_volume(client, volume) self._remove_volume(volume)
def _api_call(self, func, **kwargs): def _api_call(self, func, **kwargs):
try: try:
@ -260,7 +270,7 @@ class GarbageCollector:
def _build_exclude_set(self): def _build_exclude_set(self):
config = self.config.config config = self.config.config
exclude_set = set(config["gc"]["exclude_image"]) exclude_set = set(config["gc"]["exclude_images"])
def is_image_tag(line): def is_image_tag(line):
return line and not line.startswith("#") return line and not line.startswith("#")
@ -288,23 +298,26 @@ class GarbageCollector:
def _get_docker_client(self): def _get_docker_client(self):
config = self.config.config config = self.config.config
return docker.APIClient(version="auto", timeout=config["timeout"]) return docker.APIClient(version="auto", timeout=config["http_timeout"])
def run(self):
"""Garbage collector main method."""
self.logger.info("Start garbage collection")
config = self.config.config
# def main(): if config["gc"]["max_container_age"]:
# exclude_container_labels = format_exclude_labels(args.exclude_container_label) self.cleanup_containers()
# if args.max_container_age: if config["gc"]["max_image_age"]:
# cleanup_containers( exclude_set = self._build_exclude_set()
# client, self.cleanup_images(exclude_set)
# args.max_container_age,
# args.dry_run,
# exclude_container_labels,
# )
# if args.max_image_age: if config["gc"]["dangling_volumes"]:
# exclude_set = build_exclude_set(args.exclude_image, args.exclude_image_file) self.logger.info("Remove dangling volumes")
# cleanup_images(client, args.max_image_age, args.dry_run, exclude_set) self.cleanup_volumes()
# if args.dangling_volumes: if (
# cleanup_volumes(client, args.dry_run) not config["gc"]["max_container_age"] or not config["gc"]["max_image_age"]
or not config["gc"]["dangling_volumes"]
):
self.logger.info("Skipped, no arguments given")

View File

@ -26,7 +26,7 @@ def timedelta_validator(value):
raise raise
def timedelta(value): def timedelta(value, dt_format=None):
"""Return the :class:`datetime.datetime.DateTime` for a time in the past. """Return the :class:`datetime.datetime.DateTime` for a time in the past.
:param value: a string containing a time format supported by :param value: a string containing a time format supported by
@ -34,7 +34,12 @@ def timedelta(value):
""" """
if value is None: if value is None:
return None return None
return _datetime_seconds_ago(timeparse.timeparse(value))
timedelta = _datetime_seconds_ago(timeparse.timeparse(value))
if dt_format:
timedelta = timedelta.strftime(dt_format)
return timedelta
def _datetime_seconds_ago(seconds): def _datetime_seconds_ago(seconds):