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
This commit is contained in:
Owen W. Taylor 2018-07-11 15:12:27 +02:00 committed by Atomic Bot
parent 4dfa7721bb
commit 6838206e2a
6 changed files with 431 additions and 4 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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

134
tests/test-oci-registry.sh Executable file
View File

@ -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 '<id>org.test.Hello.desktop</id>'
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 '<id>org.test.Hello.desktop</id>'
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"

View File

@ -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