From 82988518403fb5c5d9c3048a0c911056357d478b Mon Sep 17 00:00:00 2001 From: Radoslav Pitonak Date: Sat, 25 Aug 2018 20:19:28 +0200 Subject: [PATCH 1/6] pod readiness --- conu/backend/k8s/pod.py | 52 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/conu/backend/k8s/pod.py b/conu/backend/k8s/pod.py index 1b2af873..2d3b54c5 100644 --- a/conu/backend/k8s/pod.py +++ b/conu/backend/k8s/pod.py @@ -105,14 +105,28 @@ def get_phase(self): return self.phase + def get_conditions(self): + """ + get conditions through which the pod has or has not passed + :return: list of PodCondition enum + """ + + return [PodCondition.get_from_string(c.type) for c in self.get_status().conditions + if c.status == 'True'] + + def is_ready(self): + if PodCondition.READY in self.get_conditions(): + return True + return False + def wait(self, timeout=15): """ - block until pod is not running, raises an exc ProbeTimeout if timeout is reached + block until pod is not ready, raises an exc ProbeTimeout if timeout is reached :param timeout: int or float (seconds), time to wait for pod to run :return: None """ - Probe(timeout=timeout, fnc=self.get_phase, expected_retval=PodPhase.RUNNING).run() + Probe(timeout=timeout, fnc=self.is_ready, expected_retval=True).run() @staticmethod def create(image_data): @@ -193,3 +207,37 @@ def get_from_string(cls, string_phase): return cls.UNKNOWN return cls.UNKNOWN + + +class PodCondition(enum.Enum): + """ + https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions + """ + + SCHEDULED = 0 + READY = 1 + INITIALIZED = 2 + UNSCHEDULABLE = 3 + CONTAINERS_READY = 4 + UNKNOWN = 5 + + @classmethod + def get_from_string(cls, string_condition): + """ + Convert string value obtained from k8s API to PodCondition enum value + :param string_condition: str, condition value from Kubernetes API + :return: PodCondition + """ + + if string_condition == 'PodScheduled': + return cls.SCHEDULED + elif string_condition == 'Ready': + return cls.READY + elif string_condition == 'Initialized': + return cls.INITIALIZED + elif string_condition == 'Unschedulable': + return cls.UNSCHEDULABLE + elif string_condition == 'ContainersReady': + return cls.CONTAINERS_READY + + return cls.UNKNOWN From bfb0e04cba6a42a18b0333e93f36981b5c34020c Mon Sep 17 00:00:00 2001 From: Radoslav Pitonak Date: Sat, 25 Aug 2018 21:24:30 +0200 Subject: [PATCH 2/6] pod logs and change test image to nginx --- conu/backend/k8s/pod.py | 15 +++++++++++++++ tests/integration/test_k8s.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/conu/backend/k8s/pod.py b/conu/backend/k8s/pod.py index 2d3b54c5..1d32748b 100644 --- a/conu/backend/k8s/pod.py +++ b/conu/backend/k8s/pod.py @@ -93,6 +93,21 @@ def get_ip(self): return self.get_status().pod_ip + def get_logs(self): + """ + print logs from pod + :return: None + """ + try: + api_response = self.core_api.read_namespaced_pod_log(self.name, self.namespace) + except ApiException as e: + raise ConuException( + "Exception when calling Kubernetes API - read_namespaced_pod_log: %s\n" % e) + + logger.info("Logs from pod: %s in namespace: %s", self.name, self.namespace) + for line in api_response.split('\n'): + logger.info(line) + def get_phase(self): """ get phase of the pod diff --git a/tests/integration/test_k8s.py b/tests/integration/test_k8s.py index 2148748a..53a5ac9a 100644 --- a/tests/integration/test_k8s.py +++ b/tests/integration/test_k8s.py @@ -39,7 +39,7 @@ def test_pod(): namespace = k8s_backend.create_namespace() with DockerBackend() as backend: - image = backend.ImageClass(FEDORA_MINIMAL_REPOSITORY, tag=FEDORA_MINIMAL_REPOSITORY_TAG) + image = backend.ImageClass('nginx') pod = image.run_in_pod(namespace=namespace) @@ -95,7 +95,7 @@ def test_list_pods(): with DockerBackend() as backend: - image = backend.ImageClass(FEDORA_MINIMAL_REPOSITORY, tag=FEDORA_MINIMAL_REPOSITORY_TAG) + image = backend.ImageClass('nginx') pod = image.run_in_pod(namespace=namespace) From 5eadf52fa3fd4f81b41d75f5df90944ac5c74e87 Mon Sep 17 00:00:00 2001 From: Radoslav Pitonak Date: Tue, 4 Sep 2018 08:13:25 +0200 Subject: [PATCH 3/6] pod example --- conu/backend/k8s/pod.py | 4 +++- docs/source/examples/k8s_pod.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 docs/source/examples/k8s_pod.py diff --git a/conu/backend/k8s/pod.py b/conu/backend/k8s/pod.py index 1d32748b..f9458eb3 100644 --- a/conu/backend/k8s/pod.py +++ b/conu/backend/k8s/pod.py @@ -99,7 +99,8 @@ def get_logs(self): :return: None """ try: - api_response = self.core_api.read_namespaced_pod_log(self.name, self.namespace) + api_response = self.core_api.read_namespaced_pod_log(self.name, self.namespace, + previous=True) except ApiException as e: raise ConuException( "Exception when calling Kubernetes API - read_namespaced_pod_log: %s\n" % e) @@ -131,6 +132,7 @@ def get_conditions(self): def is_ready(self): if PodCondition.READY in self.get_conditions(): + logger.info("Pod: %s in namespace: %s is ready!", self.name, self.namespace) return True return False diff --git a/docs/source/examples/k8s_pod.py b/docs/source/examples/k8s_pod.py new file mode 100644 index 00000000..b7ade3c6 --- /dev/null +++ b/docs/source/examples/k8s_pod.py @@ -0,0 +1,22 @@ +from conu.backend.k8s.backend import K8sBackend +from conu.backend.docker.backend import DockerBackend +from conu.backend.k8s.pod import PodPhase + + +with K8sBackend() as k8s_backend: + + namespace = k8s_backend.create_namespace() + + with DockerBackend() as backend: + image = backend.ImageClass('nginx') + + pod = image.run_in_pod(namespace=namespace) + + try: + pod.wait(200) + assert pod.is_ready() + finally: + pod.get_logs() + pod.delete() + assert pod.get_phase() == PodPhase.TERMINATING + k8s_backend.delete_namespace(namespace) From c9240b02d10e8f89eca90c3093fa30b9ea5d7335 Mon Sep 17 00:00:00 2001 From: Radoslav Pitonak Date: Thu, 6 Sep 2018 21:06:27 +0200 Subject: [PATCH 4/6] remove logs exception, update pod example --- conu/backend/k8s/pod.py | 13 +++++-------- docs/source/examples/k8s_pod.py | 8 +++++--- tests/integration/test_k8s.py | 1 - 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/conu/backend/k8s/pod.py b/conu/backend/k8s/pod.py index f9458eb3..4ada8c25 100644 --- a/conu/backend/k8s/pod.py +++ b/conu/backend/k8s/pod.py @@ -99,15 +99,12 @@ def get_logs(self): :return: None """ try: - api_response = self.core_api.read_namespaced_pod_log(self.name, self.namespace, - previous=True) + api_response = self.core_api.read_namespaced_pod_log(self.name, self.namespace) + logger.info("Logs from pod: %s in namespace: %s", self.name, self.namespace) + for line in api_response.split('\n'): + logger.info(line) except ApiException as e: - raise ConuException( - "Exception when calling Kubernetes API - read_namespaced_pod_log: %s\n" % e) - - logger.info("Logs from pod: %s in namespace: %s", self.name, self.namespace) - for line in api_response.split('\n'): - logger.info(line) + logger.info("Cannot get pod logs because of exeption during calling Kubernetes API %s\n", e) def get_phase(self): """ diff --git a/docs/source/examples/k8s_pod.py b/docs/source/examples/k8s_pod.py index b7ade3c6..ad273736 100644 --- a/docs/source/examples/k8s_pod.py +++ b/docs/source/examples/k8s_pod.py @@ -2,17 +2,19 @@ from conu.backend.docker.backend import DockerBackend from conu.backend.k8s.pod import PodPhase +import logging -with K8sBackend() as k8s_backend: +API_KEY = "M0XufKHjTsl87t1A4y7Vp0qAYSiKq8n7QauYI3sAHcU" +with K8sBackend(api_key=API_KEY, logging_level=logging.DEBUG) as k8s_backend: namespace = k8s_backend.create_namespace() - with DockerBackend() as backend: + with DockerBackend(logging_level=logging.DEBUG) as backend: image = backend.ImageClass('nginx') pod = image.run_in_pod(namespace=namespace) - try: + pod.get_logs() pod.wait(200) assert pod.is_ready() finally: diff --git a/tests/integration/test_k8s.py b/tests/integration/test_k8s.py index 53a5ac9a..585590a1 100644 --- a/tests/integration/test_k8s.py +++ b/tests/integration/test_k8s.py @@ -28,7 +28,6 @@ from conu.backend.k8s.deployment import Deployment from conu.backend.k8s.client import get_core_api -from ..constants import FEDORA_MINIMAL_REPOSITORY, FEDORA_MINIMAL_REPOSITORY_TAG urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) From ed4cdf9cd592f49396d7c67d6ac33cbfb98bd58a Mon Sep 17 00:00:00 2001 From: Radoslav Pitonak Date: Fri, 7 Sep 2018 09:43:04 +0200 Subject: [PATCH 5/6] docs for new methods in Pod class --- conu/backend/k8s/pod.py | 13 ++++++++++--- docs/source/examples/k8s_pod.py | 1 + tests/integration/test_k8s.py | 1 + 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/conu/backend/k8s/pod.py b/conu/backend/k8s/pod.py index 4ada8c25..f22b725e 100644 --- a/conu/backend/k8s/pod.py +++ b/conu/backend/k8s/pod.py @@ -104,7 +104,9 @@ def get_logs(self): for line in api_response.split('\n'): logger.info(line) except ApiException as e: - logger.info("Cannot get pod logs because of exeption during calling Kubernetes API %s\n", e) + # no reason to throw exception when logs cannot be obtain, just notify user + logger.info("Cannot get pod logs because of " + "exception during calling Kubernetes API %s\n", e) def get_phase(self): """ @@ -120,14 +122,19 @@ def get_phase(self): def get_conditions(self): """ - get conditions through which the pod has or has not passed - :return: list of PodCondition enum + get conditions through which the pod has passed + :return: list of PodCondition enum or empty list """ + # filter just values that are true (means that pod has that condition right now) return [PodCondition.get_from_string(c.type) for c in self.get_status().conditions if c.status == 'True'] def is_ready(self): + """ + Check if pod is in READY condition + :return: bool + """ if PodCondition.READY in self.get_conditions(): logger.info("Pod: %s in namespace: %s is ready!", self.name, self.namespace) return True diff --git a/docs/source/examples/k8s_pod.py b/docs/source/examples/k8s_pod.py index ad273736..857ba2b7 100644 --- a/docs/source/examples/k8s_pod.py +++ b/docs/source/examples/k8s_pod.py @@ -4,6 +4,7 @@ import logging +# insert your API key API_KEY = "M0XufKHjTsl87t1A4y7Vp0qAYSiKq8n7QauYI3sAHcU" with K8sBackend(api_key=API_KEY, logging_level=logging.DEBUG) as k8s_backend: diff --git a/tests/integration/test_k8s.py b/tests/integration/test_k8s.py index 585590a1..2649e4f6 100644 --- a/tests/integration/test_k8s.py +++ b/tests/integration/test_k8s.py @@ -44,6 +44,7 @@ def test_pod(): try: pod.wait(200) + assert pod.is_ready() assert pod.get_phase() == PodPhase.RUNNING finally: pod.delete() From 42c422ca0c3cbcebe4e7bd4d70a45d925f4ae326 Mon Sep 17 00:00:00 2001 From: Radoslav Pitonak Date: Wed, 12 Sep 2018 09:10:15 +0200 Subject: [PATCH 6/6] return logs in pod get_logs() method --- conu/backend/k8s/pod.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conu/backend/k8s/pod.py b/conu/backend/k8s/pod.py index f22b725e..7fdc875d 100644 --- a/conu/backend/k8s/pod.py +++ b/conu/backend/k8s/pod.py @@ -96,18 +96,21 @@ def get_ip(self): def get_logs(self): """ print logs from pod - :return: None + :return: str or None """ try: api_response = self.core_api.read_namespaced_pod_log(self.name, self.namespace) logger.info("Logs from pod: %s in namespace: %s", self.name, self.namespace) for line in api_response.split('\n'): logger.info(line) + return api_response except ApiException as e: # no reason to throw exception when logs cannot be obtain, just notify user logger.info("Cannot get pod logs because of " "exception during calling Kubernetes API %s\n", e) + return None + def get_phase(self): """ get phase of the pod