diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3b9ff39bf269abffc0331185c88aa2cb38a37616..e8d010bd2e12dff9c16febab4aa9725052bfd2e3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -43,11 +43,10 @@ backup: - >- docker run --volume /var/backups/registry:/var/backups/registry - --volume /run/docker.sock:/run/docker.sock + --user nobody + --rm registry.shore.co.il/registry-backup backup registry.shore.co.il /var/backups/registry - after_script: - - docker image prune -f retry: max: 2 timeout: 3h diff --git a/backup/.dockerignore b/backup/.dockerignore index ff99b532cd160e0d083867a08d8842b64bb138a8..5664021a5516007889500c3fb8d99da128793e9a 100644 --- a/backup/.dockerignore +++ b/backup/.dockerignore @@ -1,2 +1,3 @@ * !backup +!restore diff --git a/backup/Dockerfile b/backup/Dockerfile index 7be4b236985ce4948597f6650460153e8255b32e..fc73888b364bd25805383452c423d6a2e53333a9 100644 --- a/backup/Dockerfile +++ b/backup/Dockerfile @@ -1,4 +1,10 @@ FROM docker.io/library/alpine:3.14 # hadolint ignore=DL3018 -RUN apk add --update --no-cache docker-py +RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories && \ + echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && \ + apk add --update --no-cache \ + skopeo \ + reg \ + ; COPY --chown=root:root backup /usr/local/bin/backup +COPY --chown=root:root restore /usr/local/bin/restore diff --git a/backup/backup b/backup/backup index 83c0ea731e9c50698aa275e2cc880980a65f8a4d..fb889fab80b3f585808b0a1c10bea4cee0b5f85a 100755 --- a/backup/backup +++ b/backup/backup @@ -1,74 +1,49 @@ -#!/usr/bin/env python3 -"""Backup a container image registry.""" - - -import argparse -import pathlib -import sys -import docker -import requests - - -def get_images(registry): - """Return a list of images from the registry.""" - return requests.get(f"https://{registry}/v2/_catalog").json()[ - "repositories" - ] - - -def get_image_tags(registry, image): - """Return a list of tags for an image in the registry.""" - return requests.get(f"https://{registry}/v2/{image}/tags/list").json()[ - "tags" - ] - - -def backup(docker_client, registry, image, tag, dest): - """Backup an image tag from the registry to a file in the destination - directory.""" - full_name = f"{registry}/{image}:{tag}" - print(f"Backing up {full_name}.", file=sys.stderr) - try: - docker_client.images.get(full_name) - image_existed = True - except docker.errors.ImageNotFound: - image_existed = False - docker_image = docker_client.images.pull(full_name) - with open(dest / f"{tag}.tar", "wb") as tarball: - for chunk in docker_image.save(): - tarball.write(chunk) - if not image_existed: - docker_client.images.remove(full_name) - - -def backup_registry(registry, dest): - """Backup the images in the registry to the destination.""" - docker_client = docker.from_env(timeout=600) - docker_client.ping() - for image in get_images(registry): - (dest / image).mkdir(exist_ok=True) - for tag in get_image_tags(registry, image): - backup(docker_client, registry, image, tag, dest / image) - - -if __name__ == "__main__": - arg_parser = argparse.ArgumentParser(description=__doc__) - arg_parser.add_argument("registry", help="FQDN of the registry to backup.") - arg_parser.add_argument( - "destination", help="Location to store the images.", type=pathlib.Path - ) - args = arg_parser.parse_args() - destination = args.destination.expanduser() - if not destination.exists() or not destination.is_dir(): - arg_parser.error( - "Backup destination doesn't exists or isn't a directory." - ) - try: - destination.touch() - except IsADirectoryError: - arg_parser.error("Can't write to the backup destination.") - try: - backup_registry(args.registry, destination) - except Exception as exception: - arg_parser.error(str(exception)) - sys.exit() +#!/bin/sh +set -eu + +usage() { + echo "$0: REGISTRY_DOMAIN BACKUP_DEST" +} + +if [ "${1:-}" = -h ] || [ "${1:-}" = --help ] +then + usage + exit 0 +fi + +if [ "$#" -ne 2 ] +then + usage + exit 1 +fi + +registry="$1" +dest="$2" + +mkdir -p "$dest" + +reg ls "$registry" | \ + sed 's/,//g' | \ + awk -v "registry=$registry" -v "dest=$dest" ' +BEGIN { + exitcode = 0 +} +NR>2 { + system("mkdir -p " dest "/" $1) + for (i=2; i<=NF; i++) { + image_url = registry "/" $1 ":" $(i) + image_file = dest "/" $1 "/" $(i) ".tar" + printf "Saving %s to %s.\n", image_url, image_file + system("rm " image_file) + if (system("skopeo copy docker://" image_url " docker-archive://" image_file) == 0) + printf "Backup of %s was successful.\n", image_url + else { + exitcode = 1 + printf "Backup of %s failed, continuing with other images.\n", image_url + } + } +} +END { + exit retruncode +} +' diff --git a/backup/restore b/backup/restore new file mode 100755 index 0000000000000000000000000000000000000000..1a2485251c33a70432394c93fb89330ef214bfc9 --- /dev/null +++ b/backup/restore @@ -0,0 +1 @@ +#!/bin/sh