pyxecm 1.6__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.

Files changed (56) hide show
  1. pyxecm/__init__.py +6 -4
  2. pyxecm/avts.py +673 -246
  3. pyxecm/coreshare.py +686 -467
  4. pyxecm/customizer/__init__.py +16 -4
  5. pyxecm/customizer/__main__.py +58 -0
  6. pyxecm/customizer/api/__init__.py +5 -0
  7. pyxecm/customizer/api/__main__.py +6 -0
  8. pyxecm/customizer/api/app.py +914 -0
  9. pyxecm/customizer/api/auth.py +154 -0
  10. pyxecm/customizer/api/metrics.py +92 -0
  11. pyxecm/customizer/api/models.py +13 -0
  12. pyxecm/customizer/api/payload_list.py +865 -0
  13. pyxecm/customizer/api/settings.py +103 -0
  14. pyxecm/customizer/browser_automation.py +332 -139
  15. pyxecm/customizer/customizer.py +1007 -1130
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +713 -378
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +2867 -909
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +16817 -7467
  24. pyxecm/customizer/pht.py +699 -285
  25. pyxecm/customizer/salesforce.py +516 -342
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +593 -371
  28. pyxecm/customizer/settings.py +442 -0
  29. pyxecm/customizer/successfactors.py +408 -346
  30. pyxecm/customizer/translate.py +83 -48
  31. pyxecm/helper/__init__.py +5 -2
  32. pyxecm/helper/assoc.py +83 -43
  33. pyxecm/helper/data.py +2406 -870
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +527 -171
  37. pyxecm/maintenance_page/__init__.py +5 -0
  38. pyxecm/maintenance_page/__main__.py +6 -0
  39. pyxecm/maintenance_page/app.py +51 -0
  40. pyxecm/maintenance_page/settings.py +28 -0
  41. pyxecm/maintenance_page/static/favicon.avif +0 -0
  42. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  43. pyxecm/otac.py +234 -140
  44. pyxecm/otawp.py +1436 -557
  45. pyxecm/otcs.py +7716 -3161
  46. pyxecm/otds.py +2150 -919
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1272 -325
  49. pyxecm/otpd.py +231 -127
  50. pyxecm-2.0.0.dist-info/METADATA +145 -0
  51. pyxecm-2.0.0.dist-info/RECORD +54 -0
  52. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.6.dist-info/METADATA +0 -53
  54. pyxecm-1.6.dist-info/RECORD +0 -32
  55. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {pyxecm-1.6.dist-info → pyxecm-2.0.0.dist-info}/top_level.txt +0 -0
pyxecm/customizer/k8s.py CHANGED
@@ -1,66 +1,61 @@
1
- """
2
- Kubernetes Module to implement functions to read / write Kubernetes objects
3
- such as Pods, Stateful Sets, Config Maps, ...
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.stream import stream
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
- # Configure Kubernetes API authentication to use pod serviceAccount
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
- """Used to automate stettings in Kubernetes."""
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
- in_cluster: bool,
73
- kubeconfig_file: str = "~/.kube/config",
67
+ kubeconfig_file: str | None = None,
74
68
  namespace: str = "default",
75
- ):
76
- """Initialize the Kubernetes object."""
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
- if in_cluster:
89
+
90
+ try:
80
91
  config.load_incluster_config()
81
- else:
82
- if kubeconfig_file:
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
- else:
85
- logger.warning(
86
- "Not runnig in cluster but kubeconfig file not specified!"
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 = client.CoreV1Api()
90
- self._apps_v1_api = client.AppsV1Api()
91
- self._networking_v1_api = client.NetworkingV1Api()
92
- if namespace and not in_cluster:
93
- self._namespace = namespace
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
- with open(
97
- "/var/run/secrets/kubernetes.io/serviceaccount/namespace",
98
- "r",
99
- encoding="utf-8",
100
- ) as namespace_file:
101
- self._namespace = namespace_file.read()
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
128
+
129
+ # end method definition
102
130
 
103
- def get_core_v1_api(self):
104
- """Returns Kubernetes Core V1 API object
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
- def get_apps_v1_api(self):
112
- """Returns Kubernetes Apps V1 API object
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
- def get_networking_v1_api(self):
120
- """Returns Kubernetes Networking V1 API object
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
- def get_namespace(self):
128
- """Returns Kubernetes Namespace
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
- def get_pod(self, pod_name: str):
136
- """Get a pod in the configured namespace (the namespace is defined
137
- in the class constructor).
138
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_pod
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): name of the Kubernetes pod in the current namespace
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, namespace=self.get_namespace()
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
- return None
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(self, field_selector: str = "", label_selector: str = ""):
166
- """List all Kubernetes pods in a given namespace. The list can be further restricted
167
- by specifying a field or label selector.
168
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_pod
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 as exception:
191
- logger.error(
192
- "Failed to list Pods with field_selector -> '%s' and label_selector -> '%s'; error -> %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,34 +257,46 @@ class K8s:
201
257
  # end method definition
202
258
 
203
259
  def wait_pod_condition(
204
- self, pod_name: str, condition_name: str, sleep_time: int = 30
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): name of the Kubernetes pod in the current namespace
210
- condition_name (str): name of the condition, e.g. "Ready"
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, self.get_namespace()
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'!", pod_name, condition_name
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(
299
+ self.logger.info(
232
300
  "Pod -> '%s' is not yet in state -> '%s'. Waiting...",
233
301
  pod_name,
234
302
  condition_name,
@@ -236,11 +304,10 @@ class K8s:
236
304
  time.sleep(sleep_time)
237
305
  continue
238
306
 
239
- except ApiException as exception:
240
- logger.error(
241
- "Failed to wait for pod -> '%s'; error -> %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
@@ -252,23 +319,35 @@ class K8s:
252
319
  max_retry: int = 3,
253
320
  time_retry: int = 10,
254
321
  container: str | None = None,
255
- ):
322
+ ) -> str:
256
323
  """Execute a command inside a Kubernetes Pod (similar to kubectl exec on command line).
