Commit a5b469f6 authored by nimrod's avatar nimrod
Browse files

Recreate the backup script with shell, AWK, reg and skopeo.

A few reasons. First of all, it doesn't require a running Docker daemon
instead outputting directly to a file (faster and saves space). Also,
the restore script will probably use skopeo so this the codebase is more
uniform. Without the Docker daemon it can run with lower privileges.
Lastly, it should work without setting the really high timeout that bugs
me a little.
parent 7c0fdbeb
Loading
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -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
+1 −0
Original line number Diff line number Diff line
*
!backup
!restore
+7 −1
Original line number Diff line number Diff line
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
+49 −74
Original line number Diff line number Diff line
#!/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
}
'

backup/restore

0 → 100755
+1 −0
Original line number Diff line number Diff line
#!/bin/sh