pyxecm 1.6__py3-none-any.whl → 2.0.1__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 (78) hide show
  1. pyxecm/__init__.py +7 -4
  2. pyxecm/avts.py +727 -254
  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 +163 -0
  9. pyxecm/customizer/api/auth/__init__.py +1 -0
  10. pyxecm/customizer/api/auth/functions.py +92 -0
  11. pyxecm/customizer/api/auth/models.py +13 -0
  12. pyxecm/customizer/api/auth/router.py +78 -0
  13. pyxecm/customizer/api/common/__init__.py +1 -0
  14. pyxecm/customizer/api/common/functions.py +47 -0
  15. pyxecm/customizer/api/common/metrics.py +92 -0
  16. pyxecm/customizer/api/common/models.py +21 -0
  17. pyxecm/customizer/api/common/payload_list.py +870 -0
  18. pyxecm/customizer/api/common/router.py +72 -0
  19. pyxecm/customizer/api/settings.py +128 -0
  20. pyxecm/customizer/api/terminal/__init__.py +1 -0
  21. pyxecm/customizer/api/terminal/router.py +87 -0
  22. pyxecm/customizer/api/v1_csai/__init__.py +1 -0
  23. pyxecm/customizer/api/v1_csai/router.py +87 -0
  24. pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
  25. pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
  26. pyxecm/customizer/api/v1_maintenance/models.py +12 -0
  27. pyxecm/customizer/api/v1_maintenance/router.py +76 -0
  28. pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
  29. pyxecm/customizer/api/v1_otcs/functions.py +61 -0
  30. pyxecm/customizer/api/v1_otcs/router.py +179 -0
  31. pyxecm/customizer/api/v1_payload/__init__.py +1 -0
  32. pyxecm/customizer/api/v1_payload/functions.py +179 -0
  33. pyxecm/customizer/api/v1_payload/models.py +51 -0
  34. pyxecm/customizer/api/v1_payload/router.py +499 -0
  35. pyxecm/customizer/browser_automation.py +721 -286
  36. pyxecm/customizer/customizer.py +1076 -1425
  37. pyxecm/customizer/exceptions.py +35 -0
  38. pyxecm/customizer/guidewire.py +1186 -0
  39. pyxecm/customizer/k8s.py +901 -379
  40. pyxecm/customizer/log.py +107 -0
  41. pyxecm/customizer/m365.py +2967 -920
  42. pyxecm/customizer/nhc.py +1169 -0
  43. pyxecm/customizer/openapi.py +258 -0
  44. pyxecm/customizer/payload.py +18228 -7820
  45. pyxecm/customizer/pht.py +717 -286
  46. pyxecm/customizer/salesforce.py +516 -342
  47. pyxecm/customizer/sap.py +58 -41
  48. pyxecm/customizer/servicenow.py +611 -372
  49. pyxecm/customizer/settings.py +445 -0
  50. pyxecm/customizer/successfactors.py +408 -346
  51. pyxecm/customizer/translate.py +83 -48
  52. pyxecm/helper/__init__.py +5 -2
  53. pyxecm/helper/assoc.py +83 -43
  54. pyxecm/helper/data.py +2406 -870
  55. pyxecm/helper/logadapter.py +27 -0
  56. pyxecm/helper/web.py +229 -101
  57. pyxecm/helper/xml.py +596 -171
  58. pyxecm/maintenance_page/__init__.py +5 -0
  59. pyxecm/maintenance_page/__main__.py +6 -0
  60. pyxecm/maintenance_page/app.py +51 -0
  61. pyxecm/maintenance_page/settings.py +28 -0
  62. pyxecm/maintenance_page/static/favicon.avif +0 -0
  63. pyxecm/maintenance_page/templates/maintenance.html +165 -0
  64. pyxecm/otac.py +235 -141
  65. pyxecm/otawp.py +2668 -1220
  66. pyxecm/otca.py +569 -0
  67. pyxecm/otcs.py +7956 -3237
  68. pyxecm/otds.py +2178 -925
  69. pyxecm/otiv.py +36 -21
  70. pyxecm/otmm.py +1272 -325
  71. pyxecm/otpd.py +231 -127
  72. pyxecm-2.0.1.dist-info/METADATA +122 -0
  73. pyxecm-2.0.1.dist-info/RECORD +76 -0
  74. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
  75. pyxecm-1.6.dist-info/METADATA +0 -53
  76. pyxecm-1.6.dist-info/RECORD +0 -32
  77. {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
  78. {pyxecm-1.6.dist-info → pyxecm-2.0.1.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.debug("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
+ None
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,39 @@ class K8s:
252
319
  max_retry: int = 3,
253
320
  time_retry: int = 10,
254
321
  container: str | None = None,
255
- ):
322
+ timeout: int = 60,
323
+ ) -> str:
256
324
  """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
325
+
326
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#connect_get_namespaced_pod_exec
327
+
258
328
  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
329
+ pod_name (str):
330
+ The name of the Kubernetes pod in the current namespace.
331
+ command (list):
332
+ A list of command and its parameters, e.g. ["/bin/bash", "-c", "pwd"]
333
+ The "-c" is required to make the shell executing the command.
334
+ max_retry (int):
335
+ The maximum number of attempts to execute the command.
336
+ time_retry (int):
337
+ Wait time in seconds between retries.
338
+ container (str):
339
+ The container name if the pod runs multiple containers inside.
340
+ timeout (int):
341
+ Timeout duration that is waited for any response in seconds.
342
+ Each time a response is found in stdout or stderr we wait another timeout duration [60]
343
+
263
344
  Returns:
264
- Response of the command or None if the call fails
345
+ str:
346
+ Response of the command or None if the call fails.
347
+
265
348
  """
266
349
 
267
- pod = self.get_pod(pod_name)
350
+ pod = self.get_pod(pod_name=pod_name)
268
351
  if not pod:
269
- logger.error("Pod -> '%s' does not exist", pod_name)
352
+ self.logger.error("Pod -> '%s' does not exist", pod_name)
270
353
 
271
- logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
354
+ self.logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
272
355
 
273
356
  retry_counter = 1
274
357
 
@@ -284,11 +367,10 @@ class K8s:
284
367
  stdin=False,
285
368
  stdout=True,
286
369
  tty=False,
370
+ _request_timeout=timeout,
287
371
  )
288
- logger.debug(response)
289
- return response
290
372
  except ApiException as exc:
