Skip to content
This repository was archived by the owner on Jul 16, 2025. It is now read-only.

Commit c0b6a77

Browse files
author
fred-labs
authored
add support for more kubernetes functionality (#153)
1 parent e50339c commit c0b6a77

File tree

9 files changed

+315
-5
lines changed

9 files changed

+315
-5
lines changed

docs/libraries.rst

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,70 @@ Patch an existing Kubernetes network policy.
447447
- key-value pair to match (e.g., ``key_value("app", "pod_name"))``
448448

449449

450+
``kubernetes_patch_pod()``
451+
^^^^^^^^^^^^^^^^^^^^^^^^^^
452+
453+
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>`__
454+
455+
.. list-table::
456+
:widths: 15 15 5 65
457+
:header-rows: 1
458+
:class: tight-table
459+
460+
* - Parameter
461+
- Type
462+
- Default
463+
- Description
464+
* - ``namespace``
465+
- ``string``
466+
- ``default``
467+
- Kubernetes namespace
468+
* - ``within_cluster``
469+
- ``bool``
470+
- ``false``
471+
- set to true if you want to access the cluster from within a running container/pod
472+
* - ``target``
473+
- ``string``
474+
-
475+
- The target pod to patch
476+
* - ``body``
477+
- ``string``
478+
-
479+
- Patch to apply. Example: ``'{\"spec\":{\"containers\":[{\"name\":\"main\", \"resources\":{\"requests\":{\"cpu\":\"200m\"}, \"limits\":{\"cpu\":\"200m\"}}}]}}'``
480+
481+
482+
``kubernetes_pod_exec()``
483+
^^^^^^^^^^^^^^^^^^^^^^^^^
484+
485+
Execute a command within a running pod
486+
487+
.. list-table::
488+
:widths: 15 15 5 65
489+
:header-rows: 1
490+
:class: tight-table
491+
492+
* - Parameter
493+
- Type
494+
- Default
495+
- Description
496+
* - ``namespace``
497+
- ``string``
498+
- ``default``
499+
- Kubernetes namespace
500+
* - ``within_cluster``
501+
- ``bool``
502+
- ``false``
503+
- set to true if you want to access the cluster from within a running container/pod
504+
* - ``target``
505+
- ``string``
506+
-
507+
- The target pod to execute the command in
508+
* - ``command``
509+
- ``list of string``
510+
-
511+
- Command to execute
512+
513+
450514
``kubernetes_wait_for_network_policy_status()``
451515
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
452516

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright (C) 2024 Intel Corporation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing,
10+
# software distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions
13+
# and limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
from kubernetes import client, config
18+
from enum import Enum
19+
import py_trees
20+
import json
21+
from scenario_execution.actions.base_action import BaseAction
22+
23+
24+
class KubernetesBaseActionState(Enum):
25+
IDLE = 1
26+
REQUEST_SENT = 2
27+
FAILURE = 3
28+
29+
30+
class KubernetesBaseAction(BaseAction):
31+
32+
def __init__(self, namespace: str, within_cluster: bool):
33+
super().__init__()
34+
self.namespace = namespace
35+
self.within_cluster = within_cluster
36+
self.client = None
37+
self.current_state = KubernetesBaseActionState.IDLE
38+
self.current_request = None
39+
40+
def setup(self, **kwargs):
41+
if self.within_cluster:
42+
config.load_incluster_config()
43+
else:
44+
config.load_kube_config()
45+
self.client = client.CoreV1Api()
46+
47+
def execute(self, namespace: str, within_cluster: bool):
48+
self.namespace = namespace
49+
if within_cluster != self.within_cluster:
50+
raise ValueError("parameter 'within_cluster' is not allowed to change since initialization.")
51+
52+
def update(self) -> py_trees.common.Status: # pylint: disable=too-many-return-statements
53+
if self.current_state == KubernetesBaseActionState.IDLE:
54+
self.current_request = self.kubernetes_call()
55+
self.current_state = KubernetesBaseActionState.REQUEST_SENT
56+
return py_trees.common.Status.RUNNING
57+
elif self.current_state == KubernetesBaseActionState.REQUEST_SENT:
58+
success = True
59+
if self.current_request.ready():
60+
if not self.current_request.successful():
61+
try:
62+
self.current_request.get()
63+
except client.exceptions.ApiException as e:
64+
message = ""
65+
body = json.loads(e.body)
66+
if "message" in body:
67+
message = f", message: '{body['message']}'"
68+
self.feedback_message = f"Failure! Reason: {e.reason} {message}" # pylint: disable= attribute-defined-outside-init
69+
success = False
70+
if success:
71+
return py_trees.common.Status.SUCCESS
72+
else:
73+
return py_trees.common.Status.FAILURE
74+
return py_trees.common.Status.FAILURE
75+
76+
def kubernetes_call(self):
77+
# Use async_req = True, namespace=self.namespace
78+
raise NotImplementedError("Implement in derived action")

libs/scenario_execution_kubernetes/scenario_execution_kubernetes/kubernetes_create_from_yaml.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ def setup(self, **kwargs):
4747

4848
def update(self) -> py_trees.common.Status: # pylint: disable=too-many-return-statements
4949
if self.current_state == KubernetesCreateFromYamlActionState.IDLE:
50-
self.current_request = utils.create_from_yaml(
51-
self.client, self.yaml_file, verbose=False, namespace=self.namespace, async_req=True)
50+
try:
51+
self.current_request = utils.create_from_yaml(
52+
self.client, self.yaml_file, verbose=False, namespace=self.namespace, async_req=True)
53+
except Exception as e: # pylint: disable=broad-except
54+
self.feedback_message = f"Error while creating from yaml: {e}"
55+
return py_trees.common.Status.FAILURE
5256
self.current_state = KubernetesCreateFromYamlActionState.CREATION_REQUESTED
5357
self.feedback_message = f"Requested creation from yaml file '{self.yaml_file}' in namespace '{self.namespace}'" # pylint: disable= attribute-defined-outside-init
5458
return py_trees.common.Status.RUNNING
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (C) 2024 Intel Corporation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing,
10+
# software distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions
13+
# and limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
from ast import literal_eval
18+
from .kubernetes_base_action import KubernetesBaseAction
19+
20+
21+
class KubernetesPatchPod(KubernetesBaseAction):
22+
23+
def __init__(self, namespace: str, target: str, body: str, within_cluster: bool):
24+
super().__init__(namespace, within_cluster)
25+
self.target = target
26+
self.body = None
27+
28+
def execute(self, namespace: str, target: str, body: str, within_cluster: bool): # pylint: disable=arguments-differ
29+
super().execute(namespace, within_cluster)
30+
self.target = target
31+
trimmed_data = body.encode('utf-8').decode('unicode_escape')
32+
try:
33+
self.body = literal_eval(trimmed_data)
34+
except ValueError as e:
35+
raise ValueError(f"Could not parse body '{trimmed_data}': {e}") from e
36+
37+
def kubernetes_call(self):
38+
self.feedback_message = f"Requested patching '{self.target}' in namespace '{self.namespace}'" # pylint: disable= attribute-defined-outside-init
39+
return self.client.patch_namespaced_pod(self.target, body=self.body, namespace=self.namespace, async_req=True)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright (C) 2024 Intel Corporation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing,
10+
# software distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions
13+
# and limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
import py_trees
18+
from scenario_execution.actions.base_action import BaseAction
19+
import queue
20+
import threading
21+
from kubernetes import client, config, stream
22+
from enum import Enum
23+
24+
25+
class KubernetesPodExecState(Enum):
26+
IDLE = 1
27+
RUNNING = 2
28+
FAILURE = 3
29+
30+
31+
class KubernetesPodExec(BaseAction):
32+
33+
def __init__(self, target: str, command: list, namespace: str, within_cluster: bool):
34+
super().__init__()
35+
self.target = target
36+
self.namespace = namespace
37+
self.command = command
38+
self.within_cluster = within_cluster
39+
self.client = None
40+
self.reponse_queue = queue.Queue()
41+
self.current_state = KubernetesPodExecState.IDLE
42+
self.output_queue = queue.Queue()
43+
44+
def setup(self, **kwargs):
45+
if self.within_cluster:
46+
config.load_incluster_config()
47+
else:
48+
config.load_kube_config()
49+
self.client = client.CoreV1Api()
50+
51+
self.exec_thread = threading.Thread(target=self.pod_exec, daemon=True)
52+
53+
def execute(self, target: str, command: list, namespace: str, within_cluster: bool):
54+
if within_cluster != self.within_cluster:
55+
raise ValueError("parameter 'within_cluster' is not allowed to change since initialization.")
56+
self.target = target
57+
self.namespace = namespace
58+
self.command = command
59+
self.current_state = KubernetesPodExecState.IDLE
60+
61+
def update(self) -> py_trees.common.Status:
62+
if self.current_state == KubernetesPodExecState.IDLE:
63+
self.current_state = KubernetesPodExecState.RUNNING
64+
self.feedback_message = f"Executing on pod '{self.target}': {self.command}..." # pylint: disable= attribute-defined-outside-init
65+
self.exec_thread.start()
66+
return py_trees.common.Status.RUNNING
67+
elif self.current_state == KubernetesPodExecState.RUNNING:
68+
while not self.output_queue.empty():
69+
self.logger.debug(self.output_queue.get())
70+
try:
71+
response = self.reponse_queue.get_nowait()
72+
try:
73+
if response.returncode == 0:
74+
self.feedback_message = f"Execution successful." # pylint: disable= attribute-defined-outside-init
75+
return py_trees.common.Status.SUCCESS
76+
except ValueError:
77+
self.feedback_message = f"Error while executing." # pylint: disable= attribute-defined-outside-init
78+
except queue.Empty:
79+
return py_trees.common.Status.RUNNING
80+
81+
return py_trees.common.Status.FAILURE
82+
83+
def pod_exec(self):
84+
resp = stream.stream(self.client.connect_get_namespaced_pod_exec,
85+
self.target,
86+
self.namespace,
87+
command=self.command,
88+
stderr=True, stdin=False,
89+
stdout=True, tty=False,
90+
_preload_content=False)
91+
92+
while resp.is_open():
93+
resp.update(timeout=0.1)
94+
if resp.peek_stdout():
95+
self.output_queue.put(resp.read_stdout())
96+
if resp.peek_stderr():
97+
self.output_queue.put(resp.read_stderr())
98+
99+
self.reponse_queue.put(resp)

libs/scenario_execution_kubernetes/scenario_execution_kubernetes/lib_osc/kubernetes.osc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ action kubernetes_delete inherits kubernetes_base_action:
3535

3636
action kubernetes_patch_network_policy inherits kubernetes_base_action:
3737
# patch an existing network policy
38-
target: string # network-policy name to monitor
38+
target: string # network-policy to patch
3939
network_enabled: bool # should the network be enabled?
40-
match_label: key_value # key-value pair to match
40+
match_label: key_value
41+
42+
action kubernetes_patch_pod inherits kubernetes_base_action:
43+
# 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
44+
target: string # pod to patch
45+
body: string # patch to apply
46+
47+
action kubernetes_pod_exec inherits kubernetes_base_action:
48+
# execute a command within a running pod
49+
target: string # pod to patch
50+
command: list of string # command to execute
4151

4252
action kubernetes_wait_for_network_policy_status inherits kubernetes_base_action:
4353
# wait for a network-policy to reach the specified state

libs/scenario_execution_kubernetes/scenarios/test_kubernetes_create_delete.osc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import osc.kubernetes
33
import osc.helpers
44

55
scenario test_kubernetes_create_from_yaml:
6-
timeout(30s)
6+
timeout(60s)
77
do serial:
88
kubernetes_create_from_yaml(yaml_file: "test.yaml")
99
kubernetes_wait_for_pod_status(target: "test", status: kubernetes_pod_status!running)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import osc.standard.base
2+
import osc.kubernetes
3+
import osc.helpers
4+
5+
scenario test_kubernetes_create_from_yaml:
6+
timeout(60s)
7+
do serial:
8+
kubernetes_create_from_yaml(yaml_file: "test.yaml")
9+
kubernetes_wait_for_pod_status(target: "test", status: kubernetes_pod_status!running)
10+
kubernetes_patch_pod(target: "test", body: '{\"spec\":{\"containers\":[{\"name\":\"main\", \"resources\":{\"requests\":{\"cpu\":\"200m\"}, \"limits\":{\"cpu\":\"200m\"}}}]}}')
11+
kubernetes_pod_exec(target: "test", command: ['sysbench', 'cpu', 'run'])
12+
kubernetes_patch_pod(target: "test", body: '{\"spec\":{\"containers\":[{\"name\":\"main\", \"resources\":{\"requests\":{\"cpu\":\"800m\"}, \"limits\":{\"cpu\":\"800m\"}}}]}}')
13+
kubernetes_pod_exec(target: "test", command: ['sysbench', 'cpu', 'run'])
14+
kubernetes_delete(target: "test", element_type: kubernetes_element_type!pod)

libs/scenario_execution_kubernetes/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
'kubernetes_create_from_yaml = scenario_execution_kubernetes.kubernetes_create_from_yaml:KubernetesCreateFromYaml',
4545
'kubernetes_delete = scenario_execution_kubernetes.kubernetes_delete:KubernetesDelete',
4646
'kubernetes_patch_network_policy = scenario_execution_kubernetes.kubernetes_patch_network_policy:KubernetesPatchNetworkPolicy',
47+
'kubernetes_patch_pod = scenario_execution_kubernetes.kubernetes_patch_pod:KubernetesPatchPod',
48+
'kubernetes_pod_exec = scenario_execution_kubernetes.kubernetes_pod_exec:KubernetesPodExec',
4749
'kubernetes_wait_for_network_policy_status = scenario_execution_kubernetes.kubernetes_wait_for_network_policy_status:KubernetesWaitForNetworkPolicyStatus',
4850
'kubernetes_wait_for_pod_status = scenario_execution_kubernetes.kubernetes_wait_for_pod_status:KubernetesWaitForPodStatus',
4951
],

0 commit comments

Comments
 (0)