Add --exclude-container-label argument to dcgc

Add an argument to dcgc that allows dcgc to ignore removing a container
and its child images based on the container's label
This commit is contained in:
Andy Tran 2018-03-06 15:21:36 -08:00
parent 091c6ea44b
commit 95ded14891
3 changed files with 119 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

@ -23,14 +23,26 @@ log = logging.getLogger(__name__)
YEAR_ZERO = "0001-01-01T00:00:00Z"
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 +51,43 @@ 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):
def include_container(container):
if exclude_container_labels and 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_container_label in exclude_container_labels:
split_exclude_label = exclude_container_label.split('=', 1)
if len(split_exclude_label) == 2:
exclude_key, exclude_value = split_exclude_label
matching_keys = fnmatch.filter(
container['Labels'].keys(), exclude_key
)
label_values_to_check = [
container['Labels'][matching_key]
for matching_key in matching_keys
]
if fnmatch.filter(label_values_to_check, exclude_value):
return True
else:
exclude_key = split_exclude_label[0]
if fnmatch.filter(container['Labels'].keys(), exclude_key):
return True
return False
def should_remove_container(container, min_date):
@ -226,7 +273,12 @@ def main():
**kwargs_from_env())
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,
args.exclude_container_label,
)
if args.max_image_age:
exclude_set = build_exclude_set(
@ -271,6 +323,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',
help="Never remove containers with this label key or label key=value")
return parser.parse_args(args=args)

View File

@ -51,24 +51,47 @@ 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 = ['too', 'foo']
result = docker_gc.filter_excluded_containers(
mock_containers,
exclude_labels,
)
assert [mock_containers[0], mock_containers[2]] == list(result)
exclude_labels = ['too*=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 = [