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.

Files changed (56) hide show
  1. pyxecm/__init__.py +6 -2
  2. pyxecm/avts.py +1492 -0
  3. pyxecm/coreshare.py +1075 -960
  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 +1075 -1057
  16. pyxecm/customizer/exceptions.py +35 -0
  17. pyxecm/customizer/guidewire.py +322 -0
  18. pyxecm/customizer/k8s.py +787 -338
  19. pyxecm/customizer/log.py +107 -0
  20. pyxecm/customizer/m365.py +3424 -2270
  21. pyxecm/customizer/nhc.py +1169 -0
  22. pyxecm/customizer/openapi.py +258 -0
  23. pyxecm/customizer/payload.py +18201 -7030
  24. pyxecm/customizer/pht.py +1047 -210
  25. pyxecm/customizer/salesforce.py +836 -727
  26. pyxecm/customizer/sap.py +58 -41
  27. pyxecm/customizer/servicenow.py +851 -383
  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 +98 -38
  33. pyxecm/helper/data.py +2482 -742
  34. pyxecm/helper/logadapter.py +27 -0
  35. pyxecm/helper/web.py +229 -101
  36. pyxecm/helper/xml.py +528 -172
  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 +2689 -0
  45. pyxecm/otcs.py +12344 -7547
  46. pyxecm/otds.py +3166 -2219
  47. pyxecm/otiv.py +36 -21
  48. pyxecm/otmm.py +1363 -296
  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.5.dist-info → pyxecm-2.0.0.dist-info}/WHEEL +1 -1
  53. pyxecm-1.5.dist-info/METADATA +0 -51
  54. pyxecm-1.5.dist-info/RECORD +0 -30
  55. {pyxecm-1.5.dist-info → pyxecm-2.0.0.dist-info/licenses}/LICENSE +0 -0
  56. {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
- 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
102
128
 
103
- def get_core_v1_api(self):
104
- """Returns Kubernetes Core V1 API object
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
- 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,69 +257,97 @@ 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(
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 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
247
314
 
248
315
  def exec_pod_command(
249
- self, pod_name: str, command: list, max_retry: int = 3, time_retry: int = 10
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
- 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
+
253
327
  Args:
254
- pod_name (str): name of the Kubernetes pod in the current namespace
255
- command (list): list of command and its parameters, e.g. ["/bin/bash", "-c", "pwd"]
256
- The "-c" is required to make the shell executing the command.
257
- 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
+
258
340
  Returns:
259
- Response of the command or None if the call fails
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("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
+ )
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
- Other than exec_pod_command() method above this is an interactive execution using
321
- stdin and reading the output from stdout and stderr. This is required for longer
322
- running commands. It is currently used for restarting the spawner of Archive Center.
323
- 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.
324
414
 
325
415
  Args:
326
- pod_name (str): name of the Kubernetes pod in the current namespace
327
- commands (list): list of command and its parameters, e.g. ["/bin/bash", "/etc/init.d/spawner restart"]
328
- Here we should NOT have a "-c" parameter!
329
- timeout (int): timeout duration that is waited for any response.
330
- Each time a resonse is found in stdout or stderr we wait another timeout duration
331
- to make sure we get the full output of the command.
332
- write_stderr_to_error_log (bool): flag to control if output in stderr should be written to info or error log stream.
333
- 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
+
334
429
  Returns:
335
- 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
+
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 -> %s", pod_name)
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 as exception:
362
- logger.error(
363
- "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'",
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("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
+ )
385
485
  response.write_stdin(command + "\n")
386
- else:
387
- # We continue as long as we get some response during timeout period
388
- if not got_response:
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
- 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
400
500
 
401
501
  Args:
402
- pod_name (str): name of the Kubernetes pod in the current namespace
403
- Return:
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, namespace=self.get_namespace()
524
+ pod_name,
525
+ namespace=self.get_namespace(),
422
526
  )
423
- except ApiException as exception:
424
- logger.error(
425
- "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,
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
- 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
436
542
 
437
543
  Args:
438
- 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
+
439
547
  Returns:
440
- V1ConfigMap (object): Kubernetes Config Map object that includes these fields:
441
- - api_version: The Kubernetes API version.
442
- - metadata: A V1ObjectMeta object representing metadata about the V1ConfigMap object,
443
- such as its name, labels, and annotations.
444
- - 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,
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
- - binary_data: A dictionary containing the binary data stored in the ConfigMap,
448
- where the keys represent the keys of the binary data items and the values
449
- represent the values of the binary data items. Binary data is encoded as base64
450
- 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
+
451
565
  """
452
566
 
453
567
  try:
454
568
  response = self.get_core_v1_api().read_namespaced_config_map(
455
- name=config_map_name, namespace=self.get_namespace()
569
+ name=config_map_name,
570
+ namespace=self.get_namespace(),
456
571
  )
457
- except ApiException as exception:
458
- logger.error(
459
- "Failed to get Config Map -> %s; error -> %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(self, field_selector: str = "", label_selector: str = ""):
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
- The list can be filtered by providing field selectors and label selectors.
472
- 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
473
592
 
474
593
  Args:
475
- field_selector (str): filter result based on fields
476
- 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
+
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 as exception:
495
- logger.error(
496
- "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'",
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
- This is just a wrapper method for list_config_maps()
510
- 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.
511
634
 
512
635
  Args:
513
- 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
+
514
639
  Returns:
515
- object: V1ConfigMapList (object) or None if the call fails
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 as exception:
523
- logger.error(
524
- "Failed to find Config Map -> %s; error -> %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(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:
535
665
  """Replace a Config Map with a new specification.
536
- 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
537
668
 
538
669
  Args:
539
- config_map_name (str): name of the Kubernetes Config Map
540
- 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
+
541
675
  Returns:
542
- V1ConfigMap (object): updated Kubernetes Config Map object or None if the call fails.
543
- 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
+
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 as exception:
558
- logger.error(
559
- "Failed to replace Config Map -> %s; error -> %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
- 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
572
708
 
573
709
  Args:
574
- sts_name (str): name of the Kubernetes stateful set
710
+ sts_name (str):
711
+ The name of the Kubernetes stateful set
712
+
575
713
  Returns:
576
- V1StatefulSet (object): Kubernetes Stateful Set object or None if the call fails.
577
- 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
+
578
718
  """
579
719
 
580
720
  try:
581
721
  response = self.get_apps_v1_api().read_namespaced_stateful_set(
582
- name=sts_name, namespace=self.get_namespace()
722
+ name=sts_name,
723
+ namespace=self.get_namespace(),
583
724
  )
584
- except ApiException as exception:
585
- logger.error(
586
- "Failed to get Stateful Set -> %s; error -> %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
- 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
599
740
 
600
741
  Args:
601
- sts_name (str): name of the Kubernetes Stateful Set
742
+ sts_name (str):
743
+ The name of the Kubernetes Stateful Set.
744
+
602
745
  Returns:
603
- V1Scale (object): Kubernetes Scale object or None if the call fails.
604
- 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
+
605
750
  """
606
751
 
607
752
  try:
608
753
  response = self.get_apps_v1_api().read_namespaced_stateful_set_scale(
609
- name=sts_name, namespace=self.get_namespace()
754
+ name=sts_name,
755
+ namespace=self.get_namespace(),
610
756
  )
611
- except ApiException as exception:
612
- logger.error(
613
- "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'",
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
- 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
626
772
 
627
773
  Args:
628
- sts_name (str): name of the Kubernetes stateful set in the current namespace
629
- 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
+
630
779
  Returns:
631
- V1StatefulSet (object): patched Kubernetes Stateful Set object or None if the call fails.
632
- 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
+
633
784
  """
634
785
 
635
786
  try:
636
787
  response = self.get_apps_v1_api().patch_namespaced_stateful_set(
637
- name=sts_name, namespace=self.get_namespace(), body=sts_body
788
+ name=sts_name,
789
+ namespace=self.get_namespace(),
790
+ body=sts_body,
638
791
  )
639
- except ApiException as exception:
640
- logger.error(
641
- "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",
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
- It uses the class method patch_stateful_set() above.
806
+
807
+ It uses the class method patch_stateful_set() above.
655
808
 
656
809
  Args:
657
- sts_name (str): name of the Kubernetes stateful set in the current namespace
658
- 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
+
659
815
  Returns:
660
- V1StatefulSet (object): Kubernetes Stateful Set object or None if the call fails.
661
- 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
+
662
820
  """
663
821
 
664
822
  try:
665
823
  response = self.patch_stateful_set(
666
- sts_name, sts_body={"spec": {"replicas": scale}}
824
+ sts_name,
825
+ sts_body={"spec": {"replicas": scale}},
667
826
  )
668
- except ApiException as exception:
669
- logger.error(
670
- "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",
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): 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
+
686
846
  Returns:
687
- V1Service (object): Kubernetes Service object or None if the call fails
688
- This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
689
- 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
+
690
852
  """
691
853
 
692
854
  try:
693
855
  response = self.get_core_v1_api().read_namespaced_service(
694
- name=service_name, namespace=self.get_namespace()
856
+ name=service_name,
857
+ namespace=self.get_namespace(),
695
858
  )
696
- except ApiException as exception:
697
- logger.error(
698
- "Failed to get Service -> %s; error -> %s", service_name, str(exception)
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
- The list can be filtered by providing field selectors and label selectors.
709
- 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
710
875
 
711
876
  Args:
712
- field_selector (str): filter result based on fields
713
- 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
+
714
882
  Returns:
715
- V1ServiceList (object): list of Kubernetes Services or None if the call fails
716
- Properties can be accessed with the "." notation (this is an object not a dict!):
717
- - api_version: The Kubernetes API version.
718
- - items: A list of V1Service objects, each representing a service.
719
- You can access the fields of a V1Service object using dot notation,
720
- for example, service.metadata.name to access the name of the service
721
- - kind: The Kubernetes object kind, which is always "ServiceList".
722
- - metadata: Additional metadata about the pod list, such as the resource version.
723
- 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
+
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 as exception:
733
- logger.error(
734
- "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'",
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
- """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.
747
916
 
748
917
  Args:
749
- service_name (str): name of the Kubernetes Ingress in the current namespace
750
- service_body (dict): new / updated Service body spec
751
- (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
+
752
924
  Returns:
753
- V1Service (object): patched Kubernetes Service or None if the call fails
754
- This is NOT a dict but an object - you have to use the "." syntax
755
- to access to returned elements
756
- 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
+
757
931
  """
758
932
 
759
933
  try:
760
934
  response = self.get_core_v1_api().patch_namespaced_service(
761
- name=service_name, namespace=self.get_namespace(), body=service_body
935
+ name=service_name,
936
+ namespace=self.get_namespace(),
937
+ body=service_body,
762
938
  )
763
- except ApiException as exception:
764
- logger.error(
765
- "Failed to patch Service -> %s with -> %s; error -> %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): 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
+
781
958
  Returns:
782
- V1Ingress (object): Kubernetes Ingress or None if the call fails
783
- This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
784
- 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
+
785
964
  """
786
965
 
787
966
  try:
788
967
  response = self.get_networking_v1_api().read_namespaced_ingress(
789
- name=ingress_name, namespace=self.get_namespace()
968
+ name=ingress_name,
969
+ namespace=self.get_namespace(),
790
970
  )
791
- except ApiException as exception:
792
- logger.error(
793
- "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,
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
- 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
804
986
 
805
987
  Args:
806
- ingress_name (str): name of the Kubernetes Ingress in the current namespace
807
- ingress_body (dict): new / updated ingress body spec
808
- (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
+
809
994
  Returns:
810
- V1Ingress (object): patched Kubernetes Ingress object or None if the call fails
811
- This is NOT a dict but an object - you have to use the
812
- "." syntax to access to returned elements
813
- 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
+
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 as exception:
823
- logger.error(
824
- "Failed to patch Ingress -> %s with -> %s; error -> %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, ingress_name: str, hostname: str, service_name: str, service_port: int
837
- ):
838
- """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.
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): name of the Kubernetes Ingress in the current namespace
867
- hostname (str): hostname that should get an updated backend service / port
868
- service_name (str): new backend service name
869
- 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
+
870
1065
  Returns:
871
- V1Ingress (object): updated Kubernetes Ingress object or None if the call fails
872
- This is NOT a dict but an object - you have to use the "." syntax
873
- to access to returned elements
874
- 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
+
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
- else:
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