Merge pull request #47 from ATRAN2/add-exclude-container-regex-argument

Add --exclude-container-label argument to dcgc
This commit is contained in:
Kyle Anderson 2018-03-21 15:23:46 -07:00 committed by GitHub
commit 95b5351ab8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 164 additions and 16 deletions

View File

@ -92,6 +92,30 @@ You also can use basic pattern matching to exclude images with generic tags.
user/repositoryB:?.?
user/repositoryC-*:tag
Prevent containers and associated images from being removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``dcgc`` also supports a container exclude list based on labels. If there are
stopped containers that you'd like to keep, then you can check the labels to
prevent them from being removed.
::
--exclude-container-label
Never remove containers that have the label key=value. =value can be
omitted and in that case only the key is checked. May be specified
more than once.
You also can use basic pattern matching to exclude generic labels.
.. code::
foo*
com.docker.compose.project=test*
com.docker*=*bar*
dcstop
------

View File

@ -13,6 +13,7 @@ import docker
import docker.errors
import requests.exceptions
from collections import namedtuple
from docker_custodian.args import timedelta_type
from docker.utils import kwargs_from_env
@ -22,15 +23,29 @@ log = logging.getLogger(__name__)
# This seems to be something docker uses for a null/zero date
YEAR_ZERO = "0001-01-01T00:00:00Z"
ExcludeLabel = namedtuple('ExcludeLabel', ['key', 'value'])
def cleanup_containers(client, max_container_age, dry_run):
def cleanup_containers(
client,
max_container_age,
dry_run,
exclude_container_labels,
):
all_containers = get_all_containers(client)
for container_summary in reversed(all_containers):
container = api_call(client.inspect_container,
container=container_summary['Id'])
if not container or not should_remove_container(container,
max_container_age):
filtered_containers = filter_excluded_containers(
all_containers,
exclude_container_labels,
)
for container_summary in reversed(list(filtered_containers)):
container = api_call(
client.inspect_container,
container=container_summary['Id'],
)
if not container or not should_remove_container(
container,
max_container_age,
):
continue
log.info("Removing container %s %s %s" % (
@ -39,8 +54,44 @@ def cleanup_containers(client, max_container_age, dry_run):
container['State']['FinishedAt']))
if not dry_run:
api_call(client.remove_container, container=container['Id'],
v=True)
api_call(
client.remove_container,
container=container['Id'],
v=True,
)
def filter_excluded_containers(containers, exclude_container_labels):
if not exclude_container_labels:
return containers
def include_container(container):
if should_exclude_container_with_labels(
container,
exclude_container_labels,
):
return False
return True
return filter(include_container, containers)
def should_exclude_container_with_labels(container, exclude_container_labels):
for exclude_label in exclude_container_labels:
if exclude_label.value:
matching_keys = fnmatch.filter(
container['Labels'].keys(),
exclude_label.key,
)
label_values_to_check = [
container['Labels'][matching_key]
for matching_key in matching_keys
]
if fnmatch.filter(label_values_to_check, exclude_label.value):
return True
else:
if fnmatch.filter(container['Labels'].keys(), exclude_label.key):
return True
return False
def should_remove_container(container, min_date):
@ -214,6 +265,24 @@ def build_exclude_set(image_tags, exclude_file):
return exclude_set
def format_exclude_labels(exclude_label_args):
exclude_labels = []
for exclude_label_arg in exclude_label_args:
split_exclude_label = exclude_label_arg.split('=', 1)
exclude_label_key = split_exclude_label[0]
if len(split_exclude_label) == 2:
exclude_label_value = split_exclude_label[1]
else:
exclude_label_value = None
exclude_labels.append(
ExcludeLabel(
key=exclude_label_key,
value=exclude_label_value,
)
)
return exclude_labels
def main():
logging.basicConfig(
level=logging.INFO,
@ -225,8 +294,17 @@ def main():
timeout=args.timeout,
**kwargs_from_env())
exclude_container_labels = format_exclude_labels(
args.exclude_container_label
)
if args.max_container_age:
cleanup_containers(client, args.max_container_age, args.dry_run)
cleanup_containers(
client,
args.max_container_age,
args.dry_run,
exclude_container_labels,
)
if args.max_image_age:
exclude_set = build_exclude_set(
@ -271,6 +349,10 @@ def get_args(args=None):
type=argparse.FileType('r'),
help="Path to a file which contains a list of images to exclude, one "
"image tag per line.")
parser.add_argument(
'--exclude-container-label',
action='append', type=str, default=[],
help="Never remove containers with this label key or label key=value")
return parser.parse_args(args=args)

View File

@ -51,24 +51,52 @@ def test_cleanup_containers(mock_client, now):
'Name': 'one',
'State': {
'Running': False,
'FinishedAt': '2014-01-01T01:01:01Z'
}
'FinishedAt': '2014-01-01T01:01:01Z',
},
},
{
'Id': 'abbb',
'Name': 'two',
'State': {
'Running': True,
'FinishedAt': '2014-01-01T01:01:01Z'
}
}
'FinishedAt': '2014-01-01T01:01:01Z',
},
},
]
mock_client.inspect_container.side_effect = iter(mock_containers)
docker_gc.cleanup_containers(mock_client, max_container_age, False)
docker_gc.cleanup_containers(mock_client, max_container_age, False, None)
mock_client.remove_container.assert_called_once_with(container='abcd',
v=True)
def test_filter_excluded_containers():
mock_containers = [
{'Labels': {'toot': ''}},
{'Labels': {'too': 'lol'}},
{'Labels': {'toots': 'lol'}},
{'Labels': {'foo': 'bar'}},
]
result = docker_gc.filter_excluded_containers(mock_containers, None)
assert mock_containers == list(result)
exclude_labels = [
docker_gc.ExcludeLabel(key='too', value=None),
docker_gc.ExcludeLabel(key='foo', value=None),
]
result = docker_gc.filter_excluded_containers(
mock_containers,
exclude_labels,
)
assert [mock_containers[0], mock_containers[2]] == list(result)
exclude_labels = [
docker_gc.ExcludeLabel(key='too*', value='lol'),
]
result = docker_gc.filter_excluded_containers(
mock_containers,
exclude_labels,
)
assert [mock_containers[0], mock_containers[3]] == list(result)
def test_cleanup_images(mock_client, now):
max_image_age = now
mock_client.images.return_value = images = [
@ -459,6 +487,19 @@ def test_build_exclude_set():
assert exclude_set == expected
def test_format_exclude_labels():
exclude_label_args = [
'voo*',
'doo=poo',
]
expected = [
docker_gc.ExcludeLabel(key='voo*', value=None),
docker_gc.ExcludeLabel(key='doo', value='poo'),
]
exclude_labels = docker_gc.format_exclude_labels(exclude_label_args)
assert expected == exclude_labels
def test_build_exclude_set_empty():
exclude_set = docker_gc.build_exclude_set(None, None)
assert exclude_set == set()
@ -477,5 +518,6 @@ def test_main(mock_client):
max_container_age=200,
exclude_image=[],
exclude_image_file=None,
exclude_container_label=[],
)
docker_gc.main()