257
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#connect_get_namespaced_pod_exec
324
+
325
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#connect_get_namespaced_pod_exec
326
+
258
327
  Args:
259
- pod_name (str): name of the Kubernetes pod in the current namespace
260
- command (list): list of command and its parameters, e.g. ["/bin/bash", "-c", "pwd"]
261
- The "-c" is required to make the shell executing the command.
262
- max_retry (int): Max amount of attempts to execute the command
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
+
263
340
  Returns:
264
- Response of the command or None if the call fails
341
+ str:
342
+ Response of the command or None if the call fails.
343
+
265
344
  """
266
345
 
267
- pod = self.get_pod(pod_name)
346
+ pod = self.get_pod(pod_name=pod_name)
268
347
  if not pod:
269
- logger.error("Pod -> '%s' does not exist", pod_name)
348
+ self.logger.error("Pod -> '%s' does not exist", pod_name)
270
349
 
271
- logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
350
+ self.logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
272
351
 
273
352
  retry_counter = 1
274
353
 
@@ -285,10 +364,8 @@ class K8s:
285
364
  stdout=True,
286
365
  tty=False,
287
366
  )
288
- logger.debug(response)
289
- return response
290
367
  except ApiException as exc:
291
- logger.warning(
368
+ self.logger.warning(
292
369
  "Failed to execute command, retry (%s/%s) -> %s in pod -> '%s'; error -> %s",
293
370
  retry_counter,
294
371
  max_retry,
@@ -298,11 +375,17 @@ class K8s:
298
375
  )
299
376
  retry_counter = retry_counter + 1
300
377
  exception = exc
301
- logger.debug("Wait %s seconds before next retry...", str(time_retry))
378
+ self.logger.debug(
379
+ "Wait %s seconds before next retry...",
380
+ str(time_retry),
381
+ )
302
382
  time.sleep(time_retry)
303
383
  continue
384
+ else:
385
+ self.logger.debug(response)
386
+ return response
304
387
 
305
- logger.error(
388
+ self.logger.error(
306
389
  "Failed to execute command with %s retries -> %s in pod -> '%s'; error -> %s",
307
390
  max_retry,
308
391
  command,
@@ -321,32 +404,40 @@ class K8s:
321
404
  commands: list,
322
405
  timeout: int = 30,
323
406
  write_stderr_to_error_log: bool = True,
324
- ):
407
+ ) -> str:
325
408
  """Execute a command inside a Kubernetes pod (similar to kubectl exec on command line).
326
- Other than exec_pod_command() method above this is an interactive execution using
327
- stdin and reading the output from stdout and stderr. This is required for longer
328
- running commands. It is currently used for restarting the spawner of Archive Center.
329
- The output of the command is pushed into the logging.
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.
330
414
 
331
415
  Args:
332
- pod_name (str): name of the Kubernetes pod in the current namespace
333
- commands (list): list of command and its parameters, e.g. ["/bin/bash", "/etc/init.d/spawner restart"]
334
- Here we should NOT have a "-c" parameter!
335
- timeout (int): timeout duration that is waited for any response.
336
- Each time a resonse is found in stdout or stderr we wait another timeout duration
337
- to make sure we get the full output of the command.
338
- write_stderr_to_error_log (bool): flag to control if output in stderr should be written to info or error log stream.
339
- Default is write to error log (True)
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
+
340
429
  Returns:
341
- str: Response of the command or None if the call fails
430
+ str:
431
+ Response of the command or None if the call fails.
432
+
342
433
  """
343
434
 
344
- pod = self.get_pod(pod_name)
435
+ pod = self.get_pod(pod_name=pod_name)
345
436
  if not pod:
346
- logger.error("Pod -> %s does not exist", pod_name)
437
+ self.logger.error("Pod -> '%s' does not exist", pod_name)
347
438
 
348
439
  if not commands:
349
- logger.error("No commands to execute on Pod -> %s", pod_name)
440
+ self.logger.error("No commands to execute on Pod ->'%s'!", pod_name)
350
441
  return None
351
442
 
352
443
  # Get first command - this should be the shell:
@@ -364,12 +455,11 @@ class K8s:
364
455
  tty=False,
365
456
  _preload_content=False, # This is important!
366
457
  )
367
- except ApiException as exception:
368
- logger.error(
369
- "Failed to execute command -> %s in pod -> '%s'; error -> %s",
458
+ except ApiException:
459
+ self.logger.error(
460
+ "Failed to execute command -> %s in pod -> '%s'",
370
461
  command,
371
462
  pod_name,
372
- str(exception),
373
463
  )
374
464
  return None
375
465
 
@@ -377,22 +467,25 @@ class K8s:
377
467
  got_response = False
378
468
  response.update(timeout=timeout)
379
469
  if response.peek_stdout():
380
- logger.debug(response.read_stdout().replace("\n", " "))
470
+ self.logger.debug(response.read_stdout().replace("\n", " "))
381
471
  got_response = True
382
472
  if response.peek_stderr():
383
473
  if write_stderr_to_error_log:
384
- logger.error(response.read_stderr().replace("\n", " "))
474
+ self.logger.error(response.read_stderr().replace("\n", " "))
385
475
  else:
386
- logger.debug(response.read_stderr().replace("\n", " "))
476
+ self.logger.debug(response.read_stderr().replace("\n", " "))
387
477
  got_response = True
388
478
  if commands:
389
479
  command = commands.pop(0)
390
- logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
480
+ self.logger.debug(
481
+ "Execute command -> %s in pod -> '%s'",
482
+ command,
483
+ pod_name,
484
+ )
391
485
  response.write_stdin(command + "\n")
392
- else:
393
- # We continue as long as we get some response during timeout period
394
- if not got_response:
395
- break
486
+ # We continue as long as we get some response during timeout period
487
+ elif not got_response:
488
+ break
396
489
 
397
490
  response.close()
398
491
 
@@ -400,13 +493,16 @@ class K8s:
400
493
 
401
494
  # end method definition
402
495
 
403
- def delete_pod(self, pod_name: str):
496
+ def delete_pod(self, pod_name: str) -> None:
404
497
  """Delete a pod in the configured namespace (the namespace is defined in the class constructor).
405
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#delete_namespaced_pod
498
+
499
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#delete_namespaced_pod
406
500
 
407
501
  Args:
408
- pod_name (str): name of the Kubernetes pod in the current namespace
409
- Return:
502
+ pod_name (str):
503
+ The name of the Kubernetes pod in the current namespace.
504
+
505
+ Returns:
410
506
  V1Status (object) or None if the call fails.
411
507
  - api_version: The Kubernetes API version.
412
508
  - kind: The Kubernetes object kind, which is always "Status".
@@ -416,19 +512,22 @@ class K8s:
416
512
  - reason: A short string that describes the reason for the status.
417
513
  - code: An HTTP status code that corresponds to the status.
418
514
  See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Status.md
515
+
419
516
  """
420
517
 
421
- pod = self.get_pod(pod_name)
518
+ pod = self.get_pod(pod_name=pod_name)
422
519
  if not pod:
423
- logger.error("Pod -> %s does not exist", pod_name)
520
+ self.logger.error("Pod -> '%s' does not exist!", pod_name)
424
521
 
425
522
  try:
426
523
  response = self.get_core_v1_api().delete_namespaced_pod(
427
- pod_name, namespace=self.get_namespace()
524
+ pod_name,
525
+ namespace=self.get_namespace(),
428
526
  )
429
- except ApiException as exception:
430
- logger.error(
431
- "Failed to delete Pod -> '%s'; error -> %s", pod_name, str(exception)
527
+ except ApiException:
528
+ self.logger.error(
529
+ "Failed to delete Pod -> '%s'",
530
+ pod_name,
432
531
  )
433
532
  return None
434
533
 
@@ -436,35 +535,44 @@ class K8s:
436
535
 
437
536
  # end method definition
438
537
 
439
- def get_config_map(self, config_map_name: str):
538
+ def get_config_map(self, config_map_name: str) -> V1ConfigMap:
440
539
  """Get a config map in the configured namespace (the namespace is defined in the class constructor).
441
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_config_map
540
+
541
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_config_map
442
542
 
443
543
  Args:
444
- config_map_name (str): name of the Kubernetes config map in the current namespace
544
+ config_map_name (str):
545
+ The name of the Kubernetes config map in the current namespace.
546
+
445
547
  Returns:
446
- V1ConfigMap (object): Kubernetes Config Map object that includes these fields:
447
- - api_version: The Kubernetes API version.
448
- - metadata: A V1ObjectMeta object representing metadata about the V1ConfigMap object,
449
- such as its name, labels, and annotations.
450
- - data: A dictionary containing the non-binary data stored in the ConfigMap,
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,
451
557
  where the keys represent the keys of the data items and the values represent
452
558
  the values of the data items.
453
- - binary_data: A dictionary containing the binary data stored in the ConfigMap,
454
- where the keys represent the keys of the binary data items and the values
455
- represent the values of the binary data items. Binary data is encoded as base64
456
- strings in the dictionary values.
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
+
457
565
  """
458
566
 
459
567
  try:
460
568
  response = self.get_core_v1_api().read_namespaced_config_map(
461
- name=config_map_name, namespace=self.get_namespace()
569
+ name=config_map_name,
570
+ namespace=self.get_namespace(),
462
571
  )
463
- except ApiException as exception:
464
- logger.error(
465
- "Failed to get Config Map -> '%s'; error -> %s",
572
+ except ApiException:
573
+ self.logger.error(
574
+ "Failed to get Config Map -> '%s'",
466
575
  config_map_name,
467
- str(exception),
468
576
  )
469
577
  return None
470
578
 
@@ -472,14 +580,22 @@ class K8s:
472
580
 
473
581
  # end method definition
474
582
 
475
- def list_config_maps(self, field_selector: str = "", label_selector: str = ""):
583
+ def list_config_maps(
584
+ self,
585
+ field_selector: str = "",
586
+ label_selector: str = "",
587
+ ) -> V1ConfigMapList:
476
588
  """List all Kubernetes Config Maps in the current namespace.