291
- logger.warning(
373
+ self.logger.warning(
292
374
  "Failed to execute command, retry (%s/%s) -> %s in pod -> '%s'; error -> %s",
293
375
  retry_counter,
294
376
  max_retry,
@@ -298,11 +380,17 @@ class K8s:
298
380
  )
299
381
  retry_counter = retry_counter + 1
300
382
  exception = exc
301
- logger.debug("Wait %s seconds before next retry...", str(time_retry))
383
+ self.logger.debug(
384
+ "Wait %s seconds before next retry...",
385
+ str(time_retry),
386
+ )
302
387
  time.sleep(time_retry)
303
388
  continue
389
+ else:
390
+ self.logger.debug("Command execution response -> %s", response if response else "<empty>")
391
+ return response
304
392
 
305
- logger.error(
393
+ self.logger.error(
306
394
  "Failed to execute command with %s retries -> %s in pod -> '%s'; error -> %s",
307
395
  max_retry,
308
396
  command,
@@ -321,32 +409,40 @@ class K8s:
321
409
  commands: list,
322
410
  timeout: int = 30,
323
411
  write_stderr_to_error_log: bool = True,
324
- ):
412
+ ) -> str:
325
413
  """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.
414
+
415
+ Other than exec_pod_command() method above this is an interactive execution using
416
+ stdin and reading the output from stdout and stderr. This is required for longer
417
+ running commands. It is currently used for restarting the spawner of Archive Center.
418
+ The output of the command is pushed into the logging.
330
419
 
331
420
  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)
421
+ pod_name (str):
422
+ The name of the Kubernetes pod in the current namespace
423
+ commands (list):
424
+ A list of command and its parameters, e.g. ["/bin/bash", "/etc/init.d/spawner restart"]
425
+ Here we should NOT have a "-c" parameter!
426
+ timeout (int):
427
+ Timeout duration that is waited for any response.
428
+ Each time a resonse is found in stdout or stderr we wait another timeout duration
429
+ to make sure we get the full output of the command.
430
+ write_stderr_to_error_log (bool):
431
+ Flag to control if output in stderr should be written to info or error log stream.
432
+ Default is write to error log (True).
433
+
340
434
  Returns:
341
- str: Response of the command or None if the call fails
435
+ str:
436
+ Response of the command or None if the call fails.
437
+
342
438
  """
343
439
 
344
- pod = self.get_pod(pod_name)
440
+ pod = self.get_pod(pod_name=pod_name)
345
441
  if not pod:
346
- logger.error("Pod -> %s does not exist", pod_name)
442
+ self.logger.error("Pod -> '%s' does not exist", pod_name)
347
443
 
348
444
  if not commands:
349
- logger.error("No commands to execute on Pod -> %s", pod_name)
445
+ self.logger.error("No commands to execute on pod ->'%s'!", pod_name)
350
446
  return None
351
447
 
352
448
  # Get first command - this should be the shell:
@@ -363,13 +459,13 @@ class K8s:
363
459
  stdout=True,
364
460
  tty=False,
365
461
  _preload_content=False, # This is important!
462
+ _request_timeout=timeout,
366
463
  )
