Skip to content

Commit

Permalink
cmd/prune-contaiers: add a GC script for containers images
Browse files Browse the repository at this point in the history
This script calls skopeo delete to prune image from a remote
directory. Currently only supports the FCOS tag structure.

This consumes the same policy.yaml defined in
#3798

See coreos/fedora-coreos-tracker#1367
See coreos/fedora-coreos-pipeline#995
  • Loading branch information
jbtrystram committed Jul 11, 2024
1 parent 3823521 commit 52858ef
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 1 deletion.
2 changes: 1 addition & 1 deletion cmd/coreos-assembler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var buildCommands = []string{"init", "fetch", "build", "run", "prune", "clean",
var advancedBuildCommands = []string{"buildfetch", "buildupload", "oc-adm-release", "push-container"}
var buildextendCommands = []string{"aliyun", "applehv", "aws", "azure", "digitalocean", "exoscale", "extensions-container", "gcp", "hashlist-experimental", "hyperv", "ibmcloud", "kubevirt", "live", "metal", "metal4k", "nutanix", "openstack", "qemu", "secex", "virtualbox", "vmware", "vultr"}

var utilityCommands = []string{"aws-replicate", "compress", "copy-container", "koji-upload", "kola", "push-container-manifest", "remote-build-container", "remote-prune", "remote-session", "sign", "tag", "update-variant"}
var utilityCommands = []string{"aws-replicate", "compress", "copy-container", "koji-upload", "kola", "push-container-manifest", "prune-containers", "remote-build-container", "remote-prune", "remote-session", "sign", "tag", "update-variant"}
var otherCommands = []string{"shell", "meta"}

func init() {
Expand Down
159 changes: 159 additions & 0 deletions src/cmd-prune-containers
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/usr/bin/python3 -u

# Prune containers from a remote registry
# according to the images age

import argparse
import datetime
import json
import re as regexp
import os
import subprocess
from dateutil.relativedelta import relativedelta
import requests
import yaml

# Dict of known streams
STREAMS = {"next": 1, "testing": 2, "stable": 3, "next-devel": 10, "testing-devel": 20, "rawhide": 91, "branched": 92}

def parse_args():
parser = argparse.ArgumentParser(prog="coreos-assembler prune-containers")
parser.add_argument("--policy", required=True, type=str, help="Path to policy YAML file")
parser.add_argument("--dry-run", help="Don't actually delete anything", action='store_true')
parser.add_argument("-v", help="Increase verbosity", action='store_true')
parser.add_argument("--registry-auth-file", default=os.environ.get("REGISTRY_AUTHFILE"), help="Path to AWS config file")
parser.add_argument("--stream", type=str, help="CoreOS stream", required=True, choices=STREAMS.keys())
parser.add_argument("repository_url", help="container images URL")
return parser.parse_args()


def convert_to_days(duration_arg):

days = 0
match = regexp.match(r'^([0-9]+)([dDmMyYwW])$', duration_arg)

if match is None:
raise ValueError(f"Incorrect duration '{duration_arg}'. Valid values are in the form of 1d, 2w, 3m, 4y")

unit = match.group(2)
value = int(match.group(1))
match unit:
case "y" | "Y":
days = value * 365
case "m" | "M":
days = value * 30
case "w" | "W":
days = value * 7
case "d" | "D":
days = value
case _:
raise ValueError(f"Invalid unit '{m.group(2)}'. Please use y (years), m (months), w (weeks), or d (days).")

return days


def skopeo_delete(repo, image, auth):

skopeo_args = ["skopeo", "delete", f"docker://{repo}:{image}"]
if auth is not None:
args.append(f"--authfile {auth}")

subprocess.run(skopeo_args)


# FIXME : move to cosa_lib
# https://github.com/coreos/coreos-assembler/pull/3798/files#r1673481990

def parse_fcos_version(version):
m = regexp.match(r'^([0-9]{2})\.([0-9]{8})\.([0-9]+)\.([0-9]+)$', version)
if m is None:
raise Exception(f"Incorrect versioning for FCOS build {version}")
try:
timestamp = datetime.datetime.strptime(m.group(2), '%Y%m%d')
except ValueError:
raise Exception(f"FCOS build {version} has incorrect date format. It should be in (%Y%m%d)")
return (timestamp, int(m.group(3)))


def get_update_graph(stream):

url = f"https://raw.githubusercontent.com/coreos/fedora-coreos-streams/main/updates/{stream}.json"
r = requests.get(url)
if r.status_code != 200:
raise Exception(f"Could not download update graph for {stream}. HTTP {r.status_code}")
return r.json()


class BarrierRelease(Exception):
pass


def main():

args = parse_args()

print(f"Pulling tags from {args.repository_url}")
# This is a JSON object:
# {"Repository": "quay.io/jbtrystramtestimages/fcos",
# "Tags": [
# "40.20"40.20240301.1.0",.....]}
tags_cmd = subprocess.run(["skopeo", "list-tags", f"docker://{args.repository_url}"], capture_output=True)
tags_data = tags_cmd.stdout
if tags_cmd.returncode != 0:
raise Exception(f"Error retrieving tags list from repo: {tags_cmd.stdout}")

tags_json = json.loads(tags_data)
tags = tags_json['Tags']

# Load the policy file
with open(args.policy, "r") as f:
policy = yaml.safe_load(f)
policy = policy[args.stream]["containers"]

# Compute the date before we should prune images
# today - prune-policy
today = datetime.datetime.now()
date_limit = today - relativedelta(days=convert_to_days(policy))
if args.v:
print(f"This will delete any images older than {date_limit} from the stream {args.stream}")

# Get the update graph
update_graph = get_update_graph(args.stream)['releases']

stream_id = STREAMS[args.stream]
for tag in tags:
# ignore the named moving tags ("stable", "next" etc..)
if tag in STREAMS.keys():
if args.v:
print(f"skipping tag {tag}.")
continue

# Process the build id and stream number
# TODO reuse this from https://github.com/coreos/coreos-assembler/pull/3798/files#r1673481990
(build_date, tag_stream) = parse_fcos_version(tag)

if stream_id != tag_stream:
if args.v:
print(f"Skipping tag {tag} not in {args.stream} stream")
continue

# Make sure this is not a barrier release
try:
for release in update_graph:
if release["version"] == tag and "barrier" in release["metadata"]:
print(f"Release {tag} is a karrier release, keeping.")
raise BarrierRelease
except BarrierRelease:
continue

if build_date > date_limit:
if args.dry_run:
print(f"Dry-run: would prune image {args.repository_url}:{tag}")
continue
else:
print(f"Production tag {tag} is older than {date_limit}, pruning.")
skopeo_delete(args.repository_url, tag, args.registry_auth_file)


if __name__ == "__main__":
main()

0 comments on commit 52858ef

Please sign in to comment.