openshift-python-wrapper 4.11.61__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.
- ocp_resources/__init__.py +0 -0
- ocp_resources/api_service.py +9 -0
- ocp_resources/backup.py +52 -0
- ocp_resources/benchmark.py +92 -0
- ocp_resources/catalog_source.py +63 -0
- ocp_resources/catalog_source_config.py +86 -0
- ocp_resources/cdi.py +13 -0
- ocp_resources/cdi_config.py +52 -0
- ocp_resources/chaos_engine.py +30 -0
- ocp_resources/chaos_result.py +11 -0
- ocp_resources/cluster_operator.py +5 -0
- ocp_resources/cluster_role.py +70 -0
- ocp_resources/cluster_role_binding.py +36 -0
- ocp_resources/cluster_service_version.py +8 -0
- ocp_resources/cluster_version.py +5 -0
- ocp_resources/configmap.py +37 -0
- ocp_resources/console_cli_download.py +9 -0
- ocp_resources/console_plugin.py +9 -0
- ocp_resources/console_quick_start.py +9 -0
- ocp_resources/constants.py +26 -0
- ocp_resources/csidriver.py +9 -0
- ocp_resources/custom_resource_definition.py +5 -0
- ocp_resources/daemonset.py +61 -0
- ocp_resources/data_import_cron.py +100 -0
- ocp_resources/data_source.py +59 -0
- ocp_resources/datavolume.py +275 -0
- ocp_resources/deployment.py +65 -0
- ocp_resources/destination_rule.py +11 -0
- ocp_resources/event.py +114 -0
- ocp_resources/forklift_controller.py +10 -0
- ocp_resources/gateway.py +11 -0
- ocp_resources/hook.py +52 -0
- ocp_resources/host.py +64 -0
- ocp_resources/hostpath_provisioner.py +50 -0
- ocp_resources/hyperconverged.py +43 -0
- ocp_resources/image.py +9 -0
- ocp_resources/image_content_source_policy.py +9 -0
- ocp_resources/image_stream.py +48 -0
- ocp_resources/imagestreamtag.py +9 -0
- ocp_resources/infrastructure.py +18 -0
- ocp_resources/installplan.py +8 -0
- ocp_resources/job.py +65 -0
- ocp_resources/kube_descheduler.py +65 -0
- ocp_resources/kubevirt.py +5 -0
- ocp_resources/kubevirt_common_templates_bundle.py +5 -0
- ocp_resources/kubevirt_metrics_aggregation.py +5 -0
- ocp_resources/kubevirt_node_labeller_bundle.py +5 -0
- ocp_resources/kubevirt_template_validaotr.py +5 -0
- ocp_resources/logger.py +76 -0
- ocp_resources/machine.py +50 -0
- ocp_resources/machine_config.py +5 -0
- ocp_resources/machine_config_pool.py +9 -0
- ocp_resources/machine_health_check.py +76 -0
- ocp_resources/machine_set.py +189 -0
- ocp_resources/migration.py +63 -0
- ocp_resources/migration_policy.py +66 -0
- ocp_resources/mtv.py +171 -0
- ocp_resources/mutating_webhook_config.py +9 -0
- ocp_resources/namespace.py +40 -0
- ocp_resources/network.py +5 -0
- ocp_resources/network_addons_config.py +9 -0
- ocp_resources/network_attachment_definition.py +24 -0
- ocp_resources/network_map.py +64 -0
- ocp_resources/network_policy.py +9 -0
- ocp_resources/nmstate.py +9 -0
- ocp_resources/node.py +41 -0
- ocp_resources/node_health_check.py +10 -0
- ocp_resources/node_maintenance.py +40 -0
- ocp_resources/node_network_configuration_enactment.py +20 -0
- ocp_resources/node_network_configuration_policy.py +472 -0
- ocp_resources/node_network_state.py +121 -0
- ocp_resources/oauth.py +9 -0
- ocp_resources/operator_condition.py +5 -0
- ocp_resources/operator_group.py +37 -0
- ocp_resources/operator_hub.py +5 -0
- ocp_resources/operator_source.py +50 -0
- ocp_resources/package_manifest.py +5 -0
- ocp_resources/peer_authentication.py +18 -0
- ocp_resources/persistent_volume.py +27 -0
- ocp_resources/persistent_volume_claim.py +103 -0
- ocp_resources/plan.py +143 -0
- ocp_resources/pod.py +175 -0
- ocp_resources/pod_disruption_budget.py +53 -0
- ocp_resources/priority_class.py +48 -0
- ocp_resources/project.py +60 -0
- ocp_resources/prometheus.py +9 -0
- ocp_resources/prometheus_rule.py +9 -0
- ocp_resources/provider.py +59 -0
- ocp_resources/replicaset.py +11 -0
- ocp_resources/resource.py +1383 -0
- ocp_resources/restore.py +52 -0
- ocp_resources/role.py +11 -0
- ocp_resources/role_binding.py +67 -0
- ocp_resources/route.py +76 -0
- ocp_resources/secret.py +75 -0
- ocp_resources/security_context_constraints.py +9 -0
- ocp_resources/service.py +16 -0
- ocp_resources/service_account.py +9 -0
- ocp_resources/service_mesh_control_plane.py +11 -0
- ocp_resources/service_mesh_member_roll.py +14 -0
- ocp_resources/service_monitor.py +9 -0
- ocp_resources/sriov_network.py +54 -0
- ocp_resources/sriov_network_node_policy.py +67 -0
- ocp_resources/sriov_network_node_state.py +44 -0
- ocp_resources/ssp.py +9 -0
- ocp_resources/storage_class.py +47 -0
- ocp_resources/storage_map.py +67 -0
- ocp_resources/storage_profile.py +7 -0
- ocp_resources/subscription.py +66 -0
- ocp_resources/tekton_tasks.py +9 -0
- ocp_resources/template.py +74 -0
- ocp_resources/upload_token_request.py +39 -0
- ocp_resources/utils.py +278 -0
- ocp_resources/validating_webhook_config.py +9 -0
- ocp_resources/virtual_machine.py +177 -0
- ocp_resources/virtual_machine_Instance_replica_set.py +9 -0
- ocp_resources/virtual_machine_cluster_flavor.py +5 -0
- ocp_resources/virtual_machine_flavor.py +5 -0
- ocp_resources/virtual_machine_import.py +307 -0
- ocp_resources/virtual_machine_instance.py +322 -0
- ocp_resources/virtual_machine_instance_migration.py +36 -0
- ocp_resources/virtual_machine_instance_preset.py +9 -0
- ocp_resources/virtual_machine_restore.py +99 -0
- ocp_resources/virtual_machine_snapshot.py +96 -0
- ocp_resources/virtual_service.py +11 -0
- ocp_resources/vm_import_config.py +9 -0
- ocp_resources/volume_snapshot.py +9 -0
- ocp_resources/volume_snapshot_class.py +9 -0
- openshift_python_wrapper-4.11.61.dist-info/LICENSE +201 -0
- openshift_python_wrapper-4.11.61.dist-info/METADATA +188 -0
- openshift_python_wrapper-4.11.61.dist-info/RECORD +132 -0
- openshift_python_wrapper-4.11.61.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from io import StringIO
|
|
7
|
+
from signal import SIGINT, signal
|
|
8
|
+
|
|
9
|
+
import kubernetes
|
|
10
|
+
import yaml
|
|
11
|
+
from kubernetes.dynamic.exceptions import MethodNotAllowedError
|
|
12
|
+
from openshift.dynamic import DynamicClient
|
|
13
|
+
from openshift.dynamic.exceptions import ConflictError, NotFoundError
|
|
14
|
+
from openshift.dynamic.resource import ResourceField
|
|
15
|
+
from packaging.version import Version
|
|
16
|
+
|
|
17
|
+
from ocp_resources.constants import (
|
|
18
|
+
DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
|
|
19
|
+
NOT_FOUND_ERROR_EXCEPTION_DICT,
|
|
20
|
+
PROTOCOL_ERROR_EXCEPTION_DICT,
|
|
21
|
+
TIMEOUT_1MINUTE,
|
|
22
|
+
TIMEOUT_4MINUTES,
|
|
23
|
+
)
|
|
24
|
+
from ocp_resources.event import Event
|
|
25
|
+
from ocp_resources.logger import get_logger
|
|
26
|
+
from ocp_resources.utils import (
|
|
27
|
+
TimeoutExpiredError,
|
|
28
|
+
TimeoutSampler,
|
|
29
|
+
skip_existing_resource_creation_teardown,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
LOGGER = get_logger(__name__)
|
|
34
|
+
MAX_SUPPORTED_API_VERSION = "v2"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def kube_v1_api(api_client):
|
|
38
|
+
return kubernetes.client.CoreV1Api(api_client=api_client)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _collect_instance_data(directory, resource_object):
|
|
42
|
+
with open(os.path.join(directory, f"{resource_object.name}.yaml"), "w") as fd:
|
|
43
|
+
fd.write(resource_object.instance.to_str())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _collect_pod_logs(dyn_client, resource_item, **kwargs):
|
|
47
|
+
return kube_v1_api(api_client=dyn_client.client).read_namespaced_pod_log(
|
|
48
|
+
name=resource_item.metadata.name,
|
|
49
|
+
namespace=resource_item.metadata.namespace,
|
|
50
|
+
**kwargs,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _collect_virt_launcher_data(dyn_client, directory, resource_object):
|
|
55
|
+
if resource_object.kind == "VirtualMachineInstance":
|
|
56
|
+
for pod in dyn_client.resources.get(kind="Pod").get().items:
|
|
57
|
+
pod_name = pod.metadata.name
|
|
58
|
+
pod_instance = dyn_client.resources.get(
|
|
59
|
+
api_version=pod.apiVersion, kind=pod.kind
|
|
60
|
+
).get(name=pod_name, namespace=pod.metadata.namespace)
|
|
61
|
+
if pod_name.startswith("virt-launcher"):
|
|
62
|
+
with open(os.path.join(directory, f"{pod_name}.log"), "w") as fd:
|
|
63
|
+
fd.write(
|
|
64
|
+
_collect_pod_logs(
|
|
65
|
+
dyn_client=dyn_client,
|
|
66
|
+
resource_item=pod,
|
|
67
|
+
container="compute",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with open(os.path.join(directory, f"{pod_name}.yaml"), "w") as fd:
|
|
72
|
+
fd.write(pod_instance.to_str())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _collect_data_volume_data(dyn_client, directory, resource_object):
|
|
76
|
+
if resource_object.kind == "DataVolume":
|
|
77
|
+
cdi_worker_prefixes = ("importer", "cdi-upload")
|
|
78
|
+
for pod in dyn_client.resources.get(kind="Pod").get().items:
|
|
79
|
+
pod_name = pod.metadata.name
|
|
80
|
+
pod_instance = dyn_client.resources.get(
|
|
81
|
+
api_version=pod.apiVersion, kind=pod.kind
|
|
82
|
+
).get(name=pod_name, namespace=pod.metadata.namespace)
|
|
83
|
+
if pod_name.startswith(cdi_worker_prefixes) or pod_name.endswith(
|
|
84
|
+
"source-pod"
|
|
85
|
+
):
|
|
86
|
+
with open(os.path.join(directory, f"{pod_name}.log"), "w") as fd:
|
|
87
|
+
fd.write(
|
|
88
|
+
_collect_pod_logs(dyn_client=dyn_client, resource_item=pod)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
with open(os.path.join(directory, f"{pod_name}.yaml"), "w") as fd:
|
|
92
|
+
fd.write(pod_instance.to_str())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _collect_data(resource_object, dyn_client=None):
|
|
96
|
+
dyn_client = (
|
|
97
|
+
dyn_client
|
|
98
|
+
if dyn_client
|
|
99
|
+
else DynamicClient(kubernetes.config.new_client_from_config())
|
|
100
|
+
)
|
|
101
|
+
directory = os.environ.get("TEST_DIR_LOG")
|
|
102
|
+
_collect_instance_data(directory=directory, resource_object=resource_object)
|
|
103
|
+
_collect_virt_launcher_data(
|
|
104
|
+
dyn_client=dyn_client, directory=directory, resource_object=resource_object
|
|
105
|
+
)
|
|
106
|
+
_collect_data_volume_data(
|
|
107
|
+
dyn_client=dyn_client, directory=directory, resource_object=resource_object
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _find_supported_resource(dyn_client, api_group, kind):
|
|
112
|
+
results = dyn_client.resources.search(group=api_group, kind=kind)
|
|
113
|
+
sorted_results = sorted(
|
|
114
|
+
results, key=lambda result: KubeAPIVersion(result.api_version), reverse=True
|
|
115
|
+
)
|
|
116
|
+
for result in sorted_results:
|
|
117
|
+
if KubeAPIVersion(result.api_version) <= KubeAPIVersion(
|
|
118
|
+
MAX_SUPPORTED_API_VERSION
|
|
119
|
+
):
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _get_api_version(dyn_client, api_group, kind):
|
|
124
|
+
# Returns api_group/api_version
|
|
125
|
+
res = _find_supported_resource(
|
|
126
|
+
dyn_client=dyn_client, api_group=api_group, kind=kind
|
|
127
|
+
)
|
|
128
|
+
if not res:
|
|
129
|
+
log = f"Couldn't find {kind} in {api_group} api group"
|
|
130
|
+
LOGGER.warning(log)
|
|
131
|
+
raise NotImplementedError(log)
|
|
132
|
+
|
|
133
|
+
LOGGER.info(f"kind: {kind} api version: {res.group_version}")
|
|
134
|
+
return res.group_version
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_client(config_file=None, config_dict=None, context=None):
|
|
138
|
+
"""
|
|
139
|
+
Get a kubernetes client.
|
|
140
|
+
|
|
141
|
+
Pass either config_file or config_dict.
|
|
142
|
+
If none of them are passed, client will be created from default OS kubeconfig
|
|
143
|
+
(environment variable or .kube folder).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
config_file (str): path to a kubeconfig file.
|
|
147
|
+
config_dict (dict): dict with kubeconfig configuration.
|
|
148
|
+
context (str): name of the context to use.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
DynamicClient: a kubernetes client.
|
|
152
|
+
"""
|
|
153
|
+
if config_dict:
|
|
154
|
+
return DynamicClient(
|
|
155
|
+
client=kubernetes.config.new_client_from_config_dict(
|
|
156
|
+
config_dict=config_dict,
|
|
157
|
+
context=context,
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
return DynamicClient(
|
|
161
|
+
client=kubernetes.config.new_client_from_config(
|
|
162
|
+
config_file=config_file,
|
|
163
|
+
context=context,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def sub_resource_level(current_class, owner_class, parent_class):
|
|
169
|
+
# return the name of the last class in MRO list that is not one of base
|
|
170
|
+
# classes; otherwise return None
|
|
171
|
+
for class_iterator in reversed(
|
|
172
|
+
[
|
|
173
|
+
class_iterator
|
|
174
|
+
for class_iterator in current_class.mro()
|
|
175
|
+
if class_iterator not in owner_class.mro()
|
|
176
|
+
and issubclass(class_iterator, parent_class)
|
|
177
|
+
]
|
|
178
|
+
):
|
|
179
|
+
return class_iterator.__name__
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class KubeAPIVersion(Version):
|
|
183
|
+
"""
|
|
184
|
+
Implement the Kubernetes API versioning scheme from
|
|
185
|
+
https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
component_re = re.compile(r"(\d+ | [a-z]+)", re.VERBOSE)
|
|
189
|
+
|
|
190
|
+
def __init__(self, vstring=None):
|
|
191
|
+
self.vstring = vstring
|
|
192
|
+
self.version = None
|
|
193
|
+
super().__init__(version=vstring)
|
|
194
|
+
|
|
195
|
+
def parse(self, vstring):
|
|
196
|
+
components = [comp for comp in self.component_re.split(vstring) if comp]
|
|
197
|
+
for idx, obj in enumerate(components):
|
|
198
|
+
with contextlib.suppress(ValueError):
|
|
199
|
+
components[idx] = int(obj)
|
|
200
|
+
|
|
201
|
+
errmsg = f"version '{vstring}' does not conform to kubernetes api versioning guidelines"
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
len(components) not in (2, 4)
|
|
205
|
+
or components[0] != "v"
|
|
206
|
+
or not isinstance(components[1], int)
|
|
207
|
+
):
|
|
208
|
+
raise ValueError(errmsg)
|
|
209
|
+
if len(components) == 4 and (
|
|
210
|
+
components[2] not in ("alpha", "beta") or not isinstance(components[3], int)
|
|
211
|
+
):
|
|
212
|
+
raise ValueError(errmsg)
|
|
213
|
+
|
|
214
|
+
self.version = components
|
|
215
|
+
|
|
216
|
+
def __str__(self):
|
|
217
|
+
return self.vstring
|
|
218
|
+
|
|
219
|
+
def __repr__(self):
|
|
220
|
+
return "KubeAPIVersion ('{0}')".format(str(self))
|
|
221
|
+
|
|
222
|
+
def _cmp(self, other):
|
|
223
|
+
if isinstance(other, str):
|
|
224
|
+
other = KubeAPIVersion(vstring=other)
|
|
225
|
+
|
|
226
|
+
myver = self.version
|
|
227
|
+
otherver = other.version
|
|
228
|
+
|
|
229
|
+
for ver in myver, otherver:
|
|
230
|
+
if len(ver) == 2:
|
|
231
|
+
ver.extend(["zeta", 9999])
|
|
232
|
+
|
|
233
|
+
if myver == otherver:
|
|
234
|
+
return 0
|
|
235
|
+
if myver < otherver:
|
|
236
|
+
return -1
|
|
237
|
+
if myver > otherver:
|
|
238
|
+
return 1
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class ClassProperty:
|
|
242
|
+
def __init__(self, func):
|
|
243
|
+
self.func = func
|
|
244
|
+
|
|
245
|
+
def __get__(self, obj, owner):
|
|
246
|
+
return self.func(owner)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class ValueMismatch(Exception):
|
|
250
|
+
"""
|
|
251
|
+
Raises when value doesn't match the class value
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class Resource:
|
|
256
|
+
"""
|
|
257
|
+
Base class for API resources
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
api_group = None
|
|
261
|
+
api_version = None
|
|
262
|
+
singular_name = None
|
|
263
|
+
timeout_seconds = TIMEOUT_1MINUTE
|
|
264
|
+
|
|
265
|
+
class Status:
|
|
266
|
+
SUCCEEDED = "Succeeded"
|
|
267
|
+
FAILED = "Failed"
|
|
268
|
+
DELETING = "Deleting"
|
|
269
|
+
DEPLOYED = "Deployed"
|
|
270
|
+
PENDING = "Pending"
|
|
271
|
+
COMPLETED = "Completed"
|
|
272
|
+
RUNNING = "Running"
|
|
273
|
+
TERMINATING = "Terminating"
|
|
274
|
+
ERROR = "Error"
|
|
275
|
+
|
|
276
|
+
class Condition:
|
|
277
|
+
UPGRADEABLE = "Upgradeable"
|
|
278
|
+
AVAILABLE = "Available"
|
|
279
|
+
DEGRADED = "Degraded"
|
|
280
|
+
PROGRESSING = "Progressing"
|
|
281
|
+
CREATED = "Created"
|
|
282
|
+
RECONCILE_COMPLETE = "ReconcileComplete"
|
|
283
|
+
READY = "Ready"
|
|
284
|
+
FAILING = "Failing"
|
|
285
|
+
|
|
286
|
+
class Status:
|
|
287
|
+
TRUE = "True"
|
|
288
|
+
FALSE = "False"
|
|
289
|
+
UNKNOWN = "Unknown"
|
|
290
|
+
|
|
291
|
+
class Phase:
|
|
292
|
+
INSTALL_READY = "InstallReady"
|
|
293
|
+
SUCCEEDED = "Succeeded"
|
|
294
|
+
|
|
295
|
+
class Reason:
|
|
296
|
+
ALL_REQUIREMENTS_MET = "AllRequirementsMet"
|
|
297
|
+
INSTALL_SUCCEEDED = "InstallSucceeded"
|
|
298
|
+
|
|
299
|
+
class Interface:
|
|
300
|
+
class State:
|
|
301
|
+
UP = "up"
|
|
302
|
+
DOWN = "down"
|
|
303
|
+
ABSENT = "absent"
|
|
304
|
+
|
|
305
|
+
class ApiGroup:
|
|
306
|
+
ADMISSIONREGISTRATION_K8S_IO = "admissionregistration.k8s.io"
|
|
307
|
+
APIEXTENSIONS_K8S_IO = "apiextensions.k8s.io"
|
|
308
|
+
APIREGISTRATION_K8S_IO = "apiregistration.k8s.io"
|
|
309
|
+
APP_KUBERNETES_IO = "app.kubernetes.io"
|
|
310
|
+
APPS = "apps"
|
|
311
|
+
BATCH = "batch"
|
|
312
|
+
CDI_KUBEVIRT_IO = "cdi.kubevirt.io"
|
|
313
|
+
CONFIG_OPENSHIFT_IO = "config.openshift.io"
|
|
314
|
+
CONSOLE_OPENSHIFT_IO = "console.openshift.io"
|
|
315
|
+
DATA_IMPORT_CRON_TEMPLATE_KUBEVIRT_IO = "dataimportcrontemplate.kubevirt.io"
|
|
316
|
+
EVENTS_K8S_IO = "events.k8s.io"
|
|
317
|
+
FORKLIFT_KONVEYOR_IO = "forklift.konveyor.io"
|
|
318
|
+
FLAVOR_KUBEVIRT_IO = "flavor.kubevirt.io"
|
|
319
|
+
HCO_KUBEVIRT_IO = "hco.kubevirt.io"
|
|
320
|
+
HOSTPATHPROVISIONER_KUBEVIRT_IO = "hostpathprovisioner.kubevirt.io"
|
|
321
|
+
IMAGE_OPENSHIFT_IO = "image.openshift.io"
|
|
322
|
+
IMAGE_REGISTRY = "registry.redhat.io"
|
|
323
|
+
K8S_CNI_CNCF_IO = "k8s.cni.cncf.io"
|
|
324
|
+
K8S_V1_CNI_CNCF_IO = "k8s.v1.cni.cncf.io"
|
|
325
|
+
KUBERNETES_IO = "kubernetes.io"
|
|
326
|
+
KUBEVIRT_IO = "kubevirt.io"
|
|
327
|
+
KUBEVIRT_KUBEVIRT_IO = "kubevirt.kubevirt.io"
|
|
328
|
+
LITMUS_IO = "litmuschaos.io"
|
|
329
|
+
MACHINE_OPENSHIFT_IO = "machine.openshift.io"
|
|
330
|
+
MACHINECONFIGURATION_OPENSHIFT_IO = "machineconfiguration.openshift.io"
|
|
331
|
+
MAISTRA_IO = "maistra.io"
|
|
332
|
+
MIGRATIONS_KUBEVIRT_IO = "migrations.kubevirt.io"
|
|
333
|
+
MONITORING_COREOS_COM = "monitoring.coreos.com"
|
|
334
|
+
NETWORKADDONSOPERATOR_NETWORK_KUBEVIRT_IO = (
|
|
335
|
+
"networkaddonsoperator.network.kubevirt.io"
|
|
336
|
+
)
|
|
337
|
+
NETWORKING_ISTIO_IO = "networking.istio.io"
|
|
338
|
+
NETWORKING_K8S_IO = "networking.k8s.io"
|
|
339
|
+
NODE_LABELLER_KUBEVIRT_IO = "node-labeller.kubevirt.io"
|
|
340
|
+
NMSTATE_IO = "nmstate.io"
|
|
341
|
+
NODEMAINTENANCE_KUBEVIRT_IO = "nodemaintenance.kubevirt.io"
|
|
342
|
+
OPERATOR_OPENSHIFT_IO = "operator.openshift.io"
|
|
343
|
+
OPERATORS_COREOS_COM = "operators.coreos.com"
|
|
344
|
+
OPERATORS_OPENSHIFT_IO = "operators.openshift.io"
|
|
345
|
+
OS_TEMPLATE_KUBEVIRT_IO = "os.template.kubevirt.io"
|
|
346
|
+
PACKAGES_OPERATORS_COREOS_COM = "packages.operators.coreos.com"
|
|
347
|
+
POLICY = "policy"
|
|
348
|
+
POOL_KUBEVIRT_IO = "pool.kubevirt.io"
|
|
349
|
+
PROJECT_OPENSHIFT_IO = "project.openshift.io"
|
|
350
|
+
RBAC_AUTHORIZATION_K8S_IO = "rbac.authorization.k8s.io"
|
|
351
|
+
REMEDIATION_MEDIK8S_IO = "remediation.medik8s.io"
|
|
352
|
+
RIPSAW_CLOUDBULLDOZER_IO = "ripsaw.cloudbulldozer.io"
|
|
353
|
+
ROUTE_OPENSHIFT_IO = "route.openshift.io"
|
|
354
|
+
SCHEDULING_K8S_IO = "scheduling.k8s.io"
|
|
355
|
+
SECURITY_ISTIO_IO = "security.istio.io"
|
|
356
|
+
SECURITY_OPENSHIFT_IO = "security.openshift.io"
|
|
357
|
+
SNAPSHOT_STORAGE_K8S_IO = "snapshot.storage.k8s.io"
|
|
358
|
+
SNAPSHOT_KUBEVIRT_IO = "snapshot.kubevirt.io"
|
|
359
|
+
SRIOVNETWORK_OPENSHIFT_IO = "sriovnetwork.openshift.io"
|
|
360
|
+
SSP_KUBEVIRT_IO = "ssp.kubevirt.io"
|
|
361
|
+
STORAGE_K8S_IO = "storage.k8s.io"
|
|
362
|
+
STORAGECLASS_KUBERNETES_IO = "storageclass.kubernetes.io"
|
|
363
|
+
SUBRESOURCES_KUBEVIRT_IO = "subresources.kubevirt.io"
|
|
364
|
+
TEKTONTASKS_KUBEVIRT_IO = "tektontasks.kubevirt.io"
|
|
365
|
+
TEMPLATE_KUBEVIRT_IO = "template.kubevirt.io"
|
|
366
|
+
TEMPLATE_OPENSHIFT_IO = "template.openshift.io"
|
|
367
|
+
UPLOAD_CDI_KUBEVIRT_IO = "upload.cdi.kubevirt.io"
|
|
368
|
+
V2V_KUBEVIRT_IO = "v2v.kubevirt.io"
|
|
369
|
+
VELERO_IO = "velero.io"
|
|
370
|
+
VM_KUBEVIRT_IO = "vm.kubevirt.io"
|
|
371
|
+
|
|
372
|
+
class ApiVersion:
|
|
373
|
+
V1 = "v1"
|
|
374
|
+
V1BETA1 = "v1beta1"
|
|
375
|
+
V1ALPHA1 = "v1alpha1"
|
|
376
|
+
V1ALPHA3 = "v1alpha3"
|
|
377
|
+
|
|
378
|
+
def __init__(
|
|
379
|
+
self,
|
|
380
|
+
name=None,
|
|
381
|
+
client=None,
|
|
382
|
+
teardown=True,
|
|
383
|
+
timeout=TIMEOUT_4MINUTES,
|
|
384
|
+
privileged_client=None,
|
|
385
|
+
yaml_file=None,
|
|
386
|
+
delete_timeout=TIMEOUT_4MINUTES,
|
|
387
|
+
dry_run=None,
|
|
388
|
+
node_selector=None,
|
|
389
|
+
node_selector_labels=None,
|
|
390
|
+
config_file=None,
|
|
391
|
+
context=None,
|
|
392
|
+
timeout_seconds=TIMEOUT_1MINUTE,
|
|
393
|
+
):
|
|
394
|
+
"""
|
|
395
|
+
Create an API resource
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
name (str): Resource name
|
|
399
|
+
client (DynamicClient): Dynamic client for connecting to a remote cluster
|
|
400
|
+
teardown (bool): Indicates if this resource would need to be deleted
|
|
401
|
+
privileged_client (DynamicClient): Instance of Dynamic client
|
|
402
|
+
yaml_file (str): yaml file for the resource
|
|
403
|
+
delete_timeout (int): timeout associated with delete action
|
|
404
|
+
dry_run (bool): dry run
|
|
405
|
+
node_selector (str): node selector
|
|
406
|
+
node_selector_labels (str): node selector labels
|
|
407
|
+
config_file (str): Path to config file for connecting to remote cluster.
|
|
408
|
+
context (str): Context name for connecting to remote cluster.
|
|
409
|
+
timeout_seconds (int): timeout for a get api call, call out be terminated after this many seconds
|
|
410
|
+
|
|
411
|
+
"""
|
|
412
|
+
if not self.api_group and not self.api_version:
|
|
413
|
+
raise NotImplementedError(
|
|
414
|
+
"Subclasses of Resource require self.api_group or self.api_version to be defined"
|
|
415
|
+
)
|
|
416
|
+
self.namespace = None
|
|
417
|
+
self.name = name
|
|
418
|
+
self.client = client
|
|
419
|
+
self.privileged_client = privileged_client
|
|
420
|
+
self.yaml_file = yaml_file
|
|
421
|
+
self.resource_dict = None # Filled in case yaml_file is not None
|
|
422
|
+
self.config_file = config_file
|
|
423
|
+
self.context = context
|
|
424
|
+
if not (self.name or self.yaml_file):
|
|
425
|
+
raise ValueError("name or yaml file is required")
|
|
426
|
+
|
|
427
|
+
self.teardown = teardown
|
|
428
|
+
self.timeout = timeout
|
|
429
|
+
self.delete_timeout = delete_timeout
|
|
430
|
+
self.dry_run = dry_run
|
|
431
|
+
self.node_selector = node_selector
|
|
432
|
+
self.node_selector_labels = node_selector_labels
|
|
433
|
+
self.node_selector_spec = self._prepare_node_selector_spec()
|
|
434
|
+
self.res = None
|
|
435
|
+
self.yaml_file_contents = None
|
|
436
|
+
self.logger = get_logger(name=f"{__name__.rsplit('.')[0]} {self.kind}")
|
|
437
|
+
self.timeout_seconds = timeout_seconds
|
|
438
|
+
self._set_client_and_api_version()
|
|
439
|
+
|
|
440
|
+
def _prepare_node_selector_spec(self):
|
|
441
|
+
if self.node_selector:
|
|
442
|
+
return {f"{self.ApiGroup.KUBERNETES_IO}/hostname": self.node_selector}
|
|
443
|
+
if self.node_selector_labels:
|
|
444
|
+
return self.node_selector_labels
|
|
445
|
+
|
|
446
|
+
@ClassProperty
|
|
447
|
+
def kind(cls): # noqa: N805
|
|
448
|
+
return sub_resource_level(cls, NamespacedResource, Resource)
|
|
449
|
+
|
|
450
|
+
def _base_body(self):
|
|
451
|
+
"""
|
|
452
|
+
Generate resource dict from yaml if self.yaml_file else return base resource dict.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
dict: Resource dict.
|
|
456
|
+
"""
|
|
457
|
+
if self.yaml_file:
|
|
458
|
+
if not self.yaml_file_contents:
|
|
459
|
+
if isinstance(self.yaml_file, StringIO):
|
|
460
|
+
self.yaml_file_contents = self.yaml_file.read()
|
|
461
|
+
else:
|
|
462
|
+
with open(self.yaml_file, "r") as stream:
|
|
463
|
+
self.yaml_file_contents = stream.read()
|
|
464
|
+
|
|
465
|
+
self.res = yaml.safe_load(stream=self.yaml_file_contents)
|
|
466
|
+
self.res.get("metadata", {}).pop("resourceVersion", None)
|
|
467
|
+
self.name = self.res["metadata"]["name"]
|
|
468
|
+
else:
|
|
469
|
+
self.res = {
|
|
470
|
+
"apiVersion": self.api_version,
|
|
471
|
+
"kind": self.kind,
|
|
472
|
+
"metadata": {"name": self.name},
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
def to_dict(self):
|
|
476
|
+
"""
|
|
477
|
+
Generate intended dict representation of the resource.
|
|
478
|
+
"""
|
|
479
|
+
self._base_body()
|
|
480
|
+
|
|
481
|
+
def __enter__(self):
|
|
482
|
+
signal(SIGINT, self._sigint_handler)
|
|
483
|
+
return self.deploy()
|
|
484
|
+
|
|
485
|
+
def __exit__(self, exception_type, exception_value, traceback):
|
|
486
|
+
if self.teardown:
|
|
487
|
+
self.clean_up()
|
|
488
|
+
|
|
489
|
+
def _sigint_handler(self, signal_received, frame):
|
|
490
|
+
self.__exit__(exception_type=None, exception_value=None, traceback=None)
|
|
491
|
+
sys.exit(signal_received)
|
|
492
|
+
|
|
493
|
+
def deploy(self, wait=False):
|
|
494
|
+
"""
|
|
495
|
+
For debug, export REUSE_IF_RESOURCE_EXISTS to skip resource create.
|
|
496
|
+
Spaces are important in the export dict
|
|
497
|
+
|
|
498
|
+
Examples:
|
|
499
|
+
To skip creation of all resources by kind:
|
|
500
|
+
export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}"
|
|
501
|
+
|
|
502
|
+
To skip creation of resource by name (on all namespaces or non-namespaced resources):
|
|
503
|
+
export REUSE_IF_RESOURCE_EXISTS="{Pod: {<pod-name>:}}"
|
|
504
|
+
|
|
505
|
+
To skip creation of resource by name and namespace:
|
|
506
|
+
export REUSE_IF_RESOURCE_EXISTS="{Pod: {<pod-name>: <pod-namespace>}}"
|
|
507
|
+
|
|
508
|
+
To skip creation of multiple resources:
|
|
509
|
+
export REUSE_IF_RESOURCE_EXISTS="{Namespace: {<namespace-name>:}, Pod: {<pod-name>: <pod-namespace>}}"
|
|
510
|
+
"""
|
|
511
|
+
_resource = None
|
|
512
|
+
_export_str = "REUSE_IF_RESOURCE_EXISTS"
|
|
513
|
+
skip_resource_kind_create_if_exists = os.environ.get(_export_str)
|
|
514
|
+
if skip_resource_kind_create_if_exists:
|
|
515
|
+
_resource = skip_existing_resource_creation_teardown(
|
|
516
|
+
resource=self,
|
|
517
|
+
export_str=_export_str,
|
|
518
|
+
user_exported_args=skip_resource_kind_create_if_exists,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if _resource:
|
|
522
|
+
return _resource
|
|
523
|
+
|
|
524
|
+
self.create(wait=wait)
|
|
525
|
+
return self
|
|
526
|
+
|
|
527
|
+
def clean_up(self):
|
|
528
|
+
if os.environ.get("CNV_TEST_COLLECT_LOGS", "0") == "1":
|
|
529
|
+
try:
|
|
530
|
+
_collect_data(resource_object=self)
|
|
531
|
+
except Exception as exception_:
|
|
532
|
+
LOGGER.warning(
|
|
533
|
+
f"Log collector failed to collect info for {self.kind} {self.name}\nexception: {exception_}"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
"""
|
|
537
|
+
For debug, export SKIP_RESOURCE_TEARDOWN to skip resource teardown.
|
|
538
|
+
Spaces are important in the export dict
|
|
539
|
+
|
|
540
|
+
Examples:
|
|
541
|
+
To skip teardown of all resources by kind:
|
|
542
|
+
export SKIP_RESOURCE_TEARDOWN="{Pod: {}}"
|
|
543
|
+
|
|
544
|
+
To skip teardown of resource by name (on all namespaces):
|
|
545
|
+
export SKIP_RESOURCE_TEARDOWN="{Pod: {<pod-name>:}}"
|
|
546
|
+
|
|
547
|
+
To skip teardown of resource by name and namespace:
|
|
548
|
+
export SKIP_RESOURCE_TEARDOWN="{Pod: {<pod-name>: <pod-namespace>}}"
|
|
549
|
+
|
|
550
|
+
To skip teardown of multiple resources:
|
|
551
|
+
export SKIP_RESOURCE_TEARDOWN="{Namespace: {<namespace-name>:}, Pod: {<pod-name>: <pod-namespace>}}"
|
|
552
|
+
"""
|
|
553
|
+
_export_str = "SKIP_RESOURCE_TEARDOWN"
|
|
554
|
+
skip_resource_teardown = os.environ.get(_export_str)
|
|
555
|
+
if skip_resource_teardown and skip_existing_resource_creation_teardown(
|
|
556
|
+
resource=self,
|
|
557
|
+
export_str=_export_str,
|
|
558
|
+
user_exported_args=skip_resource_teardown,
|
|
559
|
+
check_exists=False,
|
|
560
|
+
):
|
|
561
|
+
self.logger.warning(
|
|
562
|
+
f"Skip resource {self.kind} {self.name} teardown. Got {_export_str}={skip_resource_teardown}"
|
|
563
|
+
)
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
self.delete(wait=True, timeout=self.delete_timeout)
|
|
567
|
+
|
|
568
|
+
@classmethod
|
|
569
|
+
def _prepare_resources(cls, dyn_client, singular_name, *args, **kwargs):
|
|
570
|
+
if not cls.api_version:
|
|
571
|
+
cls.api_version = _get_api_version(
|
|
572
|
+
dyn_client=dyn_client, api_group=cls.api_group, kind=cls.kind
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
get_kwargs = {"singular_name": singular_name} if singular_name else {}
|
|
576
|
+
return dyn_client.resources.get(
|
|
577
|
+
kind=cls.kind,
|
|
578
|
+
api_version=cls.api_version,
|
|
579
|
+
**get_kwargs,
|
|
580
|
+
).get(*args, **kwargs, timeout_seconds=cls.timeout_seconds)
|
|
581
|
+
|
|
582
|
+
def _prepare_singular_name_kwargs(self, **kwargs):
|
|
583
|
+
kwargs = kwargs if kwargs else {}
|
|
584
|
+
if self.singular_name:
|
|
585
|
+
kwargs["singular_name"] = self.singular_name
|
|
586
|
+
|
|
587
|
+
return kwargs
|
|
588
|
+
|
|
589
|
+
def _set_client_and_api_version(self):
|
|
590
|
+
if not self.client:
|
|
591
|
+
self.client = get_client(config_file=self.config_file, context=self.context)
|
|
592
|
+
|
|
593
|
+
if not self.api_version:
|
|
594
|
+
self.api_version = _get_api_version(
|
|
595
|
+
dyn_client=self.client, api_group=self.api_group, kind=self.kind
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def full_api(self, **kwargs):
|
|
599
|
+
"""
|
|
600
|
+
Get resource API
|
|
601
|
+
|
|
602
|
+
Keyword Args:
|
|
603
|
+
pretty
|
|
604
|
+
_continue
|
|
605
|
+
include_uninitialized
|
|
606
|
+
field_selector
|
|
607
|
+
label_selector
|
|
608
|
+
limit
|
|
609
|
+
resource_version
|
|
610
|
+
timeout_seconds
|
|
611
|
+
watch
|
|
612
|
+
async_req
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
Resource: Resource object.
|
|
616
|
+
"""
|
|
617
|
+
self._set_client_and_api_version()
|
|
618
|
+
|
|
619
|
+
kwargs = self._prepare_singular_name_kwargs(**kwargs)
|
|
620
|
+
|
|
621
|
+
return self.client.resources.get(
|
|
622
|
+
api_version=self.api_version, kind=self.kind, **kwargs
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def api(self):
|
|
627
|
+
return self.full_api()
|
|
628
|
+
|
|
629
|
+
def wait(self, timeout=TIMEOUT_4MINUTES, sleep=1):
|
|
630
|
+
"""
|
|
631
|
+
Wait for resource
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
timeout (int): Time to wait for the resource.
|
|
635
|
+
sleep (int): Time to wait between retries
|
|
636
|
+
|
|
637
|
+
Raises:
|
|
638
|
+
TimeoutExpiredError: If resource not exists.
|
|
639
|
+
"""
|
|
640
|
+
self.logger.info(f"Wait until {self.kind} {self.name} is created")
|
|
641
|
+
samples = TimeoutSampler(
|
|
642
|
+
wait_timeout=timeout,
|
|
643
|
+
sleep=sleep,
|
|
644
|
+
exceptions_dict={
|
|
645
|
+
**PROTOCOL_ERROR_EXCEPTION_DICT,
|
|
646
|
+
**NOT_FOUND_ERROR_EXCEPTION_DICT,
|
|
647
|
+
**DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
|
|
648
|
+
},
|
|
649
|
+
func=lambda: self.exists,
|
|
650
|
+
)
|
|
651
|
+
for sample in samples:
|
|
652
|
+
if sample:
|
|
653
|
+
return
|
|
654
|
+
|
|
655
|
+
def wait_deleted(self, timeout=TIMEOUT_4MINUTES):
|
|
656
|
+
"""
|
|
657
|
+
Wait until resource is deleted
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
timeout (int): Time to wait for the resource.
|
|
661
|
+
|
|
662
|
+
Raises:
|
|
663
|
+
TimeoutExpiredError: If resource still exists.
|
|
664
|
+
"""
|
|
665
|
+
self.logger.info(f"Wait until {self.kind} {self.name} is deleted")
|
|
666
|
+
return self.client_wait_deleted(timeout=timeout)
|
|
667
|
+
|
|
668
|
+
@property
|
|
669
|
+
def exists(self):
|
|
670
|
+
"""
|
|
671
|
+
Whether self exists on the server
|
|
672
|
+
"""
|
|
673
|
+
try:
|
|
674
|
+
return self.instance
|
|
675
|
+
except NotFoundError:
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
def client_wait_deleted(self, timeout):
|
|
679
|
+
"""
|
|
680
|
+
client-side Wait until resource is deleted
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
timeout (int): Time to wait for the resource.
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
TimeoutExpiredError: If resource still exists.
|
|
687
|
+
"""
|
|
688
|
+
samples = TimeoutSampler(
|
|
689
|
+
wait_timeout=timeout, sleep=1, func=lambda: self.exists
|
|
690
|
+
)
|
|
691
|
+
for sample in samples:
|
|
692
|
+
if not sample:
|
|
693
|
+
return
|
|
694
|
+
|
|
695
|
+
def wait_for_status(
|
|
696
|
+
self, status, timeout=TIMEOUT_4MINUTES, stop_status=None, sleep=1
|
|
697
|
+
):
|
|
698
|
+
"""
|
|
699
|
+
Wait for resource to be in status
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
status (str): Expected status.
|
|
703
|
+
timeout (int): Time to wait for the resource.
|
|
704
|
+
stop_status (str): Status which should stop the wait and failed.
|
|
705
|
+
|
|
706
|
+
Raises:
|
|
707
|
+
TimeoutExpiredError: If resource in not in desire status.
|
|
708
|
+
"""
|
|
709
|
+
stop_status = stop_status if stop_status else self.Status.FAILED
|
|
710
|
+
self.logger.info(f"Wait for {self.kind} {self.name} status to be {status}")
|
|
711
|
+
samples = TimeoutSampler(
|
|
712
|
+
wait_timeout=timeout,
|
|
713
|
+
sleep=sleep,
|
|
714
|
+
exceptions_dict={
|
|
715
|
+
**PROTOCOL_ERROR_EXCEPTION_DICT,
|
|
716
|
+
**DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
|
|
717
|
+
},
|
|
718
|
+
func=self.api.get,
|
|
719
|
+
field_selector=f"metadata.name=={self.name}",
|
|
720
|
+
namespace=self.namespace,
|
|
721
|
+
)
|
|
722
|
+
current_status = None
|
|
723
|
+
try:
|
|
724
|
+
for sample in samples:
|
|
725
|
+
if sample.items:
|
|
726
|
+
sample_status = sample.items[0].status
|
|
727
|
+
if sample_status:
|
|
728
|
+
current_status = sample_status.phase
|
|
729
|
+
if current_status == status:
|
|
730
|
+
return
|
|
731
|
+
|
|
732
|
+
if current_status == stop_status:
|
|
733
|
+
raise TimeoutExpiredError(
|
|
734
|
+
f"Status of {self.kind} {self.name} is {current_status}"
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
except TimeoutExpiredError:
|
|
738
|
+
if current_status:
|
|
739
|
+
self.logger.error(
|
|
740
|
+
f"Status of {self.kind} {self.name} is {current_status}"
|
|
741
|
+
)
|
|
742
|
+
raise
|
|
743
|
+
|
|
744
|
+
def create(self, wait=False):
|
|
745
|
+
"""
|
|
746
|
+
Create resource.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
wait (bool) : True to wait for resource status.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
bool: True if create succeeded, False otherwise.
|
|
753
|
+
|
|
754
|
+
Raises:
|
|
755
|
+
ValueMismatch: When body value doesn't match class value
|
|
756
|
+
"""
|
|
757
|
+
if not self.res:
|
|
758
|
+
self.to_dict()
|
|
759
|
+
|
|
760
|
+
self.logger.info(f"Create {self.kind} {self.name}")
|
|
761
|
+
self.logger.info(f"Posting {self.res}")
|
|
762
|
+
self.logger.debug(f"\n{yaml.dump(self.res)}")
|
|
763
|
+
resource_ = self.api.create(
|
|
764
|
+
body=self.res, namespace=self.namespace, dry_run=self.dry_run
|
|
765
|
+
)
|
|
766
|
+
if wait and resource_:
|
|
767
|
+
return self.wait()
|
|
768
|
+
return resource_
|
|
769
|
+
|
|
770
|
+
def delete(self, wait=False, timeout=TIMEOUT_4MINUTES, body=None):
|
|
771
|
+
self.logger.info(f"Delete {self.kind} {self.name}")
|
|
772
|
+
if self.exists:
|
|
773
|
+
data = self.instance.to_dict()
|
|
774
|
+
self.logger.info(f"Deleting {data}")
|
|
775
|
+
self.logger.debug(f"\n{yaml.dump(data)}")
|
|
776
|
+
|
|
777
|
+
try:
|
|
778
|
+
res = self.api.delete(name=self.name, namespace=self.namespace, body=body)
|
|
779
|
+
except NotFoundError:
|
|
780
|
+
return False
|
|
781
|
+
|
|
782
|
+
if wait and res:
|
|
783
|
+
return self.wait_deleted(timeout=timeout)
|
|
784
|
+
return res
|
|
785
|
+
|
|
786
|
+
@property
|
|
787
|
+
def status(self):
|
|
788
|
+
"""
|
|
789
|
+
Get resource status
|
|
790
|
+
|
|
791
|
+
Status: Running, Scheduling, Pending, Unknown, CrashLoopBackOff
|
|
792
|
+
|
|
793
|
+
Returns:
|
|
794
|
+
str: Status
|
|
795
|
+
"""
|
|
796
|
+
self.logger.info(f"Get {self.kind} {self.name} status")
|
|
797
|
+
return self.instance.status.phase
|
|
798
|
+
|
|
799
|
+
def update(self, resource_dict):
|
|
800
|
+
"""
|
|
801
|
+
Update resource with resource dict
|
|
802
|
+
|
|
803
|
+
Args:
|
|
804
|
+
resource_dict: Resource dictionary
|
|
805
|
+
"""
|
|
806
|
+
self.logger.info(f"Update {self.kind} {self.name}:\n{resource_dict}")
|
|
807
|
+
self.logger.debug(f"\n{yaml.dump(resource_dict)}")
|
|
808
|
+
self.api.patch(
|
|
809
|
+
body=resource_dict,
|
|
810
|
+
namespace=self.namespace,
|
|
811
|
+
content_type="application/merge-patch+json",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
def update_replace(self, resource_dict):
|
|
815
|
+
"""
|
|
816
|
+
Replace resource metadata.
|
|
817
|
+
Use this to remove existing field. (update() will only update existing fields)
|
|
818
|
+
"""
|
|
819
|
+
self.logger.info(f"Replace {self.kind} {self.name}: \n{resource_dict}")
|
|
820
|
+
self.logger.debug(f"\n{yaml.dump(resource_dict)}")
|
|
821
|
+
self.api.replace(body=resource_dict, name=self.name, namespace=self.namespace)
|
|
822
|
+
|
|
823
|
+
@staticmethod
|
|
824
|
+
def retry_cluster_exceptions(
|
|
825
|
+
func, exceptions_dict=DEFAULT_CLUSTER_RETRY_EXCEPTIONS, **kwargs
|
|
826
|
+
):
|
|
827
|
+
sampler = TimeoutSampler(
|
|
828
|
+
wait_timeout=10,
|
|
829
|
+
sleep=1,
|
|
830
|
+
func=func,
|
|
831
|
+
print_log=False,
|
|
832
|
+
exceptions_dict=exceptions_dict,
|
|
833
|
+
**kwargs,
|
|
834
|
+
)
|
|
835
|
+
for sample in sampler:
|
|
836
|
+
return sample
|
|
837
|
+
|
|
838
|
+
@classmethod
|
|
839
|
+
def get(
|
|
840
|
+
cls,
|
|
841
|
+
dyn_client=None,
|
|
842
|
+
config_file=None,
|
|
843
|
+
context=None,
|
|
844
|
+
singular_name=None,
|
|
845
|
+
*args,
|
|
846
|
+
**kwargs,
|
|
847
|
+
):
|
|
848
|
+
"""
|
|
849
|
+
Get resources
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
dyn_client (DynamicClient): Open connection to remote cluster.
|
|
853
|
+
config_file (str): Path to config file for connecting to remote cluster.
|
|
854
|
+
context (str): Context name for connecting to remote cluster.
|
|
855
|
+
singular_name (str): Resource kind (in lowercase), in use where we have multiple matches for resource.
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
generator: Generator of Resources of cls.kind
|
|
859
|
+
"""
|
|
860
|
+
if not dyn_client:
|
|
861
|
+
dyn_client = get_client(config_file=config_file, context=context)
|
|
862
|
+
|
|
863
|
+
def _get():
|
|
864
|
+
_resources = cls._prepare_resources(
|
|
865
|
+
dyn_client=dyn_client, singular_name=singular_name, *args, **kwargs
|
|
866
|
+
)
|
|
867
|
+
try:
|
|
868
|
+
for resource_field in _resources.items:
|
|
869
|
+
yield cls(client=dyn_client, name=resource_field.metadata.name)
|
|
870
|
+
except TypeError:
|
|
871
|
+
yield cls(client=dyn_client, name=_resources.metadata.name)
|
|
872
|
+
|
|
873
|
+
return Resource.retry_cluster_exceptions(func=_get)
|
|
874
|
+
|
|
875
|
+
@property
|
|
876
|
+
def instance(self):
|
|
877
|
+
"""
|
|
878
|
+
Get resource instance
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
openshift.dynamic.client.ResourceInstance
|
|
882
|
+
"""
|
|
883
|
+
|
|
884
|
+
def _instance():
|
|
885
|
+
return self.api.get(name=self.name)
|
|
886
|
+
|
|
887
|
+
return self.retry_cluster_exceptions(func=_instance)
|
|
888
|
+
|
|
889
|
+
@property
|
|
890
|
+
def labels(self):
|
|
891
|
+
"""
|
|
892
|
+
Method to get labels for this resource
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
openshift.dynamic.resource.ResourceField: Representation of labels
|
|
896
|
+
"""
|
|
897
|
+
return self.instance["metadata"]["labels"]
|
|
898
|
+
|
|
899
|
+
def wait_for_condition(self, condition, status, timeout=300):
|
|
900
|
+
"""
|
|
901
|
+
Wait for Pod condition to be in desire status.
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
condition (str): Condition to query.
|
|
905
|
+
status (str): Expected condition status.
|
|
906
|
+
timeout (int): Time to wait for the resource.
|
|
907
|
+
|
|
908
|
+
Raises:
|
|
909
|
+
TimeoutExpiredError: If Pod condition in not in desire status.
|
|
910
|
+
"""
|
|
911
|
+
self.logger.info(
|
|
912
|
+
f"Wait for {self.kind}/{self.name}'s '{condition}' condition to be '{status}'"
|
|
913
|
+
)
|
|
914
|
+
samples = TimeoutSampler(
|
|
915
|
+
wait_timeout=timeout,
|
|
916
|
+
sleep=1,
|
|
917
|
+
exceptions_dict=PROTOCOL_ERROR_EXCEPTION_DICT,
|
|
918
|
+
func=self.api.get,
|
|
919
|
+
field_selector=f"metadata.name=={self.name}",
|
|
920
|
+
namespace=self.namespace,
|
|
921
|
+
)
|
|
922
|
+
for sample in samples:
|
|
923
|
+
if (
|
|
924
|
+
sample.items
|
|
925
|
+
and sample.items[0].get("status")
|
|
926
|
+
and sample.items[0].status.get("conditions")
|
|
927
|
+
):
|
|
928
|
+
sample_conditions = sample.items[0].status.conditions
|
|
929
|
+
if sample_conditions:
|
|
930
|
+
for cond in sample_conditions:
|
|
931
|
+
if cond.type == condition and cond.status == status:
|
|
932
|
+
return
|
|
933
|
+
|
|
934
|
+
def api_request(self, method, action, url, **params):
|
|
935
|
+
"""
|
|
936
|
+
Handle API requests to resource.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
method (str): Request method (GET/PUT etc.).
|
|
940
|
+
action (str): Action to perform (stop/start/guestosinfo etc.).
|
|
941
|
+
url (str): URL of resource.
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
data(dict): response data
|
|
945
|
+
|
|
946
|
+
"""
|
|
947
|
+
client = self.privileged_client or self.client
|
|
948
|
+
response = client.client.request(
|
|
949
|
+
method=method,
|
|
950
|
+
url=f"{url}/{action}",
|
|
951
|
+
headers=self.client.configuration.api_key,
|
|
952
|
+
**params,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
try:
|
|
956
|
+
return json.loads(response.data)
|
|
957
|
+
except json.decoder.JSONDecodeError:
|
|
958
|
+
return response.data
|
|
959
|
+
|
|
960
|
+
def wait_for_conditions(self):
|
|
961
|
+
samples = TimeoutSampler(
|
|
962
|
+
wait_timeout=30, sleep=1, func=lambda: self.instance.status.conditions
|
|
963
|
+
)
|
|
964
|
+
for sample in samples:
|
|
965
|
+
if sample:
|
|
966
|
+
return
|
|
967
|
+
|
|
968
|
+
def events(
|
|
969
|
+
self,
|
|
970
|
+
name=None,
|
|
971
|
+
label_selector=None,
|
|
972
|
+
field_selector=None,
|
|
973
|
+
resource_version=None,
|
|
974
|
+
timeout=None,
|
|
975
|
+
):
|
|
976
|
+
"""
|
|
977
|
+
get - retrieves K8s events.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
name (str): event name
|
|
981
|
+
label_selector (str): filter events by labels; comma separated string of key=value
|
|
982
|
+
field_selector (str): filter events by fields; comma separated string of key=valueevent fields;
|
|
983
|
+
comma separated string of key=value
|
|
984
|
+
resource_version (str): filter events by their resource's version
|
|
985
|
+
timeout (int): timeout in seconds
|
|
986
|
+
|
|
987
|
+
Returns
|
|
988
|
+
list: event objects
|
|
989
|
+
|
|
990
|
+
example: reading all CSV Warning events in namespace "my-namespace", with reason of "AnEventReason"
|
|
991
|
+
pod = Pod(client=client, name="pod", namespace="my-namespace")
|
|
992
|
+
for event in pod.events(
|
|
993
|
+
default_client,
|
|
994
|
+
namespace="my-namespace",
|
|
995
|
+
field_selector="involvedObject.kind==ClusterServiceVersion,type==Warning,reason=AnEventReason",
|
|
996
|
+
timeout=10,
|
|
997
|
+
):
|
|
998
|
+
print(event.object)
|
|
999
|
+
"""
|
|
1000
|
+
_field_selector = f"involvedObject.name=={self.name}"
|
|
1001
|
+
if field_selector:
|
|
1002
|
+
field_selector = f"{_field_selector},{field_selector}"
|
|
1003
|
+
yield from Event.get(
|
|
1004
|
+
dyn_client=self.client,
|
|
1005
|
+
namespace=self.namespace,
|
|
1006
|
+
name=name,
|
|
1007
|
+
label_selector=label_selector,
|
|
1008
|
+
field_selector=field_selector or _field_selector,
|
|
1009
|
+
resource_version=resource_version,
|
|
1010
|
+
timeout=timeout,
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
@staticmethod
|
|
1014
|
+
def get_all_cluster_resources(
|
|
1015
|
+
config_file=None, config_dict=None, context=None, *args, **kwargs
|
|
1016
|
+
):
|
|
1017
|
+
"""
|
|
1018
|
+
Get all cluster resources
|
|
1019
|
+
|
|
1020
|
+
Args:
|
|
1021
|
+
config_file (str): path to a kubeconfig file.
|
|
1022
|
+
config_dict (dict): dict with kubeconfig configuration.
|
|
1023
|
+
context (str): name of the context to use.
|
|
1024
|
+
*args (tuple): args to pass to client.get()
|
|
1025
|
+
**kwargs (dict): kwargs to pass to client.get()
|
|
1026
|
+
|
|
1027
|
+
Yields:
|
|
1028
|
+
kubernetes.dynamic.resource.ResourceField: Cluster resource.
|
|
1029
|
+
|
|
1030
|
+
Example:
|
|
1031
|
+
for resource in get_all_cluster_resources(label_selector="my-label=value"):
|
|
1032
|
+
print(f"Resource: {resource}")
|
|
1033
|
+
"""
|
|
1034
|
+
|
|
1035
|
+
client = get_client(
|
|
1036
|
+
config_file=config_file, config_dict=config_dict, context=context
|
|
1037
|
+
)
|
|
1038
|
+
for _resource in client.resources.search():
|
|
1039
|
+
try:
|
|
1040
|
+
_resources = client.get(_resource, *args, **kwargs)
|
|
1041
|
+
yield from _resources.items
|
|
1042
|
+
|
|
1043
|
+
except (NotFoundError, TypeError, MethodNotAllowedError):
|
|
1044
|
+
continue
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
class NamespacedResource(Resource):
|
|
1048
|
+
"""
|
|
1049
|
+
Namespaced object, inherited from Resource.
|
|
1050
|
+
"""
|
|
1051
|
+
|
|
1052
|
+
def __init__(
|
|
1053
|
+
self,
|
|
1054
|
+
name=None,
|
|
1055
|
+
namespace=None,
|
|
1056
|
+
client=None,
|
|
1057
|
+
teardown=True,
|
|
1058
|
+
timeout=TIMEOUT_4MINUTES,
|
|
1059
|
+
privileged_client=None,
|
|
1060
|
+
yaml_file=None,
|
|
1061
|
+
delete_timeout=TIMEOUT_4MINUTES,
|
|
1062
|
+
**kwargs,
|
|
1063
|
+
):
|
|
1064
|
+
super().__init__(
|
|
1065
|
+
name=name,
|
|
1066
|
+
client=client,
|
|
1067
|
+
teardown=teardown,
|
|
1068
|
+
timeout=timeout,
|
|
1069
|
+
privileged_client=privileged_client,
|
|
1070
|
+
yaml_file=yaml_file,
|
|
1071
|
+
delete_timeout=delete_timeout,
|
|
1072
|
+
**kwargs,
|
|
1073
|
+
)
|
|
1074
|
+
self.namespace = namespace
|
|
1075
|
+
if not (self.name and self.namespace) and not self.yaml_file:
|
|
1076
|
+
raise ValueError("name and namespace or yaml file is required")
|
|
1077
|
+
|
|
1078
|
+
@classmethod
|
|
1079
|
+
def get(
|
|
1080
|
+
cls,
|
|
1081
|
+
dyn_client=None,
|
|
1082
|
+
config_file=None,
|
|
1083
|
+
context=None,
|
|
1084
|
+
singular_name=None,
|
|
1085
|
+
raw=False,
|
|
1086
|
+
*args,
|
|
1087
|
+
**kwargs,
|
|
1088
|
+
):
|
|
1089
|
+
"""
|
|
1090
|
+
Get resources
|
|
1091
|
+
|
|
1092
|
+
Args:
|
|
1093
|
+
dyn_client (DynamicClient): Open connection to remote cluster
|
|
1094
|
+
config_file (str): Path to config file for connecting to remote cluster.
|
|
1095
|
+
context (str): Context name for connecting to remote cluster.
|
|
1096
|
+
singular_name (str): Resource kind (in lowercase), in use where we have multiple matches for resource
|
|
1097
|
+
raw (bool): If True return raw object from openshift-restclient-python
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
generator: Generator of Resources of cls.kind
|
|
1102
|
+
"""
|
|
1103
|
+
if not dyn_client:
|
|
1104
|
+
dyn_client = get_client(config_file=config_file, context=context)
|
|
1105
|
+
|
|
1106
|
+
_resources = cls._prepare_resources(
|
|
1107
|
+
dyn_client=dyn_client, singular_name=singular_name, *args, **kwargs
|
|
1108
|
+
)
|
|
1109
|
+
try:
|
|
1110
|
+
for resource_field in _resources.items:
|
|
1111
|
+
if raw:
|
|
1112
|
+
yield resource_field
|
|
1113
|
+
else:
|
|
1114
|
+
yield cls(
|
|
1115
|
+
client=dyn_client,
|
|
1116
|
+
name=resource_field.metadata.name,
|
|
1117
|
+
namespace=resource_field.metadata.namespace,
|
|
1118
|
+
)
|
|
1119
|
+
except TypeError:
|
|
1120
|
+
if raw:
|
|
1121
|
+
yield _resources
|
|
1122
|
+
else:
|
|
1123
|
+
yield cls(
|
|
1124
|
+
client=dyn_client,
|
|
1125
|
+
name=_resources.metadata.name,
|
|
1126
|
+
namespace=_resources.metadata.namespace,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
@property
|
|
1130
|
+
def instance(self):
|
|
1131
|
+
"""
|
|
1132
|
+
Get resource instance
|
|
1133
|
+
|
|
1134
|
+
Returns:
|
|
1135
|
+
openshift.dynamic.client.ResourceInstance
|
|
1136
|
+
"""
|
|
1137
|
+
return self.api.get(name=self.name, namespace=self.namespace)
|
|
1138
|
+
|
|
1139
|
+
def _base_body(self):
|
|
1140
|
+
if not self.res:
|
|
1141
|
+
super(NamespacedResource, self)._base_body()
|
|
1142
|
+
|
|
1143
|
+
if self.yaml_file:
|
|
1144
|
+
self.namespace = self.res["metadata"].get("namespace", self.namespace)
|
|
1145
|
+
|
|
1146
|
+
if not self.namespace:
|
|
1147
|
+
raise ValueError("Namespace must be passed or specified in the YAML file.")
|
|
1148
|
+
|
|
1149
|
+
if not self.yaml_file:
|
|
1150
|
+
self.res["metadata"]["namespace"] = self.namespace
|
|
1151
|
+
|
|
1152
|
+
def to_dict(self):
|
|
1153
|
+
self._base_body()
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
class ResourceEditor:
|
|
1157
|
+
def __init__(self, patches, action="update", user_backups=None):
|
|
1158
|
+
"""
|
|
1159
|
+
Args:
|
|
1160
|
+
patches (dict): {<Resource object>: <yaml patch as dict>}
|
|
1161
|
+
e.g. {<Resource object>:
|
|
1162
|
+
{'metadata': {'labels': {'label1': 'true'}}}
|
|
1163
|
+
|
|
1164
|
+
Allows for temporary edits to cluster resources for tests. During
|
|
1165
|
+
__enter__ user-specified patches (see args) are applied and old values
|
|
1166
|
+
are backed up, and during __exit__ these backups are used to reverse
|
|
1167
|
+
all changes made.
|
|
1168
|
+
|
|
1169
|
+
Flow:
|
|
1170
|
+
1) apply patches
|
|
1171
|
+
2) automation runs
|
|
1172
|
+
3) edits made to resources are reversed
|
|
1173
|
+
|
|
1174
|
+
May also be used without being treated as a context manager by
|
|
1175
|
+
calling the methods update() and restore() after instantiation.
|
|
1176
|
+
|
|
1177
|
+
*** the DynamicClient object used to get the resources must not be
|
|
1178
|
+
using an unprivileged_user; use default_client or similar instead.***
|
|
1179
|
+
"""
|
|
1180
|
+
|
|
1181
|
+
self._patches = self._dictify_resourcefield(res=patches)
|
|
1182
|
+
self.action = action
|
|
1183
|
+
self.user_backups = user_backups
|
|
1184
|
+
self._backups = {}
|
|
1185
|
+
|
|
1186
|
+
@property
|
|
1187
|
+
def backups(self):
|
|
1188
|
+
"""Returns a dict {<Resource object>: <backup_as_dict>}
|
|
1189
|
+
The backup dict kept for each resource edited"""
|
|
1190
|
+
return self._backups
|
|
1191
|
+
|
|
1192
|
+
@property
|
|
1193
|
+
def patches(self):
|
|
1194
|
+
"""Returns the patches dict provided in the constructor"""
|
|
1195
|
+
return self._patches
|
|
1196
|
+
|
|
1197
|
+
def update(self, backup_resources=False):
|
|
1198
|
+
"""Prepares backup dicts (where necessary) and applies patches"""
|
|
1199
|
+
# prepare update dicts and backups
|
|
1200
|
+
resource_to_patch = []
|
|
1201
|
+
if backup_resources:
|
|
1202
|
+
LOGGER.info("ResourceEdit: Backing up old data")
|
|
1203
|
+
if self.user_backups:
|
|
1204
|
+
resource_to_patch = self._patches
|
|
1205
|
+
self._backups = self.user_backups
|
|
1206
|
+
|
|
1207
|
+
else:
|
|
1208
|
+
for resource, update in self._patches.items():
|
|
1209
|
+
namespace = None
|
|
1210
|
+
# prepare backup
|
|
1211
|
+
try:
|
|
1212
|
+
original_resource_dict = resource.instance.to_dict()
|
|
1213
|
+
except NotFoundError:
|
|
1214
|
+
# Some resource cannot be found by name.
|
|
1215
|
+
# happens in 'ServiceMonitor' resource.
|
|
1216
|
+
original_resource_dict = list(
|
|
1217
|
+
resource.get(
|
|
1218
|
+
dyn_client=resource.client,
|
|
1219
|
+
field_selector=f"metadata.name={resource.name}",
|
|
1220
|
+
)
|
|
1221
|
+
)[0].to_dict()
|
|
1222
|
+
namespace = update.get("metadata", {}).get("namespace")
|
|
1223
|
+
|
|
1224
|
+
backup = self._create_backup(
|
|
1225
|
+
original=original_resource_dict, patch=update
|
|
1226
|
+
)
|
|
1227
|
+
if namespace:
|
|
1228
|
+
# Add namespace to metadata for restore.
|
|
1229
|
+
backup["metadata"]["namespace"] = namespace
|
|
1230
|
+
|
|
1231
|
+
# no need to back up if no changes have been made
|
|
1232
|
+
# if action is 'replace' we need to update even if no backup (replace update can be empty )
|
|
1233
|
+
if backup or self.action == "replace":
|
|
1234
|
+
resource_to_patch.append(resource)
|
|
1235
|
+
self._backups[resource] = backup
|
|
1236
|
+
else:
|
|
1237
|
+
LOGGER.warning(
|
|
1238
|
+
f"ResourceEdit: no diff found in patch for "
|
|
1239
|
+
f"{resource.name} -- skipping"
|
|
1240
|
+
)
|
|
1241
|
+
if not resource_to_patch:
|
|
1242
|
+
return
|
|
1243
|
+
else:
|
|
1244
|
+
resource_to_patch = self._patches
|
|
1245
|
+
|
|
1246
|
+
patches_to_apply = {
|
|
1247
|
+
resource: self._patches[resource] for resource in resource_to_patch
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
# apply changes
|
|
1251
|
+
self._apply_patches_sampler(
|
|
1252
|
+
patches=patches_to_apply, action_text="Updating", action=self.action
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
def restore(self):
|
|
1256
|
+
self._apply_patches_sampler(
|
|
1257
|
+
patches=self._backups, action_text="Restoring", action=self.action
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
def __enter__(self):
|
|
1261
|
+
self.update(backup_resources=True)
|
|
1262
|
+
return self
|
|
1263
|
+
|
|
1264
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1265
|
+
# restore backups
|
|
1266
|
+
self.restore()
|
|
1267
|
+
|
|
1268
|
+
@staticmethod
|
|
1269
|
+
def _dictify_resourcefield(res):
|
|
1270
|
+
"""Recursively turns any ResourceField objects into dicts to avoid issues caused by appending lists, etc."""
|
|
1271
|
+
if isinstance(res, ResourceField):
|
|
1272
|
+
return ResourceEditor._dictify_resourcefield(res=dict(res.items()))
|
|
1273
|
+
elif isinstance(res, dict):
|
|
1274
|
+
return {
|
|
1275
|
+
ResourceEditor._dictify_resourcefield(
|
|
1276
|
+
res=key
|
|
1277
|
+
): ResourceEditor._dictify_resourcefield(res=value)
|
|
1278
|
+
for key, value in res.items()
|
|
1279
|
+
}
|
|
1280
|
+
elif isinstance(res, list):
|
|
1281
|
+
return [ResourceEditor._dictify_resourcefield(res=x) for x in res]
|
|
1282
|
+
|
|
1283
|
+
return res
|
|
1284
|
+
|
|
1285
|
+
@staticmethod
|
|
1286
|
+
def _create_backup(original, patch):
|
|
1287
|
+
"""
|
|
1288
|
+
Args:
|
|
1289
|
+
original (dict*): source of values to back up if necessary
|
|
1290
|
+
patch (dict*): 'new' values; keys needn't necessarily all be
|
|
1291
|
+
contained in original
|
|
1292
|
+
|
|
1293
|
+
Returns a dict containing the fields in original that are different
|
|
1294
|
+
from update. Performs the
|
|
1295
|
+
|
|
1296
|
+
Places None for fields in update that don't appear in
|
|
1297
|
+
original (because that's how the API knows to remove those fields from
|
|
1298
|
+
the yaml).
|
|
1299
|
+
|
|
1300
|
+
* the first call will be with both of these arguments as dicts but
|
|
1301
|
+
this will not necessarily be the case during recursion"""
|
|
1302
|
+
|
|
1303
|
+
# when both are dicts, get the diff (recursively if need be)
|
|
1304
|
+
if isinstance(original, dict) and isinstance(patch, dict):
|
|
1305
|
+
diff_dict = {}
|
|
1306
|
+
for key, value in patch.items():
|
|
1307
|
+
if key not in original:
|
|
1308
|
+
diff_dict[key] = None
|
|
1309
|
+
continue
|
|
1310
|
+
|
|
1311
|
+
# recursive call
|
|
1312
|
+
key_diff = ResourceEditor._create_backup(
|
|
1313
|
+
original=original[key], patch=value
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
if key_diff is not None:
|
|
1317
|
+
diff_dict[key] = key_diff
|
|
1318
|
+
|
|
1319
|
+
return diff_dict
|
|
1320
|
+
|
|
1321
|
+
# for one or more non-dict values, just compare them
|
|
1322
|
+
if patch != original:
|
|
1323
|
+
return original
|
|
1324
|
+
else:
|
|
1325
|
+
# this return value will be received by key_diff above
|
|
1326
|
+
return None
|
|
1327
|
+
|
|
1328
|
+
@staticmethod
|
|
1329
|
+
def _apply_patches(patches, action_text, action):
|
|
1330
|
+
"""
|
|
1331
|
+
Updates provided Resource objects with provided yaml patches
|
|
1332
|
+
|
|
1333
|
+
Args:
|
|
1334
|
+
patches (dict): {<Resource object>: <yaml patch as dict>}
|
|
1335
|
+
action_text (str):
|
|
1336
|
+
"ResourceEdit <action_text> for resource <resource name>"
|
|
1337
|
+
will be printed for each resource; see below
|
|
1338
|
+
"""
|
|
1339
|
+
|
|
1340
|
+
for resource, patch in patches.items():
|
|
1341
|
+
LOGGER.info(
|
|
1342
|
+
f"ResourceEdits: {action_text} data for "
|
|
1343
|
+
f"resource {resource.kind} {resource.name}"
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
# add name to patch
|
|
1347
|
+
if "metadata" not in patch:
|
|
1348
|
+
patch["metadata"] = {}
|
|
1349
|
+
|
|
1350
|
+
# the api requires this field to be present in a yaml patch for
|
|
1351
|
+
# some resource kinds even if it is not changed
|
|
1352
|
+
if "name" not in patch["metadata"]:
|
|
1353
|
+
patch["metadata"]["name"] = resource.name
|
|
1354
|
+
|
|
1355
|
+
if action == "update":
|
|
1356
|
+
resource.update(resource_dict=patch) # update the resource
|
|
1357
|
+
|
|
1358
|
+
if action == "replace":
|
|
1359
|
+
if "metadata" not in patch:
|
|
1360
|
+
patch["metadata"] = {}
|
|
1361
|
+
|
|
1362
|
+
patch["metadata"]["name"] = resource.name
|
|
1363
|
+
patch["metadata"]["namespace"] = resource.namespace
|
|
1364
|
+
patch["metadata"][
|
|
1365
|
+
"resourceVersion"
|
|
1366
|
+
] = resource.instance.metadata.resourceVersion
|
|
1367
|
+
patch["kind"] = resource.kind
|
|
1368
|
+
patch["apiVersion"] = resource.api_version
|
|
1369
|
+
|
|
1370
|
+
resource.update_replace(
|
|
1371
|
+
resource_dict=patch
|
|
1372
|
+
) # replace the resource metadata
|
|
1373
|
+
|
|
1374
|
+
def _apply_patches_sampler(self, patches, action_text, action):
|
|
1375
|
+
exceptions_dict = {ConflictError: []}
|
|
1376
|
+
exceptions_dict.update(DEFAULT_CLUSTER_RETRY_EXCEPTIONS)
|
|
1377
|
+
return Resource.retry_cluster_exceptions(
|
|
1378
|
+
func=self._apply_patches,
|
|
1379
|
+
exceptions_dict=exceptions_dict,
|
|
1380
|
+
patches=patches,
|
|
1381
|
+
action_text=action_text,
|
|
1382
|
+
action=action,
|
|
1383
|
+
)
|