Implement project tags API controller and router

This change adds the new API routes for project tags as well as
the controller methods for listening for /v3/project/[id]/tags.

Change-Id: Ic2e5874a427341f2edb6e9122386fb98af2c24ef
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: Ieb68bd2c9c216b25ad74d320a1c9a297d2b251e7
This commit is contained in:
Gage Hugo 2017-08-31 11:05:57 -05:00
parent ee900029db
commit de788453d9
6 changed files with 452 additions and 0 deletions

View File

@ -35,6 +35,7 @@ class RequestContext(oslo_context.RequestContext):
self.username = kwargs.pop('username', None)
self.user_domain_name = kwargs.pop('user_domain_name', None)
self.project_domain_name = kwargs.pop('project_domain_name', None)
self.project_tag_name = kwargs.pop('project_tag_name', None)
self.is_delegated_auth = kwargs.pop('is_delegated_auth', False)

View File

@ -53,6 +53,7 @@ class Parameters(object):
ROLE_ID = build_v3_parameter_relation('role_id')
SERVICE_ID = build_v3_parameter_relation('service_id')
USER_ID = build_v3_parameter_relation('user_id')
TAG_VALUE = build_v3_parameter_relation('tag_value')
class Status(object):

View File

@ -190,6 +190,12 @@ class ProjectV3(controller.V3Controller):
# False (which in query terms means '0')
if 'is_domain' not in request.params:
hints.add_filter('is_domain', '0')
# If any tags filters are passed in when listing projects, add them
# to the hint filters
tag_params = ['tags', 'tags-any', 'not-tags', 'not-tags-any']
for t in tag_params:
if t in request.params:
hints.add_filter(t, request.params[t])
refs = self.resource_api.list_projects(hints=hints)
return ProjectV3.wrap_collection(request.context_dict,
refs, hints=hints)
@ -258,3 +264,69 @@ class ProjectV3(controller.V3Controller):
return self.resource_api.delete_project(
project_id,
initiator=request.audit_initiator)
@dependency.requires('resource_api')
class ProjectTagV3(controller.V3Controller):
collection_name = 'projects'
member_name = 'tags'
def __init__(self):
super(ProjectTagV3, self).__init__()
self.get_member_from_driver = self.resource_api.get_project_tag
@classmethod
def wrap_member(cls, context, ref):
# NOTE(gagehugo): Overriding this due to how the common controller
# expects the ref to have an id, which for tags it does not.
new_ref = {'links': {'self': cls.full_url(context)}}
new_ref[cls.member_name] = (ref or [])
return new_ref
@classmethod
def wrap_header(cls, context, query):
# NOTE(gagehugo: The API spec for tags has a specific guideline for
# what to return when adding a single tag. This wrapper handles
# returning the specified url in the header while the body is empty.
context['environment']['QUERY_STRING'] = '/' + query
url = cls.full_url(context)
headers = [('Location', url.replace('?', ''))]
status = (http_client.CREATED,
http_client.responses[http_client.CREATED])
return wsgi.render_response(status=status, headers=headers)
@controller.protected()
def create_project_tag(self, request, project_id, value):
validation.lazy_validate(schema.project_tag_create, value)
# Check if we will exceed the max number of tags on this project
tags = self.resource_api.list_project_tags(project_id)
tags.append(value)
validation.lazy_validate(schema.project_tags_update, tags)
self.resource_api.create_project_tag(
project_id, value, initiator=request.audit_initiator)
query = '/'.join((project_id, 'tags', value))
return ProjectTagV3.wrap_header(request.context_dict, query)
@controller.protected()
def get_project_tag(self, request, project_id, value):
self.resource_api.get_project_tag(project_id, value)
@controller.protected()
def delete_project_tag(self, request, project_id, value):
self.resource_api.delete_project_tag(project_id, value)
@controller.protected()
def list_project_tags(self, request, project_id):
ref = self.resource_api.list_project_tags(project_id)
return ProjectTagV3.wrap_member(request.context_dict, ref)
@controller.protected()
def update_project_tags(self, request, project_id, tags):
validation.lazy_validate(schema.project_tags_update, tags)
ref = self.resource_api.update_project_tags(
project_id, tags, initiator=request.audit_initiator)
return ProjectTagV3.wrap_member(request.context_dict, ref)
@controller.protected()
def delete_project_tags(self, request, project_id):
self.resource_api.update_project_tags(project_id, [])

View File

