Implement category management API
Adds new API calls, responsible for add, browse and delete categories. Implements blueprint enable-category-management Change-Id: I9da0680cfa244ef225be0706a54f492644c0dcba
This commit is contained in:
parent
42c320a085
commit
2c23f73e72
@ -25,6 +25,7 @@ General information
|
||||
Murano Service API is a programmatic interface used for interaction with
|
||||
Murano. Other interaction mechanisms like Murano Dashboard or Murano CLI
|
||||
should use API as underlying protocol for interaction.
|
||||
|
||||
* **Allowed HTTPs requests**
|
||||
|
||||
* *POST* : To create a resource
|
||||
|
@ -235,6 +235,10 @@ See the full specification `here <http://tools.ietf.org/html/rfc6902>`_.
|
||||
|
||||
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
|
||||
**Content type**
|
||||
|
||||
application/murano-packages-json-patch
|
||||
|
||||
Allowed operations:
|
||||
|
||||
::
|
||||
@ -305,7 +309,7 @@ Delete application definition from the catalog
|
||||
|
||||
**Parameters**
|
||||
|
||||
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete
|
||||
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete
|
||||
|
||||
**Response 404**
|
||||
|
||||
@ -321,7 +325,7 @@ Get application definition package
|
||||
|
||||
**Parameters**
|
||||
|
||||
* id (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
|
||||
**Response 200 (application/octetstream)**
|
||||
|
||||
@ -341,7 +345,7 @@ Retrieve UI definition for a application which described in a package with provi
|
||||
|
||||
**Parameters**
|
||||
|
||||
* id (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
* ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
|
||||
**Response 200 (application/octet-stream)**
|
||||
|
||||
@ -369,7 +373,7 @@ Retrieve application logo which described in a package with provided id
|
||||
|
||||
**Parameters**
|
||||
|
||||
id (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
``id`` (required) Hexadecimal `id` (or fully qualified name) of the package
|
||||
|
||||
**Response 200 (application/octet-stream)**
|
||||
|
||||
@ -377,26 +381,208 @@ The sequence of bytes representing application logo
|
||||
|
||||
**Response 403**
|
||||
|
||||
Specified package is not public and not owned by user tenant, performing the request
|
||||
Specified package is not public and not owned by user tenant,
|
||||
performing the request
|
||||
|
||||
**Response 404**
|
||||
|
||||
Specified package is not public and not owned by user tenant, performing the request
|
||||
Specified package is not public and not owned by user tenant,
|
||||
performing the request
|
||||
|
||||
Categories
|
||||
==========
|
||||
|
||||
Provides category management. Categories are used in the Application Catalog
|
||||
to group application for easy browsing and search.
|
||||
|
||||
List categories
|
||||
---------------
|
||||
|
||||
`/v1/catalog/packages/categories [GET]`
|
||||
* `/v1/catalog/packages/categories [GET]`
|
||||
|
||||
Retrieve list of all available application categories
|
||||
!DEPRECATED (Plan to remove in L release) Retrieve list of all available application categories
|
||||
|
||||
**Response 200 (application/json)**
|
||||
|
||||
A list, containing category names
|
||||
|
||||
*Content-Type*
|
||||
application/json
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"categories": ["Web service", "Directory", "Database", "Storage"]
|
||||
}
|
||||
|
||||
|
||||
* `/v1/catalog/categories [GET]`
|
||||
|
||||
+----------+----------------------------------+----------------------------------+
|
||||
| Method | URI | Description |
|
||||
+==========+==================================+==================================+
|
||||
| GET | /catalog/categories | Get list of existing categories |
|
||||
+----------+----------------------------------+----------------------------------+
|
||||
|
||||
|
||||
Retrieve list of all available application categories
|
||||
|
||||
**Response 200 (application/json)**
|
||||
|
||||
A list, containing detailed information about each category
|
||||
|
||||
*Content-Type*
|
||||
application/json
|
||||
|
||||
::
|
||||
|
||||
{"categories": [
|
||||
{
|
||||
"id": "0420045dce7445fabae7e5e61fff9e2f",
|
||||
"updated": "2014-12-26T13:57:04",
|
||||
"name": "Web",
|
||||
"created": "2014-12-26T13:57:04"
|
||||
},
|
||||
{
|
||||
"id": "3dd486b1e26f40ac8f35416b63f52042",
|
||||
"updated": "2014-12-26T13:57:04",
|
||||
"name": "Databases",
|
||||
"created": "2014-12-26T13:57:04"
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
|
||||
Get category details
|
||||
--------------------
|
||||
|
||||
`/catalog/categories/<category_id> [GET]`
|
||||
|
||||
Return detailed information for a provided category
|
||||
|
||||
*Request*
|
||||
|
||||
+----------+-----------------------------------+----------------------------------+
|
||||
| Method | URI | Description |
|
||||
+==========+===================================+==================================+
|
||||
| GET | /catalog/categories/<category_id> | Get category detail |
|
||||
+----------+-----------------------------------+----------------------------------+
|
||||
|
||||
*Parameters*
|
||||
|
||||
* ``category_id`` - required, category ID, required
|
||||
|
||||
*Response*
|
||||
|
||||
*Content-Type*
|
||||
application/json
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"id": "b308f7fa8a2f4a5eb419970c827f4466",
|
||||
"updated": "2015-01-28T17:00:19",
|
||||
"packages": [
|
||||
{
|
||||
"fully_qualified_name": "io.murano.apps.ZabbixServer",
|
||||
"id": "4dfb566e69e6445fbd4aea5099fe95e9",
|
||||
"name": "Zabbix Server"
|
||||
}
|
||||
],
|
||||
"name": "Web",
|
||||
"created": "2015-01-28T17:00:19"
|
||||
}
|
||||
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| Code | Description |
|
||||
+================+===========================================================+
|
||||
| 200 | OK. Category deleted successfully |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 401 | User is not authorized to access this session |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 404 | Not found. Specified category doesn`t exist |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
|
||||
Add new category
|
||||
----------------
|
||||
|
||||
`/catalog/categories [POST]`
|
||||
|
||||
Add new category to the Application Catalog
|
||||
|
||||
*Parameters*
|
||||
|
||||
+----------------------+------------+--------------------------------------------------------+
|
||||
| Attribute | Type | Description |
|
||||
+======================+============+========================================================+
|
||||
| name | string | Environment name; only alphanumeric characters and '-' |
|
||||
+----------------------+------------+--------------------------------------------------------+
|
||||
|
||||
*Request*
|
||||
|
||||
+----------+----------------------------------+----------------------------------+
|
||||
| Method | URI | Description |
|
||||
+==========+==================================+==================================+
|
||||
| POST | /catalog/categories | Create new category |
|
||||
+----------+----------------------------------+----------------------------------+
|
||||
|
||||
*Content-Type*
|
||||
application/json
|
||||
|
||||
*Example*
|
||||
{"name": "category_name"}
|
||||
|
||||
*Response*
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"id": "ce373a477f211e187a55404a662f968",
|
||||
"name": "category_name",
|
||||
"created": "2013-11-30T03:23:42Z",
|
||||
"updated": "2013-11-30T03:23:44Z",
|
||||
}
|
||||
|
||||
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| Code | Description |
|
||||
+================+===========================================================+
|
||||
| 200 | OK. Category created successfully |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 401 | User is not authorized to access this session |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 409 | Conflict. Category with specified name already exist |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
|
||||
|
||||
Delete category
|
||||
---------------
|
||||
|
||||
`/catalog/categories [DELETE]`
|
||||
|
||||
*Request*
|
||||
|
||||
+----------+-----------------------------------+-----------------------------------+
|
||||
| Method | URI | Description |
|
||||
+==========+===================================+===================================+
|
||||
| DELETE | /catalog/categories/<category_id> | Delete category with specified id |
|
||||
+----------+-----------------------------------+-----------------------------------+
|
||||
|
||||
*Parameters:*
|
||||
|
||||
* ``category_id`` - required, category ID, required
|
||||
|
||||
*Response*
|
||||
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| Code | Description |
|
||||
+================+===========================================================+
|
||||
| 200 | OK. Category deleted successfully |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 401 | User is not authorized to access this session |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 404 | Not found. Specified category doesn`t exist |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
| 403 | Forbidden. Category with specified name is assigned to |
|
||||
| | the package, presented in the catalog |
|
||||
+----------------+-----------------------------------------------------------+
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
"update_package": "rule:admin_api",
|
||||
"upload_package": "rule:admin_api",
|
||||
"delete_package": "rule:admin_api"
|
||||
"delete_package": "rule:admin_api",
|
||||
"delete_category": "rule:admin_api",
|
||||
"add_category": "rule:admin_api"
|
||||
}
|
||||
|
||||
|
@ -268,12 +268,45 @@ class Controller(object):
|
||||
|
||||
db_api.package_delete(package_id, req.context)
|
||||
|
||||
def get_category(self, req, category_id):
|
||||
policy.check("get_category", req.context)
|
||||
category = db_api.category_get(category_id, packages=True)
|
||||
return category.to_dict()
|
||||
|
||||
def show_categories(self, req):
|
||||
policy.check("show_categories", req.context)
|
||||
|
||||
categories = db_api.categories_list()
|
||||
return {'categories': [category.name for category in categories]}
|
||||
|
||||
def list_categories(self, req):
|
||||
policy.check("list_categories", req.context)
|
||||
categories = db_api.categories_list()
|
||||
return {'categories': [category.to_dict() for category in categories]}
|
||||
|
||||
def add_category(self, req, body=None):
|
||||
policy.check("add_category", req.context)
|
||||
|
||||
if not body.get('name'):
|
||||
raise exc.HTTPBadRequest(
|
||||
explanation='Please, specify a name of the category to create')
|
||||
try:
|
||||
category = db_api.category_add(body['name'])
|
||||
except db_exc.DBDuplicateEntry:
|
||||
msg = _('Category with specified name is already exist')
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPConflict(explanation=msg)
|
||||
return category.to_dict()
|
||||
|
||||
def delete_category(self, req, category_id):
|
||||
target = {'category_id': category_id}
|
||||
policy.check("delete_category", req.context, target)
|
||||
category = db_api.category_get(category_id, packages=True)
|
||||
if category.packages:
|
||||
msg = _("It's impossible to delete categories assigned"
|
||||
" to the package, uploaded to the catalog")
|
||||
raise exc.HTTPForbidden(explanation=msg)
|
||||
db_api.category_delete(category_id)
|
||||
|
||||
|
||||
class PackageSerializer(wsgi.ResponseSerializer):
|
||||
def serialize(self, action_result, accept, action):
|
||||
|
@ -235,7 +235,22 @@ class API(wsgi.Router):
|
||||
controller=catalog_resource,
|
||||
action='download',
|
||||
conditions={'method': ['GET']})
|
||||
|
||||
mapper.connect('/catalog/categories',
|
||||
controller=catalog_resource,
|
||||
action='list_categories',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/catalog/categories/{category_id}',
|
||||
controller=catalog_resource,
|
||||
action='get_category',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/catalog/categories',
|
||||
controller=catalog_resource,
|
||||
action='add_category',
|
||||
conditions={'method': ['POST']})
|
||||
mapper.connect('/catalog/categories/{category_id}',
|
||||
controller=catalog_resource,
|
||||
action='delete_category',
|
||||
conditions={'method': ['DELETE']})
|
||||
req_stats_resource = request_statistics.create_resource()
|
||||
mapper.connect('/stats',
|
||||
controller=req_stats_resource,
|
||||
|
@ -200,6 +200,27 @@ def _do_remove(package, change):
|
||||
return package
|
||||
|
||||
|
||||
def _get_packages_for_category(session, category_id):
|
||||
"""Return detailed list of packages, belonging to the provided category
|
||||
:param session:
|
||||
:param category_id:
|
||||
:return: list of dictionaries, containing id, name and package frn
|
||||
"""
|
||||
pkg = models.Package
|
||||
packages = (session.query(pkg.id, pkg.name, pkg.fully_qualified_name)
|
||||
.filter(pkg.categories
|
||||
.any(models.Category.id == category_id))
|
||||
.all())
|
||||
|
||||
result_packages = []
|
||||
for package in packages:
|
||||
id, name, fqn = package
|
||||
result_packages.append({'id': id,
|
||||
'name': name,
|
||||
'fully_qualified_name': fqn})
|
||||
return result_packages
|
||||
|
||||
|
||||
def package_update(pkg_id_or_name, changes, context):
|
||||
"""Update package information
|
||||
:param changes: parameters to update
|
||||
@ -357,6 +378,24 @@ def package_delete(package_id_or_name, context):
|
||||
session.delete(package)
|
||||
|
||||
|
||||
def category_get(category_id, session=None, packages=False):
|
||||
"""Return category details
|
||||
:param category_id: ID of a category, string
|
||||
:returns: detailed information about category, dict
|
||||
"""
|
||||
if not session:
|
||||
session = db_session.get_session()
|
||||
|
||||
category = session.query(models.Category).get(category_id)
|
||||
if not category:
|
||||
msg = _("Category id or '{0}' not found").format(category_id)
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPNotFound(msg)
|
||||
if packages:
|
||||
category.packages = _get_packages_for_category(session, category_id)
|
||||
return category
|
||||
|
||||
|
||||
def categories_list():
|
||||
session = db_session.get_session()
|
||||
return session.query(models.Category).all()
|
||||
@ -380,3 +419,16 @@ def category_add(category_name):
|
||||
category.save(session)
|
||||
|
||||
return category
|
||||
|
||||
|
||||
def category_delete(category_id):
|
||||
"""Delete a category by ID."""
|
||||
session = db_session.get_session()
|
||||
|
||||
with session.begin():
|
||||
category = session.query(models.Category).get(category_id)
|
||||
if not category:
|
||||
msg = _("Category id '{0}' not found").format(category_id)
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPNotFound(msg)
|
||||
session.delete(category)
|
||||
|
@ -285,6 +285,10 @@ class Category(Base, TimestampMixin):
|
||||
default=uuidutils.generate_uuid)
|
||||
name = sa.Column(sa.String(80), nullable=False, index=True, unique=True)
|
||||
|
||||
def to_dict(self):
|
||||
dictionary = super(Category, self).to_dict()
|
||||
return dictionary
|
||||
|
||||
|
||||
class Tag(Base, TimestampMixin):
|
||||
"""Represents tags in the datastore."""
|
||||
|
@ -16,15 +16,19 @@
|
||||
import cgi
|
||||
import cStringIO
|
||||
import imghdr
|
||||
import json
|
||||
import os
|
||||
|
||||
import mock
|
||||
from oslo.utils import timeutils
|
||||
|
||||
from murano.api.v1 import catalog
|
||||
from murano.common import policy
|
||||
from murano.db.catalog import api as db_catalog_api
|
||||
from murano.db import models
|
||||
from murano.packages import load_utils
|
||||
import murano.tests.unit.api.base as test_base
|
||||
import murano.tests.unit.utils as test_utils
|
||||
|
||||
|
||||
class TestCatalogApi(test_base.ControllerTest, test_base.MuranoApiTestCase):
|
||||
@ -132,3 +136,64 @@ Content-Type: application/json
|
||||
content_type='multipart/form-data; ; boundary=BOUNDARY',
|
||||
params={"is_public": "true"})
|
||||
res = req.get_response(self.api)
|
||||
self.assertEqual(403, res.status_code)
|
||||
|
||||
def test_add_category(self):
|
||||
"""Check that category added successfully
|
||||
"""
|
||||
|
||||
self._set_policy_rules({'add_category': '@'})
|
||||
self.expect_policy_check('add_category')
|
||||
|
||||
fake_now = timeutils.utcnow()
|
||||
timeutils.utcnow.override_time = fake_now
|
||||
|
||||
expected = {'name': 'new_category',
|
||||
'created': timeutils.isotime(fake_now)[:-1],
|
||||
'updated': timeutils.isotime(fake_now)[:-1]}
|
||||
|
||||
body = {'name': 'new_category'}
|
||||
req = self._post('/catalog/categories', json.dumps(body))
|
||||
result = req.get_response(self.api)
|
||||
processed_result = json.loads(result.body)
|
||||
self.assertIn('id', processed_result.keys())
|
||||
expected['id'] = processed_result['id']
|
||||
self.assertDictEqual(expected, processed_result)
|
||||
|
||||
def test_delete_category(self):
|
||||
"""Check that category deleted successfully
|
||||
"""
|
||||
|
||||
self._set_policy_rules({'delete_category': '@'})
|
||||
self.expect_policy_check('delete_category',
|
||||
{'category_id': '12345'})
|
||||
|
||||
fake_now = timeutils.utcnow()
|
||||
expected = {'name': 'new_category',
|
||||
'created': fake_now,
|
||||
'updated': fake_now,
|
||||
'id': '12345'}
|
||||
|
||||
e = models.Category(**expected)
|
||||
test_utils.save_models(e)
|
||||
|
||||
req = self._delete('/catalog/categories/12345')
|
||||
processed_result = req.get_response(self.api)
|
||||
self.assertEqual('', processed_result.body)
|
||||
self.assertEqual(200, processed_result.status_code)
|
||||
|
||||
def test_add_category_failed_for_non_admin(self):
|
||||
"""Check that non admin user couldn't add new category
|
||||
"""
|
||||
|
||||
self._set_policy_rules({'add_category': 'role:context_admin'})
|
||||
self.is_admin = False
|
||||
self.expect_policy_check('add_category')
|
||||
|
||||
fake_now = timeutils.utcnow()
|
||||
timeutils.utcnow.override_time = fake_now
|
||||
|
||||
body = {'name': 'new_category'}
|
||||
req = self._post('/catalog/categories', json.dumps(body))
|
||||
result = req.get_response(self.api)
|
||||
self.assertEqual(403, result.status_code)
|
||||
|
Loading…
Reference in New Issue
Block a user