367
- except ApiException as exception:
368
- logger.error(
369
- "Failed to execute command -> %s in pod -> '%s'; error -> %s",
464
+ except ApiException:
465
+ self.logger.error(
466
+ "Failed to execute command -> %s in pod -> '%s'",
370
467
  command,
371
468
  pod_name,
372
- str(exception),
373
469
  )
374
470
  return None
375
471
 
@@ -377,22 +473,25 @@ class K8s:
377
473
  got_response = False
378
474
  response.update(timeout=timeout)
379
475
  if response.peek_stdout():
380
- logger.debug(response.read_stdout().replace("\n", " "))
476
+ self.logger.debug(response.read_stdout().replace("\n", " "))
381
477
  got_response = True
382
478
  if response.peek_stderr():
383
479
  if write_stderr_to_error_log:
384
- logger.error(response.read_stderr().replace("\n", " "))
480
+ self.logger.error(response.read_stderr().replace("\n", " "))
385
481
  else:
386
- logger.debug(response.read_stderr().replace("\n", " "))
482
+ self.logger.debug(response.read_stderr().replace("\n", " "))
387
483
  got_response = True
388
484
  if commands:
389
485
  command = commands.pop(0)
390
- logger.debug("Execute command -> %s in pod -> '%s'", command, pod_name)
486
+ self.logger.debug(
487
+ "Execute command -> %s in pod -> '%s'",
488
+ command,
489
+ pod_name,
490
+ )
391
491
  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
492
+ # We continue as long as we get some response during timeout period
493
+ elif not got_response:
494
+ break
396
495
 
397
496
  response.close()
398
497
 
@@ -400,13 +499,16 @@ class K8s:
400
499
 
401
500
  # end method definition
402
501
 
403
- def delete_pod(self, pod_name: str):
502
+ def delete_pod(self, pod_name: str) -> None:
404
503
  """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
504
+
505
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#delete_namespaced_pod
406
506
 
407
507
  Args:
408
- pod_name (str): name of the Kubernetes pod in the current namespace
409
- Return:
508
+ pod_name (str):
509
+ The name of the Kubernetes pod in the current namespace.
510
+
511
+ Returns:
410
512
  V1Status (object) or None if the call fails.
411
513
  - api_version: The Kubernetes API version.
412
514
  - kind: The Kubernetes object kind, which is always "Status".
@@ -416,19 +518,22 @@ class K8s:
416
518
  - reason: A short string that describes the reason for the status.
417
519
  - code: An HTTP status code that corresponds to the status.
418
520
  See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Status.md
521
+
419
522
  """
420
523
 
421
- pod = self.get_pod(pod_name)
524
+ pod = self.get_pod(pod_name=pod_name)
422
525
  if not pod:
423
- logger.error("Pod -> %s does not exist", pod_name)
526
+ self.logger.error("Pod -> '%s' does not exist!", pod_name)
424
527
 
425
528
  try:
426
529
  response = self.get_core_v1_api().delete_namespaced_pod(
427
- pod_name, namespace=self.get_namespace()
530
+ pod_name,
531
+ namespace=self.get_namespace(),
428
532
  )
429
- except ApiException as exception:
430
- logger.error(
431
- "Failed to delete Pod -> '%s'; error -> %s", pod_name, str(exception)
533
+ except ApiException:
534
+ self.logger.error(
535
+ "Failed to delete pod -> '%s'",
536
+ pod_name,
432
537
  )
433
538
  return None
434
539
 
@@ -436,35 +541,44 @@ class K8s:
436
541
 
437
542
  # end method definition
438
543
 
439
- def get_config_map(self, config_map_name: str):
544
+ def get_config_map(self, config_map_name: str) -> V1ConfigMap:
440
545
  """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
546
+
547
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_config_map
442
548
 
443
549
  Args:
444
- config_map_name (str): name of the Kubernetes config map in the current namespace
550
+ config_map_name (str):
551
+ The name of the Kubernetes config map in the current namespace.
552
+
445
553
  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,
554
+ V1ConfigMap (object):
555
+ Kubernetes Config Map object that includes these fields:
556
+ - api_version:
557
+ The Kubernetes API version.
558
+ - metadata:
559
+ A V1ObjectMeta object representing metadata about the V1ConfigMap object,
560
+ such as its name, labels, and annotations.
561
+ - data:
562
+ A dictionary containing the non-binary data stored in the ConfigMap,
451
563
  where the keys represent the keys of the data items and the values represent
452
564
  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.
565
+ - binary_data:
566
+ A dictionary containing the binary data stored in the ConfigMap,
567
+ where the keys represent the keys of the binary data items and the values
568
+ represent the values of the binary data items. Binary data is encoded as base64
569
+ strings in the dictionary values.
570
+
457
571
  """
458
572
 
459
573
  try:
460
574
  response = self.get_core_v1_api().read_namespaced_config_map(
461
- name=config_map_name, namespace=self.get_namespace()
575
+ name=config_map_name,
576
+ namespace=self.get_namespace(),
462
577
  )
463
- except ApiException as exception:
464
- logger.error(
465
- "Failed to get Config Map -> '%s'; error -> %s",
578
+ except ApiException:
579
+ self.logger.error(
580
+ "Failed to get config map -> '%s'",
466
581
  config_map_name,
467
- str(exception),
468
582
  )
469
583
  return None
470
584
 
@@ -472,14 +586,22 @@ class K8s:
472
586
 
473
587
  # end method definition
474
588
 
475
- def list_config_maps(self, field_selector: str = "", label_selector: str = ""):
589
+ def list_config_maps(
590
+ self,
591
+ field_selector: str | None = None,
592
+ label_selector: str | None = None,
593
+ ) -> V1ConfigMapList:
476
594
  """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
595
+
596
+ The list can be filtered by providing field selectors and label selectors.
597
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_config_map
479
598
 
480
599
  Args:
481
- field_selector (str): filter result based on fields
482
- label_selector (str): filter result based on labels
600
+ field_selector (str):
601
+ To filter the result based on fields.
602
+ label_selector (str):
603
+ To filter result based on labels.
604
+
483
605
  Returns:
484
606
  V1ConfigMapList (object) or None if the call fails
485
607
  Properties can be accessed with the "." notation (this is an object not a dict!):
@@ -489,6 +611,7 @@ class K8s:
489
611
  - kind: The Kubernetes object kind, which is always "ConfigMapList".
490
612
  - metadata: Additional metadata about the config map list, such as the resource version.
491
613
  See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ConfigMapList.md
614
+
492
615
  """
493
616
 
494
617
  try:
@@ -497,12 +620,11 @@ class K8s:
497
620
  label_selector=label_selector,
498
621
  namespace=self.get_namespace(),
499
622
  )
500
- except ApiException as exception:
501
- logger.error(
502
- "Failed to list Config Maps with field_selector -> '%s' and label_selector -> '%s'; error -> %s",
623
+ except ApiException:
624
+ self.logger.error(
625
+ "Failed to list config maps with field selector -> '%s' and label selector -> '%s'",
503
626
  field_selector,
504
627
  label_selector,
505
- str(exception),
506
628
  )
507
629
  return None
508
630
 
@@ -510,26 +632,30 @@ class K8s:
510
632
 
511
633
  # end method definition
512
634
 
513
- def find_config_map(self, config_map_name: str):
635
+ def find_config_map(self, config_map_name: str) -> V1ConfigMapList:
514
636
  """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.
637
+
638
+ This is just a wrapper method for list_config_maps()
639
+ that uses the name as a field selector.
517
640
 
518
641
  Args:
519
- config_map_name (str): name of the Config Map
642
+ config_map_name (str):
643
+ The name of the Kubernetes Config Map to search for.
644
+
520
645
  Returns:
521
- object: V1ConfigMapList (object) or None if the call fails
646
+ object:
647
+ V1ConfigMapList (object) or None if the call fails.
648
+
522
649
  """
523
650
 
524
651
  try:
525
652
  response = self.list_config_maps(
526
- field_selector="metadata.name={}".format(config_map_name)
653
+ field_selector="metadata.name={}".format(config_map_name),
527
654
  )
528
- except ApiException as exception:
529
- logger.error(
530
- "Failed to find Config Map -> '%s'; error -> %s",
655
+ except ApiException:
656
+ self.logger.error(
657
+ "Failed to find config map -> '%s'",
531
658
  config_map_name,
532
- str(exception),
533
659
  )
534
660
  return None
535
661
 
@@ -537,16 +663,26 @@ class K8s:
537
663
 
538
664
  # end method definition
539
665
 
540
- def replace_config_map(self, config_map_name: str, config_map_data: dict):
666
+ def replace_config_map(
667
+ self,
668
+ config_map_name: str,
669
+ config_map_data: dict,
670
+ ) -> V1ConfigMap:
541
671
  """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
672
+
673
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#replace_namespaced_config_map
543
674
 
544
675
  Args:
545
- config_map_name (str): name of the Kubernetes Config Map
546
- config_map_data (dict): new specification of the Config Map
676
+ config_map_name (str):
677
+ The name of the Kubernetes Config Map to replace.
678
+ config_map_data (dict):
679
+ The updated specification of the Config Map.
680
+
547
681
  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
682
+ V1ConfigMap (object):
683
+ Updated Kubernetes Config Map object or None if the call fails.
684
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ConfigMap.md
685
+
550
686
  """
551
687
 
552
688
  try:
@@ -560,11 +696,10 @@ class K8s:
560
696
  data=config_map_data,
561
697
  ),
562
698
  )
563
- except ApiException as exception:
564
- logger.error(
565
- "Failed to replace Config Map -> '%s'; error -> %s",
699
+ except ApiException:
700
+ self.logger.error(
701
+ "Failed to replace config map -> '%s'",
566
702
  config_map_name,
567
- str(exception),
568
703
  )
569
704
  return None
570
705
 
@@ -572,26 +707,31 @@ class K8s:
572
707
 
573
708
  # end method definition
574
709
 
575
- def get_stateful_set(self, sts_name: str):
710
+ def get_stateful_set(self, sts_name: str) -> V1StatefulSet:
576
711
  """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
712
+
713
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set
578
714
 
579
715
  Args:
580
- sts_name (str): name of the Kubernetes stateful set
716
+ sts_name (str):
717
+ The name of the Kubernetes stateful set
718
+
581
719
  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
720
+ V1StatefulSet (object):
721
+ Kubernetes Stateful Set object or None if the call fails.
722
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
723
+
584
724
  """
585
725
 
586
726
  try:
587
727
  response = self.get_apps_v1_api().read_namespaced_stateful_set(
588
- name=sts_name, namespace=self.get_namespace()
728
+ name=sts_name,
729
+ namespace=self.get_namespace(),
589
730
  )
590
- except ApiException as exception:
591
- logger.error(
592
- "Failed to get Stateful Set -> '%s'; error -> %s",
731
+ except ApiException:
732
+ self.logger.error(
733
+ "Failed to get stateful set -> '%s'",
593
734
  sts_name,
594
- str(exception),
595
735
  )
596
736
  return None
597
737
 
@@ -599,26 +739,31 @@ class K8s:
599
739
 
600
740
  # end method definition
601
741
 
602
- def get_stateful_set_scale(self, sts_name: str):
742
+ def get_stateful_set_scale(self, sts_name: str) -> V1Scale:
603
743
  """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
744
+
745
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#read_namespaced_stateful_set_scale
605
746
 
606
747
  Args:
607
- sts_name (str): name of the Kubernetes Stateful Set
748
+ sts_name (str):
749
+ The name of the Kubernetes Stateful Set.
750
+
608
751
  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
752
+ V1Scale (object):
753
+ Kubernetes Scale object or None if the call fails.
754
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Scale.md
755
+
611
756
  """
612
757
 
613
758
  try:
614
759
  response = self.get_apps_v1_api().read_namespaced_stateful_set_scale(
615
- name=sts_name, namespace=self.get_namespace()
760
+ name=sts_name,
761
+ namespace=self.get_namespace(),
616
762
  )
617
- except ApiException as exception:
618
- logger.error(
619
- "Failed to get scaling (replicas) of Stateful Set -> '%s'; error -> %s",
763
+ except ApiException:
764
+ self.logger.error(
765
+ "Failed to get scaling (replicas) of stateful set -> '%s'",
620
766
  sts_name,
621
- str(exception),
622
767
  )
623
768
  return None
624
769
 
@@ -626,28 +771,35 @@ class K8s:
626
771
 
627
772
  # end method definition
628
773
 
629
- def patch_stateful_set(self, sts_name: str, sts_body: dict):
774
+ def patch_stateful_set(self, sts_name: str, sts_body: dict) -> V1StatefulSet:
630
775
  """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
776
+
777
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/AppsV1Api.md#patch_namespaced_stateful_set
632
778
 
633
779
  Args:
634
- sts_name (str): name of the Kubernetes stateful set in the current namespace
635
- sts_body (str): patch string
780
+ sts_name (str):
781
+ The name of the Kubernetes stateful set in the current namespace.
782
+ sts_body (str):
783
+ The patch string.
784
+
636
785
  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
786
+ V1StatefulSet (object):
787
+ The patched Kubernetes Stateful Set object or None if the call fails.
788
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
789
+
639
790
  """
640
791
 
641
792
  try:
642
793
  response = self.get_apps_v1_api().patch_namespaced_stateful_set(
643
- name=sts_name, namespace=self.get_namespace(), body=sts_body
794
+ name=sts_name,
795
+ namespace=self.get_namespace(),
796
+ body=sts_body,
644
797
  )
645
- except ApiException as exception:
646
- logger.error(
647
- "Failed to patch Stateful Set -> '%s' with -> %s; error -> %s",
798
+ except ApiException:
799
+ self.logger.error(
800
+ "Failed to patch stateful set -> '%s' with -> %s",
648
801
  sts_name,
649
- sts_body,
650
- str(exception),
802
+ str(sts_body),
651
803
  )
652
804
  return None
653
805
 
@@ -655,28 +807,34 @@ class K8s:
655
807
 
656
808
  # end method definition
657
809
 
658
- def scale_stateful_set(self, sts_name: str, scale: int):
810
+ def scale_stateful_set(self, sts_name: str, scale: int) -> V1StatefulSet:
659
811
  """Scale a stateful set to a specific number of replicas.
660
- It uses the class method patch_stateful_set() above.
812
+
813
+ It uses the class method patch_stateful_set() above.
661
814
 
662
815
  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
816
+ sts_name (str):
817
+ The name of the Kubernetes stateful set in the current namespace.
818
+ scale (int):
819
+ The number of replicas (pods) the stateful set shall be scaled to.
820
+
665
821
  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
822
+ V1StatefulSet (object):
823
+ Kubernetes Stateful Set object or None if the call fails.
824
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1StatefulSet.md
825
+
668
826
  """
669
827
 
670
828
  try:
671
829
  response = self.patch_stateful_set(
672
- sts_name, sts_body={"spec": {"replicas": scale}}
830
+ sts_name,
831
+ sts_body={"spec": {"replicas": scale}},
673
832
  )
674
- except ApiException as exception:
675
- logger.error(
676
- "Failed to scale Stateful Set -> '%s' to -> %s replicas; error -> %s",
833
+ except ApiException:
834
+ self.logger.error(
835
+ "Failed to scale stateful set -> '%s' to -> %s replicas",
677
836
  sts_name,
678
837
  scale,
679
- str(exception),
680
838
  )
681
839
  return None
682
840
 
@@ -684,26 +842,30 @@ class K8s:
684
842
 
685
843
  # end method definition
686
844
 
687
- def get_service(self, service_name: str):
688
- """Get a Kubernetes Service with a defined name in the current namespace
845
+ def get_service(self, service_name: str) -> V1Service:
846
+ """Get a Kubernetes Service with a defined name in the current namespace.
689
847
 
690
848
  Args:
691
- service_name (str): name of the Kubernetes Service in the current namespace
849
+ service_name (str):
850
+ The name of the Kubernetes Service in the current namespace.
851
+
692
852
  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
853
+ V1Service (object):
854
+ Kubernetes Service object or None if the call fails
855
+ This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
856
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md
857
+
696
858
  """
697
859
 
698
860
  try:
699
861
  response = self.get_core_v1_api().read_namespaced_service(
700
- name=service_name, namespace=self.get_namespace()
862
+ name=service_name,
863
+ namespace=self.get_namespace(),
701
864
  )
702
- except ApiException as exception:
703
- logger.error(
704
- "Failed to get Service -> '%s'; error -> %s",
865
+ except ApiException:
866
+ self.logger.error(
867
+ "Failed to get service -> '%s'",
705
868
  service_name,
706
- str(exception),
707
869
  )
708
870
  return None
709
871
 
@@ -711,24 +873,30 @@ class K8s:
711
873
 
712
874
  # end method definition
713
875
 
714
- def list_services(self, field_selector: str = "", label_selector: str = ""):
876
+ def list_services(self, field_selector: str = "", label_selector: str = "") -> None:
715
877
  """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
878
+
879
+ The list can be filtered by providing field selectors and label selectors.
880
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_service
718
881
 
719
882
  Args:
720
- field_selector (str): filter result based on fields
721
- label_selector (str): filter result based on labels
883
+ field_selector (str):
884
+ To filter result based on fields.
885
+ label_selector (str):
886
+ To filter result based on labels.
887
+
722
888
  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
889
+ V1ServiceList (object):
890
+ A list of Kubernetes Services or None if the call fails.
891
+ Properties can be accessed with the "." notation (this is an object not a dict!):
892
+ - api_version: The Kubernetes API version.
893
+ - items: A list of V1Service objects, each representing a service.
894
+ You can access the fields of a V1Service object using dot notation,
895
+ for example, service.metadata.name to access the name of the service
896
+ - kind: The Kubernetes object kind, which is always "ServiceList".
897
+ - metadata: Additional metadata about the pod list, such as the resource version.
898
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1ServiceList.md
899
+
732
900
  """
733
901
 
734
902
  try:
@@ -737,12 +905,11 @@ class K8s:
737
905
  label_selector=label_selector,
738
906
  namespace=self.get_namespace(),
739
907
  )
740
- except ApiException as exception:
741
- logger.error(
742
- "Failed to list Services with field_selector -> '%s' and label_selector -> '%s'; error -> %s",
908
+ except ApiException:
909
+ self.logger.error(
910
+ "Failed to list services with field selector -> '%s' and label selector -> '%s'",
743
911
  field_selector,
744
912
  label_selector,
745
- str(exception),
746
913
  )
747
914
  return None
748
915
 
@@ -750,30 +917,36 @@ class K8s:
750
917
 
751
918
  # end method definition
752
919
 
753
- def patch_service(self, service_name: str, service_body: dict):
754
- """Patches a Kubernetes Service with an updated spec
920
+ def patch_service(self, service_name: str, service_body: dict) -> V1Service:
921
+ """Patch a Kubernetes Service with an updated spec.
755
922
 
756
923
  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)
924
+ service_name (str):
925
+ The name of the Kubernetes Ingress in the current namespace.
926
+ service_body (dict):
927
+ The new / updated Service body spec.
928
+ (will be merged with existing values)
929
+
760
930
  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
931
+ V1Service (object):
932
+ The patched Kubernetes Service or None if the call fails.
933
+ This is NOT a dict but an object - you have to use the "." syntax
934
+ to access to returned elements
935
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md
936
+
765
937
  """
766
938
 
767
939
  try:
768
940
  response = self.get_core_v1_api().patch_namespaced_service(
769
- name=service_name, namespace=self.get_namespace(), body=service_body
941
+ name=service_name,
942
+ namespace=self.get_namespace(),
943
+ body=service_body,
770
944
  )
771
- except ApiException as exception:
772
- logger.error(
773
- "Failed to patch Service -> '%s' with -> %s; error -> %s",
945
+ except ApiException:
946
+ self.logger.error(
947
+ "Failed to patch service -> '%s' with -> %s",
774
948
  service_name,
775
949
  service_body,
776
- str(exception),
777
950
  )
778
951
  return None
779
952
 
@@ -781,24 +954,30 @@ class K8s:
781
954
 
782
955
  # end method definition
783
956
 
784
- def get_ingress(self, ingress_name: str):
785
- """Get a Kubernetes Ingress with a defined name in the current namespace
957
+ def get_ingress(self, ingress_name: str) -> V1Ingress:
958
+ """Get a Kubernetes Ingress with a defined name in the current namespace.
786
959
 
787
960
  Args:
788
- ingress_name (str): name of the Kubernetes Ingress in the current namespace
961
+ ingress_name (str):
962
+ The name of the Kubernetes Ingress in the current namespace.
963
+
789
964
  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
965
+ V1Ingress (object):
966
+ Kubernetes Ingress or None if the call fails
967
+ This is NOT a dict but an object - the you have to use the "." syntax to access to returned elements.
968
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
969
+
793
970
  """
794
971
 
795
972
  try:
796
973
  response = self.get_networking_v1_api().read_namespaced_ingress(
797
- name=ingress_name, namespace=self.get_namespace()
974
+ name=ingress_name,
975
+ namespace=self.get_namespace(),
798
976
  )
799
- except ApiException as exception:
800
- logger.error(
801
- "Failed to get Ingress -> %s; error -> %s", ingress_name, str(exception)
977
+ except ApiException:
978
+ self.logger.error(
979
+ "Failed to get ingress -> '%s'!",
980
+ ingress_name,
802
981
  )
803
982
  return None
804
983
 
@@ -806,19 +985,25 @@ class K8s:
806
985
 
807
986
  # end method definition
808
987
 
809
- def patch_ingress(self, ingress_name: str, ingress_body: dict):
988
+ def patch_ingress(self, ingress_name: str, ingress_body: dict) -> V1Ingress:
810
989
  """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
990
+
991
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/NetworkingV1Api.md#patch_namespaced_ingress
812
992
 
813
993
  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)
994
+ ingress_name (str):
995
+ The name of the Kubernetes Ingress in the current namespace.
996
+ ingress_body (dict):
997
+ The new / updated ingress body spec.
998
+ (will be merged with existing values)
999
+
817
1000
  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
1001
+ V1Ingress (object):
1002
+ The patched Kubernetes Ingress object or None if the call fails
1003
+ This is NOT a dict but an object - you have to use the
1004
+ "." syntax to access to returned elements
1005
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
1006
+
822
1007
  """
823
1008
 
824
1009
  try:
@@ -827,12 +1012,11 @@ class K8s:
827
1012
  namespace=self.get_namespace(),
828
1013
  body=ingress_body,
829
1014
  )
830
- except ApiException as exception:
831
- logger.error(
832
- "Failed to patch Ingress -> %s with -> %s; error -> %s",
1015
+ except ApiException:
1016
+ self.logger.error(
1017
+ "Failed to patch ingress -> '%s' with -> %s",
833
1018
  ingress_name,
834
1019
  ingress_body,
835
- str(exception),
836
1020
  )
837
1021
  return None
838
1022
 
@@ -841,9 +1025,13 @@ class K8s:
841
1025
  # end method definition
842
1026
 
843
1027
  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
1028
+ self,
1029
+ ingress_name: str,
1030
+ hostname: str,
1031
+ service_name: str,
1032
+ service_port: int,
1033
+ ) -> V1Ingress:
1034
+ """Update a backend service and port of an Kubernetes Ingress.
847
1035
 
848
1036
  "spec": {
849
1037
  "rules": [
@@ -871,18 +1059,25 @@ class K8s:
871
1059
  }
872
1060
 
873
1061
  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
1062
+ ingress_name (str):
1063
+ The name of the Kubernetes Ingress in the current namespace.
1064
+ hostname (str):
1065
+ The hostname that should get an updated backend service / port.
1066
+ service_name (str):
1067
+ The new backend service name.
1068
+ service_port (int):
1069
+ The new backend service port.
1070
+
878
1071
  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
1072
+ V1Ingress (object):
1073
+ The updated Kubernetes Ingress object or None if the call fails.
1074
+ This is NOT a dict but an object - you have to use the "." syntax
1075
+ to access to returned elements
1076
+ See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Ingress.md
1077
+
883
1078
  """
884
1079
 
885
- ingress = self.get_ingress(ingress_name)
1080
+ ingress = self.get_ingress(ingress_name=ingress_name)
886
1081
  if not ingress:
887
1082
  return None
888
1083
 
@@ -896,8 +1091,8 @@ class K8s:
896
1091
  backend = path.backend
897
1092
  service = backend.service
898
1093
 
899
- logger.debug(
900
- "Replace backend service -> %s (%s) with new backend service -> %s (%s)",
1094
+ self.logger.debug(
1095
+ "Replace backend service -> '%s' (%s) with new backend service -> '%s' (%s)",
901
1096
  service.name,
902
1097
  service.port.number,
903
1098
  service_name,
@@ -907,25 +1102,24 @@ class K8s:
907
1102
  service.name = service_name
908
1103
  service.port.number = service_port
909
1104
  break
910
- else:
911
- rule_index += 1
1105
+ rule_index += 1
912
1106
 
913
1107
  if not host:
914
- logger.error("Cannot find host -> {}.")
1108
+ self.logger.error("Cannot find host.")
915
1109
  return None
916
1110
 
917
1111
  body = [
918
1112
  {
919
1113
  "op": "replace",
920
1114
  "path": "/spec/rules/{}/http/paths/0/backend/service/name".format(
921
- rule_index
1115
+ rule_index,
922
1116
  ),
923
1117
  "value": service_name,
924
1118
  },
925
1119
  {
926
1120
  "op": "replace",
927
1121
  "path": "/spec/rules/{}/http/paths/0/backend/service/port/number".format(
928
- rule_index
1122
+ rule_index,
929
1123
  ),
930
1124
  "value": service_port,
931
1125
  },
@@ -933,16 +1127,17 @@ class K8s:
933
1127
 
934
1128
  return self.patch_ingress(ingress_name, body)
935
1129
 
1130
+ # end method definition
1131
+
936
1132
  def verify_pod_status(
937
1133
  self,
938
1134
  pod_name: str,
939
- timeout: int = 1200,
1135
+ timeout: int = 1800,
940
1136
  total_containers: int = 1,
941
1137
  ready_containers: int = 1,
942
1138
  retry_interval: int = 30,
943
1139
  ) -> bool:
944
- """
945
- Verifies if a pod is in a 'Ready' state by checking the status of its containers.
1140
+ """Verify if a pod is in a 'Ready' state by checking the status of its containers.
946
1141
 
947
1142
  This function waits for a Kubernetes pod to reach the 'Ready' state, where a specified number
948
1143
  of containers are ready. It checks the pod status at regular intervals and reports the status
@@ -950,93 +1145,420 @@ class K8s:
950
1145
  it returns `False`.
951
1146
 
952
1147
  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.
1148
+ pod_name (str):
1149
+ The name of the pod to check the status for.
1150
+ timeout (int, optional):
1151
+ The maximum time (in seconds) to wait for the pod to become ready. Defaults to 1800.
1152
+ total_containers (int, optional):
1153
+ The total number of containers expected to be running in the pod. Defaults to 1.
1154
+ ready_containers (int, optional):
1155
+ The minimum number of containers that need to be in a ready state. Defaults to 1.
1156
+ retry_interval (int, optional):
1157
+ Time interval (in seconds) between each retry to check pod readiness. Defaults to 30.
958
1158
 
959
1159
  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`.
1160
+ bool:
1161
+ Returns `True` if the pod reaches the 'Ready' state with the specified number of containers ready
1162
+ within the timeout. Otherwise, returns `False`.
1163
+
962
1164
  """
963
1165
 
964
1166
  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.
1167
+ """Wait until the pod is in the 'Ready' state with the specified number of containers ready.
967
1168
 
968
- This internal function repeatedly checks the readiness of the pod, logging the
1169
+ This sub method repeatedly checks the readiness of the pod, logging the
969
1170
  status of the containers. If the pod does not exist, it retries after waiting
970
1171
  and logs detailed information at each step.
971
1172
 
972
1173
  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.
1174
+ pod_name (str):
1175
+ The name of the pod to check the status for.
1176
+ timeout (int):
1177
+ The maximum time (in seconds) to wait for the pod to become ready.
975
1178
 
976
1179
  Returns:
977
- bool: Returns `True` if the pod is ready with the specified number of containers in a 'Ready' state.
978
- Otherwise, returns `False`.
1180
+ bool:
1181
+ Returns `True` if the pod is ready with the specified number of containers in a 'Ready' state.
1182
+ Otherwise, returns `False`.
1183
+
979
1184
  """
1185
+
980
1186
  elapsed_time = 0 # Initialize elapsed time
981
1187
 
982
1188
  while elapsed_time < timeout:
983
- pod = self.get_pod(pod_name)
1189
+ pod = self.get_pod(pod_name=pod_name)
984
1190
 
985
1191
  if not pod:
986
- logger.error(
987
- "Pod -> %s does not exist, waiting 300 seconds to retry.",
1192
+ self.logger.warning(
1193
+ "Pod -> '%s' does not exist, waiting 300 seconds to retry.",
988
1194
  pod_name,
989
1195
  )
990
1196
  time.sleep(300)
991
- pod = self.get_pod(pod_name)
1197
+ pod = self.get_pod(pod_name=pod_name)
992
1198
 
993
1199
  if not pod:
994
- logger.error(
995
- "Pod -> %s still does not exist after retry!", pod_name
1200
+ self.logger.error(
1201
+ "Pod -> '%s' still does not exist after retry!",
1202
+ pod_name,
996
1203
  )
997
1204
  return False
998
1205
 
999
1206
  # Get the ready status of containers
1000
1207
  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
- )
1208
+ if container_statuses and all(container.ready for container in container_statuses):
1209
+ current_ready_containers = sum(1 for c in container_statuses if c.ready)
1007
1210
  total_containers_in_pod = len(container_statuses)
1008
1211
 
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.",
1212
+ if current_ready_containers >= ready_containers and total_containers_in_pod == total_containers:
1213
+ self.logger.info(
1214
+ "Pod -> '%s' is ready with %d/%d containers.",
1015
1215
  pod_name,
1016
1216
  current_ready_containers,
1017
1217
  total_containers_in_pod,
1018
1218
  )
1019
1219
  return True
1020
1220
  else:
1021
- logger.debug(
1022
- "Pod -> %s is not yet ready (%d/%d).",
1221
+ self.logger.debug(
1222
+ "Pod -> '%s' is not yet ready (%d/%d).",
1023
1223
  pod_name,
1024
1224
  current_ready_containers,
1025
1225
  total_containers_in_pod,
1026
1226
  )
1027
1227
  else:
1028
- logger.debug("Pod -> %s is not yet ready.", pod_name)
1228
+ self.logger.debug("Pod -> '%s' is not yet ready.", pod_name)
1029
1229
 
1030
- logger.info(
1031
- f"Waiting {retry_interval} seconds before next pod status check."
1230
+ self.logger.info(
1231
+ "Waiting %s seconds before next pod status check.",
1232
+ retry_interval,
1032
1233
  )
1033
1234
  time.sleep(
1034
- retry_interval
1235
+ retry_interval,
1035
1236
  ) # Sleep for the retry interval before checking again
1036
1237
  elapsed_time += retry_interval
1037
1238
 
1038
- logger.error("Pod -> %s is not ready after %d seconds.", pod_name, timeout)
1239
+ self.logger.error(
1240
+ "Pod -> '%s' is not ready after %d seconds.",
1241
+ pod_name,
1242
+ timeout,
1243
+ )
1039
1244
  return False
1040
1245
 
1246
+ # end method definition
1247
+
1041
1248
  # Wait until the pod is ready
1042
- return wait_for_pod_ready(pod_name, timeout)
1249
+ return wait_for_pod_ready(pod_name=pod_name, timeout=timeout)
1250
+
1251
+ # end method definition
1252
+
1253
+ def verify_pod_deleted(
1254
+ self,
1255
+ pod_name: str,
1256
+ timeout: int = 300,
1257
+ retry_interval: int = 30,
1258
+ ) -> bool:
1259
+ """Verify if a pod is deleted within the specified timeout.
1260
+
1261
+ Args:
1262
+ pod_name (str):
1263
+ The name of the pod to check.
1264
+ timeout (int):
1265
+ Maximum time to wait for the pod to be deleted (in seconds).
1266
+ retry_interval:
1267
+ Time interval between retries (in seconds).
1268
+
1269
+ Returns:
1270
+ bool:
1271
+ True if the pod is deleted, False otherwise.
1272
+
1273
+ """
1274
+
1275
+ elapsed_time = 0 # Initialize elapsed time
1276
+
1277
+ while elapsed_time < timeout:
1278
+ pod = self.get_pod(pod_name=pod_name)
1279
+
1280
+ if not pod:
1281
+ self.logger.info("Pod -> '%s' has been deleted successfully.", pod_name)
1282
+ return True
1283
+
1284
+ pod_status = self.get_core_v1_api().read_namespaced_pod_status(
1285
+ pod_name,
1286
+ self.get_namespace(),
1287
+ )
1288
+
1289
+ self.logger.info(
1290
+ "Pod -> '%s' still exists with conditions -> %s. Waiting %s seconds before next check.",
1291
+ pod_name,
1292
+ str(
1293
+ [
1294
+ pod_condition.type
1295
+ for pod_condition in pod_status.status.conditions
1296
+ if pod_condition.status == "True"
1297
+ ]
1298
+ ),
1299
+ retry_interval,
1300
+ )
1301
+ time.sleep(retry_interval)
1302
+ elapsed_time += retry_interval
1303
+
1304
+ self.logger.error("Pod -> '%s' was not deleted within %d seconds.", pod_name, timeout)
1305
+
1306
+ return False
1307
+
1308
+ # end method definition
1309
+
1310
+ def restart_deployment(
1311
+ self, deployment_name: str, force: bool = False, wait: bool = False, wait_timeout: int = 1800
1312
+ ) -> bool:
1313
+ """Restart a Kubernetes deployment using rolling restart.
1314
+
1315
+ Args:
1316
+ deployment_name (str):
1317
+ Name of the Kubernetes deployment.
1318
+ force (bool):
1319
+ If True, all pod instances will be forcefully deleted. [False]
1320
+ wait (bool):
1321
+ If True, wait for the stateful set to be ready again. [False]
1322
+ wait_timeout (int):
1323
+ Maximum time to wait for the stateful set to be ready again (in seconds). [1800]
1324
+
1325
+ Returns:
1326
+ bool:
1327
+ True if successful, False otherwise.
1328
+
1329
+ """
1330
+
1331
+ success = True
1332
+
1333
+ if not force:
1334
+ now = datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z"
1335
+
1336
+ body = {
1337
+ "spec": {
1338
+ "template": {
1339
+ "metadata": {
1340
+ "annotations": {
1341
+ "kubectl.kubernetes.io/restartedAt": now,
1342
+ },
1343
+ },
1344
+ },
1345
+ },
1346
+ }
1347
+ try:
1348
+ self.get_apps_v1_api().patch_namespaced_deployment(
1349
+ deployment_name,
1350
+ self.get_namespace(),
1351
+ body,
1352
+ pretty="true",
1353
+ )
1354
+ self.logger.info("Triggered restart of deployment -> '%s'.", deployment_name)
1355
+
1356
+ except ApiException as api_exception:
1357
+ self.logger.error(
1358
+ "Failed to restart deployment -> '%s'; error -> %s!", deployment_name, str(api_exception)
1359
+ )
1360
+ return False
1361
+
1362
+ # If force is set, all pod instances will be forcefully deleted.
1363
+ elif force:
1364
+ self.logger.info("Force deleting all pods of deployment -> '%s'.", deployment_name)
1365
+
1366
+ try:
1367
+ # Get the Deployment to retrieve its pod labels
1368
+ deployment = self.get_apps_v1_api().read_namespaced_deployment(
1369
+ name=deployment_name, namespace=self.get_namespace()
1370
+ )
1371
+
1372
+ # Get the label selector for the Deployment
1373
+ label_selector = deployment.spec.selector.match_labels
1374
+
1375
+ # List pods matching the label selector
1376
+ pods = (
1377
+ self.get_core_v1_api()
1378
+ .list_namespaced_pod(
1379
+ namespace=self.get_namespace(),
1380
+ label_selector=",".join([f"{k}={v}" for k, v in label_selector.items()]),
1381
+ )
1382
+ .items
1383
+ )
1384
+
1385
+ # Loop through the pods and delete each one
1386
+ for pod in pods:
1387
+ pod_name = pod.metadata.name
1388
+ try:
1389
+ # Define the delete options with force and grace period set to 0
1390
+ body = client.V1DeleteOptions(grace_period_seconds=0, propagation_policy="Foreground")
1391
+
1392
+ # Call the delete_namespaced_pod method
1393
+ self.get_core_v1_api().delete_namespaced_pod(
1394
+ name=pod_name, namespace=self.get_namespace(), body=body
1395
+ )
1396
+ self.logger.info(
1397
+ "Pod '%s' in namespace '%s' has been deleted forcefully.", pod_name, self.get_namespace()
1398
+ )
1399
+ except Exception as e:
1400
+ self.logger.error("Error occurred while deleting pod '%s': %s", pod_name, e)
1401
+ success = False
1402
+
1403
+ except Exception as e:
1404
+ self.logger.error("Error occurred while getting Deployment '%s': %s", deployment_name, e)
1405
+ success = False
1406
+
1407
+ start_time = time.time()
1408
+ while wait:
1409
+ self.logger.info("Waiting for restart of deployment -> '%s' to complete.", deployment_name)
1410
+ # Get the deployment
1411
+ deployment = self.get_apps_v1_api().read_namespaced_deployment_status(deployment_name, self.get_namespace())
1412
+
1413
+ # Check the availability status
1414
+ ready_replicas = deployment.status.ready_replicas or 0
1415
+ updated_replicas = deployment.status.updated_replicas or 0
1416
+ unavailable_replicas = deployment.status.unavailable_replicas or 0
1417
+ total_replicas = deployment.status.replicas or 0
1418
+ desired_replicas = deployment.spec.replicas or 0
1419
+
1420
+ self.logger.debug(
1421
+ "Deployment status -> updated pods: %s/%s -> ready replicas: %s/%s",
1422
+ updated_replicas,
1423
+ desired_replicas,
1424
+ ready_replicas,
1425
+ total_replicas,
1426
+ )
1427
+
1428
+ if (
1429
+ updated_replicas == desired_replicas
1430
+ and unavailable_replicas == 0
1431
+ and total_replicas == desired_replicas
1432
+ ):
1433
+ self.logger.info("Restart of deployment -> '%s' completed successfully", deployment_name)
1434
+ break
1435
+
1436
+ if (time.time() - start_time) > wait_timeout:
1437
+ self.logger.error("Timed out waiting for restart of deployment -> '%s' to complete.", deployment_name)
1438
+ success = False
1439
+ break
1440
+
1441
+ # Sleep for a while before checking again
1442
+ time.sleep(20)
1443
+
1444
+ return success
1445
+
1446
+ # end method definition
1447
+
1448
+ def restart_stateful_set(
1449
+ self, sts_name: str, force: bool = False, wait: bool = False, wait_timeout: int = 1800
1450
+ ) -> bool:
1451
+ """Restart a Kubernetes stateful set using rolling restart.
1452
+
1453
+ Args:
1454
+ sts_name (str):
1455
+ Name of the Kubernetes statefulset.
1456
+ force (bool, optional):
1457
+ If True, all pod instances will be forcefully deleted. [False]
1458
+ wait (bool, optional):
1459
+ If True, wait for the stateful set to be ready again. [False]
1460
+ wait_timeout (int, optional):
1461
+ Maximum time to wait for the stateful set to be ready again (in seconds). [1800]
1462
+
1463
+ Returns:
1464
+ bool:
1465
+ True if successful, False otherwise.
1466
+
1467
+ """
1468
+
1469
+ success = True
1470
+
1471
+ now = datetime.now(timezone.utc).isoformat(timespec="seconds") + "Z"
1472
+
1473
+ body = {
1474
+ "spec": {
1475
+ "template": {
1476
+ "metadata": {
1477
+ "annotations": {
1478
+ "kubectl.kubernetes.io/restartedAt": now,
1479
+ },
1480
+ },
1481
+ },
1482
+ },
1483
+ }
1484
+
1485
+ try:
1486
+ self.get_apps_v1_api().patch_namespaced_stateful_set(sts_name, self.get_namespace(), body, pretty="true")
1487
+ self.logger.info("Triggered restart of stateful set -> '%s'.", sts_name)
1488
+
1489
+ except ApiException as api_exception:
1490
+ self.logger.error("Failed to restart stateful set -> '%s'; error -> %s!", sts_name, str(api_exception))
1491
+ return False
1492
+
1493
+ # If force is set, all pod instances will be forcefully deleted.
1494
+ if force:
1495
+ self.logger.info("Force deleting all pods of stateful set -> '%s'.", sts_name)
1496
+
1497
+ try:
1498
+ # Get the StatefulSet
1499
+ statefulset = self.get_apps_v1_api().read_namespaced_stateful_set(
1500
+ name=sts_name, namespace=self.get_namespace()
1501
+ )
1502
+
1503
+ # Loop through the replicas of the StatefulSet
1504
+ for i in range(statefulset.spec.replicas):
1505
+ pod_name = f"{statefulset.metadata.name}-{i}"
1506
+ try:
1507
+ # Define the delete options with force and grace period set to 0
1508
+ body = client.V1DeleteOptions(grace_period_seconds=0, propagation_policy="Foreground")
1509
+
1510
+ # Call the delete_namespaced_pod method
1511
+ self.get_core_v1_api().delete_namespaced_pod(
1512
+ name=pod_name, namespace=self.get_namespace(), body=body
1513
+ )
1514
+ self.logger.info(
1515
+ "Pod -> '%s' in namespace -> '%s' has been deleted forcefully.",
1516
+ pod_name,
1517
+ self.get_namespace(),
1518
+ )
1519
+
1520
+ except Exception as e:
1521
+ self.logger.error("Error occurred while deleting pod -> '%s': %s", pod_name, str(e))
1522
+ success = False
1523
+
1524
+ except Exception as e:
1525
+ self.logger.error("Error occurred while getting stateful set -> '%s': %s", sts_name, str(e))
1526
+ success = False
1527
+
1528
+ start_time = time.time()
1529
+
1530
+ while wait:
1531
+ time.sleep(10) # Add delay before checking that the stateful set is ready again.
1532
+ self.logger.info("Waiting for restart of stateful set -> '%s' to complete...", sts_name)
1533
+ # Get the deployment
1534
+ statefulset = self.get_apps_v1_api().read_namespaced_stateful_set_status(sts_name, self.get_namespace())
1535
+
1536
+ # Check the availability status
1537
+ available_replicas = statefulset.status.available_replicas or 0
1538
+ desired_replicas = statefulset.spec.replicas or 0
1539
+
1540
+ current_revision = statefulset.status.current_revision or ""
1541
+ update_revision = statefulset.status.update_revision or ""
1542
+
1543
+ self.logger.debug(
1544
+ "Stateful set status -> available pods: %s/%s, revision updated: %s",
1545
+ available_replicas,
1546
+ desired_replicas,
1547
+ current_revision == update_revision,
1548
+ )
1549
+
1550
+ if available_replicas == desired_replicas and update_revision == current_revision:
1551
+ self.logger.info("Stateful set -> '%s' completed restart successfully", sts_name)
1552
+ break
1553
+
1554
+ if (time.time() - start_time) > wait_timeout:
1555
+ self.logger.error("Timed out waiting for restart of stateful set -> '%s' to complete.", sts_name)
1556
+ success = False
1557
+ break
1558
+
1559
+ # Sleep for a while before checking again
1560
+ time.sleep(10)
1561
+
1562
+ return success
1563
+
1564
+ # end method definition