Basic labels support
Adds basic labels support to the registry code (V2), and the API. Note that this does not yet add any UI related support.
This commit is contained in:
parent
427070b453
commit
608ffd9663
24 changed files with 907 additions and 36 deletions
Binary file not shown.
|
@ -16,6 +16,7 @@ class assert_action_logged(object):
|
|||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
updated_count = self._get_log_count()
|
||||
error_msg = 'Missing new log entry of kind %s' % self.log_kind
|
||||
assert self.existing_count == (updated_count - 1), error_msg
|
||||
if exc_val is None:
|
||||
updated_count = self._get_log_count()
|
||||
error_msg = 'Missing new log entry of kind %s' % self.log_kind
|
||||
assert self.existing_count == (updated_count - 1), error_msg
|
||||
|
|
|
@ -1129,6 +1129,55 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
|
|||
class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMixin,
|
||||
RegistryTestCaseMixin, LiveServerTestCase):
|
||||
""" Tests for V2 registry. """
|
||||
def test_label_invalid_manifest(self):
|
||||
images = [{
|
||||
'id': 'someid',
|
||||
'config': {'Labels': None},
|
||||
'contents': 'somecontent'
|
||||
}]
|
||||
|
||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
||||
self.do_pull('devtable', 'newrepo', 'devtable', 'password')
|
||||
|
||||
def test_labels(self):
|
||||
# Push a new repo with the latest tag.
|
||||
images = [{
|
||||
'id': 'someid',
|
||||
'config': {'Labels': {'foo': 'bar', 'baz': 'meh', 'theoretically-invalid--label': 'foo'}},
|
||||
'contents': 'somecontent'
|
||||
}]
|
||||
|
||||
(_, digest) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
||||
|
||||
self.conduct_api_login('devtable', 'password')
|
||||
labels = self.conduct('GET', '/api/v1/repository/devtable/newrepo/manifest/' + digest + '/labels').json()
|
||||
self.assertEquals(3, len(labels['labels']))
|
||||
|
||||
self.assertEquals('manifest', labels['labels'][0]['source_type'])
|
||||
self.assertEquals('manifest', labels['labels'][1]['source_type'])
|
||||
self.assertEquals('manifest', labels['labels'][2]['source_type'])
|
||||
|
||||
self.assertEquals('text/plain', labels['labels'][0]['media_type'])
|
||||
self.assertEquals('text/plain', labels['labels'][1]['media_type'])
|
||||
self.assertEquals('text/plain', labels['labels'][2]['media_type'])
|
||||
|
||||
def test_json_labels(self):
|
||||
# Push a new repo with the latest tag.
|
||||
images = [{
|
||||
'id': 'someid',
|
||||
'config': {'Labels': {'foo': 'bar', 'baz': '{"some": "json"}'}},
|
||||
'contents': 'somecontent'
|
||||
}]
|
||||
|
||||
(_, digest) = self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
||||
|
||||
self.conduct_api_login('devtable', 'password')
|
||||
labels = self.conduct('GET', '/api/v1/repository/devtable/newrepo/manifest/' + digest + '/labels').json()
|
||||
self.assertEquals(2, len(labels['labels']))
|
||||
|
||||
self.assertEquals('text/plain', labels['labels'][0]['media_type'])
|
||||
self.assertEquals('application/json', labels['labels'][1]['media_type'])
|
||||
|
||||
def test_invalid_manifest_type(self):
|
||||
namespace = 'devtable'
|
||||
repository = 'somerepo'
|
||||
|
|
|
@ -52,6 +52,7 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
|
|||
SuperUserServiceKey, SuperUserServiceKeyApproval,
|
||||
SuperUserTakeOwnership)
|
||||
from endpoints.api.secscan import RepositoryImageSecurity
|
||||
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
||||
|
||||
|
||||
try:
|
||||
|
@ -4298,5 +4299,54 @@ class TestRepositoryImageSecurity(ApiTestCase):
|
|||
self._run_test('GET', 404, 'devtable', None)
|
||||
|
||||
|
||||
class TestRepositoryManifestLabels(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(RepositoryManifestLabels, repository='devtable/simple', manifestref='sha256:abcd')
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 404, 'devtable', None)
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self._run_test('POST', 401, None, dict(key='foo', value='bar', media_type='text/plain'))
|
||||
|
||||
def test_post_freshuser(self):
|
||||
self._run_test('POST', 403, 'freshuser', dict(key='foo', value='bar', media_type='text/plain'))
|
||||
|
||||
def test_post_reader(self):
|
||||
self._run_test('POST', 403, 'reader', dict(key='foo', value='bar', media_type='text/plain'))
|
||||
|
||||
def test_post_devtable(self):
|
||||
self._run_test('POST', 404, 'devtable', dict(key='foo', value='bar', media_type='text/plain'))
|
||||
|
||||
|
||||
class TestManageRepositoryManifestLabel(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ManageRepositoryManifestLabel, repository='devtable/simple',
|
||||
manifestref='sha256:abcd', labelid='someid')
|
||||
|
||||
def test_delete_anonymous(self):
|
||||
self._run_test('DELETE', 401, None, None)
|
||||
|
||||
def test_delete_freshuser(self):
|
||||
self._run_test('DELETE', 403, 'freshuser', None)
|
||||
|
||||
def test_delete_reader(self):
|
||||
self._run_test('DELETE', 403, 'reader', None)
|
||||
|
||||
def test_delete_devtable(self):
|
||||
self._run_test('DELETE', 404, 'devtable', None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -65,6 +65,8 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
|
|||
from endpoints.api.secscan import RepositoryImageSecurity
|
||||
from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile,
|
||||
SuperUserCreateInitialSuperUser)
|
||||
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
|
||||
|
||||
|
||||
try:
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
|
@ -1765,6 +1767,15 @@ class TestDeleteRepository(ApiTestCase):
|
|||
RepositoryActionCount.create(repository=repository,
|
||||
date=datetime.datetime.now() - datetime.timedelta(days=5), count=6)
|
||||
|
||||
# Create some labels.
|
||||
tag_manifest = model.tag.load_tag_manifest(ADMIN_ACCESS_USER, 'complex', 'prod')
|
||||
model.label.create_manifest_label(tag_manifest, 'foo', 'bar', 'manifest')
|
||||
model.label.create_manifest_label(tag_manifest, 'foo', 'baz', 'manifest')
|
||||
model.label.create_manifest_label(tag_manifest, 'something', '{}', 'api',
|
||||
media_type_name='application/json')
|
||||
|
||||
model.label.create_manifest_label(tag_manifest, 'something', '{"some": "json"}', 'manifest')
|
||||
|
||||
# Delete the repository.
|
||||
with check_transitive_deletes():
|
||||
self.deleteResponse(Repository, params=dict(repository=self.COMPLEX_REPO))
|
||||
|
@ -3941,6 +3952,150 @@ class TestSuperUserKeyManagement(ApiTestCase):
|
|||
self.assertEquals('whazzup!?', json['approval']['notes'])
|
||||
|
||||
|
||||
class TestRepositoryManifestLabels(ApiTestCase):
|
||||
def test_basic_labels(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Find the manifest digest for the prod tag in the complex repo.
|
||||
tag_manifest = model.tag.load_tag_manifest(ADMIN_ACCESS_USER, 'complex', 'prod')
|
||||
repository = ADMIN_ACCESS_USER + '/complex'
|
||||
|
||||
# Check the existing labels on the complex repo, which should be empty
|
||||
json = self.getJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository, manifestref=tag_manifest.digest))
|
||||
|
||||
self.assertEquals(0, len(json['labels']))
|
||||
|
||||
# Add some labels to the manifest.
|
||||
with assert_action_logged('manifest_label_add'):
|
||||
label1 = self.postJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='hello', value='world',
|
||||
media_type='text/plain'),
|
||||
expected_code=201)
|
||||
|
||||
|
||||
with assert_action_logged('manifest_label_add'):
|
||||
label2 = self.postJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='hi', value='there',
|
||||
media_type='text/plain'),
|
||||
expected_code=201)
|
||||
|
||||
with assert_action_logged('manifest_label_add'):
|
||||
label3 = self.postJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='hello', value='someone',
|
||||
media_type='application/json'),
|
||||
expected_code=201)
|
||||
|
||||
|
||||
# Ensure we have *3* labels
|
||||
json = self.getJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest))
|
||||
|
||||
self.assertEquals(3, len(json['labels']))
|
||||
|
||||
self.assertNotEquals(label2['label']['id'], label1['label']['id'])
|
||||
self.assertNotEquals(label3['label']['id'], label1['label']['id'])
|
||||
self.assertNotEquals(label2['label']['id'], label3['label']['id'])
|
||||
|
||||
self.assertEquals('text/plain', label1['label']['media_type'])
|
||||
self.assertEquals('text/plain', label2['label']['media_type'])
|
||||
self.assertEquals('application/json', label3['label']['media_type'])
|
||||
|
||||
# Delete a label.
|
||||
with assert_action_logged('manifest_label_delete'):
|
||||
self.deleteResponse(ManageRepositoryManifestLabel,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest,
|
||||
labelid=label1['label']['id']))
|
||||
|
||||
# Ensure the label is gone.
|
||||
json = self.getJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest))
|
||||
|
||||
self.assertEquals(2, len(json['labels']))
|
||||
|
||||
# Check filtering.
|
||||
json = self.getJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest,
|
||||
filter='hello'))
|
||||
|
||||
self.assertEquals(1, len(json['labels']))
|
||||
|
||||
|
||||
def test_prefixed_labels(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Find the manifest digest for the prod tag in the complex repo.
|
||||
tag_manifest = model.tag.load_tag_manifest(ADMIN_ACCESS_USER, 'complex', 'prod')
|
||||
repository = ADMIN_ACCESS_USER + '/complex'
|
||||
|
||||
self.postJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='com.dockers.whatever', value='pants',
|
||||
media_type='text/plain'),
|
||||
expected_code=201)
|
||||
|
||||
|
||||
self.postJsonResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='my.cool.prefix.for.my.label', value='value',
|
||||
media_type='text/plain'),
|
||||
expected_code=201)
|
||||
|
||||
|
||||
|
||||
def test_add_invalid_media_type(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
tag_manifest = model.tag.load_tag_manifest(ADMIN_ACCESS_USER, 'complex', 'prod')
|
||||
repository = ADMIN_ACCESS_USER + '/complex'
|
||||
|
||||
self.postResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='hello', value='world', media_type='some/invalid'),
|
||||
expected_code=400)
|
||||
|
||||
|
||||
def test_add_invalid_key(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
tag_manifest = model.tag.load_tag_manifest(ADMIN_ACCESS_USER, 'complex', 'prod')
|
||||
repository = ADMIN_ACCESS_USER + '/complex'
|
||||
|
||||
# Try to add an empty label key.
|
||||
self.postResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='', value='world'),
|
||||
expected_code=400)
|
||||
|
||||
# Try to add an invalid label key.
|
||||
self.postResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='invalid___key', value='world'),
|
||||
expected_code=400)
|
||||
|
||||
# Try to add a label key in a reserved namespace.
|
||||
self.postResponse(RepositoryManifestLabels,
|
||||
params=dict(repository=repository,
|
||||
manifestref=tag_manifest.digest),
|
||||
data=dict(key='io.docker.whatever', value='world'),
|
||||
expected_code=400)
|
||||
|
||||
|
||||
class TestSuperUserManagement(ApiTestCase):
|
||||
def test_get_user(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
|
44
test/test_validation.py
Normal file
44
test/test_validation.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import unittest
|
||||
from util.validation import validate_label_key
|
||||
|
||||
class TestLabelKeyValidation(unittest.TestCase):
|
||||
def assertValidKey(self, key):
|
||||
self.assertTrue(validate_label_key(key))
|
||||
|
||||
def assertInvalidKey(self, key):
|
||||
self.assertFalse(validate_label_key(key))
|
||||
|
||||
def test_basic_keys(self):
|
||||
self.assertValidKey('foo')
|
||||
self.assertValidKey('bar')
|
||||
|
||||
self.assertValidKey('foo1')
|
||||
self.assertValidKey('bar2')
|
||||
|
||||
self.assertValidKey('1')
|
||||
self.assertValidKey('12')
|
||||
self.assertValidKey('123')
|
||||
self.assertValidKey('1234')
|
||||
|
||||
self.assertValidKey('git-sha')
|
||||
|
||||
self.assertValidKey('com.coreos.something')
|
||||
self.assertValidKey('io.quay.git-sha')
|
||||
|
||||
def test_invalid_keys(self):
|
||||
self.assertInvalidKey('')
|
||||
self.assertInvalidKey('git_sha')
|
||||
|
||||
def test_must_start_with_alphanumeric(self):
|
||||
self.assertInvalidKey('-125')
|
||||
self.assertInvalidKey('-foo')
|
||||
self.assertInvalidKey('foo-')
|
||||
self.assertInvalidKey('123-')
|
||||
|
||||
def test_no_double_dashesdots(self):
|
||||
self.assertInvalidKey('foo--bar')
|
||||
self.assertInvalidKey('foo..bar')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Reference in a new issue