Implement 'all' artifact type

Change-Id: Ia3bbe4f76af29e269ce25e67a6d2324e1ec57927
This commit is contained in:
Mike Fedosin 2016-10-01 14:17:06 +03:00
parent a4feb466da
commit ead97e6d33
8 changed files with 398 additions and 213 deletions

View File

@ -57,7 +57,8 @@ class ArtifactAPI(base_api.BaseDBAPI):
def list(self, context, filters, marker, limit, sort, latest):
session = api.get_session()
filters.append(('type_name', None, 'eq', None, self.type))
if self.type != 'all':
filters.append(('type_name', None, 'eq', None, self.type))
return api.get_all(context=context, session=session, filters=filters,
marker=marker, limit=limit, sort=sort,
latest=latest)

34
glare/objects/all.py Normal file
View File

@ -0,0 +1,34 @@
# Copyright (c) 2016 Mirantis, 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.
from oslo_versionedobjects import fields
from glare.objects import base
from glare.objects.meta import attribute
Field = attribute.Attribute.init
class All(base.ReadOnlyMixin, base.BaseArtifact):
"""Artifact type that allows to get artifacts regardless of their type"""
fields = {
'type_name': Field(fields.StringField,
description="Name of artifact type."),
}
@classmethod
def get_type_name(cls):
return "all"

View File

@ -1200,3 +1200,57 @@ class BaseArtifact(base.VersionedObject):
'required': ['name']}
return schemas
class ReadOnlyMixin(object):
"""Mixin that disables all modifying actions on artifacts."""
@classmethod
def create(cls, context, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def update(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def get_action_for_updates(cls, context, artifact, updates, registry):
raise exception.Forbidden("This type is read only.")
@classmethod
def delete(cls, context, af):
raise exception.Forbidden("This type is read only.")
@classmethod
def activate(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def reactivate(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def deactivate(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def publish(cls, context, af, values):
raise exception.Forbidden("This type is read only.")
@classmethod
def upload_blob(cls, context, af, field_name, fd, content_type):
raise exception.Forbidden("This type is read only.")
@classmethod
def upload_blob_dict(cls, context, af, field_name, blob_key, fd,
content_type):
raise exception.Forbidden("This type is read only.")
@classmethod
def add_blob_location(cls, context, af, field_name, location, blob_meta):
raise exception.Forbidden("This type is read only.")
@classmethod
def add_blob_dict_location(cls, context, af, field_name,
blob_key, location, blob_meta):
raise exception.Forbidden("This type is read only.")

View File

@ -103,7 +103,7 @@ class ArtifactRegistry(vo_base.VersionedObjectRegistry):
supported_types = []
for module in modules:
supported_types.extend(get_subclasses(module, base.BaseArtifact))
for type_name in CONF.glare.enabled_artifact_types:
for type_name in set(CONF.glare.enabled_artifact_types + ['all']):
for af_type in supported_types:
if type_name == af_type.get_type_name():
cls._validate_artifact_type(af_type)

View File

@ -0,0 +1,183 @@
# Copyright (c) 2016 Mirantis, 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.
import uuid
from oslo_serialization import jsonutils
import requests
from glare.tests import functional
def sort_results(lst, target='name'):
return sorted(lst, key=lambda x: x[target])
class TestArtifact(functional.FunctionalTest):
enabled_types = (u'sample_artifact', u'images', u'heat_templates',
u'heat_environments', u'tosca_templates',
u'murano_packages')
users = {
'user1': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'user2': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'admin': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'admin'
},
'anonymous': {
'id': None,
'tenant_id': None,
'token': None,
'role': None
}
}
def setUp(self):
super(TestArtifact, self).setUp()
self.set_user('user1')
self.glare_server.deployment_flavor = 'noauth'
self.glare_server.enabled_artifact_types = ','.join(
self.enabled_types)
self.glare_server.custom_artifact_types_modules = (
'glare.tests.functional.sample_artifact')
self.start_servers(**self.__dict__.copy())
def tearDown(self):
self.stop_servers()
self._reset_database(self.glare_server.sql_connection)
super(TestArtifact, self).tearDown()
def _url(self, path):
if 'schemas' in path:
return 'http://127.0.0.1:%d%s' % (self.glare_port, path)
else:
return 'http://127.0.0.1:%d/artifacts%s' % (self.glare_port, path)
def set_user(self, username):
if username not in self.users:
raise KeyError
self.current_user = username
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': self.users[self.current_user]['token'],
'X-User-Id': self.users[self.current_user]['id'],
'X-Tenant-Id': self.users[self.current_user]['tenant_id'],
'X-Project-Id': self.users[self.current_user]['tenant_id'],
'X-Roles': self.users[self.current_user]['role'],
}
base_headers.update(custom_headers or {})
return base_headers
def create_artifact(self, data=None, status=201,
type_name='sample_artifact'):
return self.post('/' + type_name, data or {}, status=status)
def _check_artifact_method(self, method, url, data=None, status=200,
headers=None):
if not headers:
headers = self._headers()
else:
headers = self._headers(headers)
headers.setdefault("Content-Type", "application/json")
if 'application/json' in headers['Content-Type'] and data is not None:
data = jsonutils.dumps(data)
response = getattr(requests, method)(self._url(url), headers=headers,
data=data)
self.assertEqual(status, response.status_code, response.text)
if status >= 400:
return response.text
if ("application/json" in response.headers["content-type"] or
"application/schema+json" in response.headers["content-type"]):
return jsonutils.loads(response.text)
return response.text
def post(self, url, data=None, status=201, headers=None):
return self._check_artifact_method("post", url, data, status=status,
headers=headers)
def get(self, url, status=200, headers=None):
return self._check_artifact_method("get", url, status=status,
headers=headers)
def delete(self, url, status=204):
response = requests.delete(self._url(url), headers=self._headers())
self.assertEqual(status, response.status_code, response.text)
return response.text
def patch(self, url, data, status=200, headers=None):
if headers is None:
headers = {}
if 'Content-Type' not in headers:
headers.update({'Content-Type': 'application/json-patch+json'})
return self._check_artifact_method("patch", url, data, status=status,
headers=headers)
def put(self, url, data=None, status=200, headers=None):
return self._check_artifact_method("put", url, data, status=status,
headers=headers)
# the test cases below are written in accordance with use cases
# each test tries to cover separate use case in Glare
# all code inside each test tries to cover all operators and data
# involved in use case execution
# each tests represents part of artifact lifecycle
# so we can easily define where is the failed code
make_active = [{"op": "replace", "path": "/status", "value": "active"}]
def activate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_active, status=status)
self.set_user(cur_user)
return af
make_deactivated = [{"op": "replace", "path": "/status",
"value": "deactivated"}]
def deactivate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_deactivated, status=status)
self.set_user(cur_user)
return af
make_public = [{"op": "replace", "path": "/visibility", "value": "public"}]
def publish_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_public, status=status)
self.set_user(cur_user)
return af

