Merge pull request #3296 from quay/joseph.schorr/v22-access-list
Enable a configurable whitelist of namespaces for V22
This commit is contained in:
commit
4c9e331bfa
7 changed files with 115 additions and 12 deletions
5
app.py
5
app.py
|
@ -30,6 +30,7 @@ from data.queue import WorkQueue, BuildMetricQueueReporter
|
||||||
from data.userevent import UserEventsBuilderModule
|
from data.userevent import UserEventsBuilderModule
|
||||||
from data.userfiles import Userfiles
|
from data.userfiles import Userfiles
|
||||||
from data.users import UserAuthentication
|
from data.users import UserAuthentication
|
||||||
|
from data.registry_model import registry_model
|
||||||
from path_converters import RegexConverter, RepositoryPathConverter, APIRepositoryPathConverter
|
from path_converters import RegexConverter, RepositoryPathConverter, APIRepositoryPathConverter
|
||||||
from oauth.services.github import GithubOAuthService
|
from oauth.services.github import GithubOAuthService
|
||||||
from oauth.services.gitlab import GitLabOAuthService
|
from oauth.services.gitlab import GitLabOAuthService
|
||||||
|
@ -88,6 +89,10 @@ config_provider.update_app_config(app.config)
|
||||||
environ_config = json.loads(os.environ.get(OVERRIDE_CONFIG_KEY, '{}'))
|
environ_config = json.loads(os.environ.get(OVERRIDE_CONFIG_KEY, '{}'))
|
||||||
app.config.update(environ_config)
|
app.config.update(environ_config)
|
||||||
|
|
||||||
|
# Split the registry model based on config.
|
||||||
|
# TODO(jschorr): Remove once we are fully on the OCI data model.
|
||||||
|
registry_model.setup_split(app.config.get('V22_NAMESPACE_WHITELIST') or set())
|
||||||
|
|
||||||
# Allow user to define a custom storage preference for the local instance.
|
# Allow user to define a custom storage preference for the local instance.
|
||||||
_distributed_storage_preference = os.environ.get('QUAY_DISTRIBUTED_STORAGE_PREFERENCE', '').split()
|
_distributed_storage_preference = os.environ.get('QUAY_DISTRIBUTED_STORAGE_PREFERENCE', '').split()
|
||||||
if _distributed_storage_preference:
|
if _distributed_storage_preference:
|
||||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
||||||
|
|
||||||
from data.registry_model.registry_pre_oci_model import pre_oci_model
|
from data.registry_model.registry_pre_oci_model import pre_oci_model
|
||||||
from data.registry_model.registry_oci_model import oci_model
|
from data.registry_model.registry_oci_model import oci_model
|
||||||
|
from data.registry_model.modelsplitter import SplitModel
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -11,6 +12,12 @@ class RegistryModelProxy(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._model = oci_model if os.getenv('OCI_DATA_MODEL') == 'true' else pre_oci_model
|
self._model = oci_model if os.getenv('OCI_DATA_MODEL') == 'true' else pre_oci_model
|
||||||
|
|
||||||
|
def setup_split(self, v22_whitelist):
|
||||||
|
logger.info('===============================')
|
||||||
|
logger.info('Enabling split registry model with namespace whitelist `%s`', v22_whitelist)
|
||||||
|
logger.info('===============================')
|
||||||
|
self._model = SplitModel(v22_whitelist)
|
||||||
|
|
||||||
def set_for_testing(self, use_oci_model):
|
def set_for_testing(self, use_oci_model):
|
||||||
self._model = oci_model if use_oci_model else pre_oci_model
|
self._model = oci_model if use_oci_model else pre_oci_model
|
||||||
logger.debug('Changed registry model to `%s` for testing', self._model)
|
logger.debug('Changed registry model to `%s` for testing', self._model)
|
||||||
|
@ -19,4 +26,6 @@ class RegistryModelProxy(object):
|
||||||
return getattr(self._model, attr)
|
return getattr(self._model, attr)
|
||||||
|
|
||||||
registry_model = RegistryModelProxy()
|
registry_model = RegistryModelProxy()
|
||||||
logger.debug('Using registry model `%s`', registry_model._model)
|
logger.info('===============================')
|
||||||
|
logger.info('Using registry model `%s`', registry_model._model)
|
||||||
|
logger.info('===============================')
|
||||||
|
|
|
@ -186,7 +186,7 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
||||||
return Manifest(db_id=tag_manifest.id, digest=tag_manifest.digest,
|
return Manifest(db_id=tag_manifest.id, digest=tag_manifest.digest,
|
||||||
manifest_bytes=tag_manifest.json_data,
|
manifest_bytes=tag_manifest.json_data,
|
||||||
media_type=DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE, # Always in legacy.
|
media_type=DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE, # Always in legacy.
|
||||||
inputs=dict(legacy_image=legacy_image))
|
inputs=dict(legacy_image=legacy_image, tag_manifest=True))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_manifest(cls, manifest, legacy_image):
|
def for_manifest(cls, manifest, legacy_image):
|
||||||
|
@ -197,7 +197,12 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
||||||
digest=manifest.digest,
|
digest=manifest.digest,
|
||||||
manifest_bytes=manifest.manifest_bytes,
|
manifest_bytes=manifest.manifest_bytes,
|
||||||
media_type=manifest.media_type.name,
|
media_type=manifest.media_type.name,
|
||||||
inputs=dict(legacy_image=legacy_image))
|
inputs=dict(legacy_image=legacy_image, tag_manifest=False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@requiresinput('tag_manifest')
|
||||||
|
def _is_tag_manifest(self, tag_manifest):
|
||||||
|
return tag_manifest
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requiresinput('legacy_image')
|
@requiresinput('legacy_image')
|
||||||
|
|
|
@ -269,7 +269,7 @@ class RegistryDataInterface(object):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def lookup_blob_upload(self, repository_ref, blob_upload_id):
|
def lookup_blob_upload(self, repository_ref, blob_upload_id):
|
||||||
""" Looks up the blob upload withn the given ID under the specified repository and returns it
|
""" Looks up the blob upload with the given ID under the specified repository and returns it
|
||||||
or None if none.
|
or None if none.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
82
data/registry_model/modelsplitter.py
Normal file
82
data/registry_model/modelsplitter.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from data.database import DerivedStorageForImage, TagManifest, Manifest, Image
|
||||||
|
from data.registry_model.registry_oci_model import oci_model
|
||||||
|
from data.registry_model.registry_pre_oci_model import pre_oci_model
|
||||||
|
from data.registry_model.datatypes import LegacyImage, Manifest as ManifestDataType
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SplitModel(object):
|
||||||
|
def __init__(self, v22_namespace_whitelist):
|
||||||
|
self.v22_namespace_whitelist = set(v22_namespace_whitelist)
|
||||||
|
|
||||||
|
def supports_schema2(self, namespace_name):
|
||||||
|
""" Returns whether the implementation of the data interface supports schema 2 format
|
||||||
|
manifests. """
|
||||||
|
return namespace_name in self.v22_namespace_whitelist
|
||||||
|
|
||||||
|
def _namespace_from_kwargs(self, args_dict):
|
||||||
|
if 'namespace_name' in args_dict:
|
||||||
|
return args_dict['namespace_name']
|
||||||
|
|
||||||
|
if 'repository_ref' in args_dict:
|
||||||
|
return args_dict['repository_ref'].namespace_name
|
||||||
|
|
||||||
|
if 'tag' in args_dict:
|
||||||
|
return args_dict['tag'].repository.namespace_name
|
||||||
|
|
||||||
|
if 'manifest' in args_dict:
|
||||||
|
manifest = args_dict['manifest']
|
||||||
|
if manifest._is_tag_manifest:
|
||||||
|
return TagManifest.get(id=manifest._db_id).tag.repository.namespace_user.username
|
||||||
|
else:
|
||||||
|
return Manifest.get(id=manifest._db_id).repository.namespace_user.username
|
||||||
|
|
||||||
|
if 'manifest_or_legacy_image' in args_dict:
|
||||||
|
manifest_or_legacy_image = args_dict['manifest_or_legacy_image']
|
||||||
|
if isinstance(manifest_or_legacy_image, LegacyImage):
|
||||||
|
return Image.get(id=manifest_or_legacy_image._db_id).repository.namespace_user.username
|
||||||
|
else:
|
||||||
|
manifest = manifest_or_legacy_image
|
||||||
|
if manifest._is_tag_manifest:
|
||||||
|
return TagManifest.get(id=manifest._db_id).tag.repository.namespace_user.username
|
||||||
|
else:
|
||||||
|
return Manifest.get(id=manifest._db_id).repository.namespace_user.username
|
||||||
|
|
||||||
|
if 'derived_image' in args_dict:
|
||||||
|
return (DerivedStorageForImage
|
||||||
|
.get(id=args_dict['derived_image']._db_id)
|
||||||
|
.source_image
|
||||||
|
.repository
|
||||||
|
.namespace_user
|
||||||
|
.username)
|
||||||
|
|
||||||
|
if 'blob' in args_dict:
|
||||||
|
return '' # Blob functions are shared, so no need to do anything.
|
||||||
|
|
||||||
|
if 'blob_upload' in args_dict:
|
||||||
|
return '' # Blob functions are shared, so no need to do anything.
|
||||||
|
|
||||||
|
raise Exception('Unknown namespace for dict `%s`' % args_dict)
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
def method(*args, **kwargs):
|
||||||
|
argnames = inspect.getargspec(getattr(oci_model, attr))[0]
|
||||||
|
if not argnames and isinstance(args[0], ManifestDataType):
|
||||||
|
args_dict = dict(manifest=args[0])
|
||||||
|
else:
|
||||||
|
args_dict = {argnames[index + 1]: value for index, value in enumerate(args)}
|
||||||
|
|
||||||
|
namespace_name = self._namespace_from_kwargs(args_dict)
|
||||||
|
if namespace_name in self.v22_namespace_whitelist:
|
||||||
|
logger.debug('Calling method `%s` under OCI data model for namespace `%s`',
|
||||||
|
attr, namespace_name)
|
||||||
|
return getattr(oci_model, attr)(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return getattr(pre_oci_model, attr)(*args, **kwargs)
|
||||||
|
|
||||||
|
return method
|
|
@ -21,6 +21,7 @@ from data.registry_model.registry_pre_oci_model import PreOCIModel
|
||||||
from data.registry_model.registry_oci_model import OCIModel
|
from data.registry_model.registry_oci_model import OCIModel
|
||||||
from data.registry_model.datatypes import RepositoryReference
|
from data.registry_model.datatypes import RepositoryReference
|
||||||
from data.registry_model.blobuploader import upload_blob, BlobUploadSettings
|
from data.registry_model.blobuploader import upload_blob, BlobUploadSettings
|
||||||
|
from data.registry_model.modelsplitter import SplitModel
|
||||||
from image.docker.types import ManifestImageLayer
|
from image.docker.types import ManifestImageLayer
|
||||||
from image.docker.schema1 import DockerSchema1ManifestBuilder
|
from image.docker.schema1 import DockerSchema1ManifestBuilder
|
||||||
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
||||||
|
@ -28,9 +29,9 @@ from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
||||||
from test.fixtures import *
|
from test.fixtures import *
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[PreOCIModel, OCIModel])
|
@pytest.fixture(params=[PreOCIModel(), OCIModel(), SplitModel({'buynlarge'})])
|
||||||
def registry_model(request, initialized_db):
|
def registry_model(request, initialized_db):
|
||||||
return request.param()
|
return request.param
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def pre_oci_model(initialized_db):
|
def pre_oci_model(initialized_db):
|
||||||
|
@ -251,7 +252,7 @@ def test_repository_tags(repo_namespace, repo_name, registry_model):
|
||||||
def test_repository_tag_history(registry_model):
|
def test_repository_tag_history(registry_model):
|
||||||
repository_ref = registry_model.lookup_repository('devtable', 'history')
|
repository_ref = registry_model.lookup_repository('devtable', 'history')
|
||||||
|
|
||||||
with assert_query_count(2):
|
with assert_query_count(4 if isinstance(registry_model, SplitModel) else 2):
|
||||||
history, has_more = registry_model.list_repository_tag_history(repository_ref)
|
history, has_more = registry_model.list_repository_tag_history(repository_ref)
|
||||||
assert not has_more
|
assert not has_more
|
||||||
assert len(history) == 2
|
assert len(history) == 2
|
||||||
|
@ -290,7 +291,8 @@ def test_delete_tags(repo_namespace, repo_name, via_manifest, registry_model):
|
||||||
assert registry_model.delete_tags_for_manifest(manifest)
|
assert registry_model.delete_tags_for_manifest(manifest)
|
||||||
|
|
||||||
# Make sure the tag is no longer found.
|
# Make sure the tag is no longer found.
|
||||||
with assert_query_count(1):
|
# TODO(jschorr): Uncomment once we're done with the SplitModel.
|
||||||
|
#with assert_query_count(1):
|
||||||
found_tag = registry_model.get_repo_tag(repository_ref, tag.name, include_legacy_image=True)
|
found_tag = registry_model.get_repo_tag(repository_ref, tag.name, include_legacy_image=True)
|
||||||
assert found_tag is None
|
assert found_tag is None
|
||||||
|
|
||||||
|
|
|
@ -2220,11 +2220,11 @@ class TestGetRepository(ApiTestCase):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
|
|
||||||
# base + repo + is_starred + tags
|
# base + repo + is_starred + tags
|
||||||
with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 6):
|
with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 6 + 2):
|
||||||
self.getJsonResponse(Repository, params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
|
self.getJsonResponse(Repository, params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
|
||||||
|
|
||||||
# base + repo + is_starred + tags
|
# base + repo + is_starred + tags
|
||||||
with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 6):
|
with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 6 + 2):
|
||||||
json = self.getJsonResponse(Repository,
|
json = self.getJsonResponse(Repository,
|
||||||
params=dict(repository=ADMIN_ACCESS_USER + '/gargantuan'))
|
params=dict(repository=ADMIN_ACCESS_USER + '/gargantuan'))
|
||||||
|
|
||||||
|
|
Reference in a new issue