From fcdf009c9f7e49652611d4100802e10369c42b67 Mon Sep 17 00:00:00 2001 From: inoration Date: Thu, 25 Aug 2016 11:31:00 +0800 Subject: [PATCH 1/7] fix some bugs --- pykube/__init__.py | 6 +++ pykube/config.py | 57 ++++++++----------------- pykube/http.py | 19 ++++----- pykube/objects.py | 103 +++++++++++++++++---------------------------- pykube/query.py | 48 ++++++--------------- 5 files changed, 85 insertions(+), 148 deletions(-) diff --git a/pykube/__init__.py b/pykube/__init__.py index c65fdfb..94a93f6 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -27,3 +27,9 @@ ThirdPartyResource, ) from .query import now, all_ as all, everything # noqa + +#temporarily +import requests +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + diff --git a/pykube/config.py b/pykube/config.py index 165e61b..fe7ac1a 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -60,58 +60,36 @@ def from_service_account(cls, path="/var/run/secrets/kubernetes.io/serviceaccoun return self @classmethod - def from_file(cls, filename): + def from_file(cls, filename="~/.kube/config", verify=False): """ Creates an instance of the KubeConfig class from a kubeconfig file. :Parameters: - `filename`: The full path to the configuration file + Default: ~/.kube/config """ filename = os.path.expanduser(filename) if not os.path.isfile(filename): raise exceptions.PyKubeError("Configuration file {} not found".format(filename)) - with open(filename) as f: + with open(filename,'r') as f: doc = yaml.safe_load(f.read()) - self = cls(doc) + self = cls(doc,verify) self.filename = filename return self - @classmethod - def from_url(cls, url): - """ - Creates an instance of the KubeConfig class from a single URL (useful - for interacting with kubectl proxy). - """ - doc = { - "clusters": [ - { - "name": "self", - "cluster": { - "server": url, - }, - }, - ], - "contexts": [ - { - "name": "self", - "context": { - "cluster": "self", - }, - } - ], - "current-context": "self", - } - self = cls(doc) - return self - - def __init__(self, doc): + def __init__(self, doc, verify=False): """ Creates an instance of the KubeConfig class. """ self.doc = doc self.current_context = None + self.verify = verify if "current-context" in doc and doc["current-context"]: self.set_current_context(doc["current-context"]) + if self.current_context == None: + self.set_current_context('default') + self.doc["clusters"][0]["name"] = 'default' #If current_context is not set, consider the first cluster as default. + def set_current_context(self, value): """ @@ -137,6 +115,7 @@ def clusters(self): self._clusters = cs return self._clusters + @property def users(self): """ @@ -144,11 +123,10 @@ def users(self): """ if not hasattr(self, "_users"): us = {} - if "users" in self.doc: - for ur in self.doc["users"]: - us[ur["name"]] = u = copy.deepcopy(ur["user"]) - BytesOrFile.maybe_set(u, "client-certificate") - BytesOrFile.maybe_set(u, "client-key") + for ur in self.doc["users"]: + us[ur["name"]] = u = copy.deepcopy(ur["user"]) + BytesOrFile.maybe_set(u, "client-certificate") + BytesOrFile.maybe_set(u, "client-key") self._users = us return self._users @@ -181,7 +159,7 @@ def user(self): """ if self.current_context is None: raise exceptions.PyKubeError("current context not set; call set_current_context") - return self.users.get(self.contexts[self.current_context].get("user", ""), {}) + return self.users[self.contexts[self.current_context]["user"]] class BytesOrFile(object): @@ -208,7 +186,7 @@ def __init__(self, data): """ self._filename = None self._bytes = None - if os.path.isfile(data): + if data.startswith("/"): self._filename = data else: self._bytes = base64.b64decode(data) @@ -233,3 +211,4 @@ def filename(self): with tempfile.NamedTemporaryFile(delete=False) as f: f.write(self._bytes) return f.name + diff --git a/pykube/http.py b/pykube/http.py index d0f0bd4..0a7c4c6 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -30,7 +30,7 @@ def __init__(self, config): """ self.config = config self.url = self.config.cluster["server"] - self.session = self.build_session() + self.session = self.build_session(config.verify) @property def url(self): @@ -39,28 +39,26 @@ def url(self): @url.setter def url(self, value): pr = urlparse(value) - if sys.version_info < (3, 5) and ("::" in pr.hostname or _ipv4_re.match(pr.hostname)): - warnings.warn("IP address hostnames are not supported with Python < 3.5. Please see https://github.com/kelproject/pykube/issues/29 for more info.", RuntimeWarning) + if sys.version_info < (2, 7, 9) and ("::" in pr.hostname or _ipv4_re.match(pr.hostname)): + warnings.warn("IP address hostnames are not supported with Python < 2.7.9. Please see https://github.com/kelproject/pykube/issues/29 for more info.", RuntimeWarning) self._url = pr.geturl() - def build_session(self): + def build_session(self, verify=False): """ Creates a new session for the client. """ s = requests.Session() if "certificate-authority" in self.config.cluster: s.verify = self.config.cluster["certificate-authority"].filename() - elif "insecure-skip-tls-verify" in self.config.cluster: - s.verify = not self.config.cluster["insecure-skip-tls-verify"] + if verify == False: + s.verify = False if "token" in self.config.user and self.config.user["token"]: s.headers["Authorization"] = "Bearer {}".format(self.config.user["token"]) - elif "client-certificate" in self.config.user: + else: s.cert = ( self.config.user["client-certificate"].filename(), self.config.user["client-key"].filename(), ) - else: # no user present; don't configure anything - pass return s def get_kwargs(self, **kwargs): @@ -73,7 +71,7 @@ def get_kwargs(self, **kwargs): version = kwargs.pop("version", "v1") if version == "v1": base = kwargs.pop("base", "/api") - elif "/" in version: + elif any(map(version.startswith, ["extensions/", "batch/"])): base = kwargs.pop("base", "/apis") else: if "base" not in kwargs: @@ -185,3 +183,4 @@ def delete(self, *args, **kwargs): - `kwargs`: Keyword arguments """ return self.session.delete(*args, **self.get_kwargs(**kwargs)) + diff --git a/pykube/objects.py b/pykube/objects.py index 5857578..949d141 100644 --- a/pykube/objects.py +++ b/pykube/objects.py @@ -1,8 +1,6 @@ import copy import json -import os.path as op - -import six +from time import sleep from .exceptions import ObjectDoesNotExist from .mixins import ReplicatedMixin, ScalableMixin @@ -13,7 +11,6 @@ DEFAULT_NAMESPACE = "default" -@six.python_2_unicode_compatible class APIObject(object): objects = ObjectManager() @@ -24,16 +21,11 @@ def __init__(self, api, obj): self.api = api self.set_obj(obj) + def set_obj(self, obj): self.obj = obj self._original_obj = copy.deepcopy(obj) - def __repr__(self): - return "<{kind} {name}>".format(kind=self.kind, name=self.name) - - def __str__(self): - return self.name - @property def name(self): return self.obj["metadata"]["name"] @@ -44,14 +36,11 @@ def annotations(self): def api_kwargs(self, **kwargs): kw = {} - # Construct url for api request - obj_list = kwargs.pop("obj_list", False) - if obj_list: + collection = kwargs.pop("collection", False) + if collection: kw["url"] = self.endpoint else: - operation = kwargs.pop("operation", "") - kw["url"] = op.normpath(op.join(self.endpoint, self.name, operation)) - + kw["url"] = "{}/{}".format(self.endpoint, self._original_obj["metadata"]["name"]) if self.base: kw["base"] = self.base kw["version"] = self.version @@ -72,7 +61,7 @@ def exists(self, ensure=False): return True def create(self): - r = self.api.post(**self.api_kwargs(data=json.dumps(self.obj), obj_list=True)) + r = self.api.post(**self.api_kwargs(data=json.dumps(self.obj), collection=True)) self.api.raise_for_status(r) self.set_obj(r.json()) @@ -95,6 +84,24 @@ def delete(self): if r.status_code != 404: self.api.raise_for_status(r) + def scale(self,replicas): + r = self.api.patch(**self.api_kwargs( + headers={"Content-Type": "application/merge-patch+json"}, + data='{"spec": {"replicas": %d}}' % replicas, + )) + if r.status_code != 404: + self.api.raise_for_status(r) + + @property + def status(self,all=False): + r = self.api.get(**self.api_kwargs()) + if r.status_code != 404: + self.api.raise_for_status(r) + if all: + return r.json() + else: + return r.json()['status'] + class NamespacedAPIObject(APIObject): @@ -128,13 +135,6 @@ class Deployment(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): endpoint = "deployments" kind = "Deployment" - @property - def ready(self): - return ( - self.obj["status"]["observedGeneration"] >= self.obj["metadata"]["generation"] and - self.obj["status"]["updatedReplicas"] == self.replicas - ) - class Endpoint(NamespacedAPIObject): @@ -143,27 +143,6 @@ class Endpoint(NamespacedAPIObject): kind = "Endpoint" -class Event(NamespacedAPIObject): - - version = "v1" - endpoint = "events" - kind = "Event" - - -class ResourceQuota(NamespacedAPIObject): - - version = "v1" - endpoint = "resourcequotas" - kind = "ResourceQuota" - - -class ServiceAccount(NamespacedAPIObject): - - version = "v1" - endpoint = "serviceaccounts" - kind = "ServiceAccount" - - class Ingress(NamespacedAPIObject): version = "extensions/v1beta1" @@ -171,13 +150,6 @@ class Ingress(NamespacedAPIObject): kind = "Ingress" -class ThirdPartyResource(APIObject): - - version = "extensions/v1beta1" - endpoint = "thirdpartyresources" - kind = "ThirdPartyResource" - - class Job(NamespacedAPIObject, ScalableMixin): version = "batch/v1" @@ -227,6 +199,20 @@ class ReplicationController(NamespacedAPIObject, ReplicatedMixin, ScalableMixin) endpoint = "replicationcontrollers" kind = "ReplicationController" + def wait_to_death(self): + while (self.status['replicas']!=0): + sleep(0.1) + r = self.api.delete(**self.api_kwargs()) + if r.status_code != 404: + self.api.raise_for_status(r) + + + def delete(self, block=True): + self.scale(0) + if block: + self.wait_to_death() + + class ReplicaSet(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): @@ -262,16 +248,3 @@ class PersistentVolumeClaim(NamespacedAPIObject): endpoint = "persistentvolumeclaims" kind = "PersistentVolumeClaim" - -class HorizontalPodAutoscaler(NamespacedAPIObject): - - version = "autoscaling/v1" - endpoint = "horizontalpodautoscalers" - kind = "HorizontalPodAutoscaler" - - -class PetSet(NamespacedAPIObject): - - version = "apps/v1alpha1" - endpoint = "petsets" - kind = "PetSet" diff --git a/pykube/query.py b/pykube/query.py index 9ba879c..17f0d15 100644 --- a/pykube/query.py +++ b/pykube/query.py @@ -74,12 +74,6 @@ def get(self, *args, **kwargs): raise ObjectDoesNotExist("get() returned zero objects") raise ValueError("get() more than one object; use filter") - def get_or_none(self, *args, **kwargs): - try: - return self.get(*args, **kwargs) - except ObjectDoesNotExist: - return None - def watch(self, since=None): kwargs = {"namespace": self.namespace} if since is now: @@ -88,31 +82,20 @@ def watch(self, since=None): kwargs["resource_version"] = since return WatchQuery(self.api, self.api_obj_class, **kwargs) - def execute(self): - kwargs = {"url": self._build_api_url()} - if self.api_obj_class.base: - kwargs["base"] = self.api_obj_class.base - if self.api_obj_class.version: - kwargs["version"] = self.api_obj_class.version - if self.namespace is not None and self.namespace is not all_: - kwargs["namespace"] = self.namespace - r = self.api.get(**kwargs) - r.raise_for_status() - return r - - def iterator(self): - """ - Execute the API request and return an iterator over the objects. This - method does not use the query cache. - """ - for obj in self.execute().json()["items"]: - yield self.api_obj_class(self.api, obj) - @property def query_cache(self): if not hasattr(self, "_query_cache"): cache = {"objects": []} - cache["response"] = self.execute().json() + kwargs = {"url": self._build_api_url()} + if self.api_obj_class.base: + kwargs["base"] = self.api_obj_class.base + if self.api_obj_class.version: + kwargs["version"] = self.api_obj_class.version + if self.namespace is not None and self.namespace is not all_: + kwargs["namespace"] = self.namespace + r = self.api.get(**kwargs) + r.raise_for_status() + cache["response"] = r.json() for obj in cache["response"]["items"]: cache["objects"].append(self.api_obj_class(self.api, obj)) self._query_cache = cache @@ -146,8 +129,6 @@ def object_stream(self): "url": url, "stream": True, } - if self.api_obj_class.version: - kwargs["version"] = self.api_obj_class.version r = self.api.get(**kwargs) self.api.raise_for_status(r) WatchEvent = namedtuple("WatchEvent", "type object") @@ -164,10 +145,8 @@ class ObjectManager(object): def __init__(self, namespace=None): self.namespace = namespace - def __call__(self, api, namespace=None): - if namespace is None: - namespace = self.namespace - return Query(api, self.api_obj_class, namespace=namespace) + def __call__(self, api): + return Query(api, self.api_obj_class, namespace=self.namespace) def __get__(self, obj, api_obj_class): assert obj is None, "cannot invoke objects on resource object." @@ -198,5 +177,6 @@ def as_selector(value): elif op == "notin": s.append("{} notin ({})".format(label, ",".join(v))) else: - raise ValueError("{} is not a valid comparison operator".format(op)) + raise ValueError("{} is not a valid comparsion operator".format(op)) return ",".join(s) + From 96eff1f3256f92cae64dc0f63596917e97267da1 Mon Sep 17 00:00:00 2001 From: inoration Date: Thu, 25 Aug 2016 15:13:01 +0800 Subject: [PATCH 2/7] fix --- pykube/__init__.py | 1 - pykube/config.py | 11 ++++------- pykube/http.py | 3 +-- pykube/objects.py | 10 +++------- pykube/query.py | 1 - 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/pykube/__init__.py b/pykube/__init__.py index 94a93f6..47b9303 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -32,4 +32,3 @@ import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - diff --git a/pykube/config.py b/pykube/config.py index fe7ac1a..d25c9e4 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -71,9 +71,9 @@ def from_file(cls, filename="~/.kube/config", verify=False): filename = os.path.expanduser(filename) if not os.path.isfile(filename): raise exceptions.PyKubeError("Configuration file {} not found".format(filename)) - with open(filename,'r') as f: + with open(filename, 'r') as f: doc = yaml.safe_load(f.read()) - self = cls(doc,verify) + self = cls(doc, verify) self.filename = filename return self @@ -86,10 +86,9 @@ def __init__(self, doc, verify=False): self.verify = verify if "current-context" in doc and doc["current-context"]: self.set_current_context(doc["current-context"]) - if self.current_context == None: + if self.current_context is None: self.set_current_context('default') - self.doc["clusters"][0]["name"] = 'default' #If current_context is not set, consider the first cluster as default. - + self.doc["clusters"][0]["name"] = 'default' # If current_context is not set, consider the first cluster as default. def set_current_context(self, value): """ @@ -115,7 +114,6 @@ def clusters(self): self._clusters = cs return self._clusters - @property def users(self): """ @@ -211,4 +209,3 @@ def filename(self): with tempfile.NamedTemporaryFile(delete=False) as f: f.write(self._bytes) return f.name - diff --git a/pykube/http.py b/pykube/http.py index 0a7c4c6..15bd988 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -50,7 +50,7 @@ def build_session(self, verify=False): s = requests.Session() if "certificate-authority" in self.config.cluster: s.verify = self.config.cluster["certificate-authority"].filename() - if verify == False: + if not verify: s.verify = False if "token" in self.config.user and self.config.user["token"]: s.headers["Authorization"] = "Bearer {}".format(self.config.user["token"]) @@ -183,4 +183,3 @@ def delete(self, *args, **kwargs): - `kwargs`: Keyword arguments """ return self.session.delete(*args, **self.get_kwargs(**kwargs)) - diff --git a/pykube/objects.py b/pykube/objects.py index 949d141..8b1f789 100644 --- a/pykube/objects.py +++ b/pykube/objects.py @@ -21,7 +21,6 @@ def __init__(self, api, obj): self.api = api self.set_obj(obj) - def set_obj(self, obj): self.obj = obj self._original_obj = copy.deepcopy(obj) @@ -84,7 +83,7 @@ def delete(self): if r.status_code != 404: self.api.raise_for_status(r) - def scale(self,replicas): + def scale(self, replicas): r = self.api.patch(**self.api_kwargs( headers={"Content-Type": "application/merge-patch+json"}, data='{"spec": {"replicas": %d}}' % replicas, @@ -93,7 +92,7 @@ def scale(self,replicas): self.api.raise_for_status(r) @property - def status(self,all=False): + def status(self, all=False): r = self.api.get(**self.api_kwargs()) if r.status_code != 404: self.api.raise_for_status(r) @@ -200,20 +199,18 @@ class ReplicationController(NamespacedAPIObject, ReplicatedMixin, ScalableMixin) kind = "ReplicationController" def wait_to_death(self): - while (self.status['replicas']!=0): + while (self.status['replicas'] != 0): sleep(0.1) r = self.api.delete(**self.api_kwargs()) if r.status_code != 404: self.api.raise_for_status(r) - def delete(self, block=True): self.scale(0) if block: self.wait_to_death() - class ReplicaSet(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): version = "extensions/v1beta1" @@ -247,4 +244,3 @@ class PersistentVolumeClaim(NamespacedAPIObject): version = "v1" endpoint = "persistentvolumeclaims" kind = "PersistentVolumeClaim" - diff --git a/pykube/query.py b/pykube/query.py index 17f0d15..e527dbe 100644 --- a/pykube/query.py +++ b/pykube/query.py @@ -179,4 +179,3 @@ def as_selector(value): else: raise ValueError("{} is not a valid comparsion operator".format(op)) return ",".join(s) - From a3aa9e8abcc369cbee5958c13a67859a7853f4b0 Mon Sep 17 00:00:00 2001 From: inoration Date: Fri, 26 Aug 2016 14:35:11 +0800 Subject: [PATCH 3/7] fix3 --- pykube/__init__.py | 2 +- pykube/config.py | 55 ++++++++++++++++++++++++++++-------- pykube/http.py | 16 +++++++---- pykube/objects.py | 70 +++++++++++++++++++++++++++++++++++++++++++--- pykube/query.py | 47 ++++++++++++++++++++++--------- 5 files changed, 155 insertions(+), 35 deletions(-) diff --git a/pykube/__init__.py b/pykube/__init__.py index 47b9303..0e13657 100644 --- a/pykube/__init__.py +++ b/pykube/__init__.py @@ -28,7 +28,7 @@ ) from .query import now, all_ as all, everything # noqa -#temporarily +# temporarily import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) diff --git a/pykube/config.py b/pykube/config.py index d25c9e4..e7942e6 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -66,17 +66,44 @@ def from_file(cls, filename="~/.kube/config", verify=False): :Parameters: - `filename`: The full path to the configuration file - Default: ~/.kube/config """ filename = os.path.expanduser(filename) if not os.path.isfile(filename): raise exceptions.PyKubeError("Configuration file {} not found".format(filename)) - with open(filename, 'r') as f: + with open(filename) as f: doc = yaml.safe_load(f.read()) self = cls(doc, verify) self.filename = filename return self + @classmethod + def from_url(cls, url): + """ + Creates an instance of the KubeConfig class from a single URL (useful + for interacting with kubectl proxy). + """ + doc = { + "clusters": [ + { + "name": "self", + "cluster": { + "server": url, + }, + }, + ], + "contexts": [ + { + "name": "self", + "context": { + "cluster": "self", + }, + } + ], + "current-context": "self", + } + self = cls(doc) + return self + def __init__(self, doc, verify=False): """ Creates an instance of the KubeConfig class. @@ -84,11 +111,16 @@ def __init__(self, doc, verify=False): self.doc = doc self.current_context = None self.verify = verify + for i in range(len(self.doc["clusters"])): + if "name" not in self.doc["clusters"][i]: + self.doc["clusters"][i]["name"] = "default" + str(i) + if not self.doc["contexts"]: + default_cluster = self.doc["clusters"][0]["name"] + self.doc["contexts"] = [{"context": {"cluster": default_cluster}, "name": "default"}] + if not self.doc["current-context"]: + self.doc["current-context"] = "default" if "current-context" in doc and doc["current-context"]: self.set_current_context(doc["current-context"]) - if self.current_context is None: - self.set_current_context('default') - self.doc["clusters"][0]["name"] = 'default' # If current_context is not set, consider the first cluster as default. def set_current_context(self, value): """ @@ -121,10 +153,11 @@ def users(self): """ if not hasattr(self, "_users"): us = {} - for ur in self.doc["users"]: - us[ur["name"]] = u = copy.deepcopy(ur["user"]) - BytesOrFile.maybe_set(u, "client-certificate") - BytesOrFile.maybe_set(u, "client-key") + if "users" in self.doc: + for ur in self.doc["users"]: + us[ur["name"]] = u = copy.deepcopy(ur["user"]) + BytesOrFile.maybe_set(u, "client-certificate") + BytesOrFile.maybe_set(u, "client-key") self._users = us return self._users @@ -157,7 +190,7 @@ def user(self): """ if self.current_context is None: raise exceptions.PyKubeError("current context not set; call set_current_context") - return self.users[self.contexts[self.current_context]["user"]] + return self.users.get(self.contexts[self.current_context].get("user", ""), {}) class BytesOrFile(object): @@ -184,7 +217,7 @@ def __init__(self, data): """ self._filename = None self._bytes = None - if data.startswith("/"): + if os.path.isfile(data): self._filename = data else: self._bytes = base64.b64decode(data) diff --git a/pykube/http.py b/pykube/http.py index 15bd988..ea1eeb6 100644 --- a/pykube/http.py +++ b/pykube/http.py @@ -39,8 +39,8 @@ def url(self): @url.setter def url(self, value): pr = urlparse(value) - if sys.version_info < (2, 7, 9) and ("::" in pr.hostname or _ipv4_re.match(pr.hostname)): - warnings.warn("IP address hostnames are not supported with Python < 2.7.9. Please see https://github.com/kelproject/pykube/issues/29 for more info.", RuntimeWarning) + if sys.version_info < (3, 5) and ("::" in pr.hostname or _ipv4_re.match(pr.hostname)): + warnings.warn("IP address hostnames are not supported with Python < 3.5. Please see https://github.com/kelproject/pykube/issues/29 for more info.", RuntimeWarning) self._url = pr.geturl() def build_session(self, verify=False): @@ -50,15 +50,19 @@ def build_session(self, verify=False): s = requests.Session() if "certificate-authority" in self.config.cluster: s.verify = self.config.cluster["certificate-authority"].filename() - if not verify: - s.verify = False + elif "insecure-skip-tls-verify" in self.config.cluster: + s.verify = not self.config.cluster["insecure-skip-tls-verify"] if "token" in self.config.user and self.config.user["token"]: s.headers["Authorization"] = "Bearer {}".format(self.config.user["token"]) - else: + elif "client-certificate" in self.config.user: s.cert = ( self.config.user["client-certificate"].filename(), self.config.user["client-key"].filename(), ) + else: # no user present; don't configure anything + pass + if not verify: + s.verify = False return s def get_kwargs(self, **kwargs): @@ -71,7 +75,7 @@ def get_kwargs(self, **kwargs): version = kwargs.pop("version", "v1") if version == "v1": base = kwargs.pop("base", "/api") - elif any(map(version.startswith, ["extensions/", "batch/"])): + elif "/" in version: base = kwargs.pop("base", "/apis") else: if "base" not in kwargs: diff --git a/pykube/objects.py b/pykube/objects.py index 8b1f789..6c9d65d 100644 --- a/pykube/objects.py +++ b/pykube/objects.py @@ -1,7 +1,10 @@ import copy import json +import os.path as op from time import sleep +import six + from .exceptions import ObjectDoesNotExist from .mixins import ReplicatedMixin, ScalableMixin from .query import ObjectManager @@ -11,6 +14,7 @@ DEFAULT_NAMESPACE = "default" +@six.python_2_unicode_compatible class APIObject(object): objects = ObjectManager() @@ -25,6 +29,12 @@ def set_obj(self, obj): self.obj = obj self._original_obj = copy.deepcopy(obj) + def __repr__(self): + return "<{kind} {name}>".format(kind=self.kind, name=self.name) + + def __str__(self): + return self.name + @property def name(self): return self.obj["metadata"]["name"] @@ -35,11 +45,14 @@ def annotations(self): def api_kwargs(self, **kwargs): kw = {} - collection = kwargs.pop("collection", False) - if collection: + # Construct url for api request + obj_list = kwargs.pop("obj_list", False) + if obj_list: kw["url"] = self.endpoint else: - kw["url"] = "{}/{}".format(self.endpoint, self._original_obj["metadata"]["name"]) + operation = kwargs.pop("operation", "") + kw["url"] = op.normpath(op.join(self.endpoint, self.name, operation)) + if self.base: kw["base"] = self.base kw["version"] = self.version @@ -60,7 +73,7 @@ def exists(self, ensure=False): return True def create(self): - r = self.api.post(**self.api_kwargs(data=json.dumps(self.obj), collection=True)) + r = self.api.post(**self.api_kwargs(data=json.dumps(self.obj), obj_list=True)) self.api.raise_for_status(r) self.set_obj(r.json()) @@ -134,6 +147,13 @@ class Deployment(NamespacedAPIObject, ReplicatedMixin, ScalableMixin): endpoint = "deployments" kind = "Deployment" + @property + def ready(self): + return ( + self.obj["status"]["observedGeneration"] >= self.obj["metadata"]["generation"] and + self.obj["status"]["updatedReplicas"] == self.replicas + ) + class Endpoint(NamespacedAPIObject): @@ -142,6 +162,27 @@ class Endpoint(NamespacedAPIObject): kind = "Endpoint" +class Event(NamespacedAPIObject): + + version = "v1" + endpoint = "events" + kind = "Event" + + +class ResourceQuota(NamespacedAPIObject): + + version = "v1" + endpoint = "resourcequotas" + kind = "ResourceQuota" + + +class ServiceAccount(NamespacedAPIObject): + + version = "v1" + endpoint = "serviceaccounts" + kind = "ServiceAccount" + + class Ingress(NamespacedAPIObject): version = "extensions/v1beta1" @@ -149,6 +190,13 @@ class Ingress(NamespacedAPIObject): kind = "Ingress" +class ThirdPartyResource(APIObject): + + version = "extensions/v1beta1" + endpoint = "thirdpartyresources" + kind = "ThirdPartyResource" + + class Job(NamespacedAPIObject, ScalableMixin): version = "batch/v1" @@ -244,3 +292,17 @@ class PersistentVolumeClaim(NamespacedAPIObject): version = "v1" endpoint = "persistentvolumeclaims" kind = "PersistentVolumeClaim" + + +class HorizontalPodAutoscaler(NamespacedAPIObject): + + version = "autoscaling/v1" + endpoint = "horizontalpodautoscalers" + kind = "HorizontalPodAutoscaler" + + +class PetSet(NamespacedAPIObject): + + version = "apps/v1alpha1" + endpoint = "petsets" + kind = "PetSet" diff --git a/pykube/query.py b/pykube/query.py index e527dbe..9ba879c 100644 --- a/pykube/query.py +++ b/pykube/query.py @@ -74,6 +74,12 @@ def get(self, *args, **kwargs): raise ObjectDoesNotExist("get() returned zero objects") raise ValueError("get() more than one object; use filter") + def get_or_none(self, *args, **kwargs): + try: + return self.get(*args, **kwargs) + except ObjectDoesNotExist: + return None + def watch(self, since=None): kwargs = {"namespace": self.namespace} if since is now: @@ -82,20 +88,31 @@ def watch(self, since=None): kwargs["resource_version"] = since return WatchQuery(self.api, self.api_obj_class, **kwargs) + def execute(self): + kwargs = {"url": self._build_api_url()} + if self.api_obj_class.base: + kwargs["base"] = self.api_obj_class.base + if self.api_obj_class.version: + kwargs["version"] = self.api_obj_class.version + if self.namespace is not None and self.namespace is not all_: + kwargs["namespace"] = self.namespace + r = self.api.get(**kwargs) + r.raise_for_status() + return r + + def iterator(self): + """ + Execute the API request and return an iterator over the objects. This + method does not use the query cache. + """ + for obj in self.execute().json()["items"]: + yield self.api_obj_class(self.api, obj) + @property def query_cache(self): if not hasattr(self, "_query_cache"): cache = {"objects": []} - kwargs = {"url": self._build_api_url()} - if self.api_obj_class.base: - kwargs["base"] = self.api_obj_class.base - if self.api_obj_class.version: - kwargs["version"] = self.api_obj_class.version - if self.namespace is not None and self.namespace is not all_: - kwargs["namespace"] = self.namespace - r = self.api.get(**kwargs) - r.raise_for_status() - cache["response"] = r.json() + cache["response"] = self.execute().json() for obj in cache["response"]["items"]: cache["objects"].append(self.api_obj_class(self.api, obj)) self._query_cache = cache @@ -129,6 +146,8 @@ def object_stream(self): "url": url, "stream": True, } + if self.api_obj_class.version: + kwargs["version"] = self.api_obj_class.version r = self.api.get(**kwargs) self.api.raise_for_status(r) WatchEvent = namedtuple("WatchEvent", "type object") @@ -145,8 +164,10 @@ class ObjectManager(object): def __init__(self, namespace=None): self.namespace = namespace - def __call__(self, api): - return Query(api, self.api_obj_class, namespace=self.namespace) + def __call__(self, api, namespace=None): + if namespace is None: + namespace = self.namespace + return Query(api, self.api_obj_class, namespace=namespace) def __get__(self, obj, api_obj_class): assert obj is None, "cannot invoke objects on resource object." @@ -177,5 +198,5 @@ def as_selector(value): elif op == "notin": s.append("{} notin ({})".format(label, ",".join(v))) else: - raise ValueError("{} is not a valid comparsion operator".format(op)) + raise ValueError("{} is not a valid comparison operator".format(op)) return ",".join(s) From 0e09320708548e59a7e8585c04054da180d24795 Mon Sep 17 00:00:00 2001 From: inoration Date: Fri, 26 Aug 2016 14:45:27 +0800 Subject: [PATCH 4/7] fix --- pykube/config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pykube/config.py b/pykube/config.py index e7942e6..f8b6f54 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -72,6 +72,14 @@ def from_file(cls, filename="~/.kube/config", verify=False): raise exceptions.PyKubeError("Configuration file {} not found".format(filename)) with open(filename) as f: doc = yaml.safe_load(f.read()) + for i in range(len(doc["clusters"])): + if "name" not in doc["clusters"][i]: + doc["clusters"][i]["name"] = "default" + str(i) + if not doc["contexts"]: + default_cluster = doc["clusters"][0]["name"] + doc["contexts"] = [{"context": {"cluster": default_cluster}, "name": "default"}] + if not doc["current-context"]: + doc["current-context"] = "default" self = cls(doc, verify) self.filename = filename return self @@ -111,14 +119,6 @@ def __init__(self, doc, verify=False): self.doc = doc self.current_context = None self.verify = verify - for i in range(len(self.doc["clusters"])): - if "name" not in self.doc["clusters"][i]: - self.doc["clusters"][i]["name"] = "default" + str(i) - if not self.doc["contexts"]: - default_cluster = self.doc["clusters"][0]["name"] - self.doc["contexts"] = [{"context": {"cluster": default_cluster}, "name": "default"}] - if not self.doc["current-context"]: - self.doc["current-context"] = "default" if "current-context" in doc and doc["current-context"]: self.set_current_context(doc["current-context"]) From de9b8f508a8576b901549835b8fee90f58ec161e Mon Sep 17 00:00:00 2001 From: inoration Date: Fri, 26 Aug 2016 14:55:04 +0800 Subject: [PATCH 5/7] fix --- pykube/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pykube/config.py b/pykube/config.py index f8b6f54..00fe19b 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -78,7 +78,7 @@ def from_file(cls, filename="~/.kube/config", verify=False): if not doc["contexts"]: default_cluster = doc["clusters"][0]["name"] doc["contexts"] = [{"context": {"cluster": default_cluster}, "name": "default"}] - if not doc["current-context"]: + if "current-context" not in doc or not doc["current-context"]: doc["current-context"] = "default" self = cls(doc, verify) self.filename = filename From 10bb825bf0f3b48b0edd345684c848d7c5b3cceb Mon Sep 17 00:00:00 2001 From: inoration Date: Fri, 26 Aug 2016 15:10:39 +0800 Subject: [PATCH 6/7] fix --- pykube/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pykube/config.py b/pykube/config.py index 00fe19b..67e47e7 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -79,7 +79,7 @@ def from_file(cls, filename="~/.kube/config", verify=False): default_cluster = doc["clusters"][0]["name"] doc["contexts"] = [{"context": {"cluster": default_cluster}, "name": "default"}] if "current-context" not in doc or not doc["current-context"]: - doc["current-context"] = "default" + doc["current-context"] = doc["contexts"][0]["name"] self = cls(doc, verify) self.filename = filename return self From af092e61d40dbfbb96f534f2cdd1f28d33fafcd6 Mon Sep 17 00:00:00 2001 From: inoration Date: Fri, 26 Aug 2016 15:40:17 +0800 Subject: [PATCH 7/7] fix --- pykube/config.py | 2 ++ test/test_config.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pykube/config.py b/pykube/config.py index 67e47e7..04afaf9 100644 --- a/pykube/config.py +++ b/pykube/config.py @@ -72,6 +72,8 @@ def from_file(cls, filename="~/.kube/config", verify=False): raise exceptions.PyKubeError("Configuration file {} not found".format(filename)) with open(filename) as f: doc = yaml.safe_load(f.read()) + if "clusters" not in doc or not doc["clusters"]: + raise exceptions.PyKubeError("Do not have cluster.") for i in range(len(doc["clusters"])): if "name" not in doc["clusters"][i]: doc["clusters"][i]["name"] = "default" + str(i) diff --git a/test/test_config.py b/test/test_config.py index 56a2238..192f0e1 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -68,6 +68,8 @@ def test_contexts(self): {"cluster": "thecluster", "user": "admin"}, self.cfg.contexts.get("thename", None)) + ''' + Now this will not fail without current_context def test_cluster(self): """ Verify cluster works as expected. @@ -101,7 +103,8 @@ def test_user(self): self.cfg.set_current_context("thename") self.assertEqual("data", self.cfg.user) - + ''' + def test_default_user(self): """ User can sometimes be specified as 'default' with no corresponding definition