Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,33 @@ docker run --rm -ti -v "./config.yaml:/app/config.yaml" -v "./compose:/app/compo
docker compose up -d
```

### Automated Garbage Collection ♻️

Each registry entry in `config.yaml` can enable automated disk cleanup by adding a `garbageCollect` section. When present, the `generate.py` script adds a companion service named `registry-gc-<name>` to the generated `compose.yaml`. This maintenance container shares the registry configuration and storage volumes, and periodically runs `registry garbage-collect --delete-untagged` to purge unused blobs.

- Use an `interval` to express a simple repetition delay (for example `24h`, `30m`, or compound values such as `1d12h`).
- Use a `cron` expression (five fields) to rely on BusyBox `crond` inside the service for calendar-based scheduling.

Example registry entry with both scheduling styles:

```yaml
registries:
- name: quay
type: cache
url: https://quay.io
username: user
password: pass
ttl: 720h
garbageCollect:
interval: 24h
- name: private
type: registry
garbageCollect:
cron: "0 3 * * *"
```

> ℹ️ The TTL value (`proxy.ttl`) in the registry configuration controls how long cached content is considered fresh before expiring from Redis. The garbage-collection service complements this by deleting untagged blobs from the backing storage, helping reclaim disk space after manifests lose their tags or TTL evicts them from the proxy cache.

## Configuring Container Runtimes 🔄

### containerd Configuration
Expand Down
4 changes: 4 additions & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ registries:
username: user
password: pass
ttl: 720h
garbageCollect:
interval: 24h
- name: private
type: registry
garbageCollect:
cron: "0 3 * * *"

docker:
# The docker compose config. This config is used to generate the docker-compose.yaml file. You can use the baseConfig
Expand Down
132 changes: 132 additions & 0 deletions generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,93 @@
Run this script to generate configuration files based on the config.yaml.
"""
import os
import re
import yaml
import functions
from functions import console
from rich.text import Text
import copy


def parse_duration_to_seconds(duration):
'''

Convert a duration string expressed with unit suffixes into seconds.

Args:
duration (str): The duration string to parse (for example "12h" or "30m").

Returns:
int: The computed duration in seconds.

'''

if not isinstance(duration, str):
raise ValueError('Duration must be provided as a string')

pattern = re.compile(r'(\d+)([smhdSMHD])')
total_seconds = 0
index = 0
duration = duration.strip()

for match in pattern.finditer(duration):
if match.start() != index:
raise ValueError('Invalid duration format')
value = int(match.group(1))
unit = match.group(2).lower()
if unit == 's':
total_seconds += value
elif unit == 'm':
total_seconds += value * 60
elif unit == 'h':
total_seconds += value * 3600
elif unit == 'd':
total_seconds += value * 86400
else:
raise ValueError('Unsupported duration unit')
index = match.end()

if index != len(duration) or total_seconds == 0:
raise ValueError('Invalid duration format')

return total_seconds


def build_garbage_collect_command(cron_expression=None, interval_seconds=None):
'''

Create the shell command that triggers registry garbage collection.

Args:
cron_expression (str, optional): Cron expression to schedule the command.
interval_seconds (int, optional): Interval in seconds for the sleep loop.

Returns:
list: A list representing the command to execute in Docker Compose.

'''

base_command = 'registry garbage-collect --delete-untagged /etc/docker/registry/config.yml'

if cron_expression:
cron_entry = f"{cron_expression} {base_command}"
escaped_entry = cron_entry.replace('"', '\\"')
shell_command = (
"printf '%s\\n' \""
+ escaped_entry
+ "\" > /etc/crontabs/root && crond -f -l 8 -L /dev/stdout"
)
return ['sh', '-c', shell_command]

if interval_seconds:
shell_command = (
f"while true; do {base_command}; sleep {interval_seconds}; done"
)
return ['sh', '-c', shell_command]

raise ValueError('A cron expression or interval must be provided for garbage collection')


# Load configuration from config.yaml file
try:
with open('config.yaml', 'r', encoding='UTF-8') as file:
Expand Down Expand Up @@ -84,6 +164,58 @@
console.print(Text(f"Error creating docker-compose and traefik configuration for {name}: {e}", style="bold red"))
raise

try:
garbage_collect = registry.get('garbageCollect')
if garbage_collect:
cron_expression = None
interval_seconds = None

if isinstance(garbage_collect, dict):
cron_expression = garbage_collect.get('cron')
interval_value = garbage_collect.get('interval')
if interval_value is not None:
interval_seconds = parse_duration_to_seconds(interval_value)
elif isinstance(garbage_collect, str):
stripped_value = garbage_collect.strip()
if len(stripped_value.split()) >= 5:
cron_expression = stripped_value
else:
interval_seconds = parse_duration_to_seconds(stripped_value)
else:
raise ValueError('garbageCollect must be a string or a mapping')

if cron_expression and interval_seconds:
raise ValueError('Specify either cron or interval for garbageCollect, not both')

if cron_expression:
cron_expression = cron_expression.strip()
if len(cron_expression.split()) < 5:
raise ValueError('Invalid cron expression for garbageCollect')

if not cron_expression and not interval_seconds:
raise ValueError('garbageCollect configuration is incomplete')

gc_service_name = f"registry-gc-{name}"
gc_service = copy.deepcopy(docker_config['services'][name])
gc_service.pop('ports', None)
gc_service.pop('container_name', None)
if 'depends_on' in gc_service:
depends_on = gc_service['depends_on']
if isinstance(depends_on, list):
gc_service['depends_on'] = [dep for dep in depends_on if dep != name]
elif isinstance(depends_on, dict):
depends_on.pop(name, None)
gc_service['restart'] = gc_service.get('restart', 'always')
gc_service['command'] = build_garbage_collect_command(
cron_expression=cron_expression,
interval_seconds=interval_seconds,
)
docker_config['services'][gc_service_name] = gc_service
console.print(Text(f"Garbage collection service configured for {name}", style="bold green"))
except Exception as e:
console.print(Text(f"Error configuring garbage collection for {name}: {e}", style="bold red"))
raise

# Increment Redis database count for next registry
count_redis_db += 1

Expand Down