Make tags filter match subset rather than exact
Currently when using the "tags" filter to search projects by tag names, the filter only matches projects that have an exact match. Projects that contain the exact tags given, but with additional tags are excluded. This behavior is not compatible with the use cases defined in both the keystone[0] and api-wg[1] specs, notibly with the "tags" and "tags-any" interaction. This change makes it so that "tags" filtering will be performed by matching a subset containing given tags against projects, rather than exact matching. This allows the "tags" and "tags-any" filters to work as described in both [0] and [1]. [0] https://specs.openstack.org/openstack/keystone-specs/specs/keystone/queens/project-tags.html [1] https://specs.openstack.org/openstack/api-wg/guidelines/tags.html#filtering-and-searching-by-tags Co-Authored By: Nicolas Helgeson <nh202b@att.com> Closes-Bug: #1756190 Change-Id: I632efdf0af2969be0a59dc5928a6c036eeca6051
This commit is contained in:
parent
3c1270e306
commit
4b572e564d
@ -177,14 +177,17 @@ class Resource(base.ResourceDriverBase):
|
||||
with sql.session_for_read() as session:
|
||||
query = session.query(ProjectTag)
|
||||
if 'tags' in filters.keys():
|
||||
filtered_ids += self._filter_ids_by_sorted_tags(
|
||||
filtered_ids += self._filter_ids_by_tags(
|
||||
query, filters['tags'].split(','))
|
||||
if 'tags-any' in filters.keys():
|
||||
any_tags = filters['tags-any'].split(',')
|
||||
subq = query.filter(ProjectTag.name.in_(any_tags))
|
||||
filtered_ids += [ptag['project_id'] for ptag in subq]
|
||||
any_tags = [ptag['project_id'] for ptag in subq]
|
||||
if 'tags' in filters.keys():
|
||||
any_tags = set(any_tags) & set(filtered_ids)
|
||||
filtered_ids = any_tags
|
||||
if 'not-tags' in filters.keys():
|
||||
blacklist_ids = self._filter_ids_by_sorted_tags(
|
||||
blacklist_ids = self._filter_ids_by_tags(
|
||||
query, filters['not-tags'].split(','))
|
||||
filtered_ids = self._filter_not_tags(session,
|
||||
filtered_ids,
|
||||
@ -206,15 +209,14 @@ class Resource(base.ResourceDriverBase):
|
||||
return [project_ref.to_dict() for project_ref in query.all()
|
||||
if not self._is_hidden_ref(project_ref)]
|
||||
|
||||
def _filter_ids_by_sorted_tags(self, query, tags):
|
||||
def _filter_ids_by_tags(self, query, tags):
|
||||
filtered_ids = []
|
||||
sorted_tags = sorted(tags)
|
||||
subq = query.filter(ProjectTag.name.in_(sorted_tags))
|
||||
subq = query.filter(ProjectTag.name.in_(tags))
|
||||
for ptag in subq:
|
||||
subq_tags = query.filter(ProjectTag.project_id ==
|
||||
ptag['project_id'])
|
||||
result = map(lambda x: x['name'], subq_tags.all())
|
||||
if sorted(result) == sorted_tags:
|
||||
if set(tags) <= set(result):
|
||||
filtered_ids.append(ptag['project_id'])
|
||||
return filtered_ids
|
||||
|
||||
|
@ -740,10 +740,14 @@ class ResourceTestCase(test_v3.RestfulTestCase,
|
||||
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)
|
||||
project1, tags1 = self._create_project_and_tags(num_of_tags=2)
|
||||
tag_string = tags[0] + ',' + tags1[0]
|
||||
resp = self.get('/projects?tags-any=%(values)s' % {
|
||||
'values': tags[0]})
|
||||
'values': tag_string})
|
||||
pids = [p['id'] for p in resp.result['projects']]
|
||||
self.assertValidProjectListResponse(resp)
|
||||
self.assertEqual(project['id'], resp.result['projects'][0]['id'])
|
||||
self.assertIn(project['id'], pids)
|
||||
self.assertIn(project1['id'], pids)
|
||||
|
||||
def test_list_projects_filtering_by_not_tags(self):
|
||||
"""Call ``GET /projects?not-tags={tags}``."""
|
||||
@ -753,11 +757,9 @@ class ResourceTestCase(test_v3.RestfulTestCase,
|
||||
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)
|
||||
pids = [p['id'] for p in resp.result['projects']]
|
||||
self.assertNotIn(project1['id'], pids)
|
||||
self.assertIn(project2['id'], pids)
|
||||
|
||||
def test_list_projects_filtering_by_not_tags_any(self):
|
||||
"""Call ``GET /projects?not-tags-any={tags}``."""
|
||||
@ -768,25 +770,30 @@ class ResourceTestCase(test_v3.RestfulTestCase,
|
||||
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)
|
||||
pids = [p['id'] for p in resp.result['projects']]
|
||||
self.assertNotIn(project1['id'], pids)
|
||||
self.assertNotIn(project2['id'], pids)
|
||||
self.assertIn(project3['id'], pids)
|
||||
|
||||
def test_list_projects_filtering_multiple_tag_filters(self):
|
||||
"""Call ``GET /projects?tags={tags}&tags-any={tags}``."""
|
||||
project1, tags1 = self._create_project_and_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)
|
||||
tags1_query = ','.join(tags1)
|
||||
resp = self.patch('/projects/%(project_id)s' %
|
||||
{'project_id': project3['id']},
|
||||
body={'project': {'tags': tags1}})
|
||||
tags1.append(tags2[0])
|
||||
resp = self.patch('/projects/%(project_id)s' %
|
||||
{'project_id': project1['id']},
|
||||
body={'project': {'tags': tags1}})
|
||||
url = '/projects?tags=%(value1)s&tags-any=%(value2)s'
|
||||
resp = self.get(url % {'value1': tags1[0],
|
||||
'value2': tags2[0]})
|
||||
resp = self.get(url % {'value1': tags1_query,
|
||||
'value2': ','.join(tags2)})
|
||||
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)
|
||||
self.assertEqual(len(resp.result['projects']), 1)
|
||||
self.assertIn(project1['id'], resp.result['projects'][0]['id'])
|
||||
|
||||
def test_list_projects_filtering_multiple_any_tag_filters(self):
|
||||
"""Call ``GET /projects?tags-any={tags}¬-tags-any={tags}``."""
|
||||
|
9
releasenotes/notes/bug-1756190-0e5d86d334555931.yaml
Normal file
9
releasenotes/notes/bug-1756190-0e5d86d334555931.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
[`bug 1756190 <https://bugs.launchpad.net/keystone/+bug/1756190>`_]
|
||||
When filtering projects based on tags, the filtering will now be performed
|
||||
by matching a subset containing the given tags against projects, rather
|
||||
than exact matching. Providing more tags when performing a search will
|
||||
yield more exact results while less will return any projects that match
|
||||
the given tags but could contain other tags as well.
|
Loading…
x
Reference in New Issue
Block a user