Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add kubernetes actions #153

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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