477
- The list can be filtered by providing field selectors and label selectors.
478
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_config_map
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
479
592
 
480
593
  Args:
481
- field_selector (str): filter result based on fields
482
- label_selector (str): filter result based on labels
594
+ field_selector (str):
595
+ To filter the result based on fields.
596
+ label_selector (str):
597
+ To filter result based on labels.
598
+
483
599
  Returns:
484
600
  V1ConfigMapList (object) or None if the call fails
485
601
  Properties can be accessed with the "." notation (this is an object not a dict!):
@@ -489,6 +605,7 @@ class K8s:
489
605
  - kind: The Kubernetes object kind, which is always "ConfigMapList".
490
606
  - metadata: Additional metadata about the config map list, such as the resource version.
491
607
  See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ConfigMapList.md
608
+
492
609
  """
493
610
 
494
611
  try:
@@ -497,12 +614,11 @@ class K8s:
497
614
  label_selector=label_selector,
498
615
  namespace=self.get_namespace(),
499
616
  )
500
- except ApiException as exception:
501
- logger.error(
502
- "Failed to list Config Maps with field_selector -> '%s' and label_selector -> '%s'; error -> %s",
617
+ except ApiException:
618
+ self.logger.error(
619
+ "Failed to list Config Maps with field_selector -> '%s' and label_selector -> '%s'",
503
620
  field_selector,
504
621
  label_selector,
505
- str(exception),
506
622
  )
507
623
  return None
508
624
 
@@ -510,26 +626,30 @@ class K8s:
510
626
 
511
627
  # end method definition
512
628
 
513
- def find_config_map(self, config_map_name: str):
629
+ def find_config_map(self, config_map_name: str) -> V1ConfigMapList:
514
630
  """Find a Kubernetes Config Map based on its name.
515
- This is just a wrapper method for list_config_maps()
516
- that uses the name as a field selector.
631
+
632
+ This is just a wrapper method for list_config_maps()
633
+ that uses the name as a field selector.
517
634
 
518
635
  Args:
519
- config_map_name (str): name of the Config Map
636
+ config_map_name (str):
637
+ The name of the Kubernetes Config Map to search for.
638
+
520
639
  Returns:
521
- object: V1ConfigMapList (object) or None if the call fails
640
+ object:
641
+ V1ConfigMapList (object) or None if the call fails.
642
+
522
643
  """
523
644
 
524
645
  try:
525
646
  response = self.list_config_maps(
526
- field_selector="metadata.name={}".format(config_map_name)
647
+ field_selector="metadata.name={}".format(config_map_name),
527
648
  )
528
- except ApiException as exception:
529
- logger.error(
530
- "Failed to find Config Map -> '%s'; error -> %s",
649
+ except ApiException:
650
+ self.logger.error(
651
+ "Failed to find Config Map -> '%s'",
531
652
  config_map_name,
532
- str(exception),
533
653
  )
534
654
  return None
535
655
 
@@ -537,16 +657,26 @@ class K8s:
537
657
 
538
658
  # end method definition
539
659
 
540
- def replace_config_map(self, config_map_name: str, config_map_data: dict):
660
+ def replace_config_map(
661
+ self,
662
+ config_map_name: str,
663
+ config_map_data: dict,
664
+ ) -> V1ConfigMap:
541
665
  """Replace a Config Map with a new specification.
542
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#replace_namespaced_config_map
666
+
667
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#replace_namespaced_config_map
543
668
 
544
669
  Args:
545
- config_map_name (str): name of the Kubernetes Config Map
546
- config_map_data (dict): new specification of the Config Map
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
+
547
675
  Returns:
548
- V1ConfigMap (object): updated Kubernetes Config Map object or None if the call fails.
549
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ConfigMap.md
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
+
550
680
  """
551
681
 
552
682
  try:
@@ -560,11 +690,10 @@ class K8s:
560
690
  data=config_map_data,
561
691
  ),
562
692
  )
563
- except ApiException as exception:
564
- logger.error(
565
- "Failed to replace Config Map -> '%s'; error -> %s",
693
+ except ApiException:
694
+ self.logger.error(
695
+ "Failed to replace Config Map -> '%s'",
566
696
  config_map_name,
567
- str(exception),
568
697
  )
569
698
  return None
570
699
 
@@ -572,26 +701,31 @@ class K8s:
572
701
 
573
702
  # end method definition
574
703
 
575
- def get_stateful_set(self, sts_name: str):
704
+ def get_stateful_set(self, sts_name: str) -> V1StatefulSet:
576
705
  """Get a Kubernetes Stateful Set based on its name.
577
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set
706
+
707
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set
578
708
 
579
709
  Args:
580
- sts_name (str): name of the Kubernetes stateful set
710
+ sts_name (str):
711
+ The name of the Kubernetes stateful set
712
+
581
713
  Returns:
