diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..48df0a7 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=tests/ +top_dir=./ diff --git a/.zuul.yaml b/.zuul.yaml index f7817d4..7165ad6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -56,9 +56,15 @@ check: jobs: - zuul-registry-build-image + - tox-pep8 + - tox-py37: + nodeset: fedora-latest gate: jobs: - zuul-registry-upload-image + - tox-pep8 + - tox-py37: + nodeset: fedora-latest promote: jobs: - zuul-registry-promote-image diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..bae23e9 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +flake8 +stestr diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..ac59202 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2019 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py new file mode 100644 index 0000000..44fe66b --- /dev/null +++ b/tests/test_filesystem.py @@ -0,0 +1,24 @@ +# Copyright 2019 Red Hat, Inc. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +import testtools + +from zuul_registry.filesystem import FilesystemDriver + + +class TestFilesystemDriver(testtools.TestCase): + def test_list_objects(self): + driver = FilesystemDriver({'root': '.'}) + print(driver.list_objects(".")) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..3033cb1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +minversion = 3.2 +skipsdist = True +envlist = pep8,py37 + +[testenv] +basepython = python3 +usedevelop = True +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = + stestr run {posargs} + stestr slowest + +[testenv:pep8] +install_command = pip install {opts} {packages} +commands = + flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[flake8] +# These are ignored intentionally in zuul projects; +# please don't submit patches that solely correct them or enable them. +ignore = E124,E125,E129,E252,E402,E741,H,W503,W504 +show-source = True +exclude = .venv,.tox,dist,doc,build,*.egg,node_modules diff --git a/zuul_registry/filesystem.py b/zuul_registry/filesystem.py index ddf91c0..22a123c 100644 --- a/zuul_registry/filesystem.py +++ b/zuul_registry/filesystem.py @@ -14,16 +14,15 @@ # along with this software. If not, see . import os -import time from . import storageutils + class FilesystemDriver(storageutils.StorageDriver): def __init__(self, conf): self.root = conf['root'] def list_objects(self, path): - now = time.time() path = os.path.join(self.root, path) if not os.path.isdir(path): return [] @@ -88,4 +87,5 @@ class FilesystemDriver(storageutils.StorageDriver): chunk_path = os.path.join(self.root, chunk_path) os.unlink(chunk_path) + Driver = FilesystemDriver diff --git a/zuul_registry/main.py b/zuul_registry/main.py index 2ba7813..e888275 100644 --- a/zuul_registry/main.py +++ b/zuul_registry/main.py @@ -31,6 +31,7 @@ DRIVERS = { 'swift': swift.Driver, } + class Authorization: def __init__(self, users): self.ro = {} @@ -52,6 +53,7 @@ class Authorization: return False return store[user] == password + class RegistryAPI: """Registry API server. @@ -125,9 +127,10 @@ class RegistryAPI: method = cherrypy.request.method uuid = self.storage.start_upload(namespace) self.log.info('Start upload %s %s uuid %s digest %s', - method, repository, uuid, digest) + method, repository, uuid, digest) res = cherrypy.response - res.headers['Location'] = '/v2/%s/blobs/uploads/%s' % (repository, uuid) + res.headers['Location'] = '/v2/%s/blobs/uploads/%s' % ( + repository, uuid) res.headers['Docker-Upload-UUID'] = uuid res.headers['Range'] = '0-0' res.status = '202 Accepted' @@ -140,11 +143,13 @@ class RegistryAPI: old_length, new_length = self.storage.upload_chunk( namespace, uuid, cherrypy.request.body) res = cherrypy.response - res.headers['Location'] = '/v2/%s/blobs/uploads/%s' % (repository, uuid) + res.headers['Location'] = '/v2/%s/blobs/uploads/%s' % ( + repository, uuid) res.headers['Docker-Upload-UUID'] = uuid res.headers['Range'] = '0-%s' % (new_length,) res.status = '204 No Content' - self.log.info('Finish Upload chunk %s %s %s', repository, uuid, new_length) + self.log.info( + 'Finish Upload chunk %s %s %s', repository, uuid, new_length) @cherrypy.expose @cherrypy.config(**{'tools.auth_basic.checkpassword': require_write}) @@ -217,8 +222,10 @@ class RegistryAPI: self.log.error('Manifest %s %s not found', repository, ref) return self.not_found() + class RegistryServer: log = logging.getLogger("registry.server") + def __init__(self, config_path): self.log.info("Loading config from %s", config_path) self._load_config(config_path) @@ -297,6 +304,7 @@ class RegistryServer: def prune(self): self.store.prune() + def main(): parser = argparse.ArgumentParser( description='Zuul registry server') @@ -321,7 +329,7 @@ def main(): logging.getLogger("urllib3").setLevel(logging.DEBUG) logging.getLogger("stevedore").setLevel(logging.INFO) logging.getLogger("openstack").setLevel(logging.DEBUG) - #cherrypy.log.error_log.propagate = False + # cherrypy.log.error_log.propagate = False s = RegistryServer(args.config) if args.command == 'serve': diff --git a/zuul_registry/storage.py b/zuul_registry/storage.py index 8fe574f..2a0999d 100644 --- a/zuul_registry/storage.py +++ b/zuul_registry/storage.py @@ -23,6 +23,7 @@ import threading import time from uuid import uuid4 + class UploadRecord: """Information about an upload. @@ -65,16 +66,19 @@ class UploadRecord: data = json.loads(data.decode('utf8')) self.chunks = data['chunks'] hash_state = data['hash_state'] - hash_state['md_data'] = base64.decodebytes(hash_state['md_data'].encode('ascii')) + hash_state['md_data'] = base64.decodebytes( + hash_state['md_data'].encode('ascii')) self.hasher.__setstate__(hash_state) def dump(self): hash_state = self.hasher.__getstate__() - hash_state['md_data'] = base64.encodebytes(hash_state['md_data']).decode('ascii') - data = dict(chunks = self.chunks, - hash_state = hash_state) + hash_state['md_data'] = base64.encodebytes( + hash_state['md_data']).decode('ascii') + data = dict(chunks=self.chunks, + hash_state=hash_state) return json.dumps(data).encode('utf8') + class UploadStreamer: """Stream an upload to the object storage. @@ -96,6 +100,7 @@ class UploadStreamer: break yield d + class Storage: """Storage abstraction layer. @@ -145,7 +150,6 @@ class Storage: """ uuid = uuid4().hex - path = os.path.join(namespace, 'uploads', uuid, 'metadata') upload = UploadRecord() self._update_upload(namespace, uuid, upload) return uuid @@ -198,7 +202,7 @@ class Storage: t.join() upload.chunks.append(dict(size=size)) self._update_upload(namespace, uuid, upload) - return upload.size-size, upload.size + return upload.size - size, upload.size def store_upload(self, namespace, uuid, digest): """Complete an upload. @@ -215,7 +219,7 @@ class Storage: # Move the chunks into the blob dir to get them out of the # uploads dir. chunks = [] - for i in range(1, upload.count+1): + for i in range(1, upload.count + 1): src_path = os.path.join(namespace, 'uploads', uuid, str(i)) dst_path = os.path.join(namespace, 'blobs', digest, str(i)) chunks.append(dst_path) @@ -271,7 +275,8 @@ class Storage: self.log.debug('Get layers %s', path) data = self.backend.get_object(path) manifest = json.loads(data) - target = manifest.get('application/vnd.docker.distribution.manifest.v2+json') + target = manifest.get( + 'application/vnd.docker.distribution.manifest.v2+json') layers = [] if not target: self.log.debug('Unknown manifest %s', path) diff --git a/zuul_registry/storageutils.py b/zuul_registry/storageutils.py index 46d6704..cf8979e 100644 --- a/zuul_registry/storageutils.py +++ b/zuul_registry/storageutils.py @@ -15,6 +15,7 @@ from abc import ABCMeta, abstractmethod + class ObjectInfo: def __init__(self, path, name, ctime, isdir): self.path = path @@ -22,6 +23,7 @@ class ObjectInfo: self.ctime = ctime self.isdir = isdir + class StorageDriver(metaclass=ABCMeta): """Base class for storage drivers. diff --git a/zuul_registry/swift.py b/zuul_registry/swift.py index b0775ea..87bb666 100644 --- a/zuul_registry/swift.py +++ b/zuul_registry/swift.py @@ -27,6 +27,7 @@ from . import storageutils POST_ATTEMPTS = 3 + def retry_function(func): for attempt in range(1, POST_ATTEMPTS + 1): try: @@ -40,6 +41,7 @@ def retry_function(func): logging.exception("Error on attempt %d" % attempt) time.sleep(attempt * 10) + class SwiftDriver(storageutils.StorageDriver): log = logging.getLogger('registry.swift') @@ -76,7 +78,8 @@ class SwiftDriver(storageutils.StorageDriver): else: objpath = obj['name'] name = obj['name'].split('/')[-1] - ctime = dateutil.parser.parse(obj['last_modified']+'Z').timestamp() + ctime = dateutil.parser.parse( + obj['last_modified'] + 'Z').timestamp() isdir = False ret.append(storageutils.ObjectInfo( objpath, name, ctime, isdir)) @@ -126,7 +129,7 @@ class SwiftDriver(storageutils.StorageDriver): dst = os.path.join(self.container_name, dst_path) retry_function( lambda: self.conn.session.request( - self.get_url(src_path)+"?multipart-manfest=get", + self.get_url(src_path) + "?multipart-manfest=get", 'COPY', headers={'Destination': dst} )) @@ -136,7 +139,7 @@ class SwiftDriver(storageutils.StorageDriver): def cat_objects(self, path, chunks): manifest = [] - #TODO: Would it be better to move 1-chunk objects? + # TODO: Would it be better to move 1-chunk objects? for chunk_path in chunks: ret = retry_function( lambda: self.conn.session.head(self.get_url(chunk_path))) @@ -148,7 +151,8 @@ class SwiftDriver(storageutils.StorageDriver): 'size_bytes': ret.headers['Content-Length']}) retry_function(lambda: self.conn.session.put( - self.get_url(path)+"?multipart-manifest=put", + self.get_url(path) + "?multipart-manifest=put", data=json.dumps(manifest))) + Driver = SwiftDriver