From f20f80013795a38eadcfcbe16510337d80f0f5aa Mon Sep 17 00:00:00 2001 From: "Pasch, Frederik" Date: Mon, 12 Aug 2024 17:07:05 +0200 Subject: [PATCH] add support for more kubernetes functionality --- docs/libraries.rst | 64 ++++++++++++ .../kubernetes_base_action.py | 78 +++++++++++++++ .../kubernetes_create_from_yaml.py | 8 +- .../kubernetes_patch_pod.py | 39 ++++++++ .../kubernetes_pod_exec.py | 99 +++++++++++++++++++ .../lib_osc/kubernetes.osc | 14 ++- .../test_kubernetes_create_delete.osc | 2 +- .../scenarios/test_kubernetes_pod_exec.osc | 14 +++ libs/scenario_execution_kubernetes/setup.py | 2 + 9 files changed, 315 insertions(+), 5 deletions(-) create mode 100644 libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_base_action.py create mode 100644 libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_patch_pod.py create mode 100644 libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_pod_exec.py create mode 100644 libs/scenario_execution_kubernetes/scenarios/test_kubernetes_pod_exec.osc diff --git a/docs/libraries.rst b/docs/libraries.rst index a3f15014..abd1c91f 100644 --- a/docs/libraries.rst +++ b/docs/libraries.rst @@ -447,6 +447,70 @@ Patch an existing Kubernetes network policy. - key-value pair to match (e.g., ``key_value("app", "pod_name"))`` +``kubernetes_patch_pod()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Patch an existing pod. If patching resources, please check `feature gates `__ + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``namespace`` + - ``string`` + - ``default`` + - Kubernetes namespace + * - ``within_cluster`` + - ``bool`` + - ``false`` + - set to true if you want to access the cluster from within a running container/pod + * - ``target`` + - ``string`` + - + - The target pod to patch + * - ``body`` + - ``string`` + - + - Patch to apply. Example: ``'{\"spec\":{\"containers\":[{\"name\":\"main\", \"resources\":{\"requests\":{\"cpu\":\"200m\"}, \"limits\":{\"cpu\":\"200m\"}}}]}}'`` + + +``kubernetes_pod_exec()`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Execute a command within a running pod + +.. list-table:: + :widths: 15 15 5 65 + :header-rows: 1 + :class: tight-table + + * - Parameter + - Type + - Default + - Description + * - ``namespace`` + - ``string`` + - ``default`` + - Kubernetes namespace + * - ``within_cluster`` + - ``bool`` + - ``false`` + - set to true if you want to access the cluster from within a running container/pod + * - ``target`` + - ``string`` + - + - The target pod to execute the command in + * - ``command`` + - ``list of string`` + - + - Command to execute + + ``kubernetes_wait_for_network_policy_status()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_base_action.py b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_base_action.py new file mode 100644 index 00000000..6c45509d --- /dev/null +++ b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_base_action.py @@ -0,0 +1,78 @@ +# Copyright (C) 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from kubernetes import client, config +from enum import Enum +import py_trees +import json +from scenario_execution.actions.base_action import BaseAction + + +class KubernetesBaseActionState(Enum): + IDLE = 1 + REQUEST_SENT = 2 + FAILURE = 3 + + +class KubernetesBaseAction(BaseAction): + + def __init__(self, namespace: str, within_cluster: bool): + super().__init__() + self.namespace = namespace + self.within_cluster = within_cluster + self.client = None + self.current_state = KubernetesBaseActionState.IDLE + self.current_request = None + + def setup(self, **kwargs): + if self.within_cluster: + config.load_incluster_config() + else: + config.load_kube_config() + self.client = client.CoreV1Api() + + def execute(self, namespace: str, within_cluster: bool): + self.namespace = namespace + if within_cluster != self.within_cluster: + raise ValueError("parameter 'within_cluster' is not allowed to change since initialization.") + + def update(self) -> py_trees.common.Status: # pylint: disable=too-many-return-statements + if self.current_state == KubernetesBaseActionState.IDLE: + self.current_request = self.kubernetes_call() + self.current_state = KubernetesBaseActionState.REQUEST_SENT + return py_trees.common.Status.RUNNING + elif self.current_state == KubernetesBaseActionState.REQUEST_SENT: + success = True + if self.current_request.ready(): + if not self.current_request.successful(): + try: + self.current_request.get() + except client.exceptions.ApiException as e: + message = "" + body = json.loads(e.body) + if "message" in body: + message = f", message: '{body['message']}'" + self.feedback_message = f"Failure! Reason: {e.reason} {message}" # pylint: disable= attribute-defined-outside-init + success = False + if success: + return py_trees.common.Status.SUCCESS + else: + return py_trees.common.Status.FAILURE + return py_trees.common.Status.FAILURE + + def kubernetes_call(self): + # Use async_req = True, namespace=self.namespace + raise NotImplementedError("Implement in derived action") diff --git a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_create_from_yaml.py b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_create_from_yaml.py index 8ea32f81..9c91a20f 100644 --- a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_create_from_yaml.py +++ b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_create_from_yaml.py @@ -47,8 +47,12 @@ def setup(self, **kwargs): def update(self) -> py_trees.common.Status: # pylint: disable=too-many-return-statements if self.current_state == KubernetesCreateFromYamlActionState.IDLE: - self.current_request = utils.create_from_yaml( - self.client, self.yaml_file, verbose=False, namespace=self.namespace, async_req=True) + try: + self.current_request = utils.create_from_yaml( + self.client, self.yaml_file, verbose=False, namespace=self.namespace, async_req=True) + except Exception as e: # pylint: disable=broad-except + self.feedback_message = f"Error while creating from yaml: {e}" + return py_trees.common.Status.FAILURE self.current_state = KubernetesCreateFromYamlActionState.CREATION_REQUESTED self.feedback_message = f"Requested creation from yaml file '{self.yaml_file}' in namespace '{self.namespace}'" # pylint: disable= attribute-defined-outside-init return py_trees.common.Status.RUNNING diff --git a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_patch_pod.py b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_patch_pod.py new file mode 100644 index 00000000..33ee2a84 --- /dev/null +++ b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_patch_pod.py @@ -0,0 +1,39 @@ +# Copyright (C) 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +from ast import literal_eval +from .kubernetes_base_action import KubernetesBaseAction + + +class KubernetesPatchPod(KubernetesBaseAction): + + def __init__(self, namespace: str, target: str, body: str, within_cluster: bool): + super().__init__(namespace, within_cluster) + self.target = target + self.body = None + + def execute(self, namespace: str, target: str, body: str, within_cluster: bool): # pylint: disable=arguments-differ + super().execute(namespace, within_cluster) + self.target = target + trimmed_data = body.encode('utf-8').decode('unicode_escape') + try: + self.body = literal_eval(trimmed_data) + except ValueError as e: + raise ValueError(f"Could not parse body '{trimmed_data}': {e}") from e + + def kubernetes_call(self): + self.feedback_message = f"Requested patching '{self.target}' in namespace '{self.namespace}'" # pylint: disable= attribute-defined-outside-init + return self.client.patch_namespaced_pod(self.target, body=self.body, namespace=self.namespace, async_req=True) diff --git a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_pod_exec.py b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_pod_exec.py new file mode 100644 index 00000000..008c90c7 --- /dev/null +++ b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_pod_exec.py @@ -0,0 +1,99 @@ +# Copyright (C) 2024 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +import py_trees +from scenario_execution.actions.base_action import BaseAction +import queue +import threading +from kubernetes import client, config, stream +from enum import Enum + + +class KubernetesPodExecState(Enum): + IDLE = 1 + RUNNING = 2 + FAILURE = 3 + + +class KubernetesPodExec(BaseAction): + + def __init__(self, target: str, command: list, namespace: str, within_cluster: bool): + super().__init__() + self.target = target + self.namespace = namespace + self.command = command + self.within_cluster = within_cluster + self.client = None + self.reponse_queue = queue.Queue() + self.current_state = KubernetesPodExecState.IDLE + self.output_queue = queue.Queue() + + def setup(self, **kwargs): + if self.within_cluster: + config.load_incluster_config() + else: + config.load_kube_config() + self.client = client.CoreV1Api() + + self.exec_thread = threading.Thread(target=self.pod_exec, daemon=True) + + def execute(self, target: str, command: list, namespace: str, within_cluster: bool): + if within_cluster != self.within_cluster: + raise ValueError("parameter 'within_cluster' is not allowed to change since initialization.") + self.target = target + self.namespace = namespace + self.command = command + self.current_state = KubernetesPodExecState.IDLE + + def update(self) -> py_trees.common.Status: + if self.current_state == KubernetesPodExecState.IDLE: + self.current_state = KubernetesPodExecState.RUNNING + self.feedback_message = f"Executing on pod '{self.target}': {self.command}..." # pylint: disable= attribute-defined-outside-init + self.exec_thread.start() + return py_trees.common.Status.RUNNING + elif self.current_state == KubernetesPodExecState.RUNNING: + while not self.output_queue.empty(): + self.logger.debug(self.output_queue.get()) + try: + response = self.reponse_queue.get_nowait() + try: + if response.returncode == 0: + self.feedback_message = f"Execution successful." # pylint: disable= attribute-defined-outside-init + return py_trees.common.Status.SUCCESS + except ValueError: + self.feedback_message = f"Error while executing." # pylint: disable= attribute-defined-outside-init + except queue.Empty: + return py_trees.common.Status.RUNNING + + return py_trees.common.Status.FAILURE + + def pod_exec(self): + resp = stream.stream(self.client.connect_get_namespaced_pod_exec, + self.target, + self.namespace, + command=self.command, + stderr=True, stdin=False, + stdout=True, tty=False, + _preload_content=False) + + while resp.is_open(): + resp.update(timeout=0.1) + if resp.peek_stdout(): + self.output_queue.put(resp.read_stdout()) + if resp.peek_stderr(): + self.output_queue.put(resp.read_stderr()) + + self.reponse_queue.put(resp) diff --git a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/lib_osc/kubernetes.osc b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/lib_osc/kubernetes.osc index 14910244..47cebcb4 100644 --- a/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/lib_osc/kubernetes.osc +++ b/libs/scenario_execution_kubernetes/scenario_execution_kubernetes/lib_osc/kubernetes.osc @@ -35,9 +35,19 @@ action kubernetes_delete inherits kubernetes_base_action: action kubernetes_patch_network_policy inherits kubernetes_base_action: # patch an existing network policy - target: string # network-policy name to monitor + target: string # network-policy to patch network_enabled: bool # should the network be enabled? - match_label: key_value # key-value pair to match + match_label: key_value + +action kubernetes_patch_pod inherits kubernetes_base_action: + # patch an existing pod. If patching resources, please check feature gates: https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#container-resize-policies + target: string # pod to patch + body: string # patch to apply + +action kubernetes_pod_exec inherits kubernetes_base_action: + # execute a command within a running pod + target: string # pod to patch + command: list of string # command to execute action kubernetes_wait_for_network_policy_status inherits kubernetes_base_action: # wait for a network-policy to reach the specified state diff --git a/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_create_delete.osc b/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_create_delete.osc index 81a558cb..7f98de05 100644 --- a/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_create_delete.osc +++ b/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_create_delete.osc @@ -3,7 +3,7 @@ import osc.kubernetes import osc.helpers scenario test_kubernetes_create_from_yaml: - timeout(30s) + timeout(60s) do serial: kubernetes_create_from_yaml(yaml_file: "test.yaml") kubernetes_wait_for_pod_status(target: "test", status: kubernetes_pod_status!running) diff --git a/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_pod_exec.osc b/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_pod_exec.osc new file mode 100644 index 00000000..786b7236 --- /dev/null +++ b/libs/scenario_execution_kubernetes/scenarios/test_kubernetes_pod_exec.osc @@ -0,0 +1,14 @@ +import osc.standard.base +import osc.kubernetes +import osc.helpers + +scenario test_kubernetes_create_from_yaml: + timeout(60s) + do serial: + kubernetes_create_from_yaml(yaml_file: "test.yaml") + kubernetes_wait_for_pod_status(target: "test", status: kubernetes_pod_status!running) + kubernetes_patch_pod(target: "test", body: '{\"spec\":{\"containers\":[{\"name\":\"main\", \"resources\":{\"requests\":{\"cpu\":\"200m\"}, \"limits\":{\"cpu\":\"200m\"}}}]}}') + kubernetes_pod_exec(target: "test", command: ['sysbench', 'cpu', 'run']) + kubernetes_patch_pod(target: "test", body: '{\"spec\":{\"containers\":[{\"name\":\"main\", \"resources\":{\"requests\":{\"cpu\":\"800m\"}, \"limits\":{\"cpu\":\"800m\"}}}]}}') + kubernetes_pod_exec(target: "test", command: ['sysbench', 'cpu', 'run']) + kubernetes_delete(target: "test", element_type: kubernetes_element_type!pod) diff --git a/libs/scenario_execution_kubernetes/setup.py b/libs/scenario_execution_kubernetes/setup.py index a75043d7..9f23de7e 100644 --- a/libs/scenario_execution_kubernetes/setup.py +++ b/libs/scenario_execution_kubernetes/setup.py @@ -44,6 +44,8 @@ 'kubernetes_create_from_yaml = scenario_execution_kubernetes.kubernetes_create_from_yaml:KubernetesCreateFromYaml', 'kubernetes_delete = scenario_execution_kubernetes.kubernetes_delete:KubernetesDelete', 'kubernetes_patch_network_policy = scenario_execution_kubernetes.kubernetes_patch_network_policy:KubernetesPatchNetworkPolicy', + 'kubernetes_patch_pod = scenario_execution_kubernetes.kubernetes_patch_pod:KubernetesPatchPod', + 'kubernetes_pod_exec = scenario_execution_kubernetes.kubernetes_pod_exec:KubernetesPodExec', 'kubernetes_wait_for_network_policy_status = scenario_execution_kubernetes.kubernetes_wait_for_network_policy_status:KubernetesWaitForNetworkPolicyStatus', 'kubernetes_wait_for_pod_status = scenario_execution_kubernetes.kubernetes_wait_for_pod_status:KubernetesWaitForPodStatus', ],