Coverage for n2vc/kubectl.py: 55%
309 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-07 06:04 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-05-07 06:04 +0000
1#######################################################################################
2# Copyright 2020 Canonical Ltd.
3# Copyright ETSI Contributors and Others.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#######################################################################################
18import base64
19import logging
20from typing import Dict
21import typing
22import uuid
23import json
24import tarfile
25import io
26from time import sleep
28from distutils.version import LooseVersion
30from kubernetes import client, config
31from kubernetes.client.api import VersionApi
32from kubernetes.client.models import (
33 V1ClusterRole,
34 V1Role,
35 V1ObjectMeta,
36 V1PolicyRule,
37 V1ServiceAccount,
38 V1ClusterRoleBinding,
39 V1RoleBinding,
40 V1RoleRef,
41 RbacV1Subject,
42 V1Secret,
43 V1SecretReference,
44 V1Namespace,
45 V1PersistentVolumeClaim,
46 V1PersistentVolumeClaimSpec,
47 V1PersistentVolumeClaimVolumeSource,
48 V1ResourceRequirements,
49 V1Pod,
50 V1PodSpec,
51 V1Volume,
52 V1VolumeMount,
53 V1Container,
54)
55from kubernetes.client.rest import ApiException
56from kubernetes.stream import stream
57from n2vc.libjuju import retry_callback
58from retrying_async import retry
60SERVICE_ACCOUNT_TOKEN_KEY = "token"
61SERVICE_ACCOUNT_ROOT_CA_KEY = "ca.crt"
62# clients
63CORE_CLIENT = "core_v1"
64RBAC_CLIENT = "rbac_v1"
65STORAGE_CLIENT = "storage_v1"
66CUSTOM_OBJECT_CLIENT = "custom_object"
69class Kubectl:
70 def __init__(self, config_file=None):
71 config.load_kube_config(config_file=config_file)
72 self._clients = {
73 CORE_CLIENT: client.CoreV1Api(),
74 RBAC_CLIENT: client.RbacAuthorizationV1Api(),
75 STORAGE_CLIENT: client.StorageV1Api(),
76 CUSTOM_OBJECT_CLIENT: client.CustomObjectsApi(),
77 }
78 self._configuration = config.kube_config.Configuration.get_default_copy()
79 self.logger = logging.getLogger("lcm.kubectl")
81 @property
82 def configuration(self):
83 return self._configuration
85 @property
86 def clients(self):
87 return self._clients
89 def get_services(
90 self,
91 field_selector: str = None,
92 label_selector: str = None,
93 ) -> typing.List[typing.Dict]:
94 """
95 Get Service list from a namespace
97 :param: field_selector: Kubernetes field selector for the namespace
98 :param: label_selector: Kubernetes label selector for the namespace
100 :return: List of the services matching the selectors specified
101 """
102 kwargs = {}
103 if field_selector:
104 kwargs["field_selector"] = field_selector
105 if label_selector:
106 kwargs["label_selector"] = label_selector
107 try:
108 result = self.clients[CORE_CLIENT].list_service_for_all_namespaces(**kwargs)
109 return [
110 {
111 "name": i.metadata.name,
112 "cluster_ip": i.spec.cluster_ip,
113 "type": i.spec.type,
114 "ports": (
115 [
116 {
117 "name": p.name,
118 "node_port": p.node_port,
119 "port": p.port,
120 "protocol": p.protocol,
121 "target_port": p.target_port,
122 }
123 for p in i.spec.ports
124 ]
125 if i.spec.ports
126 else []
127 ),
128 "external_ip": [i.ip for i in i.status.load_balancer.ingress]
129 if i.status.load_balancer.ingress
130 else None,
131 }
132 for i in result.items
133 ]
134 except ApiException as e:
135 self.logger.error("Error calling get services: {}".format(e))
136 raise e
138 def get_default_storage_class(self) -> str:
139 """
140 Default storage class
142 :return: Returns the default storage class name, if exists.
143 If not, it returns the first storage class.
144 If there are not storage classes, returns None
145 """
146 storage_classes = self.clients[STORAGE_CLIENT].list_storage_class()
147 selected_sc = None
148 default_sc_annotations = {
149 "storageclass.kubernetes.io/is-default-class": "true",
150 # Older clusters still use the beta annotation.
151 "storageclass.beta.kubernetes.io/is-default-class": "true",
152 }
153 for sc in storage_classes.items:
154 if not selected_sc:
155 # Select the first storage class in case there is no a default-class
156 selected_sc = sc.metadata.name
157 annotations = sc.metadata.annotations or {}
158 if any(
159 k in annotations and annotations[k] == v
160 for k, v in default_sc_annotations.items()
161 ):
162 # Default storage
163 selected_sc = sc.metadata.name
164 break
165 return selected_sc
167 def create_cluster_role(
168 self,
169 name: str,
170 labels: Dict[str, str],
171 namespace: str = "kube-system",
172 ):
173 """
174 Create a cluster role
176 :param: name: Name of the cluster role
177 :param: labels: Labels for cluster role metadata
178 :param: namespace: Kubernetes namespace for cluster role metadata
179 Default: kube-system
180 """
181 cluster_roles = self.clients[RBAC_CLIENT].list_cluster_role(
182 field_selector="metadata.name={}".format(name)
183 )
185 if len(cluster_roles.items) > 0:
186 raise Exception("Role with metadata.name={} already exists".format(name))
188 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
189 # Cluster role
190 cluster_role = V1ClusterRole(
191 metadata=metadata,
192 rules=[
193 V1PolicyRule(api_groups=["*"], resources=["*"], verbs=["*"]),
194 V1PolicyRule(non_resource_ur_ls=["*"], verbs=["*"]),
195 ],
196 )
198 self.clients[RBAC_CLIENT].create_cluster_role(cluster_role)
200 async def create_role(
201 self,
202 name: str,
203 labels: Dict[str, str],
204 api_groups: list,
205 resources: list,
206 verbs: list,
207 namespace: str,
208 ):
209 """
210 Create a role with one PolicyRule
212 :param: name: Name of the namespaced Role
213 :param: labels: Labels for namespaced Role metadata
214 :param: api_groups: List with api-groups allowed in the policy rule
215 :param: resources: List with resources allowed in the policy rule
216 :param: verbs: List with verbs allowed in the policy rule
217 :param: namespace: Kubernetes namespace for Role metadata
219 :return: None
220 """
222 roles = self.clients[RBAC_CLIENT].list_namespaced_role(
223 namespace, field_selector="metadata.name={}".format(name)
224 )
226 if len(roles.items) > 0:
227 raise Exception("Role with metadata.name={} already exists".format(name))
229 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
231 role = V1Role(
232 metadata=metadata,
233 rules=[
234 V1PolicyRule(api_groups=api_groups, resources=resources, verbs=verbs),
235 ],
236 )
238 self.clients[RBAC_CLIENT].create_namespaced_role(namespace, role)
240 def delete_cluster_role(self, name: str):
241 """
242 Delete a cluster role
244 :param: name: Name of the cluster role
245 """
246 self.clients[RBAC_CLIENT].delete_cluster_role(name)
248 def _get_kubectl_version(self):
249 version = VersionApi().get_code()
250 return "{}.{}".format(version.major, version.minor)
252 def _need_to_create_new_secret(self):
253 min_k8s_version = "1.24"
254 current_k8s_version = self._get_kubectl_version()
255 return LooseVersion(min_k8s_version) <= LooseVersion(current_k8s_version)
257 def _get_secret_name(self, service_account_name: str):
258 random_alphanum = str(uuid.uuid4())[:5]
259 return "{}-token-{}".format(service_account_name, random_alphanum)
261 def _create_service_account_secret(
262 self,
263 service_account_name: str,
264 namespace: str,
265 secret_name: str,
266 ):
267 """
268 Create a secret for the service account. K8s version >= 1.24
270 :param: service_account_name: Name of the service account
271 :param: namespace: Kubernetes namespace for service account metadata
272 :param: secret_name: Name of the secret
273 """
274 v1_core = self.clients[CORE_CLIENT]
275 secrets = v1_core.list_namespaced_secret(
276 namespace, field_selector="metadata.name={}".format(secret_name)
277 ).items
279 if len(secrets) > 0:
280 raise Exception(
281 "Secret with metadata.name={} already exists".format(secret_name)
282 )
284 annotations = {"kubernetes.io/service-account.name": service_account_name}
285 metadata = V1ObjectMeta(
286 name=secret_name, namespace=namespace, annotations=annotations
287 )
288 type = "kubernetes.io/service-account-token"
289 secret = V1Secret(metadata=metadata, type=type)
290 v1_core.create_namespaced_secret(namespace, secret)
292 def _get_secret_reference_list(self, namespace: str, secret_name: str):
293 """
294 Return a secret reference list with one secret.
295 K8s version >= 1.24
297 :param: namespace: Kubernetes namespace for service account metadata
298 :param: secret_name: Name of the secret
299 :rtype: list[V1SecretReference]
300 """
301 return [V1SecretReference(name=secret_name, namespace=namespace)]
303 def create_service_account(
304 self,
305 name: str,
306 labels: Dict[str, str],
307 namespace: str = "kube-system",
308 ):
309 """
310 Create a service account
312 :param: name: Name of the service account
313 :param: labels: Labels for service account metadata
314 :param: namespace: Kubernetes namespace for service account metadata
315 Default: kube-system
316 """
317 v1_core = self.clients[CORE_CLIENT]
318 service_accounts = v1_core.list_namespaced_service_account(
319 namespace, field_selector="metadata.name={}".format(name)
320 )
321 if len(service_accounts.items) > 0:
322 raise Exception(
323 "Service account with metadata.name={} already exists".format(name)
324 )
326 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
328 if self._need_to_create_new_secret():
329 secret_name = self._get_secret_name(name)
330 secrets = self._get_secret_reference_list(namespace, secret_name)
331 service_account = V1ServiceAccount(metadata=metadata, secrets=secrets)
332 v1_core.create_namespaced_service_account(namespace, service_account)
333 self._create_service_account_secret(name, namespace, secret_name)
334 else:
335 service_account = V1ServiceAccount(metadata=metadata)
336 v1_core.create_namespaced_service_account(namespace, service_account)
338 def delete_secret(self, name: str, namespace: str = "kube-system"):
339 """
340 Delete a secret
342 :param: name: Name of the secret
343 :param: namespace: Kubernetes namespace
344 Default: kube-system
345 """
346 self.clients[CORE_CLIENT].delete_namespaced_secret(name, namespace)
348 def delete_service_account(self, name: str, namespace: str = "kube-system"):
349 """
350 Delete a service account
352 :param: name: Name of the service account
353 :param: namespace: Kubernetes namespace for service account metadata
354 Default: kube-system
355 """
356 self.clients[CORE_CLIENT].delete_namespaced_service_account(name, namespace)
358 def create_cluster_role_binding(
359 self, name: str, labels: Dict[str, str], namespace: str = "kube-system"
360 ):
361 """
362 Create a cluster role binding
364 :param: name: Name of the cluster role
365 :param: labels: Labels for cluster role binding metadata
366 :param: namespace: Kubernetes namespace for cluster role binding metadata
367 Default: kube-system
368 """
369 role_bindings = self.clients[RBAC_CLIENT].list_cluster_role_binding(
370 field_selector="metadata.name={}".format(name)
371 )
372 if len(role_bindings.items) > 0:
373 raise Exception("Generated rbac id already exists")
375 role_binding = V1ClusterRoleBinding(
376 metadata=V1ObjectMeta(name=name, labels=labels),
377 role_ref=V1RoleRef(kind="ClusterRole", name=name, api_group=""),
378 subjects=[
379 RbacV1Subject(kind="ServiceAccount", name=name, namespace=namespace)
380 ],
381 )
382 self.clients[RBAC_CLIENT].create_cluster_role_binding(role_binding)
384 async def create_role_binding(
385 self,
386 name: str,
387 role_name: str,
388 sa_name: str,
389 labels: Dict[str, str],
390 namespace: str,
391 ):
392 """
393 Create a cluster role binding
395 :param: name: Name of the namespaced Role Binding
396 :param: role_name: Name of the namespaced Role to be bound
397 :param: sa_name: Name of the Service Account to be bound
398 :param: labels: Labels for Role Binding metadata
399 :param: namespace: Kubernetes namespace for Role Binding metadata
401 :return: None
402 """
403 role_bindings = self.clients[RBAC_CLIENT].list_namespaced_role_binding(
404 namespace, field_selector="metadata.name={}".format(name)
405 )
406 if len(role_bindings.items) > 0:
407 raise Exception(
408 "Role Binding with metadata.name={} already exists".format(name)
409 )
411 role_binding = V1RoleBinding(
412 metadata=V1ObjectMeta(name=name, labels=labels),
413 role_ref=V1RoleRef(kind="Role", name=role_name, api_group=""),
414 subjects=[
415 RbacV1Subject(kind="ServiceAccount", name=sa_name, namespace=namespace)
416 ],
417 )
418 self.clients[RBAC_CLIENT].create_namespaced_role_binding(
419 namespace, role_binding
420 )
422 def delete_cluster_role_binding(self, name: str):
423 """
424 Delete a cluster role binding
426 :param: name: Name of the cluster role binding
427 """
428 self.clients[RBAC_CLIENT].delete_cluster_role_binding(name)
430 @retry(
431 attempts=10,
432 delay=1,
433 fallback=Exception("Failed getting the secret from service account"),
434 callback=retry_callback,
435 )
436 async def get_secret_data(
437 self, name: str, namespace: str = "kube-system"
438 ) -> (str, str):
439 """
440 Get secret data
442 :param: name: Name of the secret data
443 :param: namespace: Name of the namespace where the secret is stored
445 :return: Tuple with the token and client certificate
446 """
447 v1_core = self.clients[CORE_CLIENT]
449 secret_name = None
451 service_accounts = v1_core.list_namespaced_service_account(
452 namespace, field_selector="metadata.name={}".format(name)
453 )
454 if len(service_accounts.items) == 0:
455 raise Exception(
456 "Service account not found with metadata.name={}".format(name)
457 )
458 service_account = service_accounts.items[0]
459 if service_account.secrets and len(service_account.secrets) > 0:
460 secret_name = service_account.secrets[0].name
461 if not secret_name:
462 raise Exception(
463 "Failed getting the secret from service account {}".format(name)
464 )
465 # TODO: refactor to use get_secret_content
466 secret = v1_core.list_namespaced_secret(
467 namespace, field_selector="metadata.name={}".format(secret_name)
468 ).items[0]
470 token = secret.data[SERVICE_ACCOUNT_TOKEN_KEY]
471 client_certificate_data = secret.data[SERVICE_ACCOUNT_ROOT_CA_KEY]
473 return (
474 base64.b64decode(token).decode("utf-8"),
475 base64.b64decode(client_certificate_data).decode("utf-8"),
476 )
478 @retry(
479 attempts=10,
480 delay=1,
481 fallback=Exception("Failed getting data from the secret"),
482 )
483 async def get_secret_content(
484 self,
485 name: str,
486 namespace: str,
487 ) -> dict:
488 """
489 Get secret data
491 :param: name: Name of the secret
492 :param: namespace: Name of the namespace where the secret is stored
494 :return: Dictionary with secret's data
495 """
496 v1_core = self.clients[CORE_CLIENT]
498 secret = v1_core.read_namespaced_secret(name, namespace)
500 return secret.data
502 @retry(
503 attempts=10,
504 delay=1,
505 fallback=Exception("Failed creating the secret"),
506 )
507 async def create_secret(
508 self, name: str, data: dict, namespace: str, secret_type: str
509 ):
510 """
511 Create secret with data
513 :param: name: Name of the secret
514 :param: data: Dict with data content. Values must be already base64 encoded
515 :param: namespace: Name of the namespace where the secret will be stored
516 :param: secret_type: Type of the secret, e.g., Opaque, kubernetes.io/service-account-token, kubernetes.io/tls
518 :return: None
519 """
520 self.logger.info("Enter create_secret function")
521 v1_core = self.clients[CORE_CLIENT]
522 self.logger.info(f"v1_core: {v1_core}")
523 metadata = V1ObjectMeta(name=name, namespace=namespace)
524 self.logger.info(f"metadata: {metadata}")
525 secret = V1Secret(metadata=metadata, data=data, type=secret_type)
526 self.logger.info(f"secret: {secret}")
527 v1_core.create_namespaced_secret(namespace, secret)
528 self.logger.info("Namespaced secret was created")
530 async def create_certificate(
531 self,
532 namespace: str,
533 name: str,
534 dns_prefix: str,
535 secret_name: str,
536 usages: list,
537 issuer_name: str,
538 ):
539 """
540 Creates cert-manager certificate object
542 :param: namespace: Name of the namespace where the certificate and secret is stored
543 :param: name: Name of the certificate object
544 :param: dns_prefix: Prefix for the dnsNames. They will be prefixed to the common k8s svc suffixes
545 :param: secret_name: Name of the secret created by cert-manager
546 :param: usages: List of X.509 key usages
547 :param: issuer_name: Name of the cert-manager's Issuer or ClusterIssuer object
549 """
550 certificate_body = {
551 "apiVersion": "cert-manager.io/v1",
552 "kind": "Certificate",
553 "metadata": {"name": name, "namespace": namespace},
554 "spec": {
555 "secretName": secret_name,
556 "privateKey": {
557 "rotationPolicy": "Always",
558 "algorithm": "ECDSA",
559 "size": 256,
560 },
561 "duration": "8760h", # 1 Year
562 "renewBefore": "2208h", # 9 months
563 "subject": {"organizations": ["osm"]},
564 "commonName": "osm",
565 "isCA": False,
566 "usages": usages,
567 "dnsNames": [
568 "{}.{}".format(dns_prefix, namespace),
569 "{}.{}.svc".format(dns_prefix, namespace),
570 "{}.{}.svc.cluster".format(dns_prefix, namespace),
571 "{}.{}.svc.cluster.local".format(dns_prefix, namespace),
572 ],
573 "issuerRef": {"name": issuer_name, "kind": "ClusterIssuer"},
574 },
575 }
576 client = self.clients[CUSTOM_OBJECT_CLIENT]
577 try:
578 client.create_namespaced_custom_object(
579 group="cert-manager.io",
580 plural="certificates",
581 version="v1",
582 body=certificate_body,
583 namespace=namespace,
584 )
585 except ApiException as e:
586 info = json.loads(e.body)
587 if info.get("reason").lower() == "alreadyexists":
588 self.logger.warning("Certificate already exists: {}".format(e))
589 else:
590 raise e
592 async def delete_certificate(self, namespace, object_name):
593 client = self.clients[CUSTOM_OBJECT_CLIENT]
594 try:
595 client.delete_namespaced_custom_object(
596 group="cert-manager.io",
597 plural="certificates",
598 version="v1",
599 name=object_name,
600 namespace=namespace,
601 )
602 except ApiException as e:
603 info = json.loads(e.body)
604 if info.get("reason").lower() == "notfound":
605 self.logger.warning("Certificate already deleted: {}".format(e))
606 else:
607 raise e
609 @retry(
610 attempts=10,
611 delay=1,
612 fallback=Exception("Failed creating the namespace"),
613 )
614 async def create_namespace(self, name: str, labels: dict = None):
615 """
616 Create a namespace
618 :param: name: Name of the namespace to be created
619 :param: labels: Dictionary with labels for the new namespace
621 """
622 v1_core = self.clients[CORE_CLIENT]
623 metadata = V1ObjectMeta(name=name, labels=labels)
624 namespace = V1Namespace(
625 metadata=metadata,
626 )
628 try:
629 v1_core.create_namespace(namespace)
630 self.logger.debug("Namespace created: {}".format(name))
631 except ApiException as e:
632 info = json.loads(e.body)
633 if info.get("reason").lower() == "alreadyexists":
634 self.logger.warning("Namespace already exists: {}".format(e))
635 else:
636 raise e
638 @retry(
639 attempts=10,
640 delay=1,
641 fallback=Exception("Failed deleting the namespace"),
642 )
643 async def delete_namespace(self, name: str):
644 """
645 Delete a namespace
647 :param: name: Name of the namespace to be deleted
649 """
650 try:
651 self.clients[CORE_CLIENT].delete_namespace(name)
652 except ApiException as e:
653 if e.reason == "Not Found":
654 self.logger.warning("Namespace already deleted: {}".format(e))
656 def get_secrets(
657 self,
658 namespace: str,
659 field_selector: str = None,
660 ) -> typing.List[typing.Dict]:
661 """
662 Get Secret list from a namespace
664 :param: namespace: Kubernetes namespace
665 :param: field_selector: Kubernetes field selector
667 :return: List of the secrets matching the selectors specified
668 """
669 try:
670 v1_core = self.clients[CORE_CLIENT]
671 secrets = v1_core.list_namespaced_secret(
672 namespace=namespace,
673 field_selector=field_selector,
674 ).items
675 return secrets
676 except ApiException as e:
677 self.logger.error("Error calling get secrets: {}".format(e))
678 raise e
680 def create_generic_object(
681 self,
682 api_group: str,
683 api_plural: str,
684 api_version: str,
685 namespace: str,
686 manifest_dict: dict,
687 ):
688 """
689 Creates generic object
691 :param: api_group: API Group
692 :param: api_plural: API Plural
693 :param: api_version: API Version
694 :param: namespace: Namespace
695 :param: manifest_dict: Dictionary with the content of the Kubernetes manifest
697 """
698 client = self.clients[CUSTOM_OBJECT_CLIENT]
699 try:
700 if namespace:
701 client.create_namespaced_custom_object(
702 group=api_group,
703 plural=api_plural,
704 version=api_version,
705 body=manifest_dict,
706 namespace=namespace,
707 )
708 else:
709 client.create_cluster_custom_object(
710 group=api_group,
711 plural=api_plural,
712 version=api_version,
713 body=manifest_dict,
714 )
715 except ApiException as e:
716 info = json.loads(e.body)
717 if info.get("reason").lower() == "alreadyexists":
718 self.logger.warning("Object already exists: {}".format(e))
719 else:
720 raise e
722 def delete_generic_object(
723 self,
724 api_group: str,
725 api_plural: str,
726 api_version: str,
727 namespace: str,
728 name: str,
729 ):
730 """
731 Deletes generic object
733 :param: api_group: API Group
734 :param: api_plural: API Plural
735 :param: api_version: API Version
736 :param: namespace: Namespace
737 :param: name: Name of the object
739 """
740 client = self.clients[CUSTOM_OBJECT_CLIENT]
741 try:
742 if namespace:
743 client.delete_namespaced_custom_object(
744 group=api_group,
745 plural=api_plural,
746 version=api_version,
747 name=name,
748 namespace=namespace,
749 )
750 else:
751 client.delete_cluster_custom_object(
752 group=api_group,
753 plural=api_plural,
754 version=api_version,
755 name=name,
756 )
757 except ApiException as e:
758 info = json.loads(e.body)
759 if info.get("reason").lower() == "notfound":
760 self.logger.warning("Object already deleted: {}".format(e))
761 else:
762 raise e
764 async def get_generic_object(
765 self,
766 api_group: str,
767 api_plural: str,
768 api_version: str,
769 namespace: str,
770 name: str,
771 ):
772 """
773 Gets generic object
775 :param: api_group: API Group
776 :param: api_plural: API Plural
777 :param: api_version: API Version
778 :param: namespace: Namespace
779 :param: name: Name of the object
781 """
782 client = self.clients[CUSTOM_OBJECT_CLIENT]
783 try:
784 if namespace:
785 object_dict = client.list_namespaced_custom_object(
786 group=api_group,
787 plural=api_plural,
788 version=api_version,
789 namespace=namespace,
790 field_selector=f"metadata.name={name}",
791 )
792 else:
793 object_dict = client.list_cluster_custom_object(
794 group=api_group,
795 plural=api_plural,
796 version=api_version,
797 field_selector=f"metadata.name={name}",
798 )
799 if len(object_dict.get("items")) == 0:
800 return None
801 return object_dict.get("items")[0]
802 except ApiException as e:
803 self.logger.debug(f"Exception: {e}")
804 info = json.loads(e.body)
805 if info.get("reason").lower() == "notfound":
806 self.logger.warning("Cannot get custom object: {}".format(e))
807 return None
808 else:
809 raise e
811 async def list_generic_object(
812 self,
813 api_group: str,
814 api_plural: str,
815 api_version: str,
816 namespace: str,
817 ):
818 """
819 Lists all generic objects of the requested API group
821 :param: api_group: API Group
822 :param: api_plural: API Plural
823 :param: api_version: API Version
824 :param: namespace: Namespace
826 """
827 client = self.clients[CUSTOM_OBJECT_CLIENT]
828 try:
829 if namespace:
830 object_dict = client.list_namespaced_custom_object(
831 group=api_group,
832 plural=api_plural,
833 version=api_version,
834 namespace=namespace,
835 )
836 else:
837 object_dict = client.list_cluster_custom_object(
838 group=api_group,
839 plural=api_plural,
840 version=api_version,
841 )
842 self.logger.debug(f"Object-list: {object_dict.get('items')}")
843 return object_dict.get("items")
844 except ApiException as e:
845 self.logger.debug(f"Exception: {e}")
846 info = json.loads(e.body)
847 if info.get("reason").lower() == "notfound":
848 self.logger.warning(
849 "Cannot find specified custom objects: {}".format(e)
850 )
851 return []
852 else:
853 raise e
855 @retry(
856 attempts=10,
857 delay=1,
858 fallback=Exception("Failed creating the secret"),
859 )
860 async def create_secret_string(
861 self, name: str, string_data: str, namespace: str, secret_type: str
862 ):
863 """
864 Create secret with data
866 :param: name: Name of the secret
867 :param: string_data: String with data content
868 :param: namespace: Name of the namespace where the secret will be stored
869 :param: secret_type: Type of the secret, e.g., Opaque, kubernetes.io/service-account-token, kubernetes.io/tls
871 :return: None
872 """
873 v1_core = self.clients[CORE_CLIENT]
874 metadata = V1ObjectMeta(name=name, namespace=namespace)
875 secret = V1Secret(metadata=metadata, string_data=string_data, type=secret_type)
876 v1_core.create_namespaced_secret(namespace, secret)
878 @retry(
879 attempts=10,
880 delay=1,
881 fallback=Exception("Failed creating the pvc"),
882 )
883 async def create_pvc(self, name: str, namespace: str):
884 """
885 Create a namespace
887 :param: name: Name of the pvc to be created
888 :param: namespace: Name of the namespace where the pvc will be stored
890 """
891 try:
892 pvc = V1PersistentVolumeClaim(
893 api_version="v1",
894 kind="PersistentVolumeClaim",
895 metadata=V1ObjectMeta(name=name),
896 spec=V1PersistentVolumeClaimSpec(
897 access_modes=["ReadWriteOnce"],
898 resources=V1ResourceRequirements(requests={"storage": "100Mi"}),
899 ),
900 )
901 self.clients[CORE_CLIENT].create_namespaced_persistent_volume_claim(
902 namespace=namespace, body=pvc
903 )
904 except ApiException as e:
905 info = json.loads(e.body)
906 if info.get("reason").lower() == "alreadyexists":
907 self.logger.warning("PVC already exists: {}".format(e))
908 else:
909 raise e
911 @retry(
912 attempts=10,
913 delay=1,
914 fallback=Exception("Failed deleting the pvc"),
915 )
916 async def delete_pvc(self, name: str, namespace: str):
917 """
918 Create a namespace
920 :param: name: Name of the pvc to be deleted
921 :param: namespace: Namespace
923 """
924 self.clients[CORE_CLIENT].delete_namespaced_persistent_volume_claim(
925 name, namespace
926 )
928 def copy_file_to_pod(
929 self, namespace, pod_name, container_name, src_file, dest_path
930 ):
931 # Create an in-memory tar file containing the source file
932 tar_buffer = io.BytesIO()
933 with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
934 tar.add(src_file, arcname=dest_path.split("/")[-1])
936 tar_buffer.seek(0)
938 # Define the command to extract the tar file in the pod
939 exec_command = ["tar", "xvf", "-", "-C", dest_path.rsplit("/", 1)[0]]
941 # Execute the command
942 resp = stream(
943 self.clients[CORE_CLIENT].connect_get_namespaced_pod_exec,
944 pod_name,
945 namespace,
946 command=exec_command,
947 container=container_name,
948 stdin=True,
949 stderr=True,
950 stdout=True,
951 tty=False,
952 _preload_content=False,
953 )
955 # Write the tar data to the pod
956 resp.write_stdin(tar_buffer.read())
957 resp.close()
959 @retry(
960 attempts=10,
961 delay=1,
962 fallback=Exception("Failed creating the pvc"),
963 )
964 async def create_pvc_with_content(
965 self, name: str, namespace: str, src_file: str, dest_filename: str
966 ):
967 """
968 Create a PVC with content
970 :param: name: Name of the pvc to be created
971 :param: namespace: Name of the namespace where the pvc will be stored
972 :param: src_file: File to be copied
973 :param: filename: Name of the file in the destination folder
974 """
975 pod_name = f"copy-pod-{name}"
976 self.logger.debug(f"Creating pvc {name}")
977 await self.create_pvc(name=name, namespace=namespace)
978 self.logger.debug("Sleeping")
979 sleep(40)
980 self.logger.debug(f"Creating pod {pod_name}")
981 await self.create_copy_pod(name=pod_name, namespace=namespace, pvc_name=name)
982 self.logger.debug("Sleeping")
983 sleep(40)
984 self.logger.debug(f"Copying files to pod {pod_name}")
985 self.copy_file_to_pod(
986 namespace=namespace,
987 pod_name=pod_name,
988 container_name="copy-container",
989 src_file=src_file,
990 dest_path=f"/mnt/data/{dest_filename}",
991 )
993 @retry(
994 attempts=10,
995 delay=1,
996 fallback=Exception("Failed creating the pvc"),
997 )
998 async def create_copy_pod(self, name: str, namespace: str, pvc_name: str):
999 """
1000 Create a pod to copy content into a PVC
1002 :param: name: Name of the pod to be created
1003 :param: namespace: Name of the namespace where the pod will be stored
1004 :param: pvc_name: Name of the PVC that the pod will mount as a volume
1006 """
1007 pod = V1Pod(
1008 api_version="v1",
1009 kind="Pod",
1010 metadata=client.V1ObjectMeta(name=name),
1011 spec=V1PodSpec(
1012 containers=[
1013 V1Container(
1014 name="copy-container",
1015 image="busybox", # Imagen ligera para copiar archivos
1016 command=["sleep", "3600"], # Mantén el contenedor en ejecución
1017 volume_mounts=[
1018 V1VolumeMount(mount_path="/mnt/data", name="my-storage")
1019 ],
1020 )
1021 ],
1022 volumes=[
1023 V1Volume(
1024 name="my-storage",
1025 persistent_volume_claim=V1PersistentVolumeClaimVolumeSource(
1026 claim_name=pvc_name
1027 ),
1028 )
1029 ],
1030 ),
1031 )
1032 # Create the pod
1033 self.clients[CORE_CLIENT].create_namespaced_pod(namespace=namespace, body=pod)
1035 @retry(
1036 attempts=10,
1037 delay=1,
1038 fallback=Exception("Failed deleting the pod"),
1039 )
1040 async def delete_pod(self, name: str, namespace: str):
1041 """
1042 Create a namespace
1044 :param: name: Name of the pod to be deleted
1045 :param: namespace: Namespace
1047 """
1048 self.clients[CORE_CLIENT].delete_namespaced_pod(name, namespace)