Skip to content

Commit

Permalink
add support for more kubernetes functionality (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
fred-labs authored Aug 12, 2024
1 parent e50339c commit c0b6a77
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 5 deletions.
64 changes: 64 additions & 0 deletions docs/libraries.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/#container-resize-policies>`__

.. 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()``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions libs/scenario_execution_kubernetes/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand Down

0 comments on commit c0b6a77

Please sign in to comment.