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.
- pyxecm/__init__.py +7 -4
- pyxecm/avts.py +727 -254
- pyxecm/coreshare.py +686 -467
- pyxecm/customizer/__init__.py +16 -4
- pyxecm/customizer/__main__.py +58 -0
- pyxecm/customizer/api/__init__.py +5 -0
- pyxecm/customizer/api/__main__.py +6 -0
- pyxecm/customizer/api/app.py +163 -0
- pyxecm/customizer/api/auth/__init__.py +1 -0
- pyxecm/customizer/api/auth/functions.py +92 -0
- pyxecm/customizer/api/auth/models.py +13 -0
- pyxecm/customizer/api/auth/router.py +78 -0
- pyxecm/customizer/api/common/__init__.py +1 -0
- pyxecm/customizer/api/common/functions.py +47 -0
- pyxecm/customizer/api/common/metrics.py +92 -0
- pyxecm/customizer/api/common/models.py +21 -0
- pyxecm/customizer/api/common/payload_list.py +870 -0
- pyxecm/customizer/api/common/router.py +72 -0
- pyxecm/customizer/api/settings.py +128 -0
- pyxecm/customizer/api/terminal/__init__.py +1 -0
- pyxecm/customizer/api/terminal/router.py +87 -0
- pyxecm/customizer/api/v1_csai/__init__.py +1 -0
- pyxecm/customizer/api/v1_csai/router.py +87 -0
- pyxecm/customizer/api/v1_maintenance/__init__.py +1 -0
- pyxecm/customizer/api/v1_maintenance/functions.py +100 -0
- pyxecm/customizer/api/v1_maintenance/models.py +12 -0
- pyxecm/customizer/api/v1_maintenance/router.py +76 -0
- pyxecm/customizer/api/v1_otcs/__init__.py +1 -0
- pyxecm/customizer/api/v1_otcs/functions.py +61 -0
- pyxecm/customizer/api/v1_otcs/router.py +179 -0
- pyxecm/customizer/api/v1_payload/__init__.py +1 -0
- pyxecm/customizer/api/v1_payload/functions.py +179 -0
- pyxecm/customizer/api/v1_payload/models.py +51 -0
- pyxecm/customizer/api/v1_payload/router.py +499 -0
- pyxecm/customizer/browser_automation.py +721 -286
- pyxecm/customizer/customizer.py +1076 -1425
- pyxecm/customizer/exceptions.py +35 -0
- pyxecm/customizer/guidewire.py +1186 -0
- pyxecm/customizer/k8s.py +901 -379
- pyxecm/customizer/log.py +107 -0
- pyxecm/customizer/m365.py +2967 -920
- pyxecm/customizer/nhc.py +1169 -0
- pyxecm/customizer/openapi.py +258 -0
- pyxecm/customizer/payload.py +18228 -7820
- pyxecm/customizer/pht.py +717 -286
- pyxecm/customizer/salesforce.py +516 -342
- pyxecm/customizer/sap.py +58 -41
- pyxecm/customizer/servicenow.py +611 -372
- pyxecm/customizer/settings.py +445 -0
- pyxecm/customizer/successfactors.py +408 -346
- pyxecm/customizer/translate.py +83 -48
- pyxecm/helper/__init__.py +5 -2
- pyxecm/helper/assoc.py +83 -43
- pyxecm/helper/data.py +2406 -870
- pyxecm/helper/logadapter.py +27 -0
- pyxecm/helper/web.py +229 -101
- pyxecm/helper/xml.py +596 -171
- pyxecm/maintenance_page/__init__.py +5 -0
- pyxecm/maintenance_page/__main__.py +6 -0
- pyxecm/maintenance_page/app.py +51 -0
- pyxecm/maintenance_page/settings.py +28 -0
- pyxecm/maintenance_page/static/favicon.avif +0 -0
- pyxecm/maintenance_page/templates/maintenance.html +165 -0
- pyxecm/otac.py +235 -141
- pyxecm/otawp.py +2668 -1220
- pyxecm/otca.py +569 -0
- pyxecm/otcs.py +7956 -3237
- pyxecm/otds.py +2178 -925
- pyxecm/otiv.py +36 -21
- pyxecm/otmm.py +1272 -325
- pyxecm/otpd.py +231 -127
- pyxecm-2.0.1.dist-info/METADATA +122 -0
- pyxecm-2.0.1.dist-info/RECORD +76 -0
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info}/WHEEL +1 -1
- pyxecm-1.6.dist-info/METADATA +0 -53
- pyxecm-1.6.dist-info/RECORD +0 -32
- {pyxecm-1.6.dist-info → pyxecm-2.0.1.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
3
|
-
|
|
1
|
+
"""Kubernetes Module to implement functions to read / write Kubernetes objects.
|
|
2
|
+
|
|
3
|
+
This includes as Pods, Stateful Sets, Config Maps, ...
|
|
4
4
|
|
|
5
|
-
https://github.com/kubernetes-client/python
|
|
5
|
+
https://github.com/kubernetes-client/python
|
|
6
6
|
https://github.com/kubernetes-client/python/blob/master/kubernetes/README.md
|
|
7
7
|
https://github.com/kubernetes-client/python/tree/master/examples
|
|
8
|
-
|
|
9
|
-
Class: K8s
|
|
10
|
-
Methods:
|
|
11
|
-
|
|
12
|
-
__init__ : class initializer
|
|
13
|
-
get_core_v1_api: Get Kubernetes API object for Core APIs
|
|
14
|
-
get_apps_v1_api: Get Kubernetes API object for Applications (e.g. Stateful Sets)
|
|
15
|
-
get_networking_v1_api: Get Kubernetes API object for Networking (e.g. Ingress)
|
|
16
|
-
get_namespace: Get the Kubernetes namespace the K8s object is configured for
|
|
17
|
-
|
|
18
|
-
get_pod: Get a Kubernetes Pod based on its name
|
|
19
|
-
list_pods: Get a list of Kubernetes pods based on field and label selectors
|
|
20
|
-
exec_pod_command: Execute a list of commands in a Kubernetes Pod
|
|
21
|
-
exec_pod_command_interactive: Write commands to stdin and wait for response
|
|
22
|
-
delete_pod: Delete a running pod (e.g. to make Kubernetes restart it)
|
|
23
|
-
|
|
24
|
-
get_config_map: Get a Kubernetes Config Map based on its name
|
|
25
|
-
list_config_maps: Get a list of Kubernetes Config Maps based on field and label selectors
|
|
26
|
-
find_config_map: Find a Kubernetes Config Map based on its name
|
|
27
|
-
replace_config_map: Replace the data body of a Kubernetes Config Map
|
|
28
|
-
|
|
29
|
-
get_stateful_set: Gets a Kubernetes Stateful Set based on its name
|
|
30
|
-
get_stateful_set_scale: Gets the number of replicas for a Kubernetes Stateful Set
|
|
31
|
-
patch_stateful_set: Updates the specification of a Kubernetes Stateful Set
|
|
32
|
-
scale_stateful_set: Changes number of replicas for a Kubernetes Stateful Set
|
|
33
|
-
|
|
34
|
-
get_service: Get a Kubernetes Service based on its name
|
|
35
|
-
list_services: Get a list of Kubernetes Services based on field and label selectors
|
|
36
|
-
patch_service: Update the specification of a Kubernetes Service
|
|
37
|
-
|
|
38
|
-
get_ingress: Get a Kubernetes Ingress based on its name
|
|
39
|
-
patch_ingress: Update the specification of a Kubernetes Ingress
|
|
40
|
-
update_ingress_backend_services: Replace the backend service and port for an ingress host
|
|
41
|
-
|
|
42
8
|
"""
|
|
43
9
|
|
|
44
10
|
__author__ = "Dr. Marc Diefenbruch"
|
|
45
|
-
__copyright__ = "Copyright 2024, OpenText"
|
|
11
|
+
__copyright__ = "Copyright (C) 2024-2025, OpenText"
|
|
46
12
|
__credits__ = ["Kai-Philip Gatzweiler"]
|
|
47
13
|
__maintainer__ = "Dr. Marc Diefenbruch"
|
|
48
14
|
__email__ = "mdiefenb@opentext.com"
|
|
49
15
|
|
|
50
16
|
import logging
|
|
17
|
+
import os
|
|
51
18
|
import time
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
|
|
52
21
|
from kubernetes import client, config
|
|
53
|
-
from kubernetes.
|
|
22
|
+
from kubernetes.client import (
|
|
23
|
+
AppsV1Api,
|
|
24
|
+
CoreV1Api,
|
|
25
|
+
NetworkingV1Api,
|
|
26
|
+
V1ConfigMap,
|
|
27
|
+
V1ConfigMapList,
|
|
28
|
+
V1Ingress,
|
|
29
|
+
V1Pod,
|
|
30
|
+
V1PodList,
|
|
31
|
+
V1Scale,
|
|
32
|
+
V1Service,
|
|
33
|
+
V1StatefulSet,
|
|
34
|
+
)
|
|
54
35
|
from kubernetes.client.exceptions import ApiException
|
|
36
|
+
from kubernetes.config.config_exception import ConfigException
|
|
37
|
+
from kubernetes.stream import stream
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
# config.load_incluster_config()
|
|
58
|
-
|
|
59
|
-
logger = logging.getLogger("pyxecm.customizer.k8s")
|
|
39
|
+
default_logger = logging.getLogger("pyxecm.customizer.k8s")
|
|
60
40
|
|
|
61
41
|
|
|
62
42
|
class K8s:
|
|
63
|
-
"""
|
|
43
|
+
"""Provides an interface to interact with the Kubernetes API.
|
|
44
|
+
|
|
45
|
+
This class can run both in-cluster and locally using kubeconfig.
|
|
46
|
+
It offers methods to interact with Kubernetes namespaces, pods,
|
|
47
|
+
and various API objects like CoreV1, AppsV1, and NetworkingV1.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
logger (logging.Logger): Logger for the class.
|
|
51
|
+
_core_v1_api (CoreV1Api): API client for Kubernetes Core V1.
|
|
52
|
+
_apps_v1_api (AppsV1Api): API client for Kubernetes Apps V1.
|
|
53
|
+
_networking_v1_api (NetworkingV1Api): API client for Kubernetes Networking V1.
|
|
54
|
+
_namespace (str): The namespace in which operations are performed.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
logger: logging.Logger = default_logger
|
|
64
59
|
|
|
65
60
|
_core_v1_api = None
|
|
66
61
|
_apps_v1_api = None
|
|
@@ -69,76 +64,127 @@ class K8s:
|
|
|
69
64
|
|
|
70
65
|
def __init__(
|
|
71
66
|
self,
|
|
72
|
-
|
|
73
|
-
kubeconfig_file: str = "~/.kube/config",
|
|
67
|
+
kubeconfig_file: str | None = None,
|
|
74
68
|
namespace: str = "default",
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
logger: logging.Logger = default_logger,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the Kubernetes object.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
kubeconfig_file (str | None, optional):
|
|
75
|
+
Path to a kubeconfig file. Defaults to None.
|
|
76
|
+
namespace (str, optional):
|
|
77
|
+
The Kubernetes name space. Defaults to "default".
|
|
78
|
+
logger (logging.Logger, optional):
|
|
79
|
+
The logger object. Defaults to default_logger.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if logger != default_logger:
|
|
84
|
+
self.logger = logger.getChild("k8s")
|
|
85
|
+
for logfilter in logger.filters:
|
|
86
|
+
self.logger.addFilter(logfilter)
|
|
77
87
|
|
|
78
88
|
# Configure Kubernetes API authentication to use pod serviceAccount
|
|
79
|
-
|
|
89
|
+
|
|
90
|
+
try:
|
|
80
91
|
config.load_incluster_config()
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
configured = True
|
|
93
|
+
except ConfigException:
|
|
94
|
+
configured = False
|
|
95
|
+
self.logger.info("Failed to load in-cluster config")
|
|
96
|
+
|
|
97
|
+
if kubeconfig_file is None:
|
|
98
|
+
kubeconfig_file = os.getenv(
|
|
99
|
+
"KUBECONFIG",
|
|
100
|
+
os.path.expanduser("~/.kube/config"),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if not configured:
|
|
104
|
+
try:
|
|
83
105
|
config.load_kube_config(config_file=kubeconfig_file)
|
|
84
|
-
|
|
85
|
-
logger.
|
|
86
|
-
"
|
|
106
|
+
except ConfigException:
|
|
107
|
+
self.logger.info(
|
|
108
|
+
"Failed to load kubernetes config with file -> '%s'",
|
|
109
|
+
kubeconfig_file,
|
|
87
110
|
)
|
|
88
111
|
|
|
89
|
-
self._core_v1_api =
|
|
90
|
-
self._apps_v1_api =
|
|
91
|
-
self._networking_v1_api =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
else:
|
|
112
|
+
self._core_v1_api = CoreV1Api()
|
|
113
|
+
self._apps_v1_api = AppsV1Api()
|
|
114
|
+
self._networking_v1_api = NetworkingV1Api()
|
|
115
|
+
|
|
116
|
+
if namespace == "default":
|
|
95
117
|
# Read current namespace
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
try:
|
|
119
|
+
with open(
|
|
120
|
+
"/var/run/secrets/kubernetes.io/serviceaccount/namespace",
|
|
121
|
+
encoding="utf-8",
|
|
122
|
+
) as namespace_file:
|
|
123
|
+
self._namespace = namespace_file.read()
|
|
124
|
+
except FileNotFoundError:
|
|
125
|
+
self._namespace = namespace
|
|
126
|
+
else:
|
|
127
|
+
self._namespace = namespace
|
|
128
|
+
|
|
129
|
+
# end method definition
|
|
102
130
|
|
|
103
|
-
def get_core_v1_api(self):
|
|
104
|
-
"""
|
|
131
|
+
def get_core_v1_api(self) -> CoreV1Api:
|
|
132
|
+
"""Return Kubernetes Core V1 API object.
|
|
105
133
|
|
|
106
134
|
Returns:
|
|
107
135
|
object: Kubernetes API object
|
|
136
|
+
|
|
108
137
|
"""
|
|
138
|
+
|
|
109
139
|
return self._core_v1_api
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
# end method definition
|
|
142
|
+
|
|
143
|
+
def get_apps_v1_api(self) -> AppsV1Api:
|
|
144
|
+
"""Return Kubernetes Apps V1 API object.
|
|
113
145
|
|
|
114
146
|
Returns:
|
|
115
147
|
object: Kubernetes API object
|
|
148
|
+
|
|
116
149
|
"""
|
|
150
|
+
|
|
117
151
|
return self._apps_v1_api
|
|
118
152
|
|
|
119
|
-
|
|
120
|
-
|
|
153
|
+
# end method definition
|
|
154
|
+
|
|
155
|
+
def get_networking_v1_api(self) -> NetworkingV1Api:
|
|
156
|
+
"""Return Kubernetes Networking V1 API object.
|
|
121
157
|
|
|
122
158
|
Returns:
|
|
123
159
|
object: Kubernetes API object
|
|
160
|
+
|
|
124
161
|
"""
|
|
162
|
+
|
|
125
163
|
return self._networking_v1_api
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
# end method definition
|
|
166
|
+
|
|
167
|
+
def get_namespace(self) -> str:
|
|
168
|
+
"""Return Kubernetes Namespace.
|
|
129
169
|
|
|
130
170
|
Returns:
|
|
131
171
|
str: Kubernetes namespace
|
|
172
|
+
|
|
132
173
|
"""
|
|
174
|
+
|
|
133
175
|
return self._namespace
|
|
134
176
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
# end method definition
|
|
178
|
+
|
|
179
|
+
def get_pod(self, pod_name: str) -> V1Pod:
|
|
180
|
+
"""Get a pod in the configured namespace (the namespace is defined in the class constructor).
|
|
181
|
+
|
|
182
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#read_namespaced_pod
|
|
139
183
|
|
|
140
184
|
Args:
|
|
141
|
-
pod_name (str):
|
|
185
|
+
pod_name (str):
|
|
186
|
+
The name of the Kubernetes pod in the current namespace.
|
|
187
|
+
|
|
142
188
|
Returns:
|
|
143
189
|
V1Pod (object) or None if the call fails.
|
|
144
190
|
- api_version='v1',
|
|
@@ -146,30 +192,40 @@ class K8s:
|
|
|
146
192
|
- metadata=V1ObjectMeta(...),
|
|
147
193
|
- spec=V1PodSpec(...),
|
|
148
194
|
- status=V1PodStatus(...)
|
|
195
|
+
|
|
149
196
|
"""
|
|
150
197
|
|
|
151
198
|
try:
|
|
152
199
|
response = self.get_core_v1_api().read_namespaced_pod(
|
|
153
|
-
name=pod_name,
|
|
154
|
-
|
|
155
|
-
except ApiException as exception:
|
|
156
|
-
logger.error(
|
|
157
|
-
"Failed to get Pod -> '%s'; error -> %s", pod_name, str(exception)
|
|
200
|
+
name=pod_name,
|
|
201
|
+
namespace=self.get_namespace(),
|
|
158
202
|
)
|
|
159
|
-
|
|
160
|
-
|
|
203
|
+
except ApiException as e:
|
|
204
|
+
if e.status == 404:
|
|
205
|
+
self.logger.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(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
214
|
+
def list_pods(
|
|
215
|
+
self,
|
|
216
|
+
field_selector: str = "",
|
|
217
|
+
label_selector: str = "",
|
|
218
|
+
) -> V1PodList:
|
|
219
|
+
"""List all Kubernetes pods in a given namespace.
|
|
220
|
+
|
|
221
|
+
The list can be further restricted by specifying a field or label selector.
|
|
222
|
+
|
|
223
|
+
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/CoreV1Api.md#list_namespaced_pod
|
|
169
224
|
|
|
170
225
|
Args:
|
|
171
226
|
field_selector (str): filter result based on fields
|
|
172
227
|
label_selector (str): filter result based on labels
|
|
228
|
+
|
|
173
229
|
Returns:
|
|
174
230
|
V1PodList (object) or None if the call fails
|
|
175
231
|
Properties can be accessed with the "." notation (this is an object not a dict!):
|
|
@@ -179,6 +235,7 @@ class K8s:
|
|
|
179
235
|
- kind: The Kubernetes object kind, which is always "PodList".
|
|
180
236
|
- metadata: Additional metadata about the pod list, such as the resource version.
|
|
181
237
|
See: https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1PodList.md
|
|
238
|
+
|
|
182
239
|
"""
|
|
183
240
|
|
|
184
241
|
try:
|
|
@@ -187,12 +244,11 @@ class K8s:
|
|
|
187
244
|
label_selector=label_selector,
|
|
188
245
|
namespace=self.get_namespace(),
|
|
189
246
|
)
|
|
190
|
-
except ApiException
|
|
191
|
-
logger.error(
|
|
192
|
-
"Failed to list
|
|
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,
|
|
205
|
-
|
|
260
|
+
self,
|
|
261
|
+
pod_name: str,
|
|
262
|
+
condition_name: str,
|
|
263
|
+
sleep_time: int = 30,
|
|
264
|
+
) -> None:
|
|
206
265
|
"""Wait for the pod to reach a defined condition (e.g. "Ready").
|
|
207
266
|
|
|
208
267
|
Args:
|
|
209
|
-
pod_name (str):
|
|
210
|
-
|
|
268
|
+
pod_name (str):
|
|
269
|
+
The name of the Kubernetes pod in the current namespace.
|
|
270
|
+
condition_name (str):
|
|
271
|
+
The name of the condition, e.g. "Ready".
|
|
272
|
+
sleep_time (int):
|
|
273
|
+
The number of seconds to wait between repetitive status checks.
|
|
274
|
+
|
|
211
275
|
Returns:
|
|
212
|
-
|
|
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,
|
|
284
|
+
pod_name,
|
|
285
|
+
self.get_namespace(),
|
|
220
286
|
)
|
|
221
287
|
|
|
222
288
|
# Check if the pod has reached the defined condition:
|
|
223
289
|
for cond in pod_status.status.conditions:
|
|
224
290
|
if cond.type == condition_name and cond.status == "True":
|
|
225
|
-
logger.info(
|
|
226
|
-
"Pod -> '%s' is in state -> '%s'!",
|
|
291
|
+
self.logger.info(
|
|
292
|
+
"Pod -> '%s' is in state -> '%s'!",
|
|
293
|
+
pod_name,
|
|
294
|
+
condition_name,
|
|
227
295
|
)
|
|
228
296
|
ready = True
|
|
229
297
|
break
|
|
230
298
|
else:
|
|
231
|
-
logger.info(
|
|
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
|
|
240
|
-
logger.error(
|
|
241
|
-
"Failed to wait for pod -> '%s'
|
|
307
|
+
except ApiException:
|
|
308
|
+
self.logger.error(
|
|
309
|
+
"Failed to wait for pod -> '%s'",
|
|
242
310
|
pod_name,
|
|
243
|
-
str(exception),
|
|
244
311
|
)
|
|
245
312
|
|
|
246
313
|
# end method definition
|
|
@@ -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
|
-
|
|
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):
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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):
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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:
|
|
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
|
|
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
|
|
368
|
-
logger.error(
|
|
369
|
-
"Failed to execute command -> %s in pod -> '%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(
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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):
|
|
409
|
-
|
|
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,
|
|
530
|
+
pod_name,
|
|
531
|
+
namespace=self.get_namespace(),
|
|
428
532
|
)
|
|
429
|
-
except ApiException
|
|
430
|
-
logger.error(
|
|
431
|
-
"Failed to delete
|
|
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
|
-
|
|
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):
|
|
550
|
+
config_map_name (str):
|
|
551
|
+
The name of the Kubernetes config map in the current namespace.
|
|
552
|
+
|
|
445
553
|
Returns:
|
|
446
|
-
V1ConfigMap (object):
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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,
|
|
575
|
+
name=config_map_name,
|
|
576
|
+
namespace=self.get_namespace(),
|
|
462
577
|
)
|
|
463
|
-
except ApiException
|
|
464
|
-
logger.error(
|
|
465
|
-
"Failed to get
|
|
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(
|
|
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
|
-
|
|
478
|
-
|
|
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):
|
|
482
|
-
|
|
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
|
|
501
|
-
logger.error(
|
|
502
|
-
"Failed to list
|
|
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
|
-
|
|
516
|
-
|
|
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):
|
|
642
|
+
config_map_name (str):
|
|
643
|
+
The name of the Kubernetes Config Map to search for.
|
|
644
|
+
|
|
520
645
|
Returns:
|
|
521
|
-
object:
|
|
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
|
|
529
|
-
logger.error(
|
|
530
|
-
"Failed to find
|
|
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(
|
|
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
|
-
|
|
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):
|
|
546
|
-
|
|
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):
|
|
549
|
-
|
|
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
|
|
564
|
-
logger.error(
|
|
565
|
-
"Failed to replace
|
|
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
|
-
|
|
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):
|
|
716
|
+
sts_name (str):
|
|
717
|
+
The name of the Kubernetes stateful set
|
|
718
|
+
|
|
581
719
|
Returns:
|
|
582
|
-
V1StatefulSet (object):
|
|
583
|
-
|
|
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,
|
|
728
|
+
name=sts_name,
|
|
729
|
+
namespace=self.get_namespace(),
|
|
589
730
|
)
|
|
590
|
-
except ApiException
|
|
591
|
-
logger.error(
|
|
592
|
-
"Failed to get
|
|
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
|
-
|
|
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):
|
|
748
|
+
sts_name (str):
|
|
749
|
+
The name of the Kubernetes Stateful Set.
|
|
750
|
+
|
|
608
751
|
Returns:
|
|
609
|
-
V1Scale (object):
|
|
610
|
-
|
|
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,
|
|
760
|
+
name=sts_name,
|
|
761
|
+
namespace=self.get_namespace(),
|
|
616
762
|
)
|
|
617
|
-
except ApiException
|
|
618
|
-
logger.error(
|
|
619
|
-
"Failed to get scaling (replicas) of
|
|
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
|
-
|
|
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):
|
|
635
|
-
|
|
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):
|
|
638
|
-
|
|
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,
|
|
794
|
+
name=sts_name,
|
|
795
|
+
namespace=self.get_namespace(),
|
|
796
|
+
body=sts_body,
|
|
644
797
|
)
|
|
645
|
-
except ApiException
|
|
646
|
-
logger.error(
|
|
647
|
-
"Failed to patch
|
|
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
|
-
|
|
812
|
+
|
|
813
|
+
It uses the class method patch_stateful_set() above.
|
|
661
814
|
|
|
662
815
|
Args:
|
|
663
|
-
sts_name (str):
|
|
664
|
-
|
|
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):
|
|
667
|
-
|
|
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,
|
|
830
|
+
sts_name,
|
|
831
|
+
sts_body={"spec": {"replicas": scale}},
|
|
673
832
|
)
|
|
674
|
-
except ApiException
|
|
675
|
-
logger.error(
|
|
676
|
-
"Failed to scale
|
|
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):
|
|
849
|
+
service_name (str):
|
|
850
|
+
The name of the Kubernetes Service in the current namespace.
|
|
851
|
+
|
|
692
852
|
Returns:
|
|
693
|
-
V1Service (object):
|
|
694
|
-
|
|
695
|
-
|
|
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,
|
|
862
|
+
name=service_name,
|
|
863
|
+
namespace=self.get_namespace(),
|
|
701
864
|
)
|
|
702
|
-
except ApiException
|
|
703
|
-
logger.error(
|
|
704
|
-
"Failed to get
|
|
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
|
-
|
|
717
|
-
|
|
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):
|
|
721
|
-
|
|
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):
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
|
741
|
-
logger.error(
|
|
742
|
-
"Failed to list
|
|
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
|
-
"""
|
|
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):
|
|
758
|
-
|
|
759
|
-
|
|
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):
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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,
|
|
941
|
+
name=service_name,
|
|
942
|
+
namespace=self.get_namespace(),
|
|
943
|
+
body=service_body,
|
|
770
944
|
)
|
|
771
|
-
except ApiException
|
|
772
|
-
logger.error(
|
|
773
|
-
"Failed to patch
|
|
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):
|
|
961
|
+
ingress_name (str):
|
|
962
|
+
The name of the Kubernetes Ingress in the current namespace.
|
|
963
|
+
|
|
789
964
|
Returns:
|
|
790
|
-
V1Ingress (object):
|
|
791
|
-
|
|
792
|
-
|
|
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,
|
|
974
|
+
name=ingress_name,
|
|
975
|
+
namespace=self.get_namespace(),
|
|
798
976
|
)
|
|
799
|
-
except ApiException
|
|
800
|
-
logger.error(
|
|
801
|
-
"Failed to get
|
|
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
|
-
|
|
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):
|
|
815
|
-
|
|
816
|
-
|
|
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):
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
831
|
-
logger.error(
|
|
832
|
-
"Failed to patch
|
|
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,
|
|
845
|
-
|
|
846
|
-
|
|
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):
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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):
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
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 =
|
|
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):
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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:
|
|
961
|
-
|
|
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
|
|
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):
|
|
974
|
-
|
|
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:
|
|
978
|
-
|
|
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.
|
|
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!",
|
|
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
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
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(
|
|
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
|