pyxecm 1.5__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyxecm might be problematic. Click here for more details.
- pyxecm/__init__.py +6 -2
- pyxecm/avts.py +1492 -0
- pyxecm/coreshare.py +1075 -960
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +914 -0
- pyxecm/customizer/api/auth.py +154 -0
- pyxecm/customizer/api/metrics.py +92 -0
- pyxecm/customizer/api/models.py +13 -0
- pyxecm/customizer/api/payload_list.py +865 -0
- pyxecm/customizer/api/settings.py +103 -0
- pyxecm/customizer/browser_automation.py +332 -139
- pyxecm/customizer/customizer.py +1075 -1057
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +322 -0
- pyxecm/customizer/k8s.py +787 -338
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +3424 -2270
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18201 -7030
- pyxecm/customizer/pht.py +1047 -210
- pyxecm/customizer/salesforce.py +836 -727
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +851 -383
- pyxecm/customizer/settings.py +442 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +98 -38
- pyxecm/helper/data.py +2482 -742
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +528 -172
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +234 -140
- pyxecm/otawp.py +2689 -0
- pyxecm/otcs.py +12344 -7547
- pyxecm/otds.py +3166 -2219
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1363 -296
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.0.dist-info/METADATA +145 -0
- pyxecm-2.0.0.dist-info/RECORD +54 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
- pyxecm-1.5.dist-info/METADATA +0 -51
- pyxecm-1.5.dist-info/RECORD +0 -30
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
pyxecm/customizer/k8s.py
CHANGED
|
@@ -1,66 +1,61 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""Kubernetes Module to implement functions to read / write Kubernetes objects.
|
|
2
|
+
|
|
3
|
+
This includes as Pods, Stateful Sets, Config Maps, ...
|
|
4
4
|
|
|
5
|
-
https://github.com/kubernetes-client/python
|
|
5
|
+
https://github.com/kubernetes-client/python
|
|
6
6
|
https://github.com/kubernetes-client/python/blob/master/kubernetes/README.md
|
|
7
7
|
https://github.com/kubernetes-client/python/tree/master/examples
|
|
8
|
-
|
|
9
|
-
Class: K8s
|
|
10
|
-
Methods:
|
|
11
|
-
|
|
12
|
-
__init__ : class initializer
|
|
13
|
-
get_core_v1_api: Get Kubernetes API object for Core APIs
|
|
14
|
-
get_apps_v1_api: Get Kubernetes API object for Applications (e.g. Stateful Sets)
|
|
15
|
-
get_networking_v1_api: Get Kubernetes API object for Networking (e.g. Ingress)
|
|
16
|
-
get_namespace: Get the Kubernetes namespace the K8s object is configured for
|
|
17
|
-
|
|
18
|
-
get_pod: Get a Kubernetes Pod based on its name
|
|
19
|
-
list_pods: Get a list of Kubernetes pods based on field and label selectors
|
|
20
|
-
exec_pod_command: Execute a list of commands in a Kubernetes Pod
|
|
21
|
-
exec_pod_command_interactive: Write commands to stdin and wait for response
|
|
22
|
-
delete_pod: Delete a running pod (e.g. to make Kubernetes restart it)
|
|
23
|
-
|
|
24
|
-
get_config_map: Get a Kubernetes Config Map based on its name
|
|
25
|
-
list_config_maps: Get a list of Kubernetes Config Maps based on field and label selectors
|
|
26
|
-
find_config_map: Find a Kubernetes Config Map based on its name
|
|
27
|
-
replace_config_map: Replace the data body of a Kubernetes Config Map
|
|
28
|
-
|
|
29
|
-
get_stateful_set: Gets a Kubernetes Stateful Set based on its name
|
|
30
|
-
get_stateful_set_scale: Gets the number of replicas for a Kubernetes Stateful Set
|
|
31
|
-
patch_stateful_set: Updates the specification of a Kubernetes Stateful Set
|
|
32
|
-
scale_stateful_set: Changes number of replicas for a Kubernetes Stateful Set
|
|
33
|
-
|
|
34
|
-
get_service: Get a Kubernetes Service based on its name
|
|
35
|
-
list_services: Get a list of Kubernetes Services based on field and label selectors
|
|
36
|
-
patch_service: Update the specification of a Kubernetes Service
|
|
37
|
-
|
|
38
|
-
get_ingress: Get a Kubernetes Ingress based on its name
|
|
39
|
-
patch_ingress: Update the specification of a Kubernetes Ingress
|
|
40
|
-
update_ingress_backend_services: Replace the backend service and port for an ingress host
|
|
41
|
-
|
|
42
8
|
"""
|
|
43
9
|
|
|
44
10
|
__author__ = "Dr. Marc Diefenbruch"
|
|
45
|
-
__copyright__ = "Copyright 2024, OpenText"
|
|
11
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
46
12
|
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
47
13
|
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
48
14
|
__email__ = "mdiefenb@opentext.com"
|
|
49
15
|
|
|
50
16
|
import logging
|
|
17
|
+
import os
|
|
51
18
|
import time
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
|
|
52
21
|
from kubernetes import client, config
|
|
53
|
-
from kubernetes.
|
|
22
|
+
from kubernetes.client import (
|
|
23
|
+
AppsV1Api,
|
|
24
|
+
CoreV1Api,
|
|
25
|
+
NetworkingV1Api,
|
|
26
|
+
V1ConfigMap,
|
|
27
|
+
V1ConfigMapList,
|
|
28
|
+
V1Ingress,
|
|
29
|
+
V1Pod,
|
|
30
|
+
V1PodList,
|
|
31
|
+
V1Scale,
|
|
32
|
+
V1Service,
|
|
33
|
+
V1StatefulSet,
|
|
34
|
+
)
|
|
54
35
|
from kubernetes.client.exceptions import ApiException
|
|
36
|
+
from kubernetes.config.config_exception import ConfigException
|
|
37
|
+
from kubernetes.stream import stream
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
# config.load_incluster_config()
|
|
58
|
-
|
|
59
|
-
logger = logging.getLogger("pyxecm.customizer.k8s")
|
|
39
|
+
default_logger = logging.getLogger("pyxecm.customizer.k8s")
|
|
60
40
|
|
|
61
41
|
|
|
62
42
|
class K8s:
|
|
63
|
-
"""
|
|
43
|
+
"""Provides an interface to interact with the Kubernetes API.
|
|
44
|
+
|
|
45
|
+
This class can run both in-cluster and locally using kubeconfig.
|
|
46
|
+
It offers methods to interact with Kubernetes namespaces, pods,
|
|
47
|
+
and various API objects like CoreV1, AppsV1, and NetworkingV1.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
logger (logging.Logger): Logger for the class.
|
|
51
|
+
_core_v1_api (CoreV1Api): API client for Kubernetes Core V1.
|
|
52
|
+
_apps_v1_api (AppsV1Api): API client for Kubernetes Apps V1.
|
|
53
|
+
_networking_v1_api (NetworkingV1Api): API client for Kubernetes Networking V1.
|
|
54
|
+
_namespace (str): The namespace in which operations are performed.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
logger: logging.Logger = default_logger
|
|
64
59
|
|
|
65
60
|
_core_v1_api = None
|
|
66
61
|
_apps_v1_api = None
|
|
@@ -69,76 +64,127 @@ class K8s:
|
|
|
69
64
|
|
|
70
65
|
def __init__(
|
|
71
66
|
self,
|
|
72
|
-
|
|
73
|
-
kubeconfig_file: str = "~/.kube/config",
|
|
67
|
+
kubeconfig_file: str | None = None,
|
|
74
68
|
namespace: str = "default",
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
logger: logging.Logger = default_logger,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the Kubernetes object.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
kubeconfig_file (str | None, optional):
|
|
75
|
+
Path to a kubeconfig file. Defaults to None.
|
|
76
|
+
namespace (str, optional):
|
|
77
|
+
The Kubernetes name space. Defaults to "default".
|
|
78
|
+
logger (logging.Logger, optional):
|
|
79
|
+
The logger object. Defaults to default_logger.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if logger != default_logger:
|
|
84
|
+
self.logger = logger.getChild("k8s")
|
|
85
|
+
for logfilter in logger.filters:
|
|
86
|
+
self.logger.addFilter(logfilter)
|
|
77
87
|
|
|
78
88
|
# Configure Kubernetes API authentication to use pod serviceAccount
|
|
79
|
-
|
|
89
|
+
|
|
90
|
+
try:
|
|
80
91
|
config.load_incluster_config()
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
configured = True
|
|
93
|
+
except ConfigException:
|
|
94
|
+
configured = False
|
|
95
|
+
self.logger.info("Failed to load in-cluster config")
|
|
96
|
+
|
|
97
|
+
if kubeconfig_file is None:
|
|
98
|
+
kubeconfig_file = os.getenv(
|
|
99
|
+
"KUBECONFIG",
|
|
100
|
+
os.path.expanduser("~/.kube/config"),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if not configured:
|
|
104
|
+
try:
|
|
83
105
|
config.load_kube_config(config_file=kubeconfig_file)
|
|
84
|
-
|
|
85
|
-
logger.
|
|
86
|
-
"
|
|
106
|
+
except ConfigException:
|
|
107
|
+
self.logger.info(
|
|
108
|
+
"Failed to load kubernetes config with file -> '%s'",
|
|
109
|
+
kubeconfig_file,
|
|
87
110
|
)
|
|
88
111
|
|
|
89
|
-
self._core_v1_api =
|
|
90
|
-
self._apps_v1_api =
|
|
91
|
-
self._networking_v1_api =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
else:
|
|
112
|
+
self._core_v1_api = CoreV1Api()
|
|
113
|
+
self._apps_v1_api = AppsV1Api()
|
|
114
|
+
self._networking_v1_api = NetworkingV1Api()
|
|
115
|
+
|
|
116
|
+
if namespace == "default":
|
|
95
117
|
# Read current namespace
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
try:
|
|
119
|
+
with open(
|
|
120
|
+
"/var/run/secrets/kubernetes.io/serviceaccount/namespace",
|
|
121
|
+
encoding="utf-8",
|
|
122
|
+
) as namespace_file:
|
|
123
|
+
self._namespace = namespace_file.read()
|
|
124
|
+
except FileNotFoundError:
|
|
125
|
+
self._namespace = namespace
|
|
126
|
+
else:
|
|
127
|
+
self._namespace = namespace
|
|
102
128
|
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
# end method definition
|
|
130
|
+
|
|
131
|
+
def get_core_v1_api(self) -> CoreV1Api:
|
|
132
|
+
"""Return Kubernetes Core V1 API object.
|
|
105
133
|
|
|
106
134
|
Returns:
|
|
107
135
|
object: Kubernetes API object
|
|
136
|
+
|
|
108
137
|
"""
|
|
138
|
+
|
|
109
139
|
return self._core_v1_api
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
# end method definition
|
|
142
|
+
|
|
143
|
+
def get_apps_v1_api(self) -> AppsV1Api:
|
|
144
|
+
"""Return Kubernetes Apps V1 API object.
|
|
113
145
|
|
|
114
146
|
Returns:
|
|
115
147
|
object: Kubernetes API object
|
|
148
|
+
|
|
116
149
|
"""
|
|
150
|
+
|
|
117
151
|
return self._apps_v1_api
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
# end method definition
|
|
154
|
+
|
|
155
|
+
def get_networking_v1_api(self) -> NetworkingV1Api:
|
|
156
|
+
"""Return Kubernetes Networking V1 API object.
|
|
121
157
|
|
|
122
158
|
Returns:
|
|
123
159
|
object: Kubernetes API object
|
|
160
|
+
|
|
124
161
|
"""
|
|
162
|
+
|
|
125
163
|
return self._networking_v1_api
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
# end method definition
|
|
166
|
+
|
|
167
|
+
def get_namespace(self) -> str:
|
|
168
|
+
"""Return Kubernetes Namespace.
|
|
129
169
|
|
|
130
170
|
Returns:
|
|
131
171
|
str: Kubernetes namespace
|
|
172
|
+
|
|
132
173
|
"""
|
|
174
|
+
|
|
133
175
|
return self._namespace
|
|
134
176
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
# end method definition
|
|
178
|
+
|
|
179
|
+
def get_pod(self, pod_name: str) -> V1Pod:
|
|
180
|
+
"""Get a pod in the configured namespace (the namespace is defined in the class constructor).
|
|
181
|
+
|
|
182
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_pod
|
|
139
183
|
|
|
140
184
|
Args:
|
|
141
|
-
pod_name (str):
|
|
185
|
+
pod_name (str):
|
|
186
|
+
The name of the Kubernetes pod in the current namespace.
|
|
187
|
+
|
|
142
188
|
Returns:
|
|
143
189
|
V1Pod (object) or None if the call fails.
|
|
144
190
|
- api_version='v1',
|
|
@@ -146,30 +192,40 @@ class K8s:
|
|
|
146
192
|
- metadata=V1ObjectMeta(...),
|
|
147
193
|
- spec=V1PodSpec(...),
|
|
148
194
|
- status=V1PodStatus(...)
|
|
195
|
+
|
|
149
196
|
"""
|
|
150
197
|
|
|
151
198
|
try:
|
|
152
199
|
response = self.get_core_v1_api().read_namespaced_pod(
|
|
153
|
-
name=pod_name,
|
|
154
|
-
|
|
155
|
-
except ApiException as exception:
|
|
156
|
-
logger.error(
|
|
157
|
-
"Failed to get Pod -> %s; error -> %s", pod_name, str(exception)
|
|
200
|
+
name=pod_name,
|
|
201
|
+
namespace=self.get_namespace(),
|
|
158
202
|
)
|
|
159
|
-
|
|
160
|
-
|
|
203
|
+
except ApiException as e:
|
|
204
|
+
if e.status == 404:
|
|
205
|
+
self.logger.info("Pod -> '%s' not found (may be deleted).", pod_name)
|
|
206
|
+
return None
|
|
207
|
+
else:
|
|
208
|
+
self.logger.error("Failed to get Pod -> '%s'!", pod_name)
|
|
209
|
+
return None # Unexpected error, return None
|
|
161
210
|
return response
|
|
162
211
|
|
|
163
212
|
# end method definition
|
|
164
213
|
|
|
165
|
-
def list_pods(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
214
|
+
def list_pods(
|
|
215
|
+
self,
|
|
216
|
+
field_selector: str = "",
|
|
217
|
+
label_selector: str = "",
|
|
218
|
+
) -> V1PodList:
|
|
219
|
+
"""List all Kubernetes pods in a given namespace.
|
|
220
|
+
|
|
221
|
+
The list can be further restricted by specifying a field or label selector.
|
|
222
|
+
|
|
223
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_pod
|
|
169
224
|
|
|
170
225
|
Args:
|
|
171
226
|
field_selector (str): filter result based on fields
|
|
172
227
|
label_selector (str): filter result based on labels
|
|
228
|
+
|
|
173
229
|
Returns:
|
|
174
230
|
V1PodList (object) or None if the call fails
|
|
175
231
|
Properties can be accessed with the "." notation (this is an object not a dict!):
|
|
@@ -179,6 +235,7 @@ class K8s:
|
|
|
179
235
|
- kind: The Kubernetes object kind, which is always "PodList".
|
|
180
236
|
- metadata: Additional metadata about the pod list, such as the resource version.
|
|
181
237
|
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1PodList.md
|
|
238
|
+
|
|
182
239
|
"""
|
|
183
240
|
|
|
184
241
|
try:
|
|
@@ -187,12 +244,11 @@ class K8s:
|
|
|
187
244
|
label_selector=label_selector,
|
|
188
245
|
namespace=self.get_namespace(),
|
|
189
246
|
)
|
|
190
|
-
except ApiException
|
|
191
|
-
logger.error(
|
|
192
|
-
"Failed to list Pods with field_selector -> %s and label_selector -> %s
|
|
247
|
+
except ApiException:
|
|
248
|
+
self.logger.error(
|
|
249
|
+
"Failed to list Pods with field_selector -> '%s' and label_selector -> '%s'",
|
|
193
250
|
field_selector,
|
|
194
251
|
label_selector,
|
|
195
|
-
str(exception),
|
|
196
252
|
)
|
|
197
253
|
return None
|
|
198
254
|
|
|
@@ -201,69 +257,97 @@ class K8s:
|
|
|
201
257
|
# end method definition
|
|
202
258
|
|
|
203
259
|
def wait_pod_condition(
|
|
204
|
-
self,
|
|
205
|
-
|
|
260
|
+
self,
|
|
261
|
+
pod_name: str,
|
|
262
|
+
condition_name: str,
|
|
263
|
+
sleep_time: int = 30,
|
|
264
|
+
) -> None:
|
|
206
265
|
"""Wait for the pod to reach a defined condition (e.g. "Ready").
|
|
207
266
|
|
|
208
267
|
Args:
|
|
209
|
-
pod_name (str):
|
|
210
|
-
|
|
268
|
+
pod_name (str):
|
|
269
|
+
The name of the Kubernetes pod in the current namespace.
|
|
270
|
+
condition_name (str):
|
|
271
|
+
The name of the condition, e.g. "Ready".
|
|
272
|
+
sleep_time (int):
|
|
273
|
+
The number of seconds to wait between repetitive status checks.
|
|
274
|
+
|
|
211
275
|
Returns:
|
|
212
|
-
True once the pod reaches the condition - otherwise wait forever
|
|
276
|
+
True once the pod reaches the condition - otherwise wait forever.
|
|
277
|
+
|
|
213
278
|
"""
|
|
214
279
|
|
|
215
280
|
ready = False
|
|
216
281
|
while not ready:
|
|
217
282
|
try:
|
|
218
283
|
pod_status = self.get_core_v1_api().read_namespaced_pod_status(
|
|
219
|
-
pod_name,
|
|
284
|
+
pod_name,
|
|
285
|
+
self.get_namespace(),
|
|
220
286
|
)
|
|
221
287
|
|
|
222
288
|
# Check if the pod has reached the defined condition:
|
|
223
289
|
for cond in pod_status.status.conditions:
|
|
224
290
|
if cond.type == condition_name and cond.status == "True":
|
|
225
|
-
logger.info(
|
|
226
|
-
"Pod -> %s is in state -> %s!",
|
|
291
|
+
self.logger.info(
|
|
292
|
+
"Pod -> '%s' is in state -> '%s'!",
|
|
293
|
+
pod_name,
|
|
294
|
+
condition_name,
|
|
227
295
|
)
|
|
228
296
|
ready = True
|
|
229
297
|
break
|
|
230
298
|
else:
|
|
231
|
-
logger.info(
|
|
232
|
-
"Pod -> %s is not yet in state -> %s. Waiting...",
|
|
299
|
+
self.logger.info(
|
|
300
|
+
"Pod -> '%s' is not yet in state -> '%s'. Waiting...",
|
|
233
301
|
pod_name,
|
|
234
302
|
condition_name,
|
|
235
303
|
)
|
|
236
304
|
time.sleep(sleep_time)
|
|
237
305
|
continue
|
|
238
306
|
|
|
239
|
-
except ApiException
|
|
240
|
-
logger.error(
|
|
241
|
-
"Failed to wait for pod -> %s
|
|
307
|
+
except ApiException:
|
|
308
|
+
self.logger.error(
|
|
309
|
+
"Failed to wait for pod -> '%s'",
|
|
242
310
|
pod_name,
|
|
243
|
-
str(exception),
|
|
244
311
|
)
|
|
245
312
|
|
|
246
313
|
# end method definition
|
|
247
314
|
|
|
248
315
|
def exec_pod_command(
|
|
249
|
-
self,
|
|
250
|
-
|
|
316
|
+
self,
|
|
317
|
+
pod_name: str,
|
|
318
|
+
command: list,
|
|
319
|
+
max_retry: int = 3,
|
|
320
|
+
time_retry: int = 10,
|
|
321
|
+
container: str | None = None,
|
|
322
|
+
) -> str:
|
|
251
323
|
"""Execute a command inside a Kubernetes Pod (similar to kubectl exec on command line).
|
|
252
|
-
|
|
324
|
+
|
|
325
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#connect_get_namespaced_pod_exec
|
|
326
|
+
|
|
253
327
|
Args:
|
|
254
|
-
pod_name (str):
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
328
|
+
pod_name (str):
|
|
329
|
+
The name of the Kubernetes pod in the current namespace.
|
|
330
|
+
command (list):
|
|
331
|
+
A list of command and its parameters, e.g. ["/bin/bash", "-c", "pwd"]
|
|
332
|
+
The "-c" is required to make the shell executing the command.
|
|
333
|
+
max_retry (int):
|
|
334
|
+
The maximum number of attempts to execute the command.
|
|
335
|
+
time_retry (int):
|
|
336
|
+
Wait time in seconds between retries.
|
|
337
|
+
container (str):
|
|
338
|
+
The container name if the pod runs multiple containers inside.
|
|
339
|
+
|
|
258
340
|
Returns:
|
|
259
|
-
|
|
341
|
+
str:
|
|
342
|
+
Response of the command or None if the call fails.
|
|
343
|
+
|
|
260
344
|
"""
|
|
261
345
|
|
|
262
|
-
pod = self.get_pod(pod_name)
|
|
346
|
+
pod = self.get_pod(pod_name=pod_name)
|
|
263
347
|
if not pod:
|
|
264
|
-
logger.error("Pod -> %s does not exist", pod_name)
|
|
348
|
+
self.logger.error("Pod -> '%s' does not exist", pod_name)
|
|
265
349
|
|
|
266
|
-
logger.debug("Execute command -> %s in pod -> %s", command, pod_name)
|
|
350
|
+
self.logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
|
|
267
351
|
|
|
268
352
|
retry_counter = 1
|
|
269
353
|
|
|
@@ -274,16 +358,15 @@ class K8s:
|
|
|
274
358
|
pod_name,
|
|
275
359
|
self.get_namespace(),
|
|
276
360
|
command=command,
|
|
361
|
+
container=container,
|
|
277
362
|
stderr=True,
|
|
278
363
|
stdin=False,
|
|
279
364
|
stdout=True,
|
|
280
365
|
tty=False,
|
|
281
366
|
)
|
|
282
|
-
logger.debug(response)
|
|
283
|
-
return response
|
|
284
367
|
except ApiException as exc:
|
|
285
|
-
logger.warning(
|
|
286
|
-
"Failed to execute command, retry (%s/%s) -> %s in pod -> %s; error -> %s",
|
|
368
|
+
self.logger.warning(
|
|
369
|
+
"Failed to execute command, retry (%s/%s) -> %s in pod -> '%s'; error -> %s",
|
|
287
370
|
retry_counter,
|
|
288
371
|
max_retry,
|
|
289
372
|
command,
|
|
@@ -292,12 +375,18 @@ class K8s:
|
|
|
292
375
|
)
|
|
293
376
|
retry_counter = retry_counter + 1
|
|
294
377
|
exception = exc
|
|
295
|
-
logger.debug(
|
|
378
|
+
self.logger.debug(
|
|
379
|
+
"Wait %s seconds before next retry...",
|
|
380
|
+
str(time_retry),
|
|
381
|
+
)
|
|
296
382
|
time.sleep(time_retry)
|
|
297
383
|
continue
|
|
384
|
+
else:
|
|
385
|
+
self.logger.debug(response)
|
|
386
|
+
return response
|
|
298
387
|
|
|
299
|
-
logger.error(
|
|
300
|
-
"Failed to execute command with %s retries -> %s in pod -> %s; error -> %s",
|
|
388
|
+
self.logger.error(
|
|
389
|
+
"Failed to execute command with %s retries -> %s in pod -> '%s'; error -> %s",
|
|
301
390
|
max_retry,
|
|
302
391
|
command,
|
|
303
392
|
pod_name,
|
|
@@ -315,32 +404,40 @@ class K8s:
|
|
|
315
404
|
commands: list,
|
|
316
405
|
timeout: int = 30,
|
|
317
406
|
write_stderr_to_error_log: bool = True,
|
|
318
|
-
):
|
|
407
|
+
) -> str:
|
|
319
408
|
"""Execute a command inside a Kubernetes pod (similar to kubectl exec on command line).
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
409
|
+
|
|
410
|
+
Other than exec_pod_command() method above this is an interactive execution using
|
|
411
|
+
stdin and reading the output from stdout and stderr. This is required for longer
|
|
412
|
+
running commands. It is currently used for restarting the spawner of Archive Center.
|
|
413
|
+
The output of the command is pushed into the logging.
|
|
324
414
|
|
|
325
415
|
Args:
|
|
326
|
-
pod_name (str):
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
416
|
+
pod_name (str):
|
|
417
|
+
The name of the Kubernetes pod in the current namespace
|
|
418
|
+
commands (list):
|
|
419
|
+
A list of command and its parameters, e.g. ["/bin/bash", "/etc/init.d/spawner restart"]
|
|
420
|
+
Here we should NOT have a "-c" parameter!
|
|
421
|
+
timeout (int):
|
|
422
|
+
Timeout duration that is waited for any response.
|
|
423
|
+
Each time a resonse is found in stdout or stderr we wait another timeout duration
|
|
424
|
+
to make sure we get the full output of the command.
|
|
425
|
+
write_stderr_to_error_log (bool):
|
|
426
|
+
Flag to control if output in stderr should be written to info or error log stream.
|
|
427
|
+
Default is write to error log (True).
|
|
428
|
+
|
|
334
429
|
Returns:
|
|
335
|
-
str:
|
|
430
|
+
str:
|
|
431
|
+
Response of the command or None if the call fails.
|
|
432
|
+
|
|
336
433
|
"""
|
|
337
434
|
|
|
338
|
-
pod = self.get_pod(pod_name)
|
|
435
|
+
pod = self.get_pod(pod_name=pod_name)
|
|
339
436
|
if not pod:
|
|
340
|
-
logger.error("Pod -> %s does not exist", pod_name)
|
|
437
|
+
self.logger.error("Pod -> '%s' does not exist", pod_name)
|
|
341
438
|
|
|
342
439
|
if not commands:
|
|
343
|
-
logger.error("No commands to execute on Pod ->
|
|
440
|
+
self.logger.error("No commands to execute on Pod ->'%s'!", pod_name)
|
|
344
441
|
return None
|
|
345
442
|
|
|
346
443
|
# Get first command - this should be the shell:
|
|
@@ -358,12 +455,11 @@ class K8s:
|
|
|
358
455
|
tty=False,
|
|
359
456
|
_preload_content=False, # This is important!
|
|
360
457
|
)
|
|
361
|
-
except ApiException
|
|
362
|
-
logger.error(
|
|
363
|
-
"Failed to execute command -> %s in pod -> %s
|
|
458
|
+
except ApiException:
|
|
459
|
+
self.logger.error(
|
|
460
|
+
"Failed to execute command -> %s in pod -> '%s'",
|
|
364
461
|
command,
|
|
365
462
|
pod_name,
|
|
366
|
-
str(exception),
|
|
367
463
|
)
|
|
368
464
|
return None
|
|
369
465
|
|
|
@@ -371,22 +467,25 @@ class K8s:
|
|
|
371
467
|
got_response = False
|
|
372
468
|
response.update(timeout=timeout)
|
|
373
469
|
if response.peek_stdout():
|
|
374
|
-
logger.debug(response.read_stdout().replace("\n", " "))
|
|
470
|
+
self.logger.debug(response.read_stdout().replace("\n", " "))
|
|
375
471
|
got_response = True
|
|
376
472
|
if response.peek_stderr():
|
|
377
473
|
if write_stderr_to_error_log:
|
|
378
|
-
logger.error(response.read_stderr().replace("\n", " "))
|
|
474
|
+
self.logger.error(response.read_stderr().replace("\n", " "))
|
|
379
475
|
else:
|
|
380
|
-
logger.debug(response.read_stderr().replace("\n", " "))
|
|
476
|
+
self.logger.debug(response.read_stderr().replace("\n", " "))
|
|
381
477
|
got_response = True
|
|
382
478
|
if commands:
|
|
383
479
|
command = commands.pop(0)
|
|
384
|
-
logger.debug(
|
|
480
|
+
self.logger.debug(
|
|
481
|
+
"Execute command -> %s in pod -> '%s'",
|
|
482
|
+
command,
|
|
483
|
+
pod_name,
|
|
484
|
+
)
|
|
385
485
|
response.write_stdin(command + "\n")
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
break
|
|
486
|
+
# We continue as long as we get some response during timeout period
|
|
487
|
+
elif not got_response:
|
|
488
|
+
break
|
|
390
489
|
|
|
391
490
|
response.close()
|
|
392
491
|
|
|
@@ -394,13 +493,16 @@ class K8s:
|
|
|
394
493
|
|
|
395
494
|
# end method definition
|
|
396
495
|
|
|
397
|
-
def delete_pod(self, pod_name: str):
|
|
496
|
+
def delete_pod(self, pod_name: str) -> None:
|
|
398
497
|
"""Delete a pod in the configured namespace (the namespace is defined in the class constructor).
|
|
399
|
-
|
|
498
|
+
|
|
499
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#delete_namespaced_pod
|
|
400
500
|
|
|
401
501
|
Args:
|
|
402
|
-
pod_name (str):
|
|
403
|
-
|
|
502
|
+
pod_name (str):
|
|
503
|
+
The name of the Kubernetes pod in the current namespace.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
404
506
|
V1Status (object) or None if the call fails.
|
|
405
507
|
- api_version: The Kubernetes API version.
|
|
406
508
|
- kind: The Kubernetes object kind, which is always "Status".
|
|
@@ -410,19 +512,22 @@ class K8s:
|
|
|
410
512
|
- reason: A short string that describes the reason for the status.
|
|
411
513
|
- code: An HTTP status code that corresponds to the status.
|
|
412
514
|
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Status.md
|
|
515
|
+
|
|
413
516
|
"""
|
|
414
517
|
|
|
415
|
-
pod = self.get_pod(pod_name)
|
|
518
|
+
pod = self.get_pod(pod_name=pod_name)
|
|
416
519
|
if not pod:
|
|
417
|
-
logger.error("Pod -> %s does not exist", pod_name)
|
|
520
|
+
self.logger.error("Pod -> '%s' does not exist!", pod_name)
|
|
418
521
|
|
|
419
522
|
try:
|
|
420
523
|
response = self.get_core_v1_api().delete_namespaced_pod(
|
|
421
|
-
pod_name,
|
|
524
|
+
pod_name,
|
|
525
|
+
namespace=self.get_namespace(),
|
|
422
526
|
)
|
|
423
|
-
except ApiException
|
|
424
|
-
logger.error(
|
|
425
|
-
"Failed to delete Pod -> %s
|
|
527
|
+
except ApiException:
|
|
528
|
+
self.logger.error(
|
|
529
|
+
"Failed to delete Pod -> '%s'",
|
|
530
|
+
pod_name,
|
|
426
531
|
)
|
|
427
532
|
return None
|
|
428
533
|
|
|
@@ -430,35 +535,44 @@ class K8s:
|
|
|
430
535
|
|
|
431
536
|
# end method definition
|
|
432
537
|
|
|
433
|
-
def get_config_map(self, config_map_name: str):
|
|
538
|
+
def get_config_map(self, config_map_name: str) -> V1ConfigMap:
|
|
434
539
|
"""Get a config map in the configured namespace (the namespace is defined in the class constructor).
|
|
435
|
-
|
|
540
|
+
|
|
541
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_config_map
|
|
436
542
|
|
|
437
543
|
Args:
|
|
438
|
-
config_map_name (str):
|
|
544
|
+
config_map_name (str):
|
|
545
|
+
The name of the Kubernetes config map in the current namespace.
|
|
546
|
+
|
|
439
547
|
Returns:
|
|
440
|
-
V1ConfigMap (object):
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
548
|
+
V1ConfigMap (object):
|
|
549
|
+
Kubernetes Config Map object that includes these fields:
|
|
550
|
+
- api_version:
|
|
551
|
+
The Kubernetes API version.
|
|
552
|
+
- metadata:
|
|
553
|
+
A V1ObjectMeta object representing metadata about the V1ConfigMap object,
|
|
554
|
+
such as its name, labels, and annotations.
|
|
555
|
+
- data:
|
|
556
|
+
A dictionary containing the non-binary data stored in the ConfigMap,
|
|
445
557
|
where the keys represent the keys of the data items and the values represent
|
|
446
558
|
the values of the data items.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
559
|
+
- binary_data:
|
|
560
|
+
A dictionary containing the binary data stored in the ConfigMap,
|
|
561
|
+
where the keys represent the keys of the binary data items and the values
|
|
562
|
+
represent the values of the binary data items. Binary data is encoded as base64
|
|
563
|
+
strings in the dictionary values.
|
|
564
|
+
|
|
451
565
|
"""
|
|
452
566
|
|
|
453
567
|
try:
|
|
454
568
|
response = self.get_core_v1_api().read_namespaced_config_map(
|
|
455
|
-
name=config_map_name,
|
|
569
|
+
name=config_map_name,
|
|
570
|
+
namespace=self.get_namespace(),
|
|
456
571
|
)
|
|
457
|
-
except ApiException
|
|
458
|
-
logger.error(
|
|
459
|
-
"Failed to get Config Map -> %s
|
|
572
|
+
except ApiException:
|
|
573
|
+
self.logger.error(
|
|
574
|
+
"Failed to get Config Map -> '%s'",
|
|
460
575
|
config_map_name,
|
|
461
|
-
str(exception),
|
|
462
576
|
)
|
|
463
577
|
return None
|
|
464
578
|
|
|
@@ -466,14 +580,22 @@ class K8s:
|
|
|
466
580
|
|
|
467
581
|
# end method definition
|
|
468
582
|
|
|
469
|
-
def list_config_maps(
|
|
583
|
+
def list_config_maps(
|
|
584
|
+
self,
|
|
585
|
+
field_selector: str = "",
|
|
586
|
+
label_selector: str = "",
|
|
587
|
+
) -> V1ConfigMapList:
|
|
470
588
|
"""List all Kubernetes Config Maps in the current namespace.
|
|
471
|
-
|
|
472
|
-
|
|
589
|
+
|
|
590
|
+
The list can be filtered by providing field selectors and label selectors.
|
|
591
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_config_map
|
|
473
592
|
|
|
474
593
|
Args:
|
|
475
|
-
field_selector (str):
|
|
476
|
-
|
|
594
|
+
field_selector (str):
|
|
595
|
+
To filter the result based on fields.
|
|
596
|
+
label_selector (str):
|
|
597
|
+
To filter result based on labels.
|
|
598
|
+
|
|
477
599
|
Returns:
|
|
478
600
|
V1ConfigMapList (object) or None if the call fails
|
|
479
601
|
Properties can be accessed with the "." notation (this is an object not a dict!):
|
|
@@ -483,6 +605,7 @@ class K8s:
|
|
|
483
605
|
- kind: The Kubernetes object kind, which is always "ConfigMapList".
|
|
484
606
|
- metadata: Additional metadata about the config map list, such as the resource version.
|
|
485
607
|
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ConfigMapList.md
|
|
608
|
+
|
|
486
609
|
"""
|
|
487
610
|
|
|
488
611
|
try:
|
|
@@ -491,12 +614,11 @@ class K8s:
|
|
|
491
614
|
label_selector=label_selector,
|
|
492
615
|
namespace=self.get_namespace(),
|
|
493
616
|
)
|
|
494
|
-
except ApiException
|
|
495
|
-
logger.error(
|
|
496
|
-
"Failed to list Config Maps with field_selector -> %s and label_selector -> %s
|
|
617
|
+
except ApiException:
|
|
618
|
+
self.logger.error(
|
|
619
|
+
"Failed to list Config Maps with field_selector -> '%s' and label_selector -> '%s'",
|
|
497
620
|
field_selector,
|
|
498
621
|
label_selector,
|
|
499
|
-
str(exception),
|
|
500
622
|
)
|
|
501
623
|
return None
|
|
502
624
|
|
|
@@ -504,26 +626,30 @@ class K8s:
|
|
|
504
626
|
|
|
505
627
|
# end method definition
|
|
506
628
|
|
|
507
|
-
def find_config_map(self, config_map_name: str):
|
|
629
|
+
def find_config_map(self, config_map_name: str) -> V1ConfigMapList:
|
|
508
630
|
"""Find a Kubernetes Config Map based on its name.
|
|
509
|
-
|
|
510
|
-
|
|
631
|
+
|
|
632
|
+
This is just a wrapper method for list_config_maps()
|
|
633
|
+
that uses the name as a field selector.
|
|
511
634
|
|
|
512
635
|
Args:
|
|
513
|
-
config_map_name (str):
|
|
636
|
+
config_map_name (str):
|
|
637
|
+
The name of the Kubernetes Config Map to search for.
|
|
638
|
+
|
|
514
639
|
Returns:
|
|
515
|
-
object:
|
|
640
|
+
object:
|
|
641
|
+
V1ConfigMapList (object) or None if the call fails.
|
|
642
|
+
|
|
516
643
|
"""
|
|
517
644
|
|
|
518
645
|
try:
|
|
519
646
|
response = self.list_config_maps(
|
|
520
|
-
field_selector="metadata.name={}".format(config_map_name)
|
|
647
|
+
field_selector="metadata.name={}".format(config_map_name),
|
|
521
648
|
)
|
|
522
|
-
except ApiException
|
|
523
|
-
logger.error(
|
|
524
|
-
"Failed to find Config Map -> %s
|
|
649
|
+
except ApiException:
|
|
650
|
+
self.logger.error(
|
|
651
|
+
"Failed to find Config Map -> '%s'",
|
|
525
652
|
config_map_name,
|
|
526
|
-
str(exception),
|
|
527
653
|
)
|
|
528
654
|
return None
|
|
529
655
|
|
|
@@ -531,16 +657,26 @@ class K8s:
|
|
|
531
657
|
|
|
532
658
|
# end method definition
|
|
533
659
|
|
|
534
|
-
def replace_config_map(
|
|
660
|
+
def replace_config_map(
|
|
661
|
+
self,
|
|
662
|
+
config_map_name: str,
|
|
663
|
+
config_map_data: dict,
|
|
664
|
+
) -> V1ConfigMap:
|
|
535
665
|
"""Replace a Config Map with a new specification.
|
|
536
|
-
|
|
666
|
+
|
|
667
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#replace_namespaced_config_map
|
|
537
668
|
|
|
538
669
|
Args:
|
|
539
|
-
config_map_name (str):
|
|
540
|
-
|
|
670
|
+
config_map_name (str):
|
|
671
|
+
The name of the Kubernetes Config Map to replace.
|
|
672
|
+
config_map_data (dict):
|
|
673
|
+
The updated specification of the Config Map.
|
|
674
|
+
|
|
541
675
|
Returns:
|
|
542
|
-
V1ConfigMap (object):
|
|
543
|
-
|
|
676
|
+
V1ConfigMap (object):
|
|
677
|
+
Updated Kubernetes Config Map object or None if the call fails.
|
|
678
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ConfigMap.md
|
|
679
|
+
|
|
544
680
|
"""
|
|
545
681
|
|
|
546
682
|
try:
|
|
@@ -554,11 +690,10 @@ class K8s:
|
|
|
554
690
|
data=config_map_data,
|
|
555
691
|
),
|
|
556
692
|
)
|
|
557
|
-
except ApiException
|
|
558
|
-
logger.error(
|
|
559
|
-
"Failed to replace Config Map -> %s
|
|
693
|
+
except ApiException:
|
|
694
|
+
self.logger.error(
|
|
695
|
+
"Failed to replace Config Map -> '%s'",
|
|
560
696
|
config_map_name,
|
|
561
|
-
str(exception),
|
|
562
697
|
)
|
|
563
698
|
return None
|
|
564
699
|
|
|
@@ -566,26 +701,31 @@ class K8s:
|
|
|
566
701
|
|
|
567
702
|
# end method definition
|
|
568
703
|
|
|
569
|
-
def get_stateful_set(self, sts_name: str):
|
|
704
|
+
def get_stateful_set(self, sts_name: str) -> V1StatefulSet:
|
|
570
705
|
"""Get a Kubernetes Stateful Set based on its name.
|
|
571
|
-
|
|
706
|
+
|
|
707
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set
|
|
572
708
|
|
|
573
709
|
Args:
|
|
574
|
-
sts_name (str):
|
|
710
|
+
sts_name (str):
|
|
711
|
+
The name of the Kubernetes stateful set
|
|
712
|
+
|
|
575
713
|
Returns:
|
|
576
|
-
V1StatefulSet (object):
|
|
577
|
-
|
|
714
|
+
V1StatefulSet (object):
|
|
715
|
+
Kubernetes Stateful Set object or None if the call fails.
|
|
716
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
|
|
717
|
+
|
|
578
718
|
"""
|
|
579
719
|
|
|
580
720
|
try:
|
|
581
721
|
response = self.get_apps_v1_api().read_namespaced_stateful_set(
|
|
582
|
-
name=sts_name,
|
|
722
|
+
name=sts_name,
|
|
723
|
+
namespace=self.get_namespace(),
|
|
583
724
|
)
|
|
584
|
-
except ApiException
|
|
585
|
-
logger.error(
|
|
586
|
-
"Failed to get Stateful Set -> %s
|
|
725
|
+
except ApiException:
|
|
726
|
+
self.logger.error(
|
|
727
|
+
"Failed to get Stateful Set -> '%s'",
|
|
587
728
|
sts_name,
|
|
588
|
-
str(exception),
|
|
589
729
|
)
|
|
590
730
|
return None
|
|
591
731
|
|
|
@@ -593,26 +733,31 @@ class K8s:
|
|
|
593
733
|
|
|
594
734
|
# end method definition
|
|
595
735
|
|
|
596
|
-
def get_stateful_set_scale(self, sts_name: str):
|
|
736
|
+
def get_stateful_set_scale(self, sts_name: str) -> V1Scale:
|
|
597
737
|
"""Get the number of replicas for a Kubernetes Stateful Set.
|
|
598
|
-
|
|
738
|
+
|
|
739
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set_scale
|
|
599
740
|
|
|
600
741
|
Args:
|
|
601
|
-
sts_name (str):
|
|
742
|
+
sts_name (str):
|
|
743
|
+
The name of the Kubernetes Stateful Set.
|
|
744
|
+
|
|
602
745
|
Returns:
|
|
603
|
-
V1Scale (object):
|
|
604
|
-
|
|
746
|
+
V1Scale (object):
|
|
747
|
+
Kubernetes Scale object or None if the call fails.
|
|
748
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Scale.md
|
|
749
|
+
|
|
605
750
|
"""
|
|
606
751
|
|
|
607
752
|
try:
|
|
608
753
|
response = self.get_apps_v1_api().read_namespaced_stateful_set_scale(
|
|
609
|
-
name=sts_name,
|
|
754
|
+
name=sts_name,
|
|
755
|
+
namespace=self.get_namespace(),
|
|
610
756
|
)
|
|
611
|
-
except ApiException
|
|
612
|
-
logger.error(
|
|
613
|
-
"Failed to get scaling (replicas) of Stateful Set -> %s
|
|
757
|
+
except ApiException:
|
|
758
|
+
self.logger.error(
|
|
759
|
+
"Failed to get scaling (replicas) of Stateful Set -> '%s'",
|
|
614
760
|
sts_name,
|
|
615
|
-
str(exception),
|
|
616
761
|
)
|
|
617
762
|
return None
|
|
618
763
|
|
|
@@ -620,28 +765,35 @@ class K8s:
|
|
|
620
765
|
|
|
621
766
|
# end method definition
|
|
622
767
|
|
|
623
|
-
def patch_stateful_set(self, sts_name: str, sts_body: dict):
|
|
768
|
+
def patch_stateful_set(self, sts_name: str, sts_body: dict) -> V1StatefulSet:
|
|
624
769
|
"""Patch a Stateful set with new values.
|
|
625
|
-
|
|
770
|
+
|
|
771
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#patch_namespaced_stateful_set
|
|
626
772
|
|
|
627
773
|
Args:
|
|
628
|
-
sts_name (str):
|
|
629
|
-
|
|
774
|
+
sts_name (str):
|
|
775
|
+
The name of the Kubernetes stateful set in the current namespace.
|
|
776
|
+
sts_body (str):
|
|
777
|
+
The patch string.
|
|
778
|
+
|
|
630
779
|
Returns:
|
|
631
|
-
V1StatefulSet (object):
|
|
632
|
-
|
|
780
|
+
V1StatefulSet (object):
|
|
781
|
+
The patched Kubernetes Stateful Set object or None if the call fails.
|
|
782
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
|
|
783
|
+
|
|
633
784
|
"""
|
|
634
785
|
|
|
635
786
|
try:
|
|
636
787
|
response = self.get_apps_v1_api().patch_namespaced_stateful_set(
|
|
637
|
-
name=sts_name,
|
|
788
|
+
name=sts_name,
|
|
789
|
+
namespace=self.get_namespace(),
|
|
790
|
+
body=sts_body,
|
|
638
791
|
)
|
|
639
|
-
except ApiException
|
|
640
|
-
logger.error(
|
|
641
|
-
"Failed to patch Stateful Set -> %s with -> %s
|
|
792
|
+
except ApiException:
|
|
793
|
+
self.logger.error(
|
|
794
|
+
"Failed to patch Stateful Set -> '%s' with -> %s",
|
|
642
795
|
sts_name,
|
|
643
796
|
sts_body,
|
|
644
|
-
str(exception),
|
|
645
797
|
)
|
|
646
798
|
return None
|
|
647
799
|
|
|
@@ -649,28 +801,34 @@ class K8s:
|
|
|
649
801
|
|
|
650
802
|
# end method definition
|
|
651
803
|
|
|
652
|
-
def scale_stateful_set(self, sts_name: str, scale: int):
|
|
804
|
+
def scale_stateful_set(self, sts_name: str, scale: int) -> V1StatefulSet:
|
|
653
805
|
"""Scale a stateful set to a specific number of replicas.
|
|
654
|
-
|
|
806
|
+
|
|
807
|
+
It uses the class method patch_stateful_set() above.
|
|
655
808
|
|
|
656
809
|
Args:
|
|
657
|
-
sts_name (str):
|
|
658
|
-
|
|
810
|
+
sts_name (str):
|
|
811
|
+
The name of the Kubernetes stateful set in the current namespace.
|
|
812
|
+
scale (int):
|
|
813
|
+
The number of replicas (pods) the stateful set shall be scaled to.
|
|
814
|
+
|
|
659
815
|
Returns:
|
|
660
|
-
V1StatefulSet (object):
|
|
661
|
-
|
|
816
|
+
V1StatefulSet (object):
|
|
817
|
+
Kubernetes Stateful Set object or None if the call fails.
|
|
818
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
|
|
819
|
+
|
|
662
820
|
"""
|
|
663
821
|
|
|
664
822
|
try:
|
|
665
823
|
response = self.patch_stateful_set(
|
|
666
|
-
sts_name,
|
|
824
|
+
sts_name,
|
|
825
|
+
sts_body={"spec": {"replicas": scale}},
|
|
667
826
|
)
|
|
668
|
-
except ApiException
|
|
669
|
-
logger.error(
|
|
670
|
-
"Failed to scale Stateful Set -> %s to -> %s replicas
|
|
827
|
+
except ApiException:
|
|
828
|
+
self.logger.error(
|
|
829
|
+
"Failed to scale Stateful Set -> '%s' to -> %s replicas",
|
|
671
830
|
sts_name,
|
|
672
831
|
scale,
|
|
673
|
-
str(exception),
|
|
674
832
|
)
|
|
675
833
|
return None
|
|
676
834
|
|
|
@@ -678,24 +836,30 @@ class K8s:
|
|
|
678
836
|
|
|
679
837
|
# end method definition
|
|
680
838
|
|
|
681
|
-
def get_service(self, service_name: str):
|
|
682
|
-
"""Get a Kubernetes Service with a defined name in the current namespace
|
|
839
|
+
def get_service(self, service_name: str) -> V1Service:
|
|
840
|
+
"""Get a Kubernetes Service with a defined name in the current namespace.
|
|
683
841
|
|
|
684
842
|
Args:
|
|
685
|
-
service_name (str):
|
|
843
|
+
service_name (str):
|
|
844
|
+
The name of the Kubernetes Service in the current namespace.
|
|
845
|
+
|
|
686
846
|
Returns:
|
|
687
|
-
V1Service (object):
|
|
688
|
-
|
|
689
|
-
|
|
847
|
+
V1Service (object):
|
|
848
|
+
Kubernetes Service object or None if the call fails
|
|
849
|
+
This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
|
|
850
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md
|
|
851
|
+
|
|
690
852
|
"""
|
|
691
853
|
|
|
692
854
|
try:
|
|
693
855
|
response = self.get_core_v1_api().read_namespaced_service(
|
|
694
|
-
name=service_name,
|
|
856
|
+
name=service_name,
|
|
857
|
+
namespace=self.get_namespace(),
|
|
695
858
|
)
|
|
696
|
-
except ApiException
|
|
697
|
-
logger.error(
|
|
698
|
-
"Failed to get Service -> %s
|
|
859
|
+
except ApiException:
|
|
860
|
+
self.logger.error(
|
|
861
|
+
"Failed to get Service -> '%s'",
|
|
862
|
+
service_name,
|
|
699
863
|
)
|
|
700
864
|
return None
|
|
701
865
|
|
|
@@ -703,24 +867,30 @@ class K8s:
|
|
|
703
867
|
|
|
704
868
|
# end method definition
|
|
705
869
|
|
|
706
|
-
def list_services(self, field_selector: str = "", label_selector: str = ""):
|
|
870
|
+
def list_services(self, field_selector: str = "", label_selector: str = "") -> None:
|
|
707
871
|
"""List all Kubernetes Service in the current namespace.
|
|
708
|
-
|
|
709
|
-
|
|
872
|
+
|
|
873
|
+
The list can be filtered by providing field selectors and label selectors.
|
|
874
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_service
|
|
710
875
|
|
|
711
876
|
Args:
|
|
712
|
-
field_selector (str):
|
|
713
|
-
|
|
877
|
+
field_selector (str):
|
|
878
|
+
To filter result based on fields.
|
|
879
|
+
label_selector (str):
|
|
880
|
+
To filter result based on labels.
|
|
881
|
+
|
|
714
882
|
Returns:
|
|
715
|
-
V1ServiceList (object):
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
883
|
+
V1ServiceList (object):
|
|
884
|
+
A list of Kubernetes Services or None if the call fails.
|
|
885
|
+
Properties can be accessed with the "." notation (this is an object not a dict!):
|
|
886
|
+
- api_version: The Kubernetes API version.
|
|
887
|
+
- items: A list of V1Service objects, each representing a service.
|
|
888
|
+
You can access the fields of a V1Service object using dot notation,
|
|
889
|
+
for example, service.metadata.name to access the name of the service
|
|
890
|
+
- kind: The Kubernetes object kind, which is always "ServiceList".
|
|
891
|
+
- metadata: Additional metadata about the pod list, such as the resource version.
|
|
892
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ServiceList.md
|
|
893
|
+
|
|
724
894
|
"""
|
|
725
895
|
|
|
726
896
|
try:
|
|
@@ -729,12 +899,11 @@ class K8s:
|
|
|
729
899
|
label_selector=label_selector,
|
|
730
900
|
namespace=self.get_namespace(),
|
|
731
901
|
)
|
|
732
|
-
except ApiException
|
|
733
|
-
logger.error(
|
|
734
|
-
"Failed to list Services with field_selector -> %s and label_selector -> %s
|
|
902
|
+
except ApiException:
|
|
903
|
+
self.logger.error(
|
|
904
|
+
"Failed to list Services with field_selector -> '%s' and label_selector -> '%s'",
|
|
735
905
|
field_selector,
|
|
736
906
|
label_selector,
|
|
737
|
-
str(exception),
|
|
738
907
|
)
|
|
739
908
|
return None
|
|
740
909
|
|
|
@@ -742,30 +911,36 @@ class K8s:
|
|
|
742
911
|
|
|
743
912
|
# end method definition
|
|
744
913
|
|
|
745
|
-
def patch_service(self, service_name: str, service_body: dict):
|
|
746
|
-
"""
|
|
914
|
+
def patch_service(self, service_name: str, service_body: dict) -> V1Service:
|
|
915
|
+
"""Patch a Kubernetes Service with an updated spec.
|
|
747
916
|
|
|
748
917
|
Args:
|
|
749
|
-
service_name (str):
|
|
750
|
-
|
|
751
|
-
|
|
918
|
+
service_name (str):
|
|
919
|
+
The name of the Kubernetes Ingress in the current namespace.
|
|
920
|
+
service_body (dict):
|
|
921
|
+
The new / updated Service body spec.
|
|
922
|
+
(will be merged with existing values)
|
|
923
|
+
|
|
752
924
|
Returns:
|
|
753
|
-
V1Service (object):
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
925
|
+
V1Service (object):
|
|
926
|
+
The patched Kubernetes Service or None if the call fails.
|
|
927
|
+
This is NOT a dict but an object - you have to use the "." syntax
|
|
928
|
+
to access to returned elements
|
|
929
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md
|
|
930
|
+
|
|
757
931
|
"""
|
|
758
932
|
|
|
759
933
|
try:
|
|
760
934
|
response = self.get_core_v1_api().patch_namespaced_service(
|
|
761
|
-
name=service_name,
|
|
935
|
+
name=service_name,
|
|
936
|
+
namespace=self.get_namespace(),
|
|
937
|
+
body=service_body,
|
|
762
938
|
)
|
|
763
|
-
except ApiException
|
|
764
|
-
logger.error(
|
|
765
|
-
"Failed to patch Service -> %s with -> %s
|
|
939
|
+
except ApiException:
|
|
940
|
+
self.logger.error(
|
|
941
|
+
"Failed to patch Service -> '%s' with -> %s",
|
|
766
942
|
service_name,
|
|
767
943
|
service_body,
|
|
768
|
-
str(exception),
|
|
769
944
|
)
|
|
770
945
|
return None
|
|
771
946
|
|
|
@@ -773,24 +948,30 @@ class K8s:
|
|
|
773
948
|
|
|
774
949
|
# end method definition
|
|
775
950
|
|
|
776
|
-
def get_ingress(self, ingress_name: str):
|
|
777
|
-
"""Get a Kubernetes Ingress with a defined name in the current namespace
|
|
951
|
+
def get_ingress(self, ingress_name: str) -> V1Ingress:
|
|
952
|
+
"""Get a Kubernetes Ingress with a defined name in the current namespace.
|
|
778
953
|
|
|
779
954
|
Args:
|
|
780
|
-
ingress_name (str):
|
|
955
|
+
ingress_name (str):
|
|
956
|
+
The name of the Kubernetes Ingress in the current namespace.
|
|
957
|
+
|
|
781
958
|
Returns:
|
|
782
|
-
V1Ingress (object):
|
|
783
|
-
|
|
784
|
-
|
|
959
|
+
V1Ingress (object):
|
|
960
|
+
Kubernetes Ingress or None if the call fails
|
|
961
|
+
This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
|
|
962
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
|
|
963
|
+
|
|
785
964
|
"""
|
|
786
965
|
|
|
787
966
|
try:
|
|
788
967
|
response = self.get_networking_v1_api().read_namespaced_ingress(
|
|
789
|
-
name=ingress_name,
|
|
968
|
+
name=ingress_name,
|
|
969
|
+
namespace=self.get_namespace(),
|
|
790
970
|
)
|
|
791
|
-
except ApiException
|
|
792
|
-
logger.error(
|
|
793
|
-
"Failed to get Ingress -> %s
|
|
971
|
+
except ApiException:
|
|
972
|
+
self.logger.error(
|
|
973
|
+
"Failed to get Ingress -> '%s'!",
|
|
974
|
+
ingress_name,
|
|
794
975
|
)
|
|
795
976
|
return None
|
|
796
977
|
|
|
@@ -798,19 +979,25 @@ class K8s:
|
|
|
798
979
|
|
|
799
980
|
# end method definition
|
|
800
981
|
|
|
801
|
-
def patch_ingress(self, ingress_name: str, ingress_body: dict):
|
|
982
|
+
def patch_ingress(self, ingress_name: str, ingress_body: dict) -> V1Ingress:
|
|
802
983
|
"""Patch a Kubernetes Ingress with a updated spec.
|
|
803
|
-
|
|
984
|
+
|
|
985
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/NetworkingV1Api.md#patch_namespaced_ingress
|
|
804
986
|
|
|
805
987
|
Args:
|
|
806
|
-
ingress_name (str):
|
|
807
|
-
|
|
808
|
-
|
|
988
|
+
ingress_name (str):
|
|
989
|
+
The name of the Kubernetes Ingress in the current namespace.
|
|
990
|
+
ingress_body (dict):
|
|
991
|
+
The new / updated ingress body spec.
|
|
992
|
+
(will be merged with existing values)
|
|
993
|
+
|
|
809
994
|
Returns:
|
|
810
|
-
V1Ingress (object):
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
995
|
+
V1Ingress (object):
|
|
996
|
+
The patched Kubernetes Ingress object or None if the call fails
|
|
997
|
+
This is NOT a dict but an object - you have to use the
|
|
998
|
+
"." syntax to access to returned elements
|
|
999
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
|
|
1000
|
+
|
|
814
1001
|
"""
|
|
815
1002
|
|
|
816
1003
|
try:
|
|
@@ -819,12 +1006,11 @@ class K8s:
|
|
|
819
1006
|
namespace=self.get_namespace(),
|
|
820
1007
|
body=ingress_body,
|
|
821
1008
|
)
|
|
822
|
-
except ApiException
|
|
823
|
-
logger.error(
|
|
824
|
-
"Failed to patch Ingress -> %s with -> %s
|
|
1009
|
+
except ApiException:
|
|
1010
|
+
self.logger.error(
|
|
1011
|
+
"Failed to patch Ingress -> '%s' with -> %s",
|
|
825
1012
|
ingress_name,
|
|
826
1013
|
ingress_body,
|
|
827
|
-
str(exception),
|
|
828
1014
|
)
|
|
829
1015
|
return None
|
|
830
1016
|
|
|
@@ -833,9 +1019,13 @@ class K8s:
|
|
|
833
1019
|
# end method definition
|
|
834
1020
|
|
|
835
1021
|
def update_ingress_backend_services(
|
|
836
|
-
self,
|
|
837
|
-
|
|
838
|
-
|
|
1022
|
+
self,
|
|
1023
|
+
ingress_name: str,
|
|
1024
|
+
hostname: str,
|
|
1025
|
+
service_name: str,
|
|
1026
|
+
service_port: int,
|
|
1027
|
+
) -> V1Ingress:
|
|
1028
|
+
"""Update a backend service and port of an Kubernetes Ingress.
|
|
839
1029
|
|
|
840
1030
|
"spec": {
|
|
841
1031
|
"rules": [
|
|
@@ -863,18 +1053,25 @@ class K8s:
|
|
|
863
1053
|
}
|
|
864
1054
|
|
|
865
1055
|
Args:
|
|
866
|
-
ingress_name (str):
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1056
|
+
ingress_name (str):
|
|
1057
|
+
The name of the Kubernetes Ingress in the current namespace.
|
|
1058
|
+
hostname (str):
|
|
1059
|
+
The hostname that should get an updated backend service / port.
|
|
1060
|
+
service_name (str):
|
|
1061
|
+
The new backend service name.
|
|
1062
|
+
service_port (int):
|
|
1063
|
+
The new backend service port.
|
|
1064
|
+
|
|
870
1065
|
Returns:
|
|
871
|
-
V1Ingress (object):
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1066
|
+
V1Ingress (object):
|
|
1067
|
+
The updated Kubernetes Ingress object or None if the call fails.
|
|
1068
|
+
This is NOT a dict but an object - you have to use the "." syntax
|
|
1069
|
+
to access to returned elements
|
|
1070
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
|
|
1071
|
+
|
|
875
1072
|
"""
|
|
876
1073
|
|
|
877
|
-
ingress = self.get_ingress(ingress_name)
|
|
1074
|
+
ingress = self.get_ingress(ingress_name=ingress_name)
|
|
878
1075
|
if not ingress:
|
|
879
1076
|
return None
|
|
880
1077
|
|
|
@@ -888,8 +1085,8 @@ class K8s:
|
|
|
888
1085
|
backend = path.backend
|
|
889
1086
|
service = backend.service
|
|
890
1087
|
|
|
891
|
-
logger.debug(
|
|
892
|
-
"Replace backend service -> %s (%s) with new backend service -> %s (%s)",
|
|
1088
|
+
self.logger.debug(
|
|
1089
|
+
"Replace backend service -> '%s' (%s) with new backend service -> '%s' (%s)",
|
|
893
1090
|
service.name,
|
|
894
1091
|
service.port.number,
|
|
895
1092
|
service_name,
|
|
@@ -899,25 +1096,24 @@ class K8s:
|
|
|
899
1096
|
service.name = service_name
|
|
900
1097
|
service.port.number = service_port
|
|
901
1098
|
break
|
|
902
|
-
|
|
903
|
-
rule_index += 1
|
|
1099
|
+
rule_index += 1
|
|
904
1100
|
|
|
905
1101
|
if not host:
|
|
906
|
-
logger.error("Cannot find host
|
|
1102
|
+
self.logger.error("Cannot find host.")
|
|
907
1103
|
return None
|
|
908
1104
|
|
|
909
1105
|
body = [
|
|
910
1106
|
{
|
|
911
1107
|
"op": "replace",
|
|
912
1108
|
"path": "/spec/rules/{}/http/paths/0/backend/service/name".format(
|
|
913
|
-
rule_index
|
|
1109
|
+
rule_index,
|
|
914
1110
|
),
|
|
915
1111
|
"value": service_name,
|
|
916
1112
|
},
|
|
917
1113
|
{
|
|
918
1114
|
"op": "replace",
|
|
919
1115
|
"path": "/spec/rules/{}/http/paths/0/backend/service/port/number".format(
|
|
920
|
-
rule_index
|
|
1116
|
+
rule_index,
|
|
921
1117
|
),
|
|
922
1118
|
"value": service_port,
|
|
923
1119
|
},
|
|
@@ -926,3 +1122,256 @@ class K8s:
|
|
|
926
1122
|
return self.patch_ingress(ingress_name, body)
|
|
927
1123
|
|
|
928
1124
|
# end method definition
|
|
1125
|
+
|
|
1126
|
+
def verify_pod_status(
|
|
1127
|
+
self,
|
|
1128
|
+
pod_name: str,
|
|
1129
|
+
timeout: int = 1800,
|
|
1130
|
+
total_containers: int = 1,
|
|
1131
|
+
ready_containers: int = 1,
|
|
1132
|
+
retry_interval: int = 30,
|
|
1133
|
+
) -> bool:
|
|
1134
|
+
"""Verify if a pod is in a 'Ready' state by checking the status of its containers.
|
|
1135
|
+
|
|
1136
|
+
This function waits for a Kubernetes pod to reach the 'Ready' state, where a specified number
|
|
1137
|
+
of containers are ready. It checks the pod status at regular intervals and reports the status
|
|
1138
|
+
using logs. If the pod does not reach the 'Ready' state within the specified timeout,
|
|
1139
|
+
it returns `False`.
|
|
1140
|
+
|
|
1141
|
+
Args:
|
|
1142
|
+
pod_name (str):
|
|
1143
|
+
The name of the pod to check the status for.
|
|
1144
|
+
timeout (int, optional):
|
|
1145
|
+
The maximum time (in seconds) to wait for the pod to become ready. Defaults to 1800.
|
|
1146
|
+
total_containers (int, optional):
|
|
1147
|
+
The total number of containers expected to be running in the pod. Defaults to 1.
|
|
1148
|
+
ready_containers (int, optional):
|
|
1149
|
+
The minimum number of containers that need to be in a ready state. Defaults to 1.
|
|
1150
|
+
retry_interval (int, optional):
|
|
1151
|
+
Time interval (in seconds) between each retry to check pod readiness. Defaults to 30.
|
|
1152
|
+
|
|
1153
|
+
Returns:
|
|
1154
|
+
bool:
|
|
1155
|
+
Returns `True` if the pod reaches the 'Ready' state with the specified number of containers ready
|
|
1156
|
+
within the timeout. Otherwise, returns `False`.
|
|
1157
|
+
|
|
1158
|
+
"""
|
|
1159
|
+
|
|
1160
|
+
def wait_for_pod_ready(pod_name: str, timeout: int) -> bool:
|
|
1161
|
+
"""Wait until the pod is in the 'Ready' state with the specified number of containers ready.
|
|
1162
|
+
|
|
1163
|
+
This sub method repeatedly checks the readiness of the pod, logging the
|
|
1164
|
+
status of the containers. If the pod does not exist, it retries after waiting
|
|
1165
|
+
and logs detailed information at each step.
|
|
1166
|
+
|
|
1167
|
+
Args:
|
|
1168
|
+
pod_name (str):
|
|
1169
|
+
The name of the pod to check the status for.
|
|
1170
|
+
timeout (int):
|
|
1171
|
+
The maximum time (in seconds) to wait for the pod to become ready.
|
|
1172
|
+
|
|
1173
|
+
Returns:
|
|
1174
|
+
bool:
|
|
1175
|
+
Returns `True` if the pod is ready with the specified number of containers in a 'Ready' state.
|
|
1176
|
+
Otherwise, returns `False`.
|
|
1177
|
+
|
|
1178
|
+
"""
|
|
1179
|
+
|
|
1180
|
+
elapsed_time = 0 # Initialize elapsed time
|
|
1181
|
+
|
|
1182
|
+
while elapsed_time < timeout:
|
|
1183
|
+
pod = self.get_pod(pod_name=pod_name)
|
|
1184
|
+
|
|
1185
|
+
if not pod:
|
|
1186
|
+
self.logger.warning(
|
|
1187
|
+
"Pod -> '%s' does not exist, waiting 300 seconds to retry.",
|
|
1188
|
+
pod_name,
|
|
1189
|
+
)
|
|
1190
|
+
time.sleep(300)
|
|
1191
|
+
pod = self.get_pod(pod_name=pod_name)
|
|
1192
|
+
|
|
1193
|
+
if not pod:
|
|
1194
|
+
self.logger.error(
|
|
1195
|
+
"Pod -> '%s' still does not exist after retry!",
|
|
1196
|
+
pod_name,
|
|
1197
|
+
)
|
|
1198
|
+
return False
|
|
1199
|
+
|
|
1200
|
+
# Get the ready status of containers
|
|
1201
|
+
container_statuses = pod.status.container_statuses
|
|
1202
|
+
if container_statuses and all(container.ready for container in container_statuses):
|
|
1203
|
+
current_ready_containers = sum(1 for c in container_statuses if c.ready)
|
|
1204
|
+
total_containers_in_pod = len(container_statuses)
|
|
1205
|
+
|
|
1206
|
+
if current_ready_containers >= ready_containers and total_containers_in_pod == total_containers:
|
|
1207
|
+
self.logger.info(
|
|
1208
|
+
"Pod -> '%s' is ready with %d/%d containers.",
|
|
1209
|
+
pod_name,
|
|
1210
|
+
current_ready_containers,
|
|
1211
|
+
total_containers_in_pod,
|
|
1212
|
+
)
|
|
1213
|
+
return True
|
|
1214
|
+
else:
|
|
1215
|
+
self.logger.debug(
|
|
1216
|
+
"Pod -> '%s' is not yet ready (%d/%d).",
|
|
1217
|
+
pod_name,
|
|
1218
|
+
current_ready_containers,
|
|
1219
|
+
total_containers_in_pod,
|
|
1220
|
+
)
|
|
1221
|
+
else:
|
|
1222
|
+
self.logger.debug("Pod -> '%s' is not yet ready.", pod_name)
|
|
1223
|
+
|
|
1224
|
+
self.logger.info(
|
|
1225
|
+
"Waiting %s seconds before next pod status check.",
|
|
1226
|
+
retry_interval,
|
|
1227
|
+
)
|
|
1228
|
+
time.sleep(
|
|
1229
|
+
retry_interval,
|
|
1230
|
+
) # Sleep for the retry interval before checking again
|
|
1231
|
+
elapsed_time += retry_interval
|
|
1232
|
+
|
|
1233
|
+
self.logger.error(
|
|
1234
|
+
"Pod -> '%s' is not ready after %d seconds.",
|
|
1235
|
+
pod_name,
|
|
1236
|
+
timeout,
|
|
1237
|
+
)
|
|
1238
|
+
return False
|
|
1239
|
+
|
|
1240
|
+
# end method definition
|
|
1241
|
+
|
|
1242
|
+
# Wait until the pod is ready
|
|
1243
|
+
return wait_for_pod_ready(pod_name=pod_name, timeout=timeout)
|
|
1244
|
+
|
|
1245
|
+
# end method definition
|
|
1246
|
+
|
|
1247
|
+
def verify_pod_deleted(
|
|
1248
|
+
self,
|
|
1249
|
+
pod_name: str,
|
|
1250
|
+
timeout: int = 300,
|
|
1251
|
+
retry_interval: int = 30,
|
|
1252
|
+
) -> bool:
|
|
1253
|
+
"""Verify if a pod is deleted within the specified timeout.
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
pod_name (str):
|
|
1257
|
+
The name of the pod to check.
|
|
1258
|
+
timeout (int):
|
|
1259
|
+
Maximum time to wait for the pod to be deleted (in seconds).
|
|
1260
|
+
retry_interval:
|
|
1261
|
+
Time interval between retries (in seconds).
|
|
1262
|
+
|
|
1263
|
+
Returns:
|
|
1264
|
+
bool:
|
|
1265
|
+
True if the pod is deleted, False otherwise.
|
|
1266
|
+
|
|
1267
|
+
"""
|
|
1268
|
+
|
|
1269
|
+
elapsed_time = 0 # Initialize elapsed time
|
|
1270
|
+
|
|
1271
|
+
while elapsed_time < timeout:
|
|
1272
|
+
pod = self.get_pod(pod_name=pod_name)
|
|
1273
|
+
|
|
1274
|
+
if not pod:
|
|
1275
|
+
self.logger.info("Pod -> '%s' has been deleted successfully.", pod_name)
|
|
1276
|
+
return True
|
|
1277
|
+
|
|
1278
|
+
self.logger.debug(
|
|
1279
|
+
"Pod -> '%s' still exists. Waiting %s seconds before next check.",
|
|
1280
|
+
pod_name,
|
|
1281
|
+
retry_interval,
|
|
1282
|
+
)
|
|
1283
|
+
time.sleep(retry_interval)
|
|
1284
|
+
elapsed_time += retry_interval
|
|
1285
|
+
|
|
1286
|
+
self.logger.error("Pod -> '%s' was not deleted within %d seconds.", pod_name, timeout)
|
|
1287
|
+
|
|
1288
|
+
return False
|
|
1289
|
+
|
|
1290
|
+
# end method definition
|
|
1291
|
+
|
|
1292
|
+
def restart_deployment(self, deployment_name: str) -> bool:
|
|
1293
|
+
"""Restart a Kubernetes deployment using rolling restart.
|
|
1294
|
+
|
|
1295
|
+
Args:
|
|
1296
|
+
deployment_name (str):
|
|
1297
|
+
Name of the Kubernetes deployment.
|
|
1298
|
+
|
|
1299
|
+
Returns:
|
|
1300
|
+
bool:
|
|
1301
|
+
True if successful, False otherwise.
|
|
1302
|
+
|
|
1303
|
+
"""
|
|
1304
|
+
|
|
1305
|
+
now = datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z"
|
|
1306
|
+
|
|
1307
|
+
body = {
|
|
1308
|
+
"spec": {
|
|
1309
|
+
"template": {
|
|
1310
|
+
"metadata": {
|
|
1311
|
+
"annotations": {
|
|
1312
|
+
"kubectl.kubernetes.io/restartedAt": now,
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1315
|
+
},
|
|
1316
|
+
},
|
|
1317
|
+
}
|
|
1318
|
+
try:
|
|
1319
|
+
self.get_apps_v1_api().patch_namespaced_deployment(
|
|
1320
|
+
deployment_name,
|
|
1321
|
+
self.get_namespace(),
|
|
1322
|
+
body,
|
|
1323
|
+
pretty="true",
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
except ApiException:
|
|
1327
|
+
self.logger.exception(
|
|
1328
|
+
"Failed to restart deployment -> '%s'!",
|
|
1329
|
+
deployment_name,
|
|
1330
|
+
)
|
|
1331
|
+
return False
|
|
1332
|
+
|
|
1333
|
+
else:
|
|
1334
|
+
return True
|
|
1335
|
+
|
|
1336
|
+
# end method definition
|
|
1337
|
+
|
|
1338
|
+
def restart_stateful_set(self, sts_name: str) -> bool:
|
|
1339
|
+
"""Restart a Kubernetes stateful set using rolling restart.
|
|
1340
|
+
|
|
1341
|
+
Args:
|
|
1342
|
+
sts_name (str):
|
|
1343
|
+
Name of the Kubernetes statefulset.
|
|
1344
|
+
|
|
1345
|
+
Returns:
|
|
1346
|
+
bool:
|
|
1347
|
+
True if successful, False otherwise.
|
|
1348
|
+
|
|
1349
|
+
"""
|
|
1350
|
+
|
|
1351
|
+
now = datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z"
|
|
1352
|
+
|
|
1353
|
+
body = {
|
|
1354
|
+
"spec": {
|
|
1355
|
+
"template": {
|
|
1356
|
+
"metadata": {
|
|
1357
|
+
"annotations": {
|
|
1358
|
+
"kubectl.kubernetes.io/restartedAt": now,
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
}
|
|
1364
|
+
try:
|
|
1365
|
+
self.get_apps_v1_api().patch_namespaced_stateful_set(sts_name, self.get_namespace(), body, pretty="true")
|
|
1366
|
+
|
|
1367
|
+
except ApiException:
|
|
1368
|
+
self.logger.exception(
|
|
1369
|
+
"Failed to restart stateful set -> '%s'!",
|
|
1370
|
+
sts_name,
|
|
1371
|
+
)
|
|
1372
|
+
return False
|
|
1373
|
+
|
|
1374
|
+
else:
|
|
1375
|
+
return True
|
|
1376
|
+
|
|
1377
|
+
# end method definition
|