diff --git a/dockertidy/Autostop.py b/dockertidy/Autostop.py index 07ed4bc..e8fca7a 100644 --- a/dockertidy/Autostop.py +++ b/dockertidy/Autostop.py @@ -68,4 +68,8 @@ class AutoStop: def _get_docker_client(self): 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") diff --git a/dockertidy/Cli.py b/dockertidy/Cli.py index e7a9f56..04e10db 100644 --- a/dockertidy/Cli.py +++ b/dockertidy/Cli.py @@ -5,7 +5,9 @@ import argparse import dockertidy.Exception from dockertidy import __version__ +from dockertidy.Autostop import AutoStop from dockertidy.Config import SingleConfig +from dockertidy.GarbageCollector import GarbageCollector from dockertidy.Logger import SingleLog from dockertidy.Parser import timedelta_validator @@ -18,6 +20,9 @@ class DockerTidy: self.logger = self.log.logger self.args = self._cli_args() self.config = self._get_config() + self.gc = GarbageCollector() + self.stop = AutoStop() + self.run() def _cli_args(self): """ @@ -54,6 +59,7 @@ class DockerTidy: ) 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.add_argument( @@ -84,7 +90,7 @@ class DockerTidy: "--exclude-image", action="append", type=str, - dest="gc.exclude_image", + dest="gc.exclude_images", metavar="EXCLUDE_IMAGE", help="Never remove images with this tag." ) @@ -92,7 +98,7 @@ class DockerTidy: "--exclude-container-label", action="append", type=str, - dest="gc.exclude_container_label", + dest="gc.exclude_container_labels", metavar="EXCLUDE_CONTAINER_LABEL", help="Never remove containers with this label key " "or label key=value" @@ -127,13 +133,19 @@ class DockerTidy: except dockertidy.Exception.ConfigError as e: self.log.sysexit_with_message(e) - print(config.config) - try: self.log.set_level(config.config["logging"]["level"]) except ValueError as 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.debug("Config dump: {}".format(config.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() diff --git a/dockertidy/Config.py b/dockertidy/Config.py index ec82397..0554e6f 100644 --- a/dockertidy/Config.py +++ b/dockertidy/Config.py @@ -63,37 +63,37 @@ class Config(): "type": environs.Env().bool }, "gc.max_container_age": { - "default": "1day", + "default": "", "env": "GC_MAX_CONTAINER_AGE", "file": True, "type": env.timedelta_validator }, "gc.max_image_age": { - "default": "1day", + "default": "", "env": "GC_MAX_IMAGE_AGE", "file": True, "type": env.timedelta_validator }, "gc.dangling_volumes": { "default": False, - "env": "GC_EXCLUDE_IMAGE", + "env": "GC_DANGLING_VOLUMES", "file": True, "type": environs.Env().bool }, - "gc.exclude_image": { + "gc.exclude_images": { "default": [], - "env": "GC_DANGLING_VOLUMES", + "env": "GC_EXCLUDE_IMAGES", "file": True, "type": environs.Env().list }, - "gc.exclude_container_label": { + "gc.exclude_container_labels": { "default": [], - "env": "GC_EXCLUDE_CONTAINER_LABEL", + "env": "GC_EXCLUDE_CONTAINER_LABELS", "file": True, "type": environs.Env().list }, "stop.max_run_time": { - "default": "3days", + "default": "", "env": "STOP_MAX_RUN_TIME", "file": True, "type": env.timedelta_validator diff --git a/dockertidy/GarbageCollector.py b/dockertidy/GarbageCollector.py index a33b840..63c9103 100644 --- a/dockertidy/GarbageCollector.py +++ b/dockertidy/GarbageCollector.py @@ -11,6 +11,7 @@ import requests.exceptions from dockertidy.Config import SingleConfig from dockertidy.Logger import SingleLog +from dockertidy.Parser import timedelta class GarbageCollector: @@ -30,10 +31,14 @@ class GarbageCollector: """Identify old containers and remove them.""" config = self.config.config client = self.docker - all_containers = self._get_all_containers(client) - filtered_containers = self._filter_excluded_containers( - all_containers, - config["gc"]["exclude_container_labels"], + all_containers = self._get_all_containers() + + filtered_containers = self._filter_excluded_containers(all_containers) + + 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)): @@ -43,14 +48,14 @@ class GarbageCollector: ) if not container or not self._should_remove_container( container, - config["gc"]["max_container_age"], + timedelta(config["gc"]["max_container_age"]), ): continue self.logger.info( - "Removing container %s %s %s" % ( - container["Id"][:16], container.get("Name", "").lstrip("/"), - container["State"]["FinishedAt"] + "Removing container {} {} {}".format( + container["Id"][:16], + 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_excluded_images(images, exclude_set) - for image_summary in reversed(list(images)): - self._rmove_image( - client, image_summary, config["gc"]["max_image_age"], config["dry_run"] + self.logger.info( + "Removing images older than '{}'".format( + 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 include_image(image_summary): image_tags = image_summary.get("RepoTags") - if self.no_image_tags(image_tags): + if self._no_image_tags(image_tags): return True for exclude_pattern in exclude_set: if fnmatch.filter(image_tags, exclude_pattern): @@ -197,7 +205,7 @@ class GarbageCollector: def _no_image_tags(self, image_tags): return not image_tags or image_tags == [":"] - def _remove_image(self, client, image_summary, min_date): + def _remove_image(self, image_summary, min_date): config = self.config.config client = self.docker image = self._api_call(client.inspect_image, image=image_summary["Id"]) @@ -219,8 +227,9 @@ class GarbageCollector: for image_tag in image_tags: self._api_call(client.remove_image, image=image_tag) - def _remove_volume(self, client, volume): + def _remove_volume(self, volume): config = self.config.config + client = self.docker if not volume: return @@ -230,13 +239,14 @@ class GarbageCollector: self._api_call(client.remove_volume, name=volume["Name"]) - def cleanup_volumes(self, client): + def cleanup_volumes(self): """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): self.logger.info("Removing dangling volume %s", volume["Name"]) - self._remove_volume(client, volume) + self._remove_volume(volume) def _api_call(self, func, **kwargs): try: @@ -260,7 +270,7 @@ class GarbageCollector: def _build_exclude_set(self): config = self.config.config - exclude_set = set(config["gc"]["exclude_image"]) + exclude_set = set(config["gc"]["exclude_images"]) def is_image_tag(line): return line and not line.startswith("#") @@ -288,23 +298,26 @@ class GarbageCollector: def _get_docker_client(self): 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(): -# exclude_container_labels = format_exclude_labels(args.exclude_container_label) + if config["gc"]["max_container_age"]: + self.cleanup_containers() -# if args.max_container_age: -# cleanup_containers( -# client, -# args.max_container_age, -# args.dry_run, -# exclude_container_labels, -# ) + if config["gc"]["max_image_age"]: + exclude_set = self._build_exclude_set() + self.cleanup_images(exclude_set) -# if args.max_image_age: -# exclude_set = build_exclude_set(args.exclude_image, args.exclude_image_file) -# cleanup_images(client, args.max_image_age, args.dry_run, exclude_set) + if config["gc"]["dangling_volumes"]: + self.logger.info("Remove dangling volumes") + self.cleanup_volumes() -# if args.dangling_volumes: -# cleanup_volumes(client, args.dry_run) + if ( + 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") diff --git a/dockertidy/Parser.py b/dockertidy/Parser.py index 60defe5..47d650d 100644 --- a/dockertidy/Parser.py +++ b/dockertidy/Parser.py @@ -26,7 +26,7 @@ def timedelta_validator(value): raise -def timedelta(value): +def timedelta(value, dt_format=None): """Return the :class:`datetime.datetime.DateTime` for a time in the past. :param value: a string containing a time format supported by @@ -34,7 +34,12 @@ def timedelta(value): """ if value is 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):