blob: fec6bf43ba4698f069214b0a265d835512c15848 [file] [log] [blame]
garciadeblasc057eb32024-07-04 11:00:13 +02001#######################################################################################
David Garcia5d799392020-07-02 13:56:58 +02002# Copyright 2020 Canonical Ltd.
garciadeblasc057eb32024-07-04 11:00:13 +02003# Copyright ETSI Contributors and Others.
David Garcia5d799392020-07-02 13:56:58 +02004#
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.
garciadeblasc057eb32024-07-04 11:00:13 +020016#######################################################################################
David Garcia5d799392020-07-02 13:56:58 +020017
David Garciad8d4b6e2021-06-24 18:47:22 +020018import base64
David Garciaf6e9b002020-11-27 15:32:02 +010019import logging
David Garciad8d4b6e2021-06-24 18:47:22 +020020from typing import Dict
21import typing
Patricia Reinoso6343d432022-08-23 06:22:01 +000022import uuid
Gabriel Cubafb03e902022-10-07 11:40:03 -050023import json
garciadeblasc057eb32024-07-04 11:00:13 +020024import tarfile
25import io
garciadeblas5d8490b2024-09-26 18:30:01 +020026from time import sleep
David Garciad8d4b6e2021-06-24 18:47:22 +020027
Patricia Reinoso6343d432022-08-23 06:22:01 +000028from distutils.version import LooseVersion
David Garciaf6e9b002020-11-27 15:32:02 +010029
David Garcia5d799392020-07-02 13:56:58 +020030from kubernetes import client, config
Patricia Reinoso6343d432022-08-23 06:22:01 +000031from kubernetes.client.api import VersionApi
David Garciad8d4b6e2021-06-24 18:47:22 +020032from kubernetes.client.models import (
33 V1ClusterRole,
Gabriel Cuba5f069332023-04-25 19:26:19 -050034 V1Role,
David Garciad8d4b6e2021-06-24 18:47:22 +020035 V1ObjectMeta,
36 V1PolicyRule,
37 V1ServiceAccount,
38 V1ClusterRoleBinding,
Gabriel Cuba5f069332023-04-25 19:26:19 -050039 V1RoleBinding,
David Garciad8d4b6e2021-06-24 18:47:22 +020040 V1RoleRef,
garciadeblasfc12ea62024-08-07 02:35:10 +020041 RbacV1Subject,
Patricia Reinoso6343d432022-08-23 06:22:01 +000042 V1Secret,
43 V1SecretReference,
Gabriel Cuba5f069332023-04-25 19:26:19 -050044 V1Namespace,
garciadeblasc057eb32024-07-04 11:00:13 +020045 V1PersistentVolumeClaim,
46 V1PersistentVolumeClaimSpec,
47 V1PersistentVolumeClaimVolumeSource,
48 V1ResourceRequirements,
49 V1Pod,
50 V1PodSpec,
51 V1Volume,
52 V1VolumeMount,
53 V1Container,
David Garciad8d4b6e2021-06-24 18:47:22 +020054)
David Garcia5d799392020-07-02 13:56:58 +020055from kubernetes.client.rest import ApiException
garciadeblasc057eb32024-07-04 11:00:13 +020056from kubernetes.stream import stream
Mark Beierlfb797862023-05-18 22:21:06 -040057from n2vc.libjuju import retry_callback
David Garciad8d4b6e2021-06-24 18:47:22 +020058from retrying_async import retry
David Garciaf6e9b002020-11-27 15:32:02 +010059
David Garciad8d4b6e2021-06-24 18:47:22 +020060SERVICE_ACCOUNT_TOKEN_KEY = "token"
61SERVICE_ACCOUNT_ROOT_CA_KEY = "ca.crt"
62# clients
David Garciaf6e9b002020-11-27 15:32:02 +010063CORE_CLIENT = "core_v1"
David Garciaf6e9b002020-11-27 15:32:02 +010064RBAC_CLIENT = "rbac_v1"
David Garciad8d4b6e2021-06-24 18:47:22 +020065STORAGE_CLIENT = "storage_v1"
Gabriel Cubafb03e902022-10-07 11:40:03 -050066CUSTOM_OBJECT_CLIENT = "custom_object"
David Garcia5d799392020-07-02 13:56:58 +020067
68
69class Kubectl:
70 def __init__(self, config_file=None):
71 config.load_kube_config(config_file=config_file)
David Garciaf6e9b002020-11-27 15:32:02 +010072 self._clients = {
David Garciad8d4b6e2021-06-24 18:47:22 +020073 CORE_CLIENT: client.CoreV1Api(),
74 RBAC_CLIENT: client.RbacAuthorizationV1Api(),
75 STORAGE_CLIENT: client.StorageV1Api(),
Gabriel Cubafb03e902022-10-07 11:40:03 -050076 CUSTOM_OBJECT_CLIENT: client.CustomObjectsApi(),
David Garciaf6e9b002020-11-27 15:32:02 +010077 }
David Garciad8d4b6e2021-06-24 18:47:22 +020078 self._configuration = config.kube_config.Configuration.get_default_copy()
garciadeblasc057eb32024-07-04 11:00:13 +020079 self.logger = logging.getLogger("lcm.kubectl")
David Garcia5d799392020-07-02 13:56:58 +020080
David Garciaf6e9b002020-11-27 15:32:02 +010081 @property
82 def configuration(self):
83 return self._configuration
84
85 @property
86 def clients(self):
87 return self._clients
David Garcia475a7222020-09-21 16:19:15 +020088
David Garciad8d4b6e2021-06-24 18:47:22 +020089 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
96
97 :param: field_selector: Kubernetes field selector for the namespace
98 :param: label_selector: Kubernetes label selector for the namespace
99
100 :return: List of the services matching the selectors specified
101 """
David Garcia5d799392020-07-02 13:56:58 +0200102 kwargs = {}
103 if field_selector:
104 kwargs["field_selector"] = field_selector
105 if label_selector:
106 kwargs["label_selector"] = label_selector
David Garcia5d799392020-07-02 13:56:58 +0200107 try:
David Garciaf6e9b002020-11-27 15:32:02 +0100108 result = self.clients[CORE_CLIENT].list_service_for_all_namespaces(**kwargs)
David Garcia5d799392020-07-02 13:56:58 +0200109 return [
110 {
111 "name": i.metadata.name,
112 "cluster_ip": i.spec.cluster_ip,
113 "type": i.spec.type,
garciadeblasc057eb32024-07-04 11:00:13 +0200114 "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 ),
David Garcia5d799392020-07-02 13:56:58 +0200128 "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
David Garcia475a7222020-09-21 16:19:15 +0200137
138 def get_default_storage_class(self) -> str:
139 """
140 Default storage class
141
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 """
David Garciaf6e9b002020-11-27 15:32:02 +0100146 storage_classes = self.clients[STORAGE_CLIENT].list_storage_class()
David Garcia475a7222020-09-21 16:19:15 +0200147 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
garciadeblas979c54e2021-05-28 14:10:59 +0200157 annotations = sc.metadata.annotations or {}
David Garcia475a7222020-09-21 16:19:15 +0200158 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
David Garciad8d4b6e2021-06-24 18:47:22 +0200166
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
175
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 )
184
185 if len(cluster_roles.items) > 0:
Gabriel Cuba5f069332023-04-25 19:26:19 -0500186 raise Exception("Role with metadata.name={} already exists".format(name))
David Garciad8d4b6e2021-06-24 18:47:22 +0200187
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 )
197
198 self.clients[RBAC_CLIENT].create_cluster_role(cluster_role)
199
Gabriel Cuba5f069332023-04-25 19:26:19 -0500200 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
211
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
218
219 :return: None
220 """
221
222 roles = self.clients[RBAC_CLIENT].list_namespaced_role(
223 namespace, field_selector="metadata.name={}".format(name)
224 )
225
226 if len(roles.items) > 0:
227 raise Exception("Role with metadata.name={} already exists".format(name))
228
229 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
230
231 role = V1Role(
232 metadata=metadata,
233 rules=[
234 V1PolicyRule(api_groups=api_groups, resources=resources, verbs=verbs),
235 ],
236 )
237
238 self.clients[RBAC_CLIENT].create_namespaced_role(namespace, role)
239
David Garciad8d4b6e2021-06-24 18:47:22 +0200240 def delete_cluster_role(self, name: str):
241 """
242 Delete a cluster role
243
244 :param: name: Name of the cluster role
245 """
246 self.clients[RBAC_CLIENT].delete_cluster_role(name)
247
Patricia Reinoso6343d432022-08-23 06:22:01 +0000248 def _get_kubectl_version(self):
249 version = VersionApi().get_code()
250 return "{}.{}".format(version.major, version.minor)
251
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)
256
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)
260
261 def _create_service_account_secret(
garciadeblasc057eb32024-07-04 11:00:13 +0200262 self,
263 service_account_name: str,
264 namespace: str,
265 secret_name: str,
Patricia Reinoso6343d432022-08-23 06:22:01 +0000266 ):
267 """
268 Create a secret for the service account. K8s version >= 1.24
269
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
278
279 if len(secrets) > 0:
280 raise Exception(
281 "Secret with metadata.name={} already exists".format(secret_name)
282 )
283
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)
291
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
296
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)]
302
David Garciad8d4b6e2021-06-24 18:47:22 +0200303 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
311
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 """
Patricia Reinoso6343d432022-08-23 06:22:01 +0000317 v1_core = self.clients[CORE_CLIENT]
318 service_accounts = v1_core.list_namespaced_service_account(
David Garciad8d4b6e2021-06-24 18:47:22 +0200319 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 )
325
326 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
David Garciad8d4b6e2021-06-24 18:47:22 +0200327
Patricia Reinoso6343d432022-08-23 06:22:01 +0000328 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)
David Garciad8d4b6e2021-06-24 18:47:22 +0200337
garciadeblasc057eb32024-07-04 11:00:13 +0200338 def delete_secret(self, name: str, namespace: str = "kube-system"):
339 """
340 Delete a secret
341
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)
347
David Garciad8d4b6e2021-06-24 18:47:22 +0200348 def delete_service_account(self, name: str, namespace: str = "kube-system"):
349 """
350 Delete a service account
351
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)
357
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
363
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")
374
375 role_binding = V1ClusterRoleBinding(
376 metadata=V1ObjectMeta(name=name, labels=labels),
377 role_ref=V1RoleRef(kind="ClusterRole", name=name, api_group=""),
garciadeblasfc12ea62024-08-07 02:35:10 +0200378 subjects=[
379 RbacV1Subject(kind="ServiceAccount", name=name, namespace=namespace)
380 ],
David Garciad8d4b6e2021-06-24 18:47:22 +0200381 )
382 self.clients[RBAC_CLIENT].create_cluster_role_binding(role_binding)
383
Gabriel Cuba5f069332023-04-25 19:26:19 -0500384 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
394
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
400
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 )
410
411 role_binding = V1RoleBinding(
412 metadata=V1ObjectMeta(name=name, labels=labels),
413 role_ref=V1RoleRef(kind="Role", name=role_name, api_group=""),
414 subjects=[
garciadeblasfc12ea62024-08-07 02:35:10 +0200415 RbacV1Subject(kind="ServiceAccount", name=sa_name, namespace=namespace)
Gabriel Cuba5f069332023-04-25 19:26:19 -0500416 ],
417 )
418 self.clients[RBAC_CLIENT].create_namespaced_role_binding(
419 namespace, role_binding
420 )
421
David Garciad8d4b6e2021-06-24 18:47:22 +0200422 def delete_cluster_role_binding(self, name: str):
423 """
424 Delete a cluster role binding
425
426 :param: name: Name of the cluster role binding
427 """
428 self.clients[RBAC_CLIENT].delete_cluster_role_binding(name)
429
430 @retry(
431 attempts=10,
432 delay=1,
433 fallback=Exception("Failed getting the secret from service account"),
Mark Beierlfb797862023-05-18 22:21:06 -0400434 callback=retry_callback,
David Garciad8d4b6e2021-06-24 18:47:22 +0200435 )
David Garcia4ae527e2021-07-26 16:04:59 +0200436 async def get_secret_data(
437 self, name: str, namespace: str = "kube-system"
438 ) -> (str, str):
David Garciad8d4b6e2021-06-24 18:47:22 +0200439 """
440 Get secret data
441
David Garcia4ae527e2021-07-26 16:04:59 +0200442 :param: name: Name of the secret data
443 :param: namespace: Name of the namespace where the secret is stored
444
David Garciad8d4b6e2021-06-24 18:47:22 +0200445 :return: Tuple with the token and client certificate
446 """
447 v1_core = self.clients[CORE_CLIENT]
448
449 secret_name = None
450
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 )
Gabriel Cuba5f069332023-04-25 19:26:19 -0500465 # TODO: refactor to use get_secret_content
David Garciad8d4b6e2021-06-24 18:47:22 +0200466 secret = v1_core.list_namespaced_secret(
467 namespace, field_selector="metadata.name={}".format(secret_name)
468 ).items[0]
469
470 token = secret.data[SERVICE_ACCOUNT_TOKEN_KEY]
471 client_certificate_data = secret.data[SERVICE_ACCOUNT_ROOT_CA_KEY]
472
473 return (
474 base64.b64decode(token).decode("utf-8"),
475 base64.b64decode(client_certificate_data).decode("utf-8"),
476 )
Gabriel Cubafb03e902022-10-07 11:40:03 -0500477
Gabriel Cuba5f069332023-04-25 19:26:19 -0500478 @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
490
491 :param: name: Name of the secret
492 :param: namespace: Name of the namespace where the secret is stored
493
494 :return: Dictionary with secret's data
495 """
496 v1_core = self.clients[CORE_CLIENT]
497
498 secret = v1_core.read_namespaced_secret(name, namespace)
499
500 return secret.data
501
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 """
garciadeblasc057eb32024-07-04 11:00:13 +0200511 Create secret with data
Gabriel Cuba5f069332023-04-25 19:26:19 -0500512
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
517
518 :return: None
519 """
garciadeblasc057eb32024-07-04 11:00:13 +0200520 self.logger.info("Enter create_secret function")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500521 v1_core = self.clients[CORE_CLIENT]
garciadeblasc057eb32024-07-04 11:00:13 +0200522 self.logger.info(f"v1_core: {v1_core}")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500523 metadata = V1ObjectMeta(name=name, namespace=namespace)
garciadeblasc057eb32024-07-04 11:00:13 +0200524 self.logger.info(f"metadata: {metadata}")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500525 secret = V1Secret(metadata=metadata, data=data, type=secret_type)
garciadeblasc057eb32024-07-04 11:00:13 +0200526 self.logger.info(f"secret: {secret}")
garciadeblas8dec9a52025-10-01 15:38:57 +0200527 try:
528 v1_core.create_namespaced_secret(namespace, secret)
529 self.logger.info("Namespaced secret was created")
530 except ApiException as e:
531 self.logger.error(f"Failed to create namespaced secret: {e}")
532 raise
Gabriel Cuba5f069332023-04-25 19:26:19 -0500533
Gabriel Cubafb03e902022-10-07 11:40:03 -0500534 async def create_certificate(
535 self,
536 namespace: str,
537 name: str,
538 dns_prefix: str,
539 secret_name: str,
540 usages: list,
541 issuer_name: str,
542 ):
543 """
544 Creates cert-manager certificate object
545
546 :param: namespace: Name of the namespace where the certificate and secret is stored
547 :param: name: Name of the certificate object
548 :param: dns_prefix: Prefix for the dnsNames. They will be prefixed to the common k8s svc suffixes
549 :param: secret_name: Name of the secret created by cert-manager
550 :param: usages: List of X.509 key usages
551 :param: issuer_name: Name of the cert-manager's Issuer or ClusterIssuer object
552
553 """
554 certificate_body = {
555 "apiVersion": "cert-manager.io/v1",
556 "kind": "Certificate",
557 "metadata": {"name": name, "namespace": namespace},
558 "spec": {
559 "secretName": secret_name,
560 "privateKey": {
561 "rotationPolicy": "Always",
562 "algorithm": "ECDSA",
563 "size": 256,
564 },
565 "duration": "8760h", # 1 Year
566 "renewBefore": "2208h", # 9 months
567 "subject": {"organizations": ["osm"]},
568 "commonName": "osm",
569 "isCA": False,
570 "usages": usages,
571 "dnsNames": [
572 "{}.{}".format(dns_prefix, namespace),
573 "{}.{}.svc".format(dns_prefix, namespace),
574 "{}.{}.svc.cluster".format(dns_prefix, namespace),
575 "{}.{}.svc.cluster.local".format(dns_prefix, namespace),
576 ],
577 "issuerRef": {"name": issuer_name, "kind": "ClusterIssuer"},
578 },
579 }
580 client = self.clients[CUSTOM_OBJECT_CLIENT]
581 try:
582 client.create_namespaced_custom_object(
583 group="cert-manager.io",
584 plural="certificates",
585 version="v1",
586 body=certificate_body,
587 namespace=namespace,
588 )
589 except ApiException as e:
590 info = json.loads(e.body)
591 if info.get("reason").lower() == "alreadyexists":
592 self.logger.warning("Certificate already exists: {}".format(e))
593 else:
594 raise e
595
596 async def delete_certificate(self, namespace, object_name):
597 client = self.clients[CUSTOM_OBJECT_CLIENT]
598 try:
599 client.delete_namespaced_custom_object(
600 group="cert-manager.io",
601 plural="certificates",
602 version="v1",
603 name=object_name,
604 namespace=namespace,
605 )
606 except ApiException as e:
607 info = json.loads(e.body)
608 if info.get("reason").lower() == "notfound":
609 self.logger.warning("Certificate already deleted: {}".format(e))
610 else:
611 raise e
Gabriel Cuba5f069332023-04-25 19:26:19 -0500612
613 @retry(
614 attempts=10,
615 delay=1,
616 fallback=Exception("Failed creating the namespace"),
617 )
Gabriel Cubad21509c2023-05-17 01:30:15 -0500618 async def create_namespace(self, name: str, labels: dict = None):
Gabriel Cuba5f069332023-04-25 19:26:19 -0500619 """
620 Create a namespace
621
622 :param: name: Name of the namespace to be created
Gabriel Cubad21509c2023-05-17 01:30:15 -0500623 :param: labels: Dictionary with labels for the new namespace
Gabriel Cuba5f069332023-04-25 19:26:19 -0500624
625 """
626 v1_core = self.clients[CORE_CLIENT]
Gabriel Cubad21509c2023-05-17 01:30:15 -0500627 metadata = V1ObjectMeta(name=name, labels=labels)
Gabriel Cuba5f069332023-04-25 19:26:19 -0500628 namespace = V1Namespace(
629 metadata=metadata,
630 )
631
632 try:
633 v1_core.create_namespace(namespace)
634 self.logger.debug("Namespace created: {}".format(name))
635 except ApiException as e:
636 info = json.loads(e.body)
637 if info.get("reason").lower() == "alreadyexists":
638 self.logger.warning("Namespace already exists: {}".format(e))
639 else:
640 raise e
641
642 @retry(
643 attempts=10,
644 delay=1,
645 fallback=Exception("Failed deleting the namespace"),
646 )
647 async def delete_namespace(self, name: str):
648 """
649 Delete a namespace
650
651 :param: name: Name of the namespace to be deleted
652
653 """
654 try:
655 self.clients[CORE_CLIENT].delete_namespace(name)
656 except ApiException as e:
657 if e.reason == "Not Found":
658 self.logger.warning("Namespace already deleted: {}".format(e))
garciadeblasc057eb32024-07-04 11:00:13 +0200659
660 def get_secrets(
661 self,
662 namespace: str,
663 field_selector: str = None,
664 ) -> typing.List[typing.Dict]:
665 """
666 Get Secret list from a namespace
667
668 :param: namespace: Kubernetes namespace
669 :param: field_selector: Kubernetes field selector
670
671 :return: List of the secrets matching the selectors specified
672 """
673 try:
674 v1_core = self.clients[CORE_CLIENT]
675 secrets = v1_core.list_namespaced_secret(
676 namespace=namespace,
677 field_selector=field_selector,
678 ).items
679 return secrets
680 except ApiException as e:
681 self.logger.error("Error calling get secrets: {}".format(e))
682 raise e
683
garciadeblasc1d37d02024-08-22 09:59:05 +0200684 def create_generic_object(
garciadeblasc057eb32024-07-04 11:00:13 +0200685 self,
686 api_group: str,
687 api_plural: str,
688 api_version: str,
689 namespace: str,
690 manifest_dict: dict,
691 ):
692 """
693 Creates generic object
694
695 :param: api_group: API Group
696 :param: api_plural: API Plural
697 :param: api_version: API Version
698 :param: namespace: Namespace
699 :param: manifest_dict: Dictionary with the content of the Kubernetes manifest
700
701 """
702 client = self.clients[CUSTOM_OBJECT_CLIENT]
703 try:
garciadeblasa43217d2024-10-22 10:00:49 +0200704 if namespace:
705 client.create_namespaced_custom_object(
706 group=api_group,
707 plural=api_plural,
708 version=api_version,
709 body=manifest_dict,
710 namespace=namespace,
711 )
712 else:
713 client.create_cluster_custom_object(
714 group=api_group,
715 plural=api_plural,
716 version=api_version,
717 body=manifest_dict,
718 )
garciadeblasc057eb32024-07-04 11:00:13 +0200719 except ApiException as e:
720 info = json.loads(e.body)
721 if info.get("reason").lower() == "alreadyexists":
722 self.logger.warning("Object already exists: {}".format(e))
723 else:
724 raise e
725
garciadeblasc1d37d02024-08-22 09:59:05 +0200726 def delete_generic_object(
garciadeblasc057eb32024-07-04 11:00:13 +0200727 self,
728 api_group: str,
729 api_plural: str,
730 api_version: str,
731 namespace: str,
732 name: str,
733 ):
734 """
735 Deletes generic object
736
737 :param: api_group: API Group
738 :param: api_plural: API Plural
739 :param: api_version: API Version
740 :param: namespace: Namespace
741 :param: name: Name of the object
742
743 """
744 client = self.clients[CUSTOM_OBJECT_CLIENT]
745 try:
garciadeblasa43217d2024-10-22 10:00:49 +0200746 if namespace:
747 client.delete_namespaced_custom_object(
748 group=api_group,
749 plural=api_plural,
750 version=api_version,
751 name=name,
752 namespace=namespace,
753 )
754 else:
755 client.delete_cluster_custom_object(
756 group=api_group,
757 plural=api_plural,
758 version=api_version,
759 name=name,
760 )
garciadeblasc057eb32024-07-04 11:00:13 +0200761 except ApiException as e:
762 info = json.loads(e.body)
763 if info.get("reason").lower() == "notfound":
764 self.logger.warning("Object already deleted: {}".format(e))
765 else:
766 raise e
767
768 async def get_generic_object(
769 self,
770 api_group: str,
771 api_plural: str,
772 api_version: str,
773 namespace: str,
774 name: str,
775 ):
776 """
777 Gets generic object
778
779 :param: api_group: API Group
780 :param: api_plural: API Plural
781 :param: api_version: API Version
782 :param: namespace: Namespace
783 :param: name: Name of the object
784
785 """
786 client = self.clients[CUSTOM_OBJECT_CLIENT]
787 try:
garciadeblasa43217d2024-10-22 10:00:49 +0200788 if namespace:
789 object_dict = client.list_namespaced_custom_object(
790 group=api_group,
791 plural=api_plural,
792 version=api_version,
793 namespace=namespace,
794 field_selector=f"metadata.name={name}",
795 )
796 else:
797 object_dict = client.list_cluster_custom_object(
798 group=api_group,
799 plural=api_plural,
800 version=api_version,
801 field_selector=f"metadata.name={name}",
802 )
garciadeblasc057eb32024-07-04 11:00:13 +0200803 if len(object_dict.get("items")) == 0:
804 return None
805 return object_dict.get("items")[0]
806 except ApiException as e:
garciadeblasa43217d2024-10-22 10:00:49 +0200807 self.logger.debug(f"Exception: {e}")
garciadeblasc057eb32024-07-04 11:00:13 +0200808 info = json.loads(e.body)
809 if info.get("reason").lower() == "notfound":
810 self.logger.warning("Cannot get custom object: {}".format(e))
garciadeblasa43217d2024-10-22 10:00:49 +0200811 return None
garciadeblasc057eb32024-07-04 11:00:13 +0200812 else:
813 raise e
814
815 async def list_generic_object(
816 self,
817 api_group: str,
818 api_plural: str,
819 api_version: str,
820 namespace: str,
821 ):
822 """
823 Lists all generic objects of the requested API group
824
825 :param: api_group: API Group
826 :param: api_plural: API Plural
827 :param: api_version: API Version
828 :param: namespace: Namespace
829
830 """
831 client = self.clients[CUSTOM_OBJECT_CLIENT]
832 try:
garciadeblasa43217d2024-10-22 10:00:49 +0200833 if namespace:
834 object_dict = client.list_namespaced_custom_object(
835 group=api_group,
836 plural=api_plural,
837 version=api_version,
838 namespace=namespace,
839 )
840 else:
841 object_dict = client.list_cluster_custom_object(
842 group=api_group,
843 plural=api_plural,
844 version=api_version,
845 )
garciadeblasc057eb32024-07-04 11:00:13 +0200846 self.logger.debug(f"Object-list: {object_dict.get('items')}")
847 return object_dict.get("items")
848 except ApiException as e:
garciadeblasa43217d2024-10-22 10:00:49 +0200849 self.logger.debug(f"Exception: {e}")
garciadeblasc057eb32024-07-04 11:00:13 +0200850 info = json.loads(e.body)
851 if info.get("reason").lower() == "notfound":
852 self.logger.warning(
garciadeblasa43217d2024-10-22 10:00:49 +0200853 "Cannot find specified custom objects: {}".format(e)
garciadeblasc057eb32024-07-04 11:00:13 +0200854 )
garciadeblasa43217d2024-10-22 10:00:49 +0200855 return []
garciadeblasc057eb32024-07-04 11:00:13 +0200856 else:
857 raise e
858
859 @retry(
860 attempts=10,
861 delay=1,
862 fallback=Exception("Failed creating the secret"),
863 )
864 async def create_secret_string(
865 self, name: str, string_data: str, namespace: str, secret_type: str
866 ):
867 """
868 Create secret with data
869
870 :param: name: Name of the secret
871 :param: string_data: String with data content
872 :param: namespace: Name of the namespace where the secret will be stored
873 :param: secret_type: Type of the secret, e.g., Opaque, kubernetes.io/service-account-token, kubernetes.io/tls
874
875 :return: None
876 """
877 v1_core = self.clients[CORE_CLIENT]
878 metadata = V1ObjectMeta(name=name, namespace=namespace)
879 secret = V1Secret(metadata=metadata, string_data=string_data, type=secret_type)
880 v1_core.create_namespaced_secret(namespace, secret)
881
882 @retry(
883 attempts=10,
884 delay=1,
885 fallback=Exception("Failed creating the pvc"),
886 )
887 async def create_pvc(self, name: str, namespace: str):
888 """
889 Create a namespace
890
891 :param: name: Name of the pvc to be created
892 :param: namespace: Name of the namespace where the pvc will be stored
893
894 """
895 try:
896 pvc = V1PersistentVolumeClaim(
897 api_version="v1",
898 kind="PersistentVolumeClaim",
899 metadata=V1ObjectMeta(name=name),
900 spec=V1PersistentVolumeClaimSpec(
901 access_modes=["ReadWriteOnce"],
902 resources=V1ResourceRequirements(requests={"storage": "100Mi"}),
903 ),
904 )
905 self.clients[CORE_CLIENT].create_namespaced_persistent_volume_claim(
906 namespace=namespace, body=pvc
907 )
908 except ApiException as e:
909 info = json.loads(e.body)
910 if info.get("reason").lower() == "alreadyexists":
911 self.logger.warning("PVC already exists: {}".format(e))
912 else:
913 raise e
914
915 @retry(
916 attempts=10,
917 delay=1,
918 fallback=Exception("Failed deleting the pvc"),
919 )
920 async def delete_pvc(self, name: str, namespace: str):
921 """
922 Create a namespace
923
924 :param: name: Name of the pvc to be deleted
925 :param: namespace: Namespace
926
927 """
928 self.clients[CORE_CLIENT].delete_namespaced_persistent_volume_claim(
929 name, namespace
930 )
931
932 def copy_file_to_pod(
933 self, namespace, pod_name, container_name, src_file, dest_path
934 ):
935 # Create an in-memory tar file containing the source file
936 tar_buffer = io.BytesIO()
937 with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
938 tar.add(src_file, arcname=dest_path.split("/")[-1])
939
940 tar_buffer.seek(0)
941
942 # Define the command to extract the tar file in the pod
943 exec_command = ["tar", "xvf", "-", "-C", dest_path.rsplit("/", 1)[0]]
944
945 # Execute the command
946 resp = stream(
947 self.clients[CORE_CLIENT].connect_get_namespaced_pod_exec,
948 pod_name,
949 namespace,
950 command=exec_command,
951 container=container_name,
952 stdin=True,
953 stderr=True,
954 stdout=True,
955 tty=False,
956 _preload_content=False,
957 )
958
959 # Write the tar data to the pod
960 resp.write_stdin(tar_buffer.read())
961 resp.close()
962
963 @retry(
964 attempts=10,
965 delay=1,
966 fallback=Exception("Failed creating the pvc"),
967 )
968 async def create_pvc_with_content(
garciadeblasf2912152024-09-24 14:41:40 +0200969 self, name: str, namespace: str, src_file: str, dest_filename: str
garciadeblasc057eb32024-07-04 11:00:13 +0200970 ):
971 """
972 Create a PVC with content
973
974 :param: name: Name of the pvc to be created
975 :param: namespace: Name of the namespace where the pvc will be stored
garciadeblasf2912152024-09-24 14:41:40 +0200976 :param: src_file: File to be copied
977 :param: filename: Name of the file in the destination folder
garciadeblasc057eb32024-07-04 11:00:13 +0200978 """
979 pod_name = f"copy-pod-{name}"
garciadeblas5d8490b2024-09-26 18:30:01 +0200980 self.logger.debug(f"Creating pvc {name}")
garciadeblasc057eb32024-07-04 11:00:13 +0200981 await self.create_pvc(name=name, namespace=namespace)
garciadeblas5d8490b2024-09-26 18:30:01 +0200982 self.logger.debug("Sleeping")
983 sleep(40)
984 self.logger.debug(f"Creating pod {pod_name}")
garciadeblasc057eb32024-07-04 11:00:13 +0200985 await self.create_copy_pod(name=pod_name, namespace=namespace, pvc_name=name)
garciadeblas5d8490b2024-09-26 18:30:01 +0200986 self.logger.debug("Sleeping")
987 sleep(40)
988 self.logger.debug(f"Copying files to pod {pod_name}")
garciadeblasc057eb32024-07-04 11:00:13 +0200989 self.copy_file_to_pod(
990 namespace=namespace,
991 pod_name=pod_name,
992 container_name="copy-container",
garciadeblasf2912152024-09-24 14:41:40 +0200993 src_file=src_file,
994 dest_path=f"/mnt/data/{dest_filename}",
garciadeblasc057eb32024-07-04 11:00:13 +0200995 )
996
997 @retry(
998 attempts=10,
999 delay=1,
1000 fallback=Exception("Failed creating the pvc"),
1001 )
1002 async def create_copy_pod(self, name: str, namespace: str, pvc_name: str):
1003 """
1004 Create a pod to copy content into a PVC
1005
1006 :param: name: Name of the pod to be created
1007 :param: namespace: Name of the namespace where the pod will be stored
1008 :param: pvc_name: Name of the PVC that the pod will mount as a volume
1009
1010 """
1011 pod = V1Pod(
1012 api_version="v1",
1013 kind="Pod",
1014 metadata=client.V1ObjectMeta(name=name),
1015 spec=V1PodSpec(
1016 containers=[
1017 V1Container(
1018 name="copy-container",
1019 image="busybox", # Imagen ligera para copiar archivos
1020 command=["sleep", "3600"], # Mantén el contenedor en ejecución
1021 volume_mounts=[
1022 V1VolumeMount(mount_path="/mnt/data", name="my-storage")
1023 ],
1024 )
1025 ],
1026 volumes=[
1027 V1Volume(
1028 name="my-storage",
1029 persistent_volume_claim=V1PersistentVolumeClaimVolumeSource(
1030 claim_name=pvc_name
1031 ),
1032 )
1033 ],
1034 ),
1035 )
1036 # Create the pod
1037 self.clients[CORE_CLIENT].create_namespaced_pod(namespace=namespace, body=pod)
1038
1039 @retry(
1040 attempts=10,
1041 delay=1,
1042 fallback=Exception("Failed deleting the pod"),
1043 )
1044 async def delete_pod(self, name: str, namespace: str):
1045 """
1046 Create a namespace
1047
1048 :param: name: Name of the pod to be deleted
1049 :param: namespace: Namespace
1050
1051 """
1052 self.clients[CORE_CLIENT].delete_namespaced_pod(name, namespace)