582
- V1StatefulSet (object): Kubernetes Stateful Set object or None if the call fails.
583
- See : https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
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
+
584
718
  """
585
719
 
586
720
  try:
587
721
  response = self.get_apps_v1_api().read_namespaced_stateful_set(
588
- name=sts_name, namespace=self.get_namespace()
722
+ name=sts_name,
723
+ namespace=self.get_namespace(),
589
724
  )
590
- except ApiException as exception:
591
- logger.error(
592
- "Failed to get Stateful Set -> '%s'; error -> %s",
725
+ except ApiException:
726
+ self.logger.error(
727
+ "Failed to get Stateful Set -> '%s'",
593
728
  sts_name,
594
- str(exception),
595
729
  )
596
730
  return None
597
731
 
@@ -599,26 +733,31 @@ class K8s:
599
733
 
600
734
  # end method definition
601
735
 
602
- def get_stateful_set_scale(self, sts_name: str):
736
+ def get_stateful_set_scale(self, sts_name: str) -> V1Scale:
603
737
  """Get the number of replicas for a Kubernetes Stateful Set.
604
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set_scale
738
+
739
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set_scale
605
740
 
606
741
  Args:
607
- sts_name (str): name of the Kubernetes Stateful Set
742
+ sts_name (str):
743
+ The name of the Kubernetes Stateful Set.
744
+
608
745
  Returns:
609
- V1Scale (object): Kubernetes Scale object or None if the call fails.
610
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Scale.md
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
+
611
750
  """
612
751
 
613
752
  try:
614
753
  response = self.get_apps_v1_api().read_namespaced_stateful_set_scale(
615
- name=sts_name, namespace=self.get_namespace()
754
+ name=sts_name,
755
+ namespace=self.get_namespace(),
616
756
  )
617
- except ApiException as exception:
618
- logger.error(
619
- "Failed to get scaling (replicas) of Stateful Set -> '%s'; error -> %s",
757
+ except ApiException:
758
+ self.logger.error(
759
+ "Failed to get scaling (replicas) of Stateful Set -> '%s'",
620
760
  sts_name,
621
- str(exception),
622
761
  )
623
762
  return None
624
763
 
@@ -626,28 +765,35 @@ class K8s:
626
765
 
627
766
  # end method definition
628
767
 
629
- def patch_stateful_set(self, sts_name: str, sts_body: dict):
768
+ def patch_stateful_set(self, sts_name: str, sts_body: dict) -> V1StatefulSet:
630
769
  """Patch a Stateful set with new values.
631
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#patch_namespaced_stateful_set
770
+
771
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#patch_namespaced_stateful_set
632
772
 
633
773
  Args:
634
- sts_name (str): name of the Kubernetes stateful set in the current namespace
635
- sts_body (str): patch string
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
+
636
779
  Returns:
637
- V1StatefulSet (object): patched Kubernetes Stateful Set object or None if the call fails.
638
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
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
+
639
784
  """
640
785
 
641
786
  try:
642
787
  response = self.get_apps_v1_api().patch_namespaced_stateful_set(
643
- name=sts_name, namespace=self.get_namespace(), body=sts_body
788
+ name=sts_name,
789
+ namespace=self.get_namespace(),
790
+ body=sts_body,
644
791
  )
645
- except ApiException as exception:
646
- logger.error(
647
- "Failed to patch Stateful Set -> '%s' with -> %s; error -> %s",
792
+ except ApiException:
793
+ self.logger.error(
794
+ "Failed to patch Stateful Set -> '%s' with -> %s",
648
795
  sts_name,
649
796
  sts_body,
650
- str(exception),
651
797
  )
652
798
  return None
653
799
 
@@ -655,28 +801,34 @@ class K8s:
655
801
 
656
802
  # end method definition
657
803
 
658
- def scale_stateful_set(self, sts_name: str, scale: int):
804
+ def scale_stateful_set(self, sts_name: str, scale: int) -> V1StatefulSet:
659
805
  """Scale a stateful set to a specific number of replicas.
660
- It uses the class method patch_stateful_set() above.
806
+
807
+ It uses the class method patch_stateful_set() above.
661
808
 
662
809
  Args:
663
- sts_name (str): name of the Kubernetes stateful set in the current namespace
664
- scale (int): number of replicas (pods) the stateful set shall be scaled to
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
+
665
815
  Returns:
666
- V1StatefulSet (object): Kubernetes Stateful Set object or None if the call fails.
667
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
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
+
668
820
  """
669
821
 
670
822
  try:
671
823
  response = self.patch_stateful_set(
672
- sts_name, sts_body={"spec": {"replicas": scale}}
824
+ sts_name,
825
+ sts_body={"spec": {"replicas": scale}},
673
826
  )
674
- except ApiException as exception:
675
- logger.error(
676
- "Failed to scale Stateful Set -> '%s' to -> %s replicas; error -> %s",
827
+ except ApiException:
828
+ self.logger.error(
829
+ "Failed to scale Stateful Set -> '%s' to -> %s replicas",
677
830
  sts_name,
678
831
  scale,
679
- str(exception),
680
832
  )
681
833
  return None
682
834
 
@@ -684,26 +836,30 @@ class K8s:
684
836
 
685
837
  # end method definition
686
838
 
687
- def get_service(self, service_name: str):
688
- """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.
689
841
 
