Implement project tags logic into manager

This change adds the tags attribute of project into the resource
manager. This change builds off of the backend logic.

Change-Id: Ie988b7a2065f0ecdb8ec953a1d01f5e3cbd67a1b
Partially-Implements: bp project-tags
Co-Authored-By: Jaewoo Park <jp655p@att.com>
Co-Authored-By: Nicolas Helgeson <nh202b@att.com>
Depends-On: I00f094a5584be40ab477cbf680a5f6d1afb4d21b
Depends-On: I5f8e4a53089b9fcc38084bb958d09f63ccc59d2a
This commit is contained in:
Gage Hugo 2017-08-30 17:47:30 -05:00
parent 6d320f75fd
commit ee900029db
3 changed files with 190 additions and 0 deletions

View File

@ -397,6 +397,10 @@ class ProjectNotFound(NotFound):
message_format = _("Could not find project: %(project_id)s.")
class ProjectTagNotFound(NotFound):
message_format = _("Could not find project tag: %(project_tag)s.")
class TokenNotFound(NotFound):
message_format = _("Could not find token: %(token_id)s.")

View File

@ -34,6 +34,8 @@ CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
MEMOIZE = cache.get_memoization_decorator(group='resource')
TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any')
@dependency.provider('resource_api')
@dependency.requires('assignment_api', 'credential_api', 'domain_config_api',
@ -50,6 +52,7 @@ class Manager(manager.Manager):
_DOMAIN = 'domain'
_PROJECT = 'project'
_PROJECT_TAG = 'project tag'
def __init__(self):
# NOTE(morgan): The resource driver must be SQL. This is because there
@ -819,6 +822,18 @@ class Manager(manager.Manager):
@manager.response_truncated
def list_projects(self, hints=None):
if hints:
tag_filters = {}
# Handle project tag filters separately
for f in list(hints.filters):
if f['name'] in TAG_SEARCH_FILTERS:
tag_filters[f['name']] = f['value']
hints.filters.remove(f)
if tag_filters:
tag_refs = self.driver.list_projects_by_tags(tag_filters)
project_refs = self.driver.list_projects(hints)
ref_ids = [ref['id'] for ref in tag_refs]
return [ref for ref in project_refs if ref['id'] in ref_ids]
return self.driver.list_projects(hints or driver_hints.Hints())
# NOTE(henry-nash): list_projects_in_domain is actually an internal method
@ -881,6 +896,78 @@ class Manager(manager.Manager):
if new_ref['domain_id'] != orig_ref['domain_id']:
raise exception.ValidationError(_('Cannot change Domain ID'))
def create_project_tag(self, project_id, tag, initiator=None):
"""Create a new tag on project.
:param project_id: ID of a project to create a tag for
:param tag: The string value of a tag to add
:returns: The value of the created tag
"""
project = self.driver.get_project(project_id)
tag_name = tag.strip()
project['tags'].append(tag_name)
self.update_project(project_id, {'tags': project['tags']})
notifications.Audit.created(
self._PROJECT_TAG, tag_name, initiator)
return tag_name
def get_project_tag(self, project_id, tag_name):
"""Return information for a single tag on a project.
:param project_id: ID of a project to retrive a tag from
:param tag_name: Name of a tag to return
:raises keystone.exception.ProjectTagNotFound: If the tag name
does not exist on the project
:returns: The tag value
"""
project = self.driver.get_project(project_id)
if tag_name not in project.get('tags'):
raise exception.ProjectTagNotFound(project_tag=tag_name)
return tag_name
def list_project_tags(self, project_id):
"""List all tags on project.
:param project_id: The ID of a project
:returns: A list of tags from a project
"""
project = self.driver.get_project(project_id)
return project.get('tags', [])
def update_project_tags(self, project_id, tags, initiator=None):
"""Update all tags on a project.
:param project_id: The ID of the project to update
:param tags: A list of tags to update on the project
:returns: A list of tags
"""
self.driver.get_project(project_id)
tag_list = [t.strip() for t in tags]
project = {'tags': tag_list}
self.update_project(project_id, project)
return tag_list
def delete_project_tag(self, project_id, tag):
"""Delete single tag from project.
:param project_id: The ID of the project
:param tag: The tag value to delete
:raises keystone.exception.ProjectTagNotFound: If the tag name
does not exist on the project
"""
project = self.driver.get_project(project_id)
try:
project['tags'].remove(tag)
except ValueError:
raise exception.ProjectTagNotFound(project_tag=tag)
self.update_project(project_id, project)
notifications.Audit.deleted(self._PROJECT_TAG, tag)
MEMOIZE_CONFIG = cache.get_memoization_decorator(group='domain_config')

View File

@ -1538,6 +1538,105 @@ class ResourceTests(object):
user = self.identity_api.get_user(user['id'])
self.assertNotIn('default_project_id', user)
def _create_project_and_tags(self, num_of_tags=1):
"""Create a project and tags associated to that project.
:param num_of_tags: the desired number of tags attached to a
project, default is 1.
:returns: A tuple of a new project and a list of random tags
"""
tags = [uuid.uuid4().hex for i in range(num_of_tags)]
ref = unit.new_project_ref(
domain_id=CONF.identity.default_domain_id, tags=tags)
project = self.resource_api.create_project(ref['id'], ref)
return project, tags
def test_create_project_with_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
tag_ref = self.resource_api.get_project_tag(project['id'], tags[0])
self.assertEqual(tags[0], tag_ref)
def test_get_project_contains_tags(self):
project, _ = self._create_project_and_tags()
tag = uuid.uuid4().hex
self.resource_api.create_project_tag(project['id'], tag)
ref = self.resource_api.get_project(project['id'])
self.assertIn(tag, ref['tags'])
def test_list_project_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=1)
tag_ref = self.resource_api.list_project_tags(project['id'])
self.assertEqual(tags[0], tag_ref[0])
def test_list_project_tags_returns_not_found(self):
self.assertRaises(exception.ProjectNotFound,
self.resource_api.list_project_tags,
uuid.uuid4().hex)
def test_get_project_tag(self):
project, tags = self._create_project_and_tags()
tag_ref = self.resource_api.get_project_tag(project['id'], tags[0])
self.assertEqual(tags[0], tag_ref)
def test_create_project_tag_with_trailing_whitespace(self):
project, _ = self._create_project_and_tags()
tag = uuid.uuid4().hex + ' '
tag_ref = self.resource_api.create_project_tag(project['id'], tag)
self.assertEqual(tag.strip(), tag_ref)
def test_create_project_tag_is_case_sensitive(self):
project, _ = self._create_project_and_tags()
new_tags = ['aaa', 'AAA']
ref = self.resource_api.update_project_tags(project['id'], new_tags)
for tag in new_tags:
self.assertIn(tag, ref)
def test_update_project_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=2)
project_tag_ref = self.resource_api.list_project_tags(project['id'])
self.assertEqual(len(project_tag_ref), 2)
# Update project to only have one tag
tags = ['one']
self.resource_api.update_project_tags(project['id'], tags)
project_tag_ref = self.resource_api.list_project_tags(project['id'])
self.assertEqual(len(project_tag_ref), 1)
def test_update_project_tags_returns_not_found(self):
_, tags = self._create_project_and_tags(num_of_tags=2)
self.assertRaises(exception.ProjectNotFound,
self.resource_api.update_project_tags,
uuid.uuid4().hex,
tags)
def test_delete_tag_from_project(self):
project, tags = self._create_project_and_tags(num_of_tags=2)
tag_to_delete = tags[-1]
self.resource_api.delete_project_tag(project['id'], tag_to_delete)
project_tag_ref = self.resource_api.list_project_tags(
project['id'])
self.assertEqual(len(project_tag_ref), 1)
self.assertEqual(project_tag_ref[0], tags[0])
def test_delete_project_tag_returns_not_found(self):
self.assertRaises(exception.ProjectNotFound,
self.resource_api.delete_project_tag,
uuid.uuid4().hex,
uuid.uuid4().hex)
def test_delete_project_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
project_tag_ref = self.resource_api.list_project_tags(
project['id'])
self.assertEqual(len(project_tag_ref), 5)
self.resource_api.update_project_tags(project['id'], [])
project_tag_ref = self.resource_api.list_project_tags(project['id'])
self.assertEqual(project_tag_ref, [])
class ResourceDriverTests(object):
"""Test for the resource driver.