Merge "Add project tags to keystoneclient"
This commit is contained in:
commit
e5d44e5cea
keystoneclient
releasenotes/notes
@ -356,6 +356,13 @@ class CrudManager(Manager):
|
||||
if params is None:
|
||||
return ''
|
||||
else:
|
||||
# NOTE(spilla) Since the manager cannot take in a hyphen as a
|
||||
# key in the kwarg, it is passed in with a _. This needs to be
|
||||
# replaced with a proper hyphen for the URL to work properly.
|
||||
tags_params = ('tags_any', 'not_tags', 'not_tags_any')
|
||||
for tag_param in tags_params:
|
||||
if tag_param in params:
|
||||
params[tag_param.replace('_', '-')] = params.pop(tag_param)
|
||||
return '?%s' % urllib.parse.urlencode(params, doseq=True)
|
||||
|
||||
def build_key_only_query(self, params_list):
|
||||
|
@ -71,9 +71,10 @@ class Domain(Base):
|
||||
|
||||
class Project(Base):
|
||||
|
||||
def __init__(self, client, domain_id=None, parent=None):
|
||||
def __init__(self, client, domain_id=None, parent=None, tags=None):
|
||||
super(Project, self).__init__(client, domain_id)
|
||||
self.parent = parent
|
||||
self.tags = tags if tags else []
|
||||
|
||||
def setUp(self):
|
||||
super(Project, self).setUp()
|
||||
@ -81,7 +82,8 @@ class Project(Base):
|
||||
self.ref = {'name': RESOURCE_NAME_PREFIX + uuid.uuid4().hex,
|
||||
'domain': self.domain_id,
|
||||
'enabled': True,
|
||||
'parent': self.parent}
|
||||
'parent': self.parent,
|
||||
'tags': self.tags}
|
||||
self.entity = self.client.projects.create(**self.ref)
|
||||
self.addCleanup(self.client.projects.delete, self.entity)
|
||||
|
||||
|
@ -53,6 +53,7 @@ class ProjectsTestCase(base.V3ClientTestCase, ProjectsTestMixin):
|
||||
|
||||
self.test_project = fixtures.Project(self.client, self.test_domain.id)
|
||||
self.useFixture(self.test_project)
|
||||
self.special_tag = '~`!@#$%^&*()-_+=<>.? \'"'
|
||||
|
||||
def test_create_subproject(self):
|
||||
project_ref = {
|
||||
@ -188,3 +189,257 @@ class ProjectsTestCase(base.V3ClientTestCase, ProjectsTestMixin):
|
||||
self.assertRaises(http.NotFound,
|
||||
self.client.projects.get,
|
||||
project.id)
|
||||
|
||||
def test_list_projects_with_tag_filters(self):
|
||||
project_one = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag1'])
|
||||
project_two = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag1', 'tag2'])
|
||||
project_three = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag2', 'tag3'])
|
||||
|
||||
self.useFixture(project_one)
|
||||
self.useFixture(project_two)
|
||||
self.useFixture(project_three)
|
||||
|
||||
projects = self.client.projects.list(tags='tag1')
|
||||
project_ids = []
|
||||
for project in projects:
|
||||
project_ids.append(project.id)
|
||||
self.assertIn(project_one.id, project_ids)
|
||||
|
||||
projects = self.client.projects.list(tags_any='tag1')
|
||||
project_ids = []
|
||||
for project in projects:
|
||||
project_ids.append(project.id)
|
||||
self.assertIn(project_one.id, project_ids)
|
||||
self.assertIn(project_two.id, project_ids)
|
||||
|
||||
projects = self.client.projects.list(not_tags='tag1')
|
||||
project_ids = []
|
||||
for project in projects:
|
||||
project_ids.append(project.id)
|
||||
self.assertNotIn(project_one.id, project_ids)
|
||||
|
||||
projects = self.client.projects.list(not_tags_any='tag1,tag2')
|
||||
project_ids = []
|
||||
for project in projects:
|
||||
project_ids.append(project.id)
|
||||
self.assertNotIn(project_one.id, project_ids)
|
||||
self.assertNotIn(project_two.id, project_ids)
|
||||
self.assertNotIn(project_three.id, project_ids)
|
||||
|
||||
projects = self.client.projects.list(tags='tag1,tag2')
|
||||
project_ids = []
|
||||
for project in projects:
|
||||
project_ids.append(project.id)
|
||||
self.assertNotIn(project_one.id, project_ids)
|
||||
self.assertIn(project_two.id, project_ids)
|
||||
self.assertNotIn(project_three.id, project_ids)
|
||||
|
||||
def test_add_tag(self):
|
||||
project = fixtures.Project(self.client, self.test_domain.id)
|
||||
self.useFixture(project)
|
||||
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual([], tags)
|
||||
|
||||
project.add_tag('tag1')
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual(['tag1'], tags)
|
||||
|
||||
# verify there is an error when you try to add the same tag
|
||||
self.assertRaises(http.BadRequest,
|
||||
project.add_tag,
|
||||
'tag1')
|
||||
|
||||
def test_update_tags(self):
|
||||
project = fixtures.Project(self.client, self.test_domain.id)
|
||||
self.useFixture(project)
|
||||
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual([], tags)
|
||||
|
||||
project.update_tags(['tag1', 'tag2', self.special_tag])
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertIn('tag1', tags)
|
||||
self.assertIn('tag2', tags)
|
||||
self.assertIn(self.special_tag, tags)
|
||||
self.assertEqual(3, len(tags))
|
||||
|
||||
project.update_tags([])
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual([], tags)
|
||||
|
||||
# cannot have duplicate tags in update
|
||||
self.assertRaises(http.BadRequest,
|
||||
project.update_tags,
|
||||
['tag1', 'tag1'])
|
||||
|
||||
def test_delete_tag(self):
|
||||
project = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag1', self.special_tag])
|
||||
self.useFixture(project)
|
||||
|
||||
project.delete_tag('tag1')
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual([self.special_tag], tags)
|
||||
|
||||
project.delete_tag(self.special_tag)
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual([], tags)
|
||||
|
||||
def test_delete_all_tags(self):
|
||||
project_one = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag1'])
|
||||
|
||||
project_two = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag1', 'tag2', self.special_tag])
|
||||
|
||||
project_three = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=[])
|
||||
|
||||
self.useFixture(project_one)
|
||||
self.useFixture(project_two)
|
||||
self.useFixture(project_three)
|
||||
|
||||
result_one = project_one.delete_all_tags()
|
||||
tags_one = self.client.projects.get(project_one.id).tags
|
||||
tags_two = self.client.projects.get(project_two.id).tags
|
||||
self.assertEqual([], result_one)
|
||||
self.assertEqual([], tags_one)
|
||||
self.assertIn('tag1', tags_two)
|
||||
|
||||
result_two = project_two.delete_all_tags()
|
||||
tags_two = self.client.projects.get(project_two.id).tags
|
||||
self.assertEqual([], result_two)
|
||||
self.assertEqual([], tags_two)
|
||||
|
||||
result_three = project_three.delete_all_tags()
|
||||
tags_three = self.client.projects.get(project_three.id).tags
|
||||
self.assertEqual([], result_three)
|
||||
self.assertEqual([], tags_three)
|
||||
|
||||
def test_list_tags(self):
|
||||
tags_one = ['tag1']
|
||||
project_one = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=tags_one)
|
||||
|
||||
tags_two = ['tag1', 'tag2']
|
||||
project_two = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=tags_two)
|
||||
|
||||
tags_three = []
|
||||
project_three = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=tags_three)
|
||||
|
||||
self.useFixture(project_one)
|
||||
self.useFixture(project_two)
|
||||
self.useFixture(project_three)
|
||||
|
||||
result_one = project_one.list_tags()
|
||||
result_two = project_two.list_tags()
|
||||
result_three = project_three.list_tags()
|
||||
|
||||
for tag in tags_one:
|
||||
self.assertIn(tag, result_one)
|
||||
self.assertEqual(1, len(result_one))
|
||||
|
||||
for tag in tags_two:
|
||||
self.assertIn(tag, result_two)
|
||||
self.assertEqual(2, len(result_two))
|
||||
|
||||
for tag in tags_three:
|
||||
self.assertIn(tag, result_three)
|
||||
self.assertEqual(0, len(result_three))
|
||||
|
||||
def test_check_tag(self):
|
||||
project = fixtures.Project(
|
||||
self.client, self.test_domain.id,
|
||||
tags=['tag1'])
|
||||
self.useFixture(project)
|
||||
|
||||
tags = self.client.projects.get(project.id).tags
|
||||
self.assertEqual(['tag1'], tags)
|
||||
self.assertTrue(project.check_tag('tag1'))
|
||||
self.assertFalse(project.check_tag('tag2'))
|
||||
self.assertFalse(project.check_tag(self.special_tag))
|
||||
|
||||
def test_add_invalid_tags(self):
|
||||
project_one = fixtures.Project(
|
||||
self.client, self.test_domain.id)
|
||||
|
||||
self.useFixture(project_one)
|
||||
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
project_one.add_tag,
|
||||
',')
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
project_one.add_tag,
|
||||
'/')
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
project_one.add_tag,
|
||||
'')
|
||||
|
||||
def test_update_invalid_tags(self):
|
||||
tags_comma = ['tag1', ',']
|
||||
tags_slash = ['tag1', '/']
|
||||
tags_blank = ['tag1', '']
|
||||
project_one = fixtures.Project(
|
||||
self.client, self.test_domain.id)
|
||||
|
||||
self.useFixture(project_one)
|
||||
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
project_one.update_tags,
|
||||
tags_comma)
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
project_one.update_tags,
|
||||
tags_slash)
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
project_one.update_tags,
|
||||
tags_blank)
|
||||
|
||||
def test_create_project_invalid_tags(self):
|
||||
project_ref = {
|
||||
'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex,
|
||||
'domain': self.test_domain.id,
|
||||
'enabled': True,
|
||||
'description': uuid.uuid4().hex,
|
||||
'tags': ','}
|
||||
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.client.projects.create,
|
||||
**project_ref)
|
||||
|
||||
project_ref = {
|
||||
'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex,
|
||||
'domain': self.test_domain.id,
|
||||
'enabled': True,
|
||||
'description': uuid.uuid4().hex,
|
||||
'tags': '/'}
|
||||
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.client.projects.create,
|
||||
**project_ref)
|
||||
|
||||
project_ref = {
|
||||
'name': fixtures.RESOURCE_NAME_PREFIX + uuid.uuid4().hex,
|
||||
'domain': self.test_domain.id,
|
||||
'enabled': True,
|
||||
'description': uuid.uuid4().hex,
|
||||
'tags': ''}
|
||||
|
||||
self.assertRaises(exceptions.BadRequest,
|
||||
self.client.projects.create,
|
||||
**project_ref)
|
||||
|
@ -312,3 +312,86 @@ class ProjectTests(utils.ClientTestCase, utils.CrudTests):
|
||||
# server, a different implementation might not fail this request.
|
||||
self.assertRaises(ksa_exceptions.Forbidden, self.manager.update,
|
||||
ref['id'], **utils.parameterize(req_ref))
|
||||
|
||||
def test_add_tag(self):
|
||||
ref = self.new_ref()
|
||||
tag_name = "blue"
|
||||
|
||||
self.stub_url("PUT",
|
||||
parts=[self.collection_key, ref['id'], "tags", tag_name],
|
||||
status_code=201)
|
||||
self.manager.add_tag(ref['id'], tag_name)
|
||||
|
||||
def test_update_tags(self):
|
||||
new_tags = ["blue", "orange"]
|
||||
ref = self.new_ref()
|
||||
|
||||
self.stub_url("PUT",
|
||||
parts=[self.collection_key, ref['id'], "tags"],
|
||||
json={"tags": new_tags},
|
||||
status_code=200)
|
||||
|
||||
ret = self.manager.update_tags(ref['id'], new_tags)
|
||||
self.assertEqual(ret, new_tags)
|
||||
|
||||
def test_delete_tag(self):
|
||||
ref = self.new_ref()
|
||||
tag_name = "blue"
|
||||
|
||||
self.stub_url("DELETE",
|
||||
parts=[self.collection_key, ref['id'], "tags", tag_name],
|
||||
status_code=204)
|
||||
|
||||
self.manager.delete_tag(ref['id'], tag_name)
|
||||
|
||||
def test_delete_all_tags(self):
|
||||
ref = self.new_ref()
|
||||
|
||||
self.stub_url("PUT",
|
||||
parts=[self.collection_key, ref['id'], "tags"],
|
||||
json={"tags": []},
|
||||
status_code=200)
|
||||
|
||||
ret = self.manager.update_tags(ref['id'], [])
|
||||
self.assertEqual([], ret)
|
||||
|
||||
def test_list_tags(self):
|
||||
ref = self.new_ref()
|
||||
tags = ["blue", "orange", "green"]
|
||||
|
||||
self.stub_url("GET",
|
||||
parts=[self.collection_key, ref['id'], "tags"],
|
||||
json={"tags": tags},
|
||||
status_code=200)
|
||||
|
||||
ret_tags = self.manager.list_tags(ref['id'])
|
||||
self.assertEqual(tags, ret_tags)
|
||||
|
||||
def test_check_tag(self):
|
||||
ref = self.new_ref()
|
||||
|
||||
tag_name = "blue"
|
||||
self.stub_url("HEAD",
|
||||
parts=[self.collection_key, ref['id'], "tags", tag_name],
|
||||
status_code=204)
|
||||
self.assertTrue(self.manager.check_tag(ref['id'], tag_name))
|
||||
|
||||
no_tag = "orange"
|
||||
self.stub_url("HEAD",
|
||||
parts=[self.collection_key, ref['id'], "tags", no_tag],
|
||||
status_code=404)
|
||||
self.assertFalse(self.manager.check_tag(ref['id'], no_tag))
|
||||
|
||||
def _build_project_response(self, tags):
|
||||
project_id = uuid.uuid4().hex
|
||||
ret = {"projects": [
|
||||
{"is_domain": False,
|
||||
"description": "",
|
||||
"tags": tags,
|
||||
"enabled": True,
|
||||
"id": project_id,
|
||||
"parent_id": "default",
|
||||
"domain_id": "default",
|
||||
"name": project_id}
|
||||
]}
|
||||
return ret
|
||||
|
@ -14,6 +14,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six.moves.urllib as urllib
|
||||
|
||||
from keystoneclient import base
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient.i18n import _
|
||||
@ -52,6 +54,24 @@ class Project(base.Resource):
|
||||
|
||||
return retval
|
||||
|
||||
def add_tag(self, tag):
|
||||
self.manager.add_tag(self, tag)
|
||||
|
||||
def update_tags(self, tags):
|
||||
return self.manager.update_tags(self, tags)
|
||||
|
||||
def delete_tag(self, tag):
|
||||
self.manager.delete_tag(self, tag)
|
||||
|
||||
def delete_all_tags(self):
|
||||
return self.manager.update_tags(self, [])
|
||||
|
||||
def list_tags(self):
|
||||
return self.manager.list_tags(self)
|
||||
|
||||
def check_tag(self, tag):
|
||||
return self.manager.check_tag(self, tag)
|
||||
|
||||
|
||||
class ProjectManager(base.CrudManager):
|
||||
"""Manager class for manipulating Identity projects."""
|
||||
@ -101,17 +121,24 @@ class ProjectManager(base.CrudManager):
|
||||
assignments on.
|
||||
:type user: str or :class:`keystoneclient.v3.users.User`
|
||||
:param kwargs: any other attribute provided will filter projects on.
|
||||
Project tags filter keyword: ``tags``, ``tags_any``,
|
||||
``not_tags``, and ``not_tags_any``. tag attribute type
|
||||
string. Pass in a comma separated string to filter
|
||||
with multiple tags.
|
||||
|
||||
:returns: a list of projects.
|
||||
:rtype: list of :class:`keystoneclient.v3.projects.Project`
|
||||
|
||||
"""
|
||||
base_url = '/users/%s' % base.getid(user) if user else None
|
||||
return super(ProjectManager, self).list(
|
||||
projects = super(ProjectManager, self).list(
|
||||
base_url=base_url,
|
||||
domain_id=base.getid(domain),
|
||||
fallback_to_auth=True,
|
||||
**kwargs)
|
||||
for p in projects:
|
||||
p.tags = self._encode_tags(getattr(p, 'tags', []))
|
||||
return projects
|
||||
|
||||
def _check_not_parents_as_ids_and_parents_as_list(self, parents_as_ids,
|
||||
parents_as_list):
|
||||
@ -174,7 +201,9 @@ class ProjectManager(base.CrudManager):
|
||||
query = self.build_key_only_query(query_params)
|
||||
dict_args = {'project_id': base.getid(project)}
|
||||
url = self.build_url(dict_args_in_out=dict_args)
|
||||
return self._get(url + query, self.key)
|
||||
p = self._get(url + query, self.key)
|
||||
p.tags = self._encode_tags(getattr(p, 'tags', []))
|
||||
return p
|
||||
|
||||
def update(self, project, name=None, domain=None, description=None,
|
||||
enabled=None, **kwargs):
|
||||
@ -213,3 +242,82 @@ class ProjectManager(base.CrudManager):
|
||||
"""
|
||||
return super(ProjectManager, self).delete(
|
||||
project_id=base.getid(project))
|
||||
|
||||
def _encode_tags(self, tags):
|
||||
"""Encode tags to non-unicode string in python2.
|
||||
|
||||
:param tags: list of unicode tags
|
||||
|
||||
:returns: List of strings
|
||||
"""
|
||||
return [str(t) for t in tags]
|
||||
|
||||
def add_tag(self, project, tag):
|
||||
"""Add a tag to a project.
|
||||
|
||||
:param project: project to add a tag to.
|
||||
:param tag: str name of tag.
|
||||
|
||||
"""
|
||||
url = "/projects/%s/tags/%s" % (base.getid(project),
|
||||
urllib.parse.quote(tag))
|
||||
self.client.put(url)
|
||||
|
||||
def update_tags(self, project, tags):
|
||||
"""Update tag list of a project.
|
||||
|
||||
Replaces current tag list with list specified in tags parameter.
|
||||
|
||||
:param project: project to update.
|
||||
:param tags: list of str tag names to add to the project
|
||||
|
||||
:returns: list of tags
|
||||
|
||||
"""
|
||||
url = "/projects/%s/tags" % base.getid(project)
|
||||
for tag in tags:
|
||||
tag = urllib.parse.quote(tag)
|
||||
resp, body = self.client.put(url, body={"tags": tags})
|
||||
return body['tags']
|
||||
|
||||
def delete_tag(self, project, tag):
|
||||
"""Remove tag from project.
|
||||
|
||||
:param projectd: project to remove tag from.
|
||||
:param tag: str name of tag to remove from project
|
||||
|
||||
"""
|
||||
self._delete(
|
||||
"/projects/%s/tags/%s" % (base.getid(project),
|
||||
urllib.parse.quote(tag)))
|
||||
|
||||
def list_tags(self, project):
|
||||
"""List tags associated with project.
|
||||
|
||||
:param project: project to list tags for.
|
||||
|
||||
:returns: list of str tag names
|
||||
|
||||
"""
|
||||
url = "/projects/%s/tags" % base.getid(project)
|
||||
resp, body = self.client.get(url)
|
||||
return self._encode_tags(body['tags'])
|
||||
|
||||
def check_tag(self, project, tag):
|
||||
"""Check if tag is associated with project.
|
||||
|
||||
:param project: project to check tags for.
|
||||
:param tag: str name of tag
|
||||
|
||||
:returns: true if tag is associated, false otherwise
|
||||
|
||||
"""
|
||||
url = "/projects/%s/tags/%s" % (base.getid(project),
|
||||
urllib.parse.quote(tag))
|
||||
try:
|
||||
self.client.head(url)
|
||||
# no errors means found the tag
|
||||
return True
|
||||
except exceptions.NotFound:
|
||||
# 404 means tag not in project
|
||||
return False
|
||||
|
8
releasenotes/notes/project-tags-1f8a32d389951e7a.yaml
Normal file
8
releasenotes/notes/project-tags-1f8a32d389951e7a.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
[`blueprint project-tags <https://blueprints.launchpad.net/keystone/+spec/project-tags>`_]
|
||||
The keystoneclient now supports project tags feature in keystone. This
|
||||
allows operators to use the client to associate tags to a project,
|
||||
retrieve tags associated with a project, delete tags associated with a
|
||||
project, and filter projects based on tags.
|
Loading…
x
Reference in New Issue
Block a user