690
842
  Args:
691
- service_name (str): name of the Kubernetes Service in the current namespace
843
+ service_name (str):
844
+ The name of the Kubernetes Service in the current namespace.
845
+
692
846
  Returns:
693
- V1Service (object): Kubernetes Service object or None if the call fails
694
- This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
695
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md
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
+
696
852
  """
697
853
 
698
854
  try:
699
855
  response = self.get_core_v1_api().read_namespaced_service(
700
- name=service_name, namespace=self.get_namespace()
856
+ name=service_name,
857
+ namespace=self.get_namespace(),
701
858
  )
702
- except ApiException as exception:
703
- logger.error(
704
- "Failed to get Service -> '%s'; error -> %s",
859
+ except ApiException:
860
+ self.logger.error(
861
+ "Failed to get Service -> '%s'",
705
862
  service_name,
706
- str(exception),
707
863
  )
708
864
  return None
709
865
 
@@ -711,24 +867,30 @@ class K8s:
711
867
 
712
868
  # end method definition
713
869
 
714
- def list_services(self, field_selector: str = "", label_selector: str = ""):
870
+ def list_services(self, field_selector: str = "", label_selector: str = "") -> None:
715
871
  """List all Kubernetes Service in the current namespace.
716
- The list can be filtered by providing field selectors and label selectors.
717
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_service
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
718
875
 
719
876
  Args:
720
- field_selector (str): filter result based on fields
721
- label_selector (str): filter result based on labels
877
+ field_selector (str):
878
+ To filter result based on fields.
879
+ label_selector (str):
880
+ To filter result based on labels.
881
+
722
882
  Returns:
723
- V1ServiceList (object): list of Kubernetes Services or None if the call fails
724
- Properties can be accessed with the "." notation (this is an object not a dict!):
725
- - api_version: The Kubernetes API version.
726
- - items: A list of V1Service objects, each representing a service.
727
- You can access the fields of a V1Service object using dot notation,
728
- for example, service.metadata.name to access the name of the service
729
- - kind: The Kubernetes object kind, which is always "ServiceList".
730
- - metadata: Additional metadata about the pod list, such as the resource version.
731
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ServiceList.md
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
+
732
894
  """
733
895
 
734
896
  try:
@@ -737,12 +899,11 @@ class K8s:
737
899
  label_selector=label_selector,
738
900
  namespace=self.get_namespace(),
739
901
  )
740
- except ApiException as exception:
741
- logger.error(
742
- "Failed to list Services with field_selector -> '%s' and label_selector -> '%s'; error -> %s",
902
+ except ApiException:
903
+ self.logger.error(
904
+ "Failed to list Services with field_selector -> '%s' and label_selector -> '%s'",
743
905
  field_selector,
744
906
  label_selector,
745
- str(exception),
746
907
  )
747
908
  return None
748
909
 
@@ -750,30 +911,36 @@ class K8s:
750
911
 
751
912
  # end method definition
752
913
 
753
- def patch_service(self, service_name: str, service_body: dict):
754
- """Patches a Kubernetes Service with an updated spec
914
+ def patch_service(self, service_name: str, service_body: dict) -> V1Service:
915
+ """Patch a Kubernetes Service with an updated spec.
755
916
 
756
917
  Args:
757
- service_name (str): name of the Kubernetes Ingress in the current namespace
758
- service_body (dict): new / updated Service body spec
759
- (will be merged with existing values)
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
+
760
924
  Returns:
761
- V1Service (object): patched Kubernetes Service or None if the call fails
762
- This is NOT a dict but an object - you have to use the "." syntax
763
- to access to returned elements
764
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md
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
+
765
931
  """
766
932
 
767
933
  try:
768
934
  response = self.get_core_v1_api().patch_namespaced_service(
769
- name=service_name, namespace=self.get_namespace(), body=service_body
935
+ name=service_name,
936
+ namespace=self.get_namespace(),
937
+ body=service_body,
770
938
  )
