From a5b469f612864454f6eb91e6ca212828bac5e42b Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Sun, 31 Oct 2021 15:26:06 +0200
Subject: [PATCH] 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.
---
 .gitlab-ci.yml       |   5 +-
 backup/.dockerignore |   1 +
 backup/Dockerfile    |   8 ++-
 backup/backup        | 123 +++++++++++++++++--------------------------
 backup/restore       |   1 +
 5 files changed, 60 insertions(+), 78 deletions(-)
 create mode 100755 backup/restore

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3b9ff39..e8d010b 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 ff99b53..5664021 100644
--- a/backup/.dockerignore
+++ b/backup/.dockerignore
@@ -1,2 +1,3 @@
 *
 !backup
+!restore
diff --git a/backup/Dockerfile b/backup/Dockerfile
index 7be4b23..fc73888 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 83c0ea7..fb889fa 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 0000000..1a24852
--- /dev/null
+++ b/backup/restore
@@ -0,0 +1 @@
+#!/bin/sh
-- 
GitLab