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.
Files changed (132) hide show
  1. ocp_resources/__init__.py +0 -0
  2. ocp_resources/api_service.py +9 -0
  3. ocp_resources/backup.py +52 -0
  4. ocp_resources/benchmark.py +92 -0
  5. ocp_resources/catalog_source.py +63 -0
  6. ocp_resources/catalog_source_config.py +86 -0
  7. ocp_resources/cdi.py +13 -0
  8. ocp_resources/cdi_config.py +52 -0
  9. ocp_resources/chaos_engine.py +30 -0
  10. ocp_resources/chaos_result.py +11 -0
  11. ocp_resources/cluster_operator.py +5 -0
  12. ocp_resources/cluster_role.py +70 -0
  13. ocp_resources/cluster_role_binding.py +36 -0
  14. ocp_resources/cluster_service_version.py +8 -0
  15. ocp_resources/cluster_version.py +5 -0
  16. ocp_resources/configmap.py +37 -0
  17. ocp_resources/console_cli_download.py +9 -0
  18. ocp_resources/console_plugin.py +9 -0
  19. ocp_resources/console_quick_start.py +9 -0
  20. ocp_resources/constants.py +26 -0
  21. ocp_resources/csidriver.py +9 -0
  22. ocp_resources/custom_resource_definition.py +5 -0
  23. ocp_resources/daemonset.py +61 -0
  24. ocp_resources/data_import_cron.py +100 -0
  25. ocp_resources/data_source.py +59 -0
  26. ocp_resources/datavolume.py +275 -0
  27. ocp_resources/deployment.py +65 -0
  28. ocp_resources/destination_rule.py +11 -0
  29. ocp_resources/event.py +114 -0
  30. ocp_resources/forklift_controller.py +10 -0
  31. ocp_resources/gateway.py +11 -0
  32. ocp_resources/hook.py +52 -0
  33. ocp_resources/host.py +64 -0
  34. ocp_resources/hostpath_provisioner.py +50 -0
  35. ocp_resources/hyperconverged.py +43 -0
  36. ocp_resources/image.py +9 -0
  37. ocp_resources/image_content_source_policy.py +9 -0
  38. ocp_resources/image_stream.py +48 -0
  39. ocp_resources/imagestreamtag.py +9 -0
  40. ocp_resources/infrastructure.py +18 -0
  41. ocp_resources/installplan.py +8 -0
  42. ocp_resources/job.py +65 -0
  43. ocp_resources/kube_descheduler.py +65 -0
  44. ocp_resources/kubevirt.py +5 -0
  45. ocp_resources/kubevirt_common_templates_bundle.py +5 -0
  46. ocp_resources/kubevirt_metrics_aggregation.py +5 -0
  47. ocp_resources/kubevirt_node_labeller_bundle.py +5 -0
  48. ocp_resources/kubevirt_template_validaotr.py +5 -0
  49. ocp_resources/logger.py +76 -0
  50. ocp_resources/machine.py +50 -0
  51. ocp_resources/machine_config.py +5 -0
  52. ocp_resources/machine_config_pool.py +9 -0
  53. ocp_resources/machine_health_check.py +76 -0
  54. ocp_resources/machine_set.py +189 -0
  55. ocp_resources/migration.py +63 -0
  56. ocp_resources/migration_policy.py +66 -0
  57. ocp_resources/mtv.py +171 -0
  58. ocp_resources/mutating_webhook_config.py +9 -0
  59. ocp_resources/namespace.py +40 -0
  60. ocp_resources/network.py +5 -0
  61. ocp_resources/network_addons_config.py +9 -0
  62. ocp_resources/network_attachment_definition.py +24 -0
  63. ocp_resources/network_map.py +64 -0
  64. ocp_resources/network_policy.py +9 -0
  65. ocp_resources/nmstate.py +9 -0
  66. ocp_resources/node.py +41 -0
  67. ocp_resources/node_health_check.py +10 -0
  68. ocp_resources/node_maintenance.py +40 -0
  69. ocp_resources/node_network_configuration_enactment.py +20 -0
  70. ocp_resources/node_network_configuration_policy.py +472 -0
  71. ocp_resources/node_network_state.py +121 -0
  72. ocp_resources/oauth.py +9 -0
  73. ocp_resources/operator_condition.py +5 -0
  74. ocp_resources/operator_group.py +37 -0
  75. ocp_resources/operator_hub.py +5 -0
  76. ocp_resources/operator_source.py +50 -0
  77. ocp_resources/package_manifest.py +5 -0
  78. ocp_resources/peer_authentication.py +18 -0
  79. ocp_resources/persistent_volume.py +27 -0
  80. ocp_resources/persistent_volume_claim.py +103 -0
  81. ocp_resources/plan.py +143 -0
  82. ocp_resources/pod.py +175 -0
  83. ocp_resources/pod_disruption_budget.py +53 -0
  84. ocp_resources/priority_class.py +48 -0
  85. ocp_resources/project.py +60 -0
  86. ocp_resources/prometheus.py +9 -0
  87. ocp_resources/prometheus_rule.py +9 -0
  88. ocp_resources/provider.py +59 -0
  89. ocp_resources/replicaset.py +11 -0
  90. ocp_resources/resource.py +1383 -0
  91. ocp_resources/restore.py +52 -0
  92. ocp_resources/role.py +11 -0
  93. ocp_resources/role_binding.py +67 -0
  94. ocp_resources/route.py +76 -0
  95. ocp_resources/secret.py +75 -0
  96. ocp_resources/security_context_constraints.py +9 -0
  97. ocp_resources/service.py +16 -0
  98. ocp_resources/service_account.py +9 -0
  99. ocp_resources/service_mesh_control_plane.py +11 -0
  100. ocp_resources/service_mesh_member_roll.py +14 -0
  101. ocp_resources/service_monitor.py +9 -0
  102. ocp_resources/sriov_network.py +54 -0
  103. ocp_resources/sriov_network_node_policy.py +67 -0
  104. ocp_resources/sriov_network_node_state.py +44 -0
  105. ocp_resources/ssp.py +9 -0
  106. ocp_resources/storage_class.py +47 -0
  107. ocp_resources/storage_map.py +67 -0
  108. ocp_resources/storage_profile.py +7 -0
  109. ocp_resources/subscription.py +66 -0
  110. ocp_resources/tekton_tasks.py +9 -0
  111. ocp_resources/template.py +74 -0
  112. ocp_resources/upload_token_request.py +39 -0
  113. ocp_resources/utils.py +278 -0
  114. ocp_resources/validating_webhook_config.py +9 -0
  115. ocp_resources/virtual_machine.py +177 -0
  116. ocp_resources/virtual_machine_Instance_replica_set.py +9 -0
  117. ocp_resources/virtual_machine_cluster_flavor.py +5 -0
  118. ocp_resources/virtual_machine_flavor.py +5 -0
  119. ocp_resources/virtual_machine_import.py +307 -0
  120. ocp_resources/virtual_machine_instance.py +322 -0
  121. ocp_resources/virtual_machine_instance_migration.py +36 -0
  122. ocp_resources/virtual_machine_instance_preset.py +9 -0
  123. ocp_resources/virtual_machine_restore.py +99 -0
  124. ocp_resources/virtual_machine_snapshot.py +96 -0
  125. ocp_resources/virtual_service.py +11 -0
  126. ocp_resources/vm_import_config.py +9 -0
  127. ocp_resources/volume_snapshot.py +9 -0
  128. ocp_resources/volume_snapshot_class.py +9 -0
  129. openshift_python_wrapper-4.11.61.dist-info/LICENSE +201 -0
  130. openshift_python_wrapper-4.11.61.dist-info/METADATA +188 -0
  131. openshift_python_wrapper-4.11.61.dist-info/RECORD +132 -0
  132. 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
+ )