771
- except ApiException as exception:
772
- logger.error(
773
- "Failed to patch Service -> '%s' with -> %s; error -> %s",
939
+ except ApiException:
940
+ self.logger.error(
941
+ "Failed to patch Service -> '%s' with -> %s",
774
942
  service_name,
775
943
  service_body,
776
- str(exception),
777
944
  )
778
945
  return None
779
946
 
@@ -781,24 +948,30 @@ class K8s:
781
948
 
782
949
  # end method definition
783
950
 
784
- def get_ingress(self, ingress_name: str):
785
- """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.
786
953
 
787
954
  Args:
788
- ingress_name (str): name of the Kubernetes Ingress in the current namespace
955
+ ingress_name (str):
956
+ The name of the Kubernetes Ingress in the current namespace.
957
+
789
958
  Returns:
790
- V1Ingress (object): Kubernetes Ingress or None if the call fails
791
- This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
792
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
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
+
793
964
  """
794
965
 
795
966
  try:
796
967
  response = self.get_networking_v1_api().read_namespaced_ingress(
797
- name=ingress_name, namespace=self.get_namespace()
968
+ name=ingress_name,
969
+ namespace=self.get_namespace(),
798
970
  )
799
- except ApiException as exception:
800
- logger.error(
801
- "Failed to get Ingress -> %s; error -> %s", ingress_name, str(exception)
971
+ except ApiException:
972
+ self.logger.error(
973
+ "Failed to get Ingress -> '%s'!",
974
+ ingress_name,
802
975
  )
803
976
  return None
804
977
 
@@ -806,19 +979,25 @@ class K8s:
806
979
 
807
980
  # end method definition
808
981
 
809
- def patch_ingress(self, ingress_name: str, ingress_body: dict):
982
+ def patch_ingress(self, ingress_name: str, ingress_body: dict) -> V1Ingress:
810
983
  """Patch a Kubernetes Ingress with a updated spec.
811
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/NetworkingV1Api.md#patch_namespaced_ingress
984
+
985
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/NetworkingV1Api.md#patch_namespaced_ingress
812
986
 
813
987
  Args:
814
- ingress_name (str): name of the Kubernetes Ingress in the current namespace
815
- ingress_body (dict): new / updated ingress body spec
816
- (will be merged with existing values)
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
+
817
994
  Returns:
818
- V1Ingress (object): patched Kubernetes Ingress object or None if the call fails
819
- This is NOT a dict but an object - you have to use the
820
- "." syntax to access to returned elements
821
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
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
+
822
1001
  """
823
1002
 
824
1003
  try:
@@ -827,12 +1006,11 @@ class K8s:
827
1006
  namespace=self.get_namespace(),
828
1007
  body=ingress_body,
829
1008
  )
830
- except ApiException as exception:
831
- logger.error(
832
- "Failed to patch Ingress -> %s with -> %s; error -> %s",
1009
+ except ApiException:
1010
+ self.logger.error(
1011
+ "Failed to patch Ingress -> '%s' with -> %s",
833
1012
  ingress_name,
834
1013
  ingress_body,
835
- str(exception),
836
1014
  )
837
1015
  return None
838
1016
 
@@ -841,9 +1019,13 @@ class K8s:
841
1019
  # end method definition
842
1020
 
843
1021
  def update_ingress_backend_services(
844
- self, ingress_name: str, hostname: str, service_name: str, service_port: int
845
- ):
846
- """Updates a backend service and port of an Kubernetes Ingress
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.
847
1029
 
848
1030
  "spec": {
849
1031
  "rules": [
@@ -871,18 +1053,25 @@ class K8s:
871
1053
  }
872
1054
 
873
1055
  Args:
874
- ingress_name (str): name of the Kubernetes Ingress in the current namespace
875
- hostname (str): hostname that should get an updated backend service / port
876
- service_name (str): new backend service name
877
- service_port (int): new backend service port
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
+
878
1065
  Returns:
879
- V1Ingress (object): updated Kubernetes Ingress object or None if the call fails
880
- This is NOT a dict but an object - you have to use the "." syntax
881
- to access to returned elements
882
- See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
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
+
883
1072
  """
884
1073
 
885
- ingress = self.get_ingress(ingress_name)
1074
+ ingress = self.get_ingress(ingress_name=ingress_name)
886
1075
  if not ingress:
887
1076
  return None
888
1077
 
@@ -896,8 +1085,8 @@ class K8s:
896
1085
  backend = path.backend
897
1086
  service = backend.service
898
1087
 
899
- logger.debug(
900
- "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)",
901
1090
  service.name,
902
1091
  service.port.number,
903
1092
  service_name,
@@ -907,25 +1096,24 @@ class K8s:
907
1096
  service.name = service_name
908
1097
  service.port.number = service_port
909
1098
  break
910
- else:
911
- rule_index += 1
1099
+ rule_index += 1
912
1100
 
913
1101
  if not host:
914
- logger.error("Cannot find host -> {}.")
1102
+ self.logger.error("Cannot find host.")
915
1103
  return None
916
1104
 
917
1105
  body = [
918
1106
  {
919
1107
  "op": "replace",
920
1108
  "path": "/spec/rules/{}/http/paths/0/backend/service/name".format(
921
- rule_index
1109
+ rule_index,
922
1110
  ),
923
1111
  "value": service_name,
924
1112
  },
925
1113
  {
926
1114
  "op": "replace",
927
1115
  "path": "/spec/rules/{}/http/paths/0/backend/service/port/number".format(
928
- rule_index
1116
+ rule_index,
929
1117
  ),
930
1118
  "value": service_port,
931
1119
  },
@@ -933,16 +1121,17 @@ class K8s:
933
1121
 
934
1122
  return self.patch_ingress(ingress_name, body)
935
1123
 
1124
+ # end method definition
1125
+
936
1126
  def verify_pod_status(
937
1127
  self,
938
1128
  pod_name: str,
939
- timeout: int = 1200,
1129
+ timeout: int = 1800,
940
1130
  total_containers: int = 1,
941
1131
  ready_containers: int = 1,
942
1132
  retry_interval: int = 30,
943
1133
  ) -> bool:
944
- """
945
- Verifies if a pod is in a 'Ready' state by checking the status of its containers.
1134
+ """Verify if a pod is in a 'Ready' state by checking the status of its containers.
946
1135
 
947
1136
  This function waits for a Kubernetes pod to reach the 'Ready' state, where a specified number
948
1137
  of containers are ready. It checks the pod status at regular intervals and reports the status
@@ -950,93 +1139,239 @@ class K8s:
950
1139
  it returns `False`.
951
1140
 
952
1141
  Args:
953
- pod_name (str): The name of the pod to check the status for.
954
- timeout (int, optional): The maximum time (in seconds) to wait for the pod to become ready. Defaults to 1200.
955
- total_containers (int, optional): The total number of containers expected to be running in the pod. Defaults to 1.
956
- ready_containers (int, optional): The minimum number of containers that need to be in a ready state. Defaults to 1.
957
- retry_interval (int, optional): Time interval (in seconds) between each retry to check pod readiness. Defaults to 30.
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.
958
1152
 
959
1153
  Returns:
960
- bool: Returns `True` if the pod reaches the 'Ready' state with the specified number of containers ready
961
- within the timeout. Otherwise, returns `False`.
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
+
962
1158
  """
963
1159
 
964
1160
  def wait_for_pod_ready(pod_name: str, timeout: int) -> bool:
965
- """
966
- Waits until the pod is in the 'Ready' state with the specified number of containers ready.
1161
+ """Wait until the pod is in the 'Ready' state with the specified number of containers ready.
967
1162
 
968
- This internal function repeatedly checks the readiness of the pod, logging the
1163
+ This sub method repeatedly checks the readiness of the pod, logging the
969
1164
  status of the containers. If the pod does not exist, it retries after waiting
970
1165
  and logs detailed information at each step.
971
1166
 
972
1167
  Args:
973
- pod_name (str): The name of the pod to check the status for.
974
- timeout (int): The maximum time (in seconds) to wait for the pod to become ready.
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.
975
1172
 
976
1173
  Returns:
977
- bool: Returns `True` if the pod is ready with the specified number of containers in a 'Ready' state.
978
- Otherwise, returns `False`.
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
+
979
1178
  """
1179
+
980
1180
  elapsed_time = 0 # Initialize elapsed time
981
1181
 
982
1182
  while elapsed_time < timeout:
983
- pod = self.get_pod(pod_name)
1183
+ pod = self.get_pod(pod_name=pod_name)
984
1184
 
985
1185
  if not pod:
986
- logger.error(
987
- "Pod -> %s does not exist, waiting 300 seconds to retry.",
1186
+ self.logger.warning(
1187
+ "Pod -> '%s' does not exist, waiting 300 seconds to retry.",
988
1188
  pod_name,
989
1189
  )
990
1190
  time.sleep(300)
991
- pod = self.get_pod(pod_name)
1191
+ pod = self.get_pod(pod_name=pod_name)
992
1192
 
993
1193
  if not pod:
994
- logger.error(
995
- "Pod -> %s still does not exist after retry!", pod_name
1194
+ self.logger.error(
1195
+ "Pod -> '%s' still does not exist after retry!",
1196
+ pod_name,
996
1197
  )
997
1198
  return False
998
1199
 
999
1200
  # Get the ready status of containers
1000
1201
  container_statuses = pod.status.container_statuses
1001
- if container_statuses and all(
1002
- container.ready for container in container_statuses
1003
- ):
1004
- current_ready_containers = sum(
1005
- 1 for c in container_statuses if c.ready
1006
- )
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)
1007
1204
  total_containers_in_pod = len(container_statuses)
1008
1205
 
1009
- if (
1010
- current_ready_containers >= ready_containers
1011
- and total_containers_in_pod == total_containers
1012
- ):
1013
- logger.info(
1014
- "Pod -> %s is ready with %d/%d containers.",
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.",
1015
1209
  pod_name,
1016
1210
  current_ready_containers,
1017
1211
  total_containers_in_pod,
1018
1212
  )
1019
1213
  return True
1020
1214
  else:
1021
- logger.debug(
1022
- "Pod -> %s is not yet ready (%d/%d).",
1215
+ self.logger.debug(
1216
+ "Pod -> '%s' is not yet ready (%d/%d).",
1023
1217
  pod_name,
1024
1218
  current_ready_containers,
1025
1219
  total_containers_in_pod,
1026
1220
  )
1027
1221
  else:
1028
- logger.debug("Pod -> %s is not yet ready.", pod_name)
1222
+ self.logger.debug("Pod -> '%s' is not yet ready.", pod_name)
1029
1223
 
1030
- logger.info(
1031
- f"Waiting {retry_interval} seconds before next pod status check."
1224
+ self.logger.info(
1225
+ "Waiting %s seconds before next pod status check.",
1226
+ retry_interval,
1032
1227
  )
1033
1228
  time.sleep(
1034
- retry_interval
1229
+ retry_interval,
1035
1230
  ) # Sleep for the retry interval before checking again
1036
1231
  elapsed_time += retry_interval
1037
1232
 
1038
- logger.error("Pod -> %s is not ready after %d seconds.", pod_name, timeout)
1233
+ self.logger.error(
1234
+ "Pod -> '%s' is not ready after %d seconds.",
1235
+ pod_name,
1236
+ timeout,
1237
+ )
1039
1238
  return False
1040
1239
 
1240
+ # end method definition
1241
+
1041
1242
  # Wait until the pod is ready
1042
- return wait_for_pod_ready(pod_name, timeout)
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