Skip to content

Commit

Permalink
Merge pull request #58 from fiaas/label-selectors
Browse files Browse the repository at this point in the history
Make it possible to use other label selectors than equality
  • Loading branch information
mortenlj authored Aug 23, 2019
2 parents e9f74a7 + 8ac01bc commit 7b4df2d
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 6 deletions.
84 changes: 79 additions & 5 deletions k8s/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,22 +65,53 @@ def _build_url(cls, **kwargs):
return cls._meta.url_template.format(**kwargs)

@classmethod
def find(cls, name, namespace="default", labels=None):
def find(cls, name="", namespace="default", labels=None):
"""Find resources using label selection
:param: :py:class:`str` name: The name of the resource, as indicated by the `app` label
:param: :py:class:`str` namespace: The namespace to search in
:param: :py:class:`dict` labels: More advanced label selection (see below)
:returns: A list of matching objects
When a `labels` dictionary is supplied, the `name` parameter is ignored.
The dictionary is used to build the `labelSelector` parameter to the API,
and supports all the operations of the API through the use of :py:class:`~k8s.base.LabelSelector`.
Each key in the dictionary is a label name. The value defines which operation to perform.
Operations that take a single string value:
- :py:class:`~k8s.base.Equality`
- :py:class:`~k8s.base.Inequality`
A plain string will automatically be wrapped by :py:class:`~k8s.base.Equality` for compatability
with older versions of this method.
Operations that take a sequence of string values:
- :py:class:`~k8s.base.In`
- :py:class:`~k8s.base.NotIn`
Operations that takes no value:
- :py:class:`~k8s.base.Exists`
"""
if namespace is None:
if not cls._meta.list_url:
raise NotImplementedError("Cannot find without namespace, no list_url defined on class {}".format(cls))
url = cls._meta.list_url
else:
url = cls._build_url(name="", namespace=namespace)
if labels:
selector = ",".join("{}={}".format(k, v) for k, v in labels.items())
else:
selector = "app={}".format(name)
if not labels:
labels = {"app": Equality(name)}
selector = ",".join("{}{}".format(k, v if isinstance(v, LabelSelector) else Equality(v))
for k, v in labels.items())
resp = cls._client.get(url, params={"labelSelector": selector})
return [cls.from_dict(item) for item in resp.json()[u"items"]]

@classmethod
def list(cls, namespace="default"):
"""List all resources in given namespace"""
if namespace is None:
if not cls._meta.list_url:
raise NotImplementedError("Cannot list without namespace, no list_url defined on class {}".format(cls))
Expand Down Expand Up @@ -136,6 +167,7 @@ def get_or_create(cls, **kwargs):

@classmethod
def delete(cls, name, namespace="default", **kwargs):
"""Delete the named resource"""
url = cls._build_url(name=name, namespace=namespace)
cls._client.delete(url, **kwargs)

Expand Down Expand Up @@ -228,3 +260,45 @@ def __init__(self, event_json, cls):
def __repr__(self):
return "{cls}(type={type}, object={object})".format(cls=self.__class__.__name__, type=self.type,
object=self.object)


class LabelSelector(object):
"""Base for label select operations"""

#: Operator used in selection query
operator = None

def __init__(self, value=""):
self.value = value

def __str__(self):
return "{}{}".format(self.operator, self.value)


class Equality(LabelSelector):
operator = "="


class Inequality(LabelSelector):
operator = "!="


class LabelSetSelector(LabelSelector):
def __str__(self):
return " {} ({})".format(self.operator, ",".join(self.value))


class In(LabelSetSelector):
operator = "in"


class NotIn(LabelSetSelector):
operator = "notin"


class Exists(LabelSelector):
def __init__(self):
super(Exists, self).__init__("")

def __str__(self):
return ""
27 changes: 26 additions & 1 deletion tests/k8s/test_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import mock
import pytest

from k8s.base import Model, Field, WatchEvent
from k8s.base import Model, Field, WatchEvent, Equality, Inequality, In, NotIn, Exists


class Example(Model):
Expand All @@ -27,3 +29,26 @@ def test_watch_event_deleted(self):
watch_event = WatchEvent({"type": "DELETED", "object": {"value": 42}}, Example)
assert watch_event.type == WatchEvent.DELETED
assert watch_event.object == Example(value=42)


class TestFind(object):
@pytest.fixture
def client(self):
with mock.patch.object(Example, "_client") as m:
yield m

def test_find_by_name(self, client):
Example.find("app_name")
client.get.assert_called_once_with("/example", params={"labelSelector": "app=app_name"})

@pytest.mark.parametrize("value, selector", (
(Equality("my_value"), "my_key=my_value"),
(Inequality("my_value"), "my_key!=my_value"),
(In(("value1", "value2")), "my_key in (value1,value2)"),
(NotIn(("value1", "value2")), "my_key notin (value1,value2)"),
(Exists(), "my_key"),
("my_unwrapped_value", "my_key=my_unwrapped_value"),
))
def test_find_by_selectors(self, client, value, selector):
Example.find(labels={"my_key": value})
client.get.assert_called_once_with("/example", params={"labelSelector": selector})

0 comments on commit 7b4df2d

Please sign in to comment.