From 6838206e2a2bb0283710124e2e78152db04cb48b Mon Sep 17 00:00:00 2001 From: "Owen W. Taylor" Date: Wed, 11 Jul 2018 15:12:27 +0200 Subject: [PATCH] test-oci-registry.sh: Tests for talking to an OCI registry Add a new test case to test the OCI remote functionality. The tests talk to a server that implements good-enough index generation and bits of the docker registry protocol. Adding and remove remotes, summary and appstream generation, and image installation are all tested. Closes: #1910 Approved by: alexlarsson --- tests/Makefile.am.inc | 2 + tests/oci-registry-client.py | 38 +++++ tests/oci-registry-server.py | 233 ++++++++++++++++++++++++++++++ tests/test-oci-registry-system.sh | 22 +++ tests/test-oci-registry.sh | 134 +++++++++++++++++ tests/test-oci.sh | 6 +- 6 files changed, 431 insertions(+), 4 deletions(-) create mode 100644 tests/oci-registry-client.py create mode 100644 tests/oci-registry-server.py create mode 100755 tests/test-oci-registry-system.sh create mode 100755 tests/test-oci-registry.sh diff --git a/tests/Makefile.am.inc b/tests/Makefile.am.inc index b378d7a0..adcdc9d8 100644 --- a/tests/Makefile.am.inc +++ b/tests/Makefile.am.inc @@ -100,6 +100,8 @@ dist_test_scripts = \ tests/test-bundle.sh \ tests/test-bundle-system.sh \ tests/test-oci.sh \ + tests/test-oci-registry.sh \ + tests/test-oci-registry-system.sh \ tests/test-unsigned-summaries.sh \ tests/test-update-remote-configuration.sh \ $(NULL) diff --git a/tests/oci-registry-client.py b/tests/oci-registry-client.py new file mode 100644 index 00000000..033b54d3 --- /dev/null +++ b/tests/oci-registry-client.py @@ -0,0 +1,38 @@ +#!/usr/bin/python2 + +import httplib +import urllib +import sys + +if sys.argv[2] == 'add': + detach_icons = '--detach-icons' in sys.argv + if detach_icons: + sys.argv.remove('--detach-icons') + params = {'d': sys.argv[5]} + if detach_icons: + params['detach-icons'] = 1 + query = urllib.urlencode(params) + conn = httplib.HTTPConnection(sys.argv[1]) + path = "/testing/{repo}/{tag}?{query}".format(repo=sys.argv[3], + tag=sys.argv[4], + query=query) + conn.request("POST", path) + response = conn.getresponse() + if response.status != 200: + print >>sys.stderr, response.read() + print >>sys.stderr, "Failed: status={}".format(response.status) + sys.exit(1) +elif sys.argv[2] == 'delete': + conn = httplib.HTTPConnection(sys.argv[1]) + path = "/testing/{repo}/{ref}".format(repo=sys.argv[3], + ref=sys.argv[4]) + conn.request("DELETE", path) + response = conn.getresponse() + if response.status != 200: + print >>sys.stderr, response.read() + print >>sys.stderr, "Failed: status={}".format(response.status) + sys.exit(1) +else: + print >>sys.stderr, "Usage: oci-registry-client.py [add|remove] ARGS" + sys.exit(1) + diff --git a/tests/oci-registry-server.py b/tests/oci-registry-server.py new file mode 100644 index 00000000..f31eecdd --- /dev/null +++ b/tests/oci-registry-server.py @@ -0,0 +1,233 @@ +#!/usr/bin/python2 + +import BaseHTTPServer +import base64 +import hashlib +import json +import os +import sys +from urlparse import parse_qs +import time + +repositories = {} +icons = {} + +def get_index(): + results = [] + for repo_name in sorted(repositories.keys()): + repo = repositories[repo_name] + results.append({ + 'Name': repo_name, + 'Images': repo['images'], + 'Lists': [], + }) + + return json.dumps({ + 'Registry': '/', + 'Results': results + }, indent=4) + +def cache_icon(data_uri): + prefix = 'data:image/png;base64,' + assert data_uri.startswith(prefix) + data = base64.b64decode(data_uri[len(prefix):]) + h = hashlib.sha256() + h.update(data) + digest = h.hexdigest() + filename = digest + '.png' + icons[filename] = data + + return '/icons/' + filename + +serial = 0 +server_start_time = int(time.time()) + +def get_etag(): + return str(server_start_time) + '-' + str(serial) + +def modified(): + global serial + serial += 1 + +def parse_http_date(date): + parsed = parsedate(date) + if parsed is not None: + return timegm(parsed) + else: + return None + +class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def check_route(self, route): + parts = self.path.split('?', 1) + path = parts[0].split('/') + + result = [] + + route_path = route.split('/') + print(route_path, path) + if len(route_path) != len(path): + return False + + matches = {} + for i in range(1, len(route_path)): + if route_path[i][0] == '@': + matches[route_path[i][1:]] = path[i] + elif route_path[i] != path[i]: + return False + + self.matches = matches + if len(parts) == 1: + self.query = {} + else: + self.query = parse_qs(parts[1], keep_blank_values=True) + + return True + + def do_GET(self): + response = 200 + response_string = None + response_content_type = "application/octet-stream" + response_file = None + + add_headers = {} + + if self.check_route('/v2/@repo_name/blobs/@digest'): + repo_name = self.matches['repo_name'] + digest = self.matches['digest'] + response_file = repositories[repo_name]['blobs'][digest] + elif self.check_route('/v2/@repo_name/manifests/@ref'): + repo_name = self.matches['repo_name'] + ref = self.matches['ref'] + response_file = repositories[repo_name]['manifests'][ref] + elif self.check_route('/index/static') or self.check_route('/index/dynamic'): + etag = get_etag() + if self.headers.get("If-None-Match") == etag: + response = 304 + else: + response_string = get_index() + add_headers['Etag'] = etag + elif self.check_route('/icons/@filename') : + response_string = icons[self.matches['filename']] + response_content_type = 'image/png' + else: + response = 404 + + self.send_response(response) + for k, v in add_headers.items(): + self.send_header(k, v) + + if response == 200: + self.send_header("Content-Type", response_content_type) + + if response == 200 or response == 304: + self.send_header('Cache-Control', 'no-cache') + + self.end_headers() + + if response == 200: + if response_file: + with open(response_file) as f: + response_string = f.read() + self.wfile.write(response_string) + + def do_POST(self): + if self.check_route('/testing/@repo_name/@tag'): + repo_name = self.matches['repo_name'] + tag = self.matches['tag'] + d = self.query['d'][0] + detach_icons = 'detach-icons' in self.query + + repo = repositories.setdefault(repo_name, {}) + blobs = repo.setdefault('blobs', {}) + manifests = repo.setdefault('manifests', {}) + images = repo.setdefault('images', []) + + with open(os.path.join(d, 'index.json')) as f: + index = json.load(f) + + manifest_digest = index['manifests'][0]['digest'] + manifest_path = os.path.join(d, 'blobs', *manifest_digest.split(':')) + manifests[manifest_digest] = manifest_path + manifests[tag] = manifest_path + + with open(manifest_path) as f: + manifest = json.load(f) + + config_digest = manifest['config']['digest'] + config_path = os.path.join(d, 'blobs', *config_digest.split(':')) + + with open(config_path) as f: + config = json.load(f) + + for dig in os.listdir(os.path.join(d, 'blobs', 'sha256')): + digest = 'sha256:' + dig + path = os.path.join(d, 'blobs', 'sha256', dig) + if digest != manifest_digest: + blobs[digest] = path + + if detach_icons: + for size in (64, 128): + annotation = 'org.freedesktop.appstream.icon-{}'.format(size) + icon = manifest['annotations'].get(annotation) + if icon: + path = cache_icon(icon) + manifest['annotations'][annotation] = path + + image = { + "Tags": [tag], + "Digest": manifest_digest, + "MediaType": "application/vnd.oci.image.manifest.v1+json", + "OS": config['os'], + "Architecture": config['architecture'], + "Annotations": manifest['annotations'], + "Labels": {}, + } + + images.append(image) + + modified() + self.send_response(200) + self.end_headers() + return + else: + self.send_response(404) + self.end_headers() + return + + def do_DELETE(self): + if self.check_route('/testing/@repo_name/@ref'): + repo_name = self.matches['repo_name'] + ref = self.matches['ref'] + + repo = repositories.setdefault(repo_name, {}) + blobs = repo.setdefault('blobs', {}) + manifests = repo.setdefault('manifests', {}) + images = repo.setdefault('images', []) + + image = None + for i in images: + if i['Digest'] == ref or ref in i['Tags']: + image = i + break + + assert image + + images.remove(image) + del manifests[image['Digest']] + for t in image['Tags']: + del manifests[t] + + modified() + self.send_response(200) + self.end_headers() + return + else: + self.send_response(404) + self.end_headers() + return + +def test(): + BaseHTTPServer.test(RequestHandler) + +if __name__ == '__main__': + test() diff --git a/tests/test-oci-registry-system.sh b/tests/test-oci-registry-system.sh new file mode 100755 index 00000000..c0847760 --- /dev/null +++ b/tests/test-oci-registry-system.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (C) 2018 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +export USE_SYSTEMDIR=yes + +. $(dirname $0)/test-oci-registry.sh diff --git a/tests/test-oci-registry.sh b/tests/test-oci-registry.sh new file mode 100755 index 00000000..49fb4b22 --- /dev/null +++ b/tests/test-oci-registry.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# +# Copyright (C) 2018 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. + +set -euo pipefail + +. $(dirname $0)/libtest.sh + +export FLATPAK_ENABLE_EXPERIMENTAL_OCI=1 + +skip_without_bwrap + +echo "1..7" + +# Start the fake registry server + +$(dirname $0)/test-webserver.sh "" "python2 $test_srcdir/oci-registry-server.py 0" +FLATPAK_HTTP_PID=$(cat httpd-pid) +mv httpd-port httpd-port-main +port=$(cat httpd-port-main) +client="python2 $test_srcdir/oci-registry-client.py 127.0.0.1:$port" + +setup_repo_no_add oci + +# Add OCI bundles to it + +${FLATPAK} build-bundle --runtime --oci $FL_GPGARGS repos/oci oci/platform-image org.test.Platform +$client add platform latest $(pwd)/oci/platform-image + +${FLATPAK} build-bundle --oci $FL_GPGARGS repos/oci oci/app-image org.test.Hello +$client add hello latest $(pwd)/oci/app-image + +# Add an OCI remote + +flatpak remote-add ${U} --oci oci-registry "http://127.0.0.1:${port}" + +# Check that the images we expect are listed + +images=$(flatpak remote-ls ${U} oci-registry | sort | tr '\n' ' ' | sed 's/ $//') +assert_streq "$images" "org.test.Hello org.test.Platform" +echo "ok list remote" + +# Pull appstream data + +flatpak update ${U} --appstream oci-registry + +# Check that the appstream and icons exist + +if [ x${USE_SYSTEMDIR-} == xyes ] ; then + appstream=$SYSTEMDIR/appstream/oci-registry/$ARCH/appstream.xml.gz + icondir=$SYSTEMDIR/appstream/oci-registry/$ARCH/icons +else + appstream=$USERDIR/appstream/oci-registry/$ARCH/appstream.xml.gz + icondir=$USERDIR/appstream/oci-registry/$ARCH/icons +fi + +gunzip -c $appstream > appstream-uncompressed +assert_file_has_content appstream-uncompressed 'org.test.Hello.desktop' +assert_has_file $icondir/64x64/org.test.Hello.png + +echo "ok appstream" + +# Test that 'flatpak search' works +flatpak search org.test.Hello > search-results +assert_file_has_content search-results "Print a greeting" + +echo "ok search" + +# Replace with the app image with detached icons, check that the icons work + +old_icon_hash=(md5sum $icondir/64x64/org.test.Hello.png) +rm $icondir/64x64/org.test.Hello.png +$client delete hello latest +$client add --detach-icons hello latest $(pwd)/oci/app-image +flatpak update ${U} --appstream oci-registry +assert_has_file $icondir/64x64/org.test.Hello.png +new_icon_hash=(md5sum $icondir/64x64/org.test.Hello.png) +assert_streq $old_icon_hash $new_icon_hash + +echo "ok detached icons" + +# Try installing from the remote + +${FLATPAK} ${U} install -y oci-registry org.test.Platform +echo "ok install" + +# Remove the app from the registry, check that things were removed properly + +$client delete hello latest + +images=$(flatpak remote-ls ${U} oci-registry | sort | tr '\n' ' ' | sed 's/ $//') +assert_streq "$images" "org.test.Platform" + +flatpak update ${U} --appstream oci-registry + +assert_not_file_has_content $appstream 'org.test.Hello.desktop' +assert_not_has_file $icondir/64x64/org.test.Hello.png +assert_not_has_file $icondir/64x64 + +echo "ok appstream change" + +# Delete the remote, check that everything was removed + +if [ x${USE_SYSTEMDIR-} == xyes ] ; then + base=$SYSTEMDIR +else + base=$USERDIR +fi + +assert_has_file $base/oci/oci-registry.index.gz +assert_has_file $base/oci/oci-registry.summary +assert_has_dir $base/appstream/oci-registry +flatpak ${U} -y uninstall org.test.Platform +flatpak ${U} remote-delete oci-registry +assert_not_has_file $base/oci/oci-registry.index.gz +assert_not_has_file $base/oci/oci-registry.summary +assert_not_has_dir $base/appstream/oci-registry + +echo "ok delete remote" diff --git a/tests/test-oci.sh b/tests/test-oci.sh index f4cd3540..44e4577a 100755 --- a/tests/test-oci.sh +++ b/tests/test-oci.sh @@ -27,12 +27,10 @@ skip_without_bwrap echo "1..2" -setup_repo - -${FLATPAK} ${U} install test-repo org.test.Platform master +setup_repo_no_add oci mkdir -p oci -${FLATPAK} build-bundle --oci $FL_GPGARGS repos/test oci/image org.test.Hello +${FLATPAK} build-bundle --oci $FL_GPGARGS repos/oci oci/image org.test.Hello assert_has_file oci/image/oci-layout assert_has_dir oci/image/blobs/sha256