View File

@ -0,0 +1,96 @@
# Copyright (c) 2016 Mirantis, 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.
from glare.tests.functional import base
class TestAll(base.TestArtifact):
def test_all(self):
for type_name in self.enabled_types:
if type_name == 'all':
continue
for i in range(3):
for j in range(3):
self.create_artifact(
data={'name': '%s_%d' % (type_name, i),
'version': '%d' % j,
'tags': ['tag%s' % i]},
type_name=type_name)
# get all possible artifacts
url = '/all?sort=name:asc&limit=100'
res = self.get(url=url, status=200)['all']
from pprint import pformat
self.assertEqual(54, len(res), pformat(res))
# get artifacts with latest versions
url = '/all?version=latest&sort=name:asc'
res = self.get(url=url, status=200)['all']
self.assertEqual(18, len(res))
for art in res:
self.assertEqual('2.0.0', art['version'])
# get images only
url = '/all?type_name=images&sort=name:asc'
res = self.get(url=url, status=200)['all']
self.assertEqual(9, len(res))
for art in res:
self.assertEqual('images', art['type_name'])
# get images and heat_templates
url = '/all?type_name=in:images,heat_templates&sort=name:asc'
res = self.get(url=url, status=200)['all']
self.assertEqual(18, len(res))
for art in res:
self.assertIn(art['type_name'], ('images', 'heat_templates'))
def test_all_readonlyness(self):
self.create_artifact(data={'name': 'all'}, type_name='all', status=403)
art = self.create_artifact(data={'name': 'image'}, type_name='images')
url = '/all/%s' % art['id']
headers = {'Content-Type': 'application/octet-stream'}
# upload to 'all' is forbidden
self.put(url=url + '/icon', data='data', status=403,
headers=headers)
# update 'all' is forbidden
data = [{
"op": "replace",
"path": "/description",
"value": "text"
}]
self.patch(url=url, data=data, status=403)
# activation is forbidden
data = [{
"op": "replace",
"path": "/status",
"value": "active"
}]
self.patch(url=url, data=data, status=403)
# publishing is forbidden
data = [{
"op": "replace",
"path": "/visibility",
"value": "public"
}]
self.patch(url=url, data=data, status=403)
# get is okay
new_art = self.get(url=url)
self.assertEqual(new_art['id'], art['id'])