@ -30,6 +30,7 @@ class Routers(wsgi.RoutersBase):
resource_descriptions=self.v3_resources))
config_controller = controllers.DomainConfigV3()
tag_controller = controllers.ProjectTagV3()
self._add_resource(
mapper, config_controller,
@ -103,3 +104,28 @@ class Routers(wsgi.RoutersBase):
router.Router(controllers.ProjectV3(),
'projects', 'project',
resource_descriptions=self.v3_resources))
self._add_resource(
mapper, tag_controller,
path='/projects/{project_id}/tags',
get_head_action='list_project_tags',
put_action='update_project_tags',
delete_action='delete_project_tags',
rel=json_home.build_v3_resource_relation(
'project_tags'),
path_vars={
'project_id': json_home.Parameters.PROJECT_ID
})
self._add_resource(
mapper, tag_controller,
path='/projects/{project_id}/tags/{value}',
get_head_action='get_project_tag',
put_action='create_project_tag',
delete_action='delete_project_tag',
rel=json_home.build_v3_resource_relation(
'project_tags'),
path_vars={
'project_id': json_home.Parameters.PROJECT_ID,
'value': json_home.Parameters.TAG_VALUE
})

View File

@ -684,6 +684,131 @@ class ResourceTestCase(test_v3.RestfulTestCase,
return projects
def _create_project_and_tags(self, num_of_tags=1):
"""Create a project and a number of tags attached to that project.
:param num_of_tags: the desired number of tags created with a specified
project.
:returns: A tuple containing 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=self.domain_id,
tags=tags)
resp = self.post('/projects', body={'project': ref})
return resp.result['project'], tags
def test_list_projects_filtering_by_tags(self):
"""Call ``GET /projects?tags={tags}``."""
project, tags = self._create_project_and_tags(num_of_tags=2)
tag_string = ','.join(tags)
resp = self.get('/projects?tags=%(values)s' % {
'values': tag_string})
self.assertValidProjectListResponse(resp)
self.assertEqual(project['id'], resp.result['projects'][0]['id'])
def test_list_projects_filtering_by_tags_any(self):
"""Call ``GET /projects?tags-any={tags}``."""
project, tags = self._create_project_and_tags(num_of_tags=2)
resp = self.get('/projects?tags-any=%(values)s' % {
'values': tags[0]})
self.assertValidProjectListResponse(resp)
self.assertEqual(project['id'], resp.result['projects'][0]['id'])
def test_list_projects_filtering_by_not_tags(self):
"""Call ``GET /projects?not-tags={tags}``."""
project1, tags1 = self._create_project_and_tags(num_of_tags=2)
project2, tags2 = self._create_project_and_tags(num_of_tags=2)
tag_string = ','.join(tags1)
resp = self.get('/projects?not-tags=%(values)s' % {
'values': tag_string})
self.assertValidProjectListResponse(resp)
project_ids = []
for project in resp.result['projects']:
project_ids.append(project['id'])
self.assertNotIn(project1['id'], project_ids)
self.assertIn(project2['id'], project_ids)
def test_list_projects_filtering_by_not_tags_any(self):
"""Call ``GET /projects?not-tags-any={tags}``."""
project1, tags1 = self._create_project_and_tags(num_of_tags=2)
project2, tags2 = self._create_project_and_tags(num_of_tags=2)
project3, tags3 = self._create_project_and_tags(num_of_tags=2)
tag_string = tags1[0] + ',' + tags2[0]
resp = self.get('/projects?not-tags-any=%(values)s' % {
'values': tag_string})
self.assertValidProjectListResponse(resp)
project_ids = []
for project in resp.result['projects']:
project_ids.append(project['id'])
self.assertNotIn(project1['id'], project_ids)
self.assertNotIn(project2['id'], project_ids)
self.assertIn(project3['id'], project_ids)
def test_list_projects_filtering_multiple_tag_filters(self):
"""Call ``GET /projects?tags={tags}&tags-any={tags}``."""
project1, tags1 = self._create_project_and_tags()
project2, tags2 = self._create_project_and_tags(num_of_tags=2)
url = '/projects?tags=%(value1)s&tags-any=%(value2)s'
resp = self.get(url % {'value1': tags1[0],
'value2': tags2[0]})
self.assertValidProjectListResponse(resp)
pids = [project1['id'], project2['id']]
self.assertEqual(len(resp.result['projects']), 2)
for p in resp.result['projects']:
self.assertIn(p['id'], pids)
def test_list_projects_filtering_multiple_any_tag_filters(self):
"""Call ``GET /projects?tags-any={tags}&not-tags-any={tags}``."""
project1, tags1 = self._create_project_and_tags()
project2, tags2 = self._create_project_and_tags(num_of_tags=2)
url = '/projects?tags-any=%(value1)s&not-tags-any=%(value2)s'
resp = self.get(url % {'value1': tags1[0],
'value2': tags2[0]})
self.assertValidProjectListResponse(resp)
pids = [p['id'] for p in resp.result['projects']]
self.assertIn(project1['id'], pids)
self.assertNotIn(project2['id'], pids)
def test_list_projects_filtering_conflict_tag_filters(self):
"""Call ``GET /projects?tags={tags}&not-tags={tags}``."""
project, tags = self._create_project_and_tags(num_of_tags=2)
tag_string = ','.join(tags)
url = '/projects?tags=%(values)s&not-tags=%(values)s'
resp = self.get(url % {'values': tag_string})
self.assertValidProjectListResponse(resp)
self.assertEqual(len(resp.result['projects']), 0)
def test_list_projects_filtering_conflict_any_tag_filters(self):
"""Call ``GET /projects?tags-any={tags}&not-tags-any={tags}``."""
project, tags = self._create_project_and_tags(num_of_tags=2)
tag_string = ','.join(tags)
url = '/projects?tags-any=%(values)s&not-tags-any=%(values)s'
resp = self.get(url % {'values': tag_string})
self.assertValidProjectListResponse(resp)
self.assertEqual(len(resp.result['projects']), 0)
def test_list_projects_by_tags_and_name(self):
"""Call ``GET /projects?tags-any={tags}&name={name}``."""
project, tags = self._create_project_and_tags(num_of_tags=2)
ref = {'project': {'name': 'tags and name'}}
resp = self.patch('/projects/%(project_id)s' %
{'project_id': project['id']},
body=ref)
url = '/projects?tags-any=%(values)s&name=%(name)s'
resp = self.get(url % {'values': tags[0],
'name': 'tags and name'})
self.assertValidProjectListResponse(resp)
pids = [p['id'] for p in resp.result['projects']]
self.assertIn(project['id'], pids)
resp = self.get(url % {'values': tags[0],
'name': 'foo'})
self.assertValidProjectListResponse(resp)
self.assertEqual(len(resp.result['projects']), 0)
def test_list_projects_filtering_by_parent_id(self):
"""Call ``GET /projects?parent_id={project_id}``."""
projects = self._create_projects_hierarchy(hierarchy_size=2)
@ -1283,3 +1408,225 @@ class ResourceTestCase(test_v3.RestfulTestCase,
'/projects/%(project_id)s' % {
'project_id': projects[0]['project']['id']},
expected_status=http_client.FORBIDDEN)
def test_create_project_with_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=10)
ref = self.get(
'/projects/%(project_id)s' % {
'project_id': project['id']},
expected_status=http_client.OK)
self.assertIn('tags', ref.result['project'])
for tag in tags:
self.assertIn(tag, ref.result['project']['tags'])
def test_update_project_with_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=9)
tag = uuid.uuid4().hex
project['tags'].append(tag)
ref = self.patch(
'/projects/%(project_id)s' % {
'project_id': self.project_id},
body={'project': {'tags': project['tags']}})
self.assertIn(tag, ref.result['project']['tags'])
def test_create_project_tag(self):
tag = uuid.uuid4().hex
url = '/projects/%(project_id)s/tags/%(value)s'
self.put(url % {'project_id': self.project_id, 'value': tag},
expected_status=http_client.CREATED)
self.get(url % {'project_id': self.project_id, 'value': tag},
expected_status=http_client.NO_CONTENT)
def test_create_project_tag_is_case_insensitive(self):
case_tags = ['case', 'CASE']
for tag in case_tags:
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': tag},
expected_status=http_client.CREATED)
resp = self.get('/projects/%(project_id)s' %
{'project_id': self.project_id},
expected_status=http_client.OK)
for tag in case_tags:
self.assertIn(tag, resp.result['project']['tags'])
def test_get_single_project_tag(self):
project, tags = self._create_project_and_tags()
self.get(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': tags[0]},
expected_status=http_client.NO_CONTENT)
self.head(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': tags[0]},
expected_status=http_client.NO_CONTENT)
def test_get_project_tag_that_does_not_exist(self):
project, _ = self._create_project_and_tags()
self.get(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': uuid.uuid4().hex},
expected_status=http_client.NOT_FOUND)
def test_delete_project_tag(self):
project, tags = self._create_project_and_tags()
self.delete(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': tags[0]},
expected_status=http_client.NO_CONTENT)
self.get(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': tags[0]},
expected_status=http_client.NOT_FOUND)
def test_delete_project_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
self.delete(
'/projects/%(project_id)s/tags/' % {
'project_id': project['id']},
expected_status=http_client.NO_CONTENT)
self.get(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': tags[0]},
expected_status=http_client.NOT_FOUND)
resp = self.get(
'/projects/%(project_id)s/tags/' % {
'project_id': self.project_id},
expected_status=http_client.OK)
self.assertEqual(len(resp.result['tags']), 0)
def test_create_project_tag_invalid_project_id(self):
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': uuid.uuid4().hex,
'value': uuid.uuid4().hex},
expected_status=http_client.NOT_FOUND)
def test_create_project_tag_unsafe_name(self):
tag = uuid.uuid4().hex + ','
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': tag},
expected_status=http_client.BAD_REQUEST)
def test_create_project_tag_already_exists(self):
project, tags = self._create_project_and_tags()
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': tags[0]},
expected_status=http_client.BAD_REQUEST)
def test_create_project_tag_over_tag_limit(self):
project, _ = self._create_project_and_tags(num_of_tags=80)
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': uuid.uuid4().hex},
expected_status=http_client.BAD_REQUEST)
def test_create_project_tag_name_over_character_limit(self):
tag = 'a' * 256
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': tag},
expected_status=http_client.BAD_REQUEST)
def test_delete_tag_invalid_project_id(self):
self.delete(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': uuid.uuid4().hex,
'value': uuid.uuid4().hex},
expected_status=http_client.NOT_FOUND)
def test_delete_project_tag_not_found(self):
self.delete(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': uuid.uuid4().hex},
expected_status=http_client.NOT_FOUND)
def test_list_project_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
resp = self.get(
'/projects/%(project_id)s/tags' % {
'project_id': project['id']},
expected_status=http_client.OK)
for tag in tags:
self.assertIn(tag, resp.result['tags'])
def test_check_if_project_tag_exists(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
self.head(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': tags[0]},
expected_status=http_client.NO_CONTENT)
def test_list_project_tags_for_project_with_no_tags(self):
resp = self.get(
'/projects/%(project_id)s/tags' % {
'project_id': self.project_id},
expected_status=http_client.OK)
self.assertEqual([], resp.result['tags'])
def test_check_project_with_no_tags(self):
self.head(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': self.project_id,
'value': uuid.uuid4().hex},
expected_status=http_client.NOT_FOUND)
def test_update_project_tags(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
resp = self.put(
'/projects/%(project_id)s/tags' % {
'project_id': project['id']},
body={'tags': tags},
expected_status=http_client.OK)
self.assertIn(tags[1], resp.result['tags'])
def test_update_project_tags_removes_previous_tags(self):
tag = uuid.uuid4().hex
project, tags = self._create_project_and_tags(num_of_tags=5)
self.put(
'/projects/%(project_id)s/tags/%(value)s' % {
'project_id': project['id'],
'value': tag},
expected_status=http_client.CREATED)
resp = self.put(
'/projects/%(project_id)s/tags' % {
'project_id': project['id']},
body={'tags': tags},
expected_status=http_client.OK)
self.assertNotIn(tag, resp.result['tags'])
self.assertIn(tags[1], resp.result['tags'])
def test_update_project_tags_unsafe_names(self):
project, tags = self._create_project_and_tags(num_of_tags=5)
invalid_chars = [',', '/']
for char in invalid_chars:
tags[0] = uuid.uuid4().hex + char
self.put(
'/projects/%(project_id)s/tags' % {
'project_id': project['id']},
body={'tags': tags},
expected_status=http_client.BAD_REQUEST)
def test_update_project_tags_with_too_many_tags(self):
project, _ = self._create_project_and_tags()
tags = [uuid.uuid4().hex for i in range(81)]
tags.append(uuid.uuid4().hex)
self.put(
'/projects/%(project_id)s/tags' % {'project_id': project['id']},
body={'tags': tags},
expected_status=http_client.BAD_REQUEST)

View File

@ -300,6 +300,11 @@ V3_JSON_HOME_RESOURCES = {
'href-vars': {
'group_id': json_home.Parameters.GROUP_ID,
'project_id': json_home.Parameters.PROJECT_ID, }},
json_home.build_v3_resource_relation('project_tags'): {
'href-template': '/projects/{project_id}/tags/{value}',
'href-vars': {
'project_id': json_home.Parameters.PROJECT_ID,
'value': json_home.Parameters.TAG_VALUE}},
json_home.build_v3_resource_relation('project_user_role'): {
'href-template':
'/projects/{project_id}/users/{user_id}/roles/{role_id}',