View File

@ -17,168 +17,15 @@ import hashlib
import uuid
from oslo_serialization import jsonutils
import requests
from glare.tests import functional
from glare.tests.functional import base
def sort_results(lst, target='name'):
return sorted(lst, key=lambda x: x[target])
class TestArtifact(functional.FunctionalTest):
users = {
'user1': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'user2': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'member'
},
'admin': {
'id': str(uuid.uuid4()),
'tenant_id': str(uuid.uuid4()),
'token': str(uuid.uuid4()),
'role': 'admin'
},
'anonymous': {
'id': None,
'tenant_id': None,
'token': None,
'role': None
}
}
def setUp(self):
super(TestArtifact, self).setUp()
self.set_user('user1')
self.glare_server.deployment_flavor = 'noauth'
self.glare_server.enabled_artifact_types = 'sample_artifact'
self.glare_server.custom_artifact_types_modules = (
'glare.tests.functional.sample_artifact')
self.start_servers(**self.__dict__.copy())
def tearDown(self):
self.stop_servers()
self._reset_database(self.glare_server.sql_connection)
super(TestArtifact, self).tearDown()
def _url(self, path):
if 'schemas' in path:
return 'http://127.0.0.1:%d%s' % (self.glare_port, path)
else:
return 'http://127.0.0.1:%d/artifacts%s' % (self.glare_port, path)
def set_user(self, username):
if username not in self.users:
raise KeyError
self.current_user = username
def _headers(self, custom_headers=None):
base_headers = {
'X-Identity-Status': 'Confirmed',
'X-Auth-Token': self.users[self.current_user]['token'],
'X-User-Id': self.users[self.current_user]['id'],
'X-Tenant-Id': self.users[self.current_user]['tenant_id'],
'X-Project-Id': self.users[self.current_user]['tenant_id'],
'X-Roles': self.users[self.current_user]['role'],
}
base_headers.update(custom_headers or {})
return base_headers
def create_artifact(self, data=None, status=201):
return self.post('/sample_artifact', data or {}, status=status)
def _check_artifact_method(self, method, url, data=None, status=200,
headers=None):
if not headers:
headers = self._headers()
else:
headers = self._headers(headers)
headers.setdefault("Content-Type", "application/json")
if 'application/json' in headers['Content-Type'] and data is not None:
data = jsonutils.dumps(data)
response = getattr(requests, method)(self._url(url), headers=headers,
data=data)
self.assertEqual(status, response.status_code, response.text)
if status >= 400:
return response.text
if ("application/json" in response.headers["content-type"] or
"application/schema+json" in response.headers["content-type"]):
return jsonutils.loads(response.text)
return response.text
def post(self, url, data=None, status=201, headers=None):
return self._check_artifact_method("post", url, data, status=status,
headers=headers)
def get(self, url, status=200, headers=None):
return self._check_artifact_method("get", url, status=status,
headers=headers)
def delete(self, url, status=204):
response = requests.delete(self._url(url), headers=self._headers())
self.assertEqual(status, response.status_code, response.text)
return response.text
def patch(self, url, data, status=200, headers=None):
if headers is None:
headers = {}
if 'Content-Type' not in headers:
headers.update({'Content-Type': 'application/json-patch+json'})
return self._check_artifact_method("patch", url, data, status=status,
headers=headers)
def put(self, url, data=None, status=200, headers=None):
return self._check_artifact_method("put", url, data, status=status,
headers=headers)
# the test cases below are written in accordance with use cases
# each test tries to cover separate use case in Glare
# all code inside each test tries to cover all operators and data
# involved in use case execution
# each tests represents part of artifact lifecycle
# so we can easily define where is the failed code
make_active = [{"op": "replace", "path": "/status", "value": "active"}]
def activate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_active, status=status)
self.set_user(cur_user)
return af
make_deactivated = [{"op": "replace", "path": "/status",
"value": "deactivated"}]
def deactivate_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_deactivated, status=status)
self.set_user(cur_user)
return af
make_public = [{"op": "replace", "path": "/visibility", "value": "public"}]
def publish_with_admin(self, artifact_id, status=200):
cur_user = self.current_user
self.set_user('admin')
url = '/sample_artifact/%s' % artifact_id
af = self.patch(url=url, data=self.make_public, status=status)
self.set_user(cur_user)
return af
class TestList(TestArtifact):
class TestList(base.TestArtifact):
def test_list_marker_and_limit(self):
# Create artifacts
art_list = [self.create_artifact({'name': 'name%s' % i,
@ -806,7 +653,7 @@ class TestList(TestArtifact):
self.assertEqual(response_url, result['first'])
class TestBlobs(TestArtifact):
class TestBlobs(base.TestArtifact):
def test_blob_dicts(self):
# Getting empty artifact list
url = '/sample_artifact'
@ -1005,7 +852,7 @@ class TestBlobs(TestArtifact):
status=400, headers=headers)
class TestTags(TestArtifact):
class TestTags(base.TestArtifact):
def test_tags(self):
# Create artifact
art = self.create_artifact({'name': 'name5',
@ -1064,7 +911,7 @@ class TestTags(TestArtifact):
self.patch(url=url, data=patch, status=400)
class TestArtifactOps(TestArtifact):
class TestArtifactOps(base.TestArtifact):
def test_create(self):
"""All tests related to artifact creation"""
# check that cannot create artifact for non-existent artifact type
@ -1346,7 +1193,7 @@ class TestArtifactOps(TestArtifact):
self.assertEqual("active", deactive_art["status"])
class TestUpdate(TestArtifact):
class TestUpdate(base.TestArtifact):
def test_update_artifact_before_activate(self):
"""Test updates for artifact before activation"""
# create artifact to update
@ -2262,7 +2109,7 @@ class TestUpdate(TestArtifact):
self.patch(url=url, data=data, status=400)
class TestDependencies(TestArtifact):
class TestDependencies(base.TestArtifact):
def test_manage_dependencies(self):
some_af = self.create_artifact(data={"name": "test_af"})
dep_af = self.create_artifact(data={"name": "test_dep_af"})

View File

@ -15,11 +15,8 @@
import jsonschema
from oslo_serialization import jsonutils
import requests
from glare.common import utils
from glare.tests import functional
from glare.tests.functional import base
fixture_base_props = {
u'activated_at': {
@ -231,10 +228,6 @@ fixture_base_props = {
u'type': u'string'}
}
enabled_artifact_types = (
u'sample_artifact', u'images', u'heat_templates',
u'heat_environments', u'tosca_templates', u'murano_packages')
def generate_type_props(props):
props.update(fixture_base_props)
@ -976,61 +969,38 @@ fixtures = {
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type heat_environments of version 1.0',
u'type': u'object'},
u'all': {
u'name': u'all',
u'properties': generate_type_props({
u'type_name': {u'description': u'Name of artifact type.',
u'filter_ops': [u'eq', u'neq', u'in'],
u'maxLength': 255,
u'type': [u'string', u'null']},
}),
u'required': [u'name'],
u'version': u'1.0',
u'title': u'Artifact type all of version 1.0',
u'type': u'object'}
}
class TestSchemas(functional.FunctionalTest):
def setUp(self):
super(TestSchemas, self).setUp()
self.glare_server.deployment_flavor = 'noauth'
self.glare_server.enabled_artifact_types = ','.join(
enabled_artifact_types)
self.glare_server.custom_artifact_types_modules = (
'glare.tests.functional.sample_artifact')
self.start_servers(**self.__dict__.copy())
def tearDown(self):
self.stop_servers()
self._reset_database(self.glare_server.sql_connection)
super(TestSchemas, self).tearDown()
def _url(self, path):
return 'http://127.0.0.1:%d%s' % (self.glare_port, path)
def _check_artifact_method(self, url, status=200):
headers = {
'X-Identity-Status': 'Confirmed',
}
response = requests.get(self._url(url), headers=headers)
self.assertEqual(status, response.status_code, response.text)
if status >= 400:
return response.text
if ("application/json" in response.headers["content-type"] or
"application/schema+json" in response.headers["content-type"]):
return jsonutils.loads(response.text)
return response.text
def get(self, url, status=200, headers=None):
return self._check_artifact_method(url, status=status)
class TestSchemas(base.TestArtifact):
def test_schemas(self):
# Get list schemas of artifacts
result = self.get(url='/schemas')
self.assertEqual(fixtures, result['schemas'], utils.DictDiffer(
result['schemas'], fixtures))
# Get schemas for specific artifact type
for at in enabled_artifact_types:
for at in self.enabled_types:
result = self.get(url='/schemas/%s' % at)
self.assertEqual(fixtures[at], result['schemas'][at],
utils.DictDiffer(
result['schemas'][at]['properties'],
fixtures[at]['properties']))
# Get list schemas of artifacts
result = self.get(url='/schemas')
self.assertEqual(fixtures, result['schemas'], utils.DictDiffer(
result['schemas'], fixtures))
# Get schema of sample_artifact
result = self.get(url='/schemas/sample_artifact')
self.assertEqual(fixtures['sample_artifact'],