blob: 7e15269e839548e546182e94efd5dcfdce7c62a2 [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
David Garciad8d4b6e2021-06-24 18:47:22 +020026
Patricia Reinoso6343d432022-08-23 06:22:01 +000027from distutils.version import LooseVersion
David Garciaf6e9b002020-11-27 15:32:02 +010028
David Garcia5d799392020-07-02 13:56:58 +020029from kubernetes import client, config
Patricia Reinoso6343d432022-08-23 06:22:01 +000030from kubernetes.client.api import VersionApi
David Garciad8d4b6e2021-06-24 18:47:22 +020031from kubernetes.client.models import (
32 V1ClusterRole,
Gabriel Cuba5f069332023-04-25 19:26:19 -050033 V1Role,
David Garciad8d4b6e2021-06-24 18:47:22 +020034 V1ObjectMeta,
35 V1PolicyRule,
36 V1ServiceAccount,
37 V1ClusterRoleBinding,
Gabriel Cuba5f069332023-04-25 19:26:19 -050038 V1RoleBinding,
David Garciad8d4b6e2021-06-24 18:47:22 +020039 V1RoleRef,
garciadeblasfc12ea62024-08-07 02:35:10 +020040 RbacV1Subject,
Patricia Reinoso6343d432022-08-23 06:22:01 +000041 V1Secret,
42 V1SecretReference,
Gabriel Cuba5f069332023-04-25 19:26:19 -050043 V1Namespace,
garciadeblasc057eb32024-07-04 11:00:13 +020044 V1PersistentVolumeClaim,
45 V1PersistentVolumeClaimSpec,
46 V1PersistentVolumeClaimVolumeSource,
47 V1ResourceRequirements,
48 V1Pod,
49 V1PodSpec,
50 V1Volume,
51 V1VolumeMount,
52 V1Container,
David Garciad8d4b6e2021-06-24 18:47:22 +020053)
David Garcia5d799392020-07-02 13:56:58 +020054from kubernetes.client.rest import ApiException
garciadeblasc057eb32024-07-04 11:00:13 +020055from kubernetes.stream import stream
Mark Beierlfb797862023-05-18 22:21:06 -040056from n2vc.libjuju import retry_callback
David Garciad8d4b6e2021-06-24 18:47:22 +020057from retrying_async import retry
David Garciaf6e9b002020-11-27 15:32:02 +010058
David Garciad8d4b6e2021-06-24 18:47:22 +020059SERVICE_ACCOUNT_TOKEN_KEY = "token"
60SERVICE_ACCOUNT_ROOT_CA_KEY = "ca.crt"
61# clients
David Garciaf6e9b002020-11-27 15:32:02 +010062CORE_CLIENT = "core_v1"
David Garciaf6e9b002020-11-27 15:32:02 +010063RBAC_CLIENT = "rbac_v1"
David Garciad8d4b6e2021-06-24 18:47:22 +020064STORAGE_CLIENT = "storage_v1"
Gabriel Cubafb03e902022-10-07 11:40:03 -050065CUSTOM_OBJECT_CLIENT = "custom_object"
David Garcia5d799392020-07-02 13:56:58 +020066
67
68class Kubectl:
69 def __init__(self, config_file=None):
70 config.load_kube_config(config_file=config_file)
David Garciaf6e9b002020-11-27 15:32:02 +010071 self._clients = {
David Garciad8d4b6e2021-06-24 18:47:22 +020072 CORE_CLIENT: client.CoreV1Api(),
73 RBAC_CLIENT: client.RbacAuthorizationV1Api(),
74 STORAGE_CLIENT: client.StorageV1Api(),
Gabriel Cubafb03e902022-10-07 11:40:03 -050075 CUSTOM_OBJECT_CLIENT: client.CustomObjectsApi(),
David Garciaf6e9b002020-11-27 15:32:02 +010076 }
David Garciad8d4b6e2021-06-24 18:47:22 +020077 self._configuration = config.kube_config.Configuration.get_default_copy()
garciadeblasc057eb32024-07-04 11:00:13 +020078 self.logger = logging.getLogger("lcm.kubectl")
David Garcia5d799392020-07-02 13:56:58 +020079
David Garciaf6e9b002020-11-27 15:32:02 +010080 @property
81 def configuration(self):
82 return self._configuration
83
84 @property
85 def clients(self):
86 return self._clients
David Garcia475a7222020-09-21 16:19:15 +020087
David Garciad8d4b6e2021-06-24 18:47:22 +020088 def get_services(
89 self,
90 field_selector: str = None,
91 label_selector: str = None,
92 ) -> typing.List[typing.Dict]:
93 """
94 Get Service list from a namespace
95
96 :param: field_selector: Kubernetes field selector for the namespace
97 :param: label_selector: Kubernetes label selector for the namespace
98
99 :return: List of the services matching the selectors specified
100 """
David Garcia5d799392020-07-02 13:56:58 +0200101 kwargs = {}
102 if field_selector:
103 kwargs["field_selector"] = field_selector
104 if label_selector:
105 kwargs["label_selector"] = label_selector
David Garcia5d799392020-07-02 13:56:58 +0200106 try:
David Garciaf6e9b002020-11-27 15:32:02 +0100107 result = self.clients[CORE_CLIENT].list_service_for_all_namespaces(**kwargs)
David Garcia5d799392020-07-02 13:56:58 +0200108 return [
109 {
110 "name": i.metadata.name,
111 "cluster_ip": i.spec.cluster_ip,
112 "type": i.spec.type,
garciadeblasc057eb32024-07-04 11:00:13 +0200113 "ports": (
114 [
115 {
116 "name": p.name,
117 "node_port": p.node_port,
118 "port": p.port,
119 "protocol": p.protocol,
120 "target_port": p.target_port,
121 }
122 for p in i.spec.ports
123 ]
124 if i.spec.ports
125 else []
126 ),
David Garcia5d799392020-07-02 13:56:58 +0200127 "external_ip": [i.ip for i in i.status.load_balancer.ingress]
128 if i.status.load_balancer.ingress
129 else None,
130 }
131 for i in result.items
132 ]
133 except ApiException as e:
134 self.logger.error("Error calling get services: {}".format(e))
135 raise e
David Garcia475a7222020-09-21 16:19:15 +0200136
137 def get_default_storage_class(self) -> str:
138 """
139 Default storage class
140
141 :return: Returns the default storage class name, if exists.
142 If not, it returns the first storage class.
143 If there are not storage classes, returns None
144 """
David Garciaf6e9b002020-11-27 15:32:02 +0100145 storage_classes = self.clients[STORAGE_CLIENT].list_storage_class()
David Garcia475a7222020-09-21 16:19:15 +0200146 selected_sc = None
147 default_sc_annotations = {
148 "storageclass.kubernetes.io/is-default-class": "true",
149 # Older clusters still use the beta annotation.
150 "storageclass.beta.kubernetes.io/is-default-class": "true",
151 }
152 for sc in storage_classes.items:
153 if not selected_sc:
154 # Select the first storage class in case there is no a default-class
155 selected_sc = sc.metadata.name
garciadeblas979c54e2021-05-28 14:10:59 +0200156 annotations = sc.metadata.annotations or {}
David Garcia475a7222020-09-21 16:19:15 +0200157 if any(
158 k in annotations and annotations[k] == v
159 for k, v in default_sc_annotations.items()
160 ):
161 # Default storage
162 selected_sc = sc.metadata.name
163 break
164 return selected_sc
David Garciad8d4b6e2021-06-24 18:47:22 +0200165
166 def create_cluster_role(
167 self,
168 name: str,
169 labels: Dict[str, str],
170 namespace: str = "kube-system",
171 ):
172 """
173 Create a cluster role
174
175 :param: name: Name of the cluster role
176 :param: labels: Labels for cluster role metadata
177 :param: namespace: Kubernetes namespace for cluster role metadata
178 Default: kube-system
179 """
180 cluster_roles = self.clients[RBAC_CLIENT].list_cluster_role(
181 field_selector="metadata.name={}".format(name)
182 )
183
184 if len(cluster_roles.items) > 0:
Gabriel Cuba5f069332023-04-25 19:26:19 -0500185 raise Exception("Role with metadata.name={} already exists".format(name))
David Garciad8d4b6e2021-06-24 18:47:22 +0200186
187 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
188 # Cluster role
189 cluster_role = V1ClusterRole(
190 metadata=metadata,
191 rules=[
192 V1PolicyRule(api_groups=["*"], resources=["*"], verbs=["*"]),
193 V1PolicyRule(non_resource_ur_ls=["*"], verbs=["*"]),
194 ],
195 )
196
197 self.clients[RBAC_CLIENT].create_cluster_role(cluster_role)
198
Gabriel Cuba5f069332023-04-25 19:26:19 -0500199 async def create_role(
200 self,
201 name: str,
202 labels: Dict[str, str],
203 api_groups: list,
204 resources: list,
205 verbs: list,
206 namespace: str,
207 ):
208 """
209 Create a role with one PolicyRule
210
211 :param: name: Name of the namespaced Role
212 :param: labels: Labels for namespaced Role metadata
213 :param: api_groups: List with api-groups allowed in the policy rule
214 :param: resources: List with resources allowed in the policy rule
215 :param: verbs: List with verbs allowed in the policy rule
216 :param: namespace: Kubernetes namespace for Role metadata
217
218 :return: None
219 """
220
221 roles = self.clients[RBAC_CLIENT].list_namespaced_role(
222 namespace, field_selector="metadata.name={}".format(name)
223 )
224
225 if len(roles.items) > 0:
226 raise Exception("Role with metadata.name={} already exists".format(name))
227
228 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
229
230 role = V1Role(
231 metadata=metadata,
232 rules=[
233 V1PolicyRule(api_groups=api_groups, resources=resources, verbs=verbs),
234 ],
235 )
236
237 self.clients[RBAC_CLIENT].create_namespaced_role(namespace, role)
238
David Garciad8d4b6e2021-06-24 18:47:22 +0200239 def delete_cluster_role(self, name: str):
240 """
241 Delete a cluster role
242
243 :param: name: Name of the cluster role
244 """
245 self.clients[RBAC_CLIENT].delete_cluster_role(name)
246
Patricia Reinoso6343d432022-08-23 06:22:01 +0000247 def _get_kubectl_version(self):
248 version = VersionApi().get_code()
249 return "{}.{}".format(version.major, version.minor)
250
251 def _need_to_create_new_secret(self):
252 min_k8s_version = "1.24"
253 current_k8s_version = self._get_kubectl_version()
254 return LooseVersion(min_k8s_version) <= LooseVersion(current_k8s_version)
255
256 def _get_secret_name(self, service_account_name: str):
257 random_alphanum = str(uuid.uuid4())[:5]
258 return "{}-token-{}".format(service_account_name, random_alphanum)
259
260 def _create_service_account_secret(
garciadeblasc057eb32024-07-04 11:00:13 +0200261 self,
262 service_account_name: str,
263 namespace: str,
264 secret_name: str,
Patricia Reinoso6343d432022-08-23 06:22:01 +0000265 ):
266 """
267 Create a secret for the service account. K8s version >= 1.24
268
269 :param: service_account_name: Name of the service account
270 :param: namespace: Kubernetes namespace for service account metadata
271 :param: secret_name: Name of the secret
272 """
273 v1_core = self.clients[CORE_CLIENT]
274 secrets = v1_core.list_namespaced_secret(
275 namespace, field_selector="metadata.name={}".format(secret_name)
276 ).items
277
278 if len(secrets) > 0:
279 raise Exception(
280 "Secret with metadata.name={} already exists".format(secret_name)
281 )
282
283 annotations = {"kubernetes.io/service-account.name": service_account_name}
284 metadata = V1ObjectMeta(
285 name=secret_name, namespace=namespace, annotations=annotations
286 )
287 type = "kubernetes.io/service-account-token"
288 secret = V1Secret(metadata=metadata, type=type)
289 v1_core.create_namespaced_secret(namespace, secret)
290
291 def _get_secret_reference_list(self, namespace: str, secret_name: str):
292 """
293 Return a secret reference list with one secret.
294 K8s version >= 1.24
295
296 :param: namespace: Kubernetes namespace for service account metadata
297 :param: secret_name: Name of the secret
298 :rtype: list[V1SecretReference]
299 """
300 return [V1SecretReference(name=secret_name, namespace=namespace)]
301
David Garciad8d4b6e2021-06-24 18:47:22 +0200302 def create_service_account(
303 self,
304 name: str,
305 labels: Dict[str, str],
306 namespace: str = "kube-system",
307 ):
308 """
309 Create a service account
310
311 :param: name: Name of the service account
312 :param: labels: Labels for service account metadata
313 :param: namespace: Kubernetes namespace for service account metadata
314 Default: kube-system
315 """
Patricia Reinoso6343d432022-08-23 06:22:01 +0000316 v1_core = self.clients[CORE_CLIENT]
317 service_accounts = v1_core.list_namespaced_service_account(
David Garciad8d4b6e2021-06-24 18:47:22 +0200318 namespace, field_selector="metadata.name={}".format(name)
319 )
320 if len(service_accounts.items) > 0:
321 raise Exception(
322 "Service account with metadata.name={} already exists".format(name)
323 )
324
325 metadata = V1ObjectMeta(name=name, labels=labels, namespace=namespace)
David Garciad8d4b6e2021-06-24 18:47:22 +0200326
Patricia Reinoso6343d432022-08-23 06:22:01 +0000327 if self._need_to_create_new_secret():
328 secret_name = self._get_secret_name(name)
329 secrets = self._get_secret_reference_list(namespace, secret_name)
330 service_account = V1ServiceAccount(metadata=metadata, secrets=secrets)
331 v1_core.create_namespaced_service_account(namespace, service_account)
332 self._create_service_account_secret(name, namespace, secret_name)
333 else:
334 service_account = V1ServiceAccount(metadata=metadata)
335 v1_core.create_namespaced_service_account(namespace, service_account)
David Garciad8d4b6e2021-06-24 18:47:22 +0200336
garciadeblasc057eb32024-07-04 11:00:13 +0200337 def delete_secret(self, name: str, namespace: str = "kube-system"):
338 """
339 Delete a secret
340
341 :param: name: Name of the secret
342 :param: namespace: Kubernetes namespace
343 Default: kube-system
344 """
345 self.clients[CORE_CLIENT].delete_namespaced_secret(name, namespace)
346
David Garciad8d4b6e2021-06-24 18:47:22 +0200347 def delete_service_account(self, name: str, namespace: str = "kube-system"):
348 """
349 Delete a service account
350
351 :param: name: Name of the service account
352 :param: namespace: Kubernetes namespace for service account metadata
353 Default: kube-system
354 """
355 self.clients[CORE_CLIENT].delete_namespaced_service_account(name, namespace)
356
357 def create_cluster_role_binding(
358 self, name: str, labels: Dict[str, str], namespace: str = "kube-system"
359 ):
360 """
361 Create a cluster role binding
362
363 :param: name: Name of the cluster role
364 :param: labels: Labels for cluster role binding metadata
365 :param: namespace: Kubernetes namespace for cluster role binding metadata
366 Default: kube-system
367 """
368 role_bindings = self.clients[RBAC_CLIENT].list_cluster_role_binding(
369 field_selector="metadata.name={}".format(name)
370 )
371 if len(role_bindings.items) > 0:
372 raise Exception("Generated rbac id already exists")
373
374 role_binding = V1ClusterRoleBinding(
375 metadata=V1ObjectMeta(name=name, labels=labels),
376 role_ref=V1RoleRef(kind="ClusterRole", name=name, api_group=""),
garciadeblasfc12ea62024-08-07 02:35:10 +0200377 subjects=[
378 RbacV1Subject(kind="ServiceAccount", name=name, namespace=namespace)
379 ],
David Garciad8d4b6e2021-06-24 18:47:22 +0200380 )
381 self.clients[RBAC_CLIENT].create_cluster_role_binding(role_binding)
382
Gabriel Cuba5f069332023-04-25 19:26:19 -0500383 async def create_role_binding(
384 self,
385 name: str,
386 role_name: str,
387 sa_name: str,
388 labels: Dict[str, str],
389 namespace: str,
390 ):
391 """
392 Create a cluster role binding
393
394 :param: name: Name of the namespaced Role Binding
395 :param: role_name: Name of the namespaced Role to be bound
396 :param: sa_name: Name of the Service Account to be bound
397 :param: labels: Labels for Role Binding metadata
398 :param: namespace: Kubernetes namespace for Role Binding metadata
399
400 :return: None
401 """
402 role_bindings = self.clients[RBAC_CLIENT].list_namespaced_role_binding(
403 namespace, field_selector="metadata.name={}".format(name)
404 )
405 if len(role_bindings.items) > 0:
406 raise Exception(
407 "Role Binding with metadata.name={} already exists".format(name)
408 )
409
410 role_binding = V1RoleBinding(
411 metadata=V1ObjectMeta(name=name, labels=labels),
412 role_ref=V1RoleRef(kind="Role", name=role_name, api_group=""),
413 subjects=[
garciadeblasfc12ea62024-08-07 02:35:10 +0200414 RbacV1Subject(kind="ServiceAccount", name=sa_name, namespace=namespace)
Gabriel Cuba5f069332023-04-25 19:26:19 -0500415 ],
416 )
417 self.clients[RBAC_CLIENT].create_namespaced_role_binding(
418 namespace, role_binding
419 )
420
David Garciad8d4b6e2021-06-24 18:47:22 +0200421 def delete_cluster_role_binding(self, name: str):
422 """
423 Delete a cluster role binding
424
425 :param: name: Name of the cluster role binding
426 """
427 self.clients[RBAC_CLIENT].delete_cluster_role_binding(name)
428
429 @retry(
430 attempts=10,
431 delay=1,
432 fallback=Exception("Failed getting the secret from service account"),
Mark Beierlfb797862023-05-18 22:21:06 -0400433 callback=retry_callback,
David Garciad8d4b6e2021-06-24 18:47:22 +0200434 )
David Garcia4ae527e2021-07-26 16:04:59 +0200435 async def get_secret_data(
436 self, name: str, namespace: str = "kube-system"
437 ) -> (str, str):
David Garciad8d4b6e2021-06-24 18:47:22 +0200438 """
439 Get secret data
440
David Garcia4ae527e2021-07-26 16:04:59 +0200441 :param: name: Name of the secret data
442 :param: namespace: Name of the namespace where the secret is stored
443
David Garciad8d4b6e2021-06-24 18:47:22 +0200444 :return: Tuple with the token and client certificate
445 """
446 v1_core = self.clients[CORE_CLIENT]
447
448 secret_name = None
449
450 service_accounts = v1_core.list_namespaced_service_account(
451 namespace, field_selector="metadata.name={}".format(name)
452 )
453 if len(service_accounts.items) == 0:
454 raise Exception(
455 "Service account not found with metadata.name={}".format(name)
456 )
457 service_account = service_accounts.items[0]
458 if service_account.secrets and len(service_account.secrets) > 0:
459 secret_name = service_account.secrets[0].name
460 if not secret_name:
461 raise Exception(
462 "Failed getting the secret from service account {}".format(name)
463 )
Gabriel Cuba5f069332023-04-25 19:26:19 -0500464 # TODO: refactor to use get_secret_content
David Garciad8d4b6e2021-06-24 18:47:22 +0200465 secret = v1_core.list_namespaced_secret(
466 namespace, field_selector="metadata.name={}".format(secret_name)
467 ).items[0]
468
469 token = secret.data[SERVICE_ACCOUNT_TOKEN_KEY]
470 client_certificate_data = secret.data[SERVICE_ACCOUNT_ROOT_CA_KEY]
471
472 return (
473 base64.b64decode(token).decode("utf-8"),
474 base64.b64decode(client_certificate_data).decode("utf-8"),
475 )
Gabriel Cubafb03e902022-10-07 11:40:03 -0500476
Gabriel Cuba5f069332023-04-25 19:26:19 -0500477 @retry(
478 attempts=10,
479 delay=1,
480 fallback=Exception("Failed getting data from the secret"),
481 )
482 async def get_secret_content(
483 self,
484 name: str,
485 namespace: str,
486 ) -> dict:
487 """
488 Get secret data
489
490 :param: name: Name of the secret
491 :param: namespace: Name of the namespace where the secret is stored
492
493 :return: Dictionary with secret's data
494 """
495 v1_core = self.clients[CORE_CLIENT]
496
497 secret = v1_core.read_namespaced_secret(name, namespace)
498
499 return secret.data
500
501 @retry(
502 attempts=10,
503 delay=1,
504 fallback=Exception("Failed creating the secret"),
505 )
506 async def create_secret(
507 self, name: str, data: dict, namespace: str, secret_type: str
508 ):
509 """
garciadeblasc057eb32024-07-04 11:00:13 +0200510 Create secret with data
Gabriel Cuba5f069332023-04-25 19:26:19 -0500511
512 :param: name: Name of the secret
513 :param: data: Dict with data content. Values must be already base64 encoded
514 :param: namespace: Name of the namespace where the secret will be stored
515 :param: secret_type: Type of the secret, e.g., Opaque, kubernetes.io/service-account-token, kubernetes.io/tls
516
517 :return: None
518 """
garciadeblasc057eb32024-07-04 11:00:13 +0200519 self.logger.info("Enter create_secret function")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500520 v1_core = self.clients[CORE_CLIENT]
garciadeblasc057eb32024-07-04 11:00:13 +0200521 self.logger.info(f"v1_core: {v1_core}")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500522 metadata = V1ObjectMeta(name=name, namespace=namespace)
garciadeblasc057eb32024-07-04 11:00:13 +0200523 self.logger.info(f"metadata: {metadata}")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500524 secret = V1Secret(metadata=metadata, data=data, type=secret_type)
garciadeblasc057eb32024-07-04 11:00:13 +0200525 self.logger.info(f"secret: {secret}")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500526 v1_core.create_namespaced_secret(namespace, secret)
garciadeblasc057eb32024-07-04 11:00:13 +0200527 self.logger.info("Namespaced secret was created")
Gabriel Cuba5f069332023-04-25 19:26:19 -0500528
Gabriel Cubafb03e902022-10-07 11:40:03 -0500529 async def create_certificate(
530 self,
531 namespace: str,
532 name: str,
533 dns_prefix: str,
534 secret_name: str,
535 usages: list,
536 issuer_name: str,
537 ):
538 """
539 Creates cert-manager certificate object
540
541 :param: namespace: Name of the namespace where the certificate and secret is stored
542 :param: name: Name of the certificate object
543 :param: dns_prefix: Prefix for the dnsNames. They will be prefixed to the common k8s svc suffixes
544 :param: secret_name: Name of the secret created by cert-manager
545 :param: usages: List of X.509 key usages
546 :param: issuer_name: Name of the cert-manager's Issuer or ClusterIssuer object
547
548 """
549 certificate_body = {
550 "apiVersion": "cert-manager.io/v1",
551 "kind": "Certificate",
552 "metadata": {"name": name, "namespace": namespace},
553 "spec": {
554 "secretName": secret_name,
555 "privateKey": {
556 "rotationPolicy": "Always",
557 "algorithm": "ECDSA",
558 "size": 256,
559 },
560 "duration": "8760h", # 1 Year
561 "renewBefore": "2208h", # 9 months
562 "subject": {"organizations": ["osm"]},
563 "commonName": "osm",
564 "isCA": False,
565 "usages": usages,
566 "dnsNames": [
567 "{}.{}".format(dns_prefix, namespace),
568 "{}.{}.svc".format(dns_prefix, namespace),
569 "{}.{}.svc.cluster".format(dns_prefix, namespace),
570 "{}.{}.svc.cluster.local".format(dns_prefix, namespace),
571 ],
572 "issuerRef": {"name": issuer_name, "kind": "ClusterIssuer"},
573 },
574 }
575 client = self.clients[CUSTOM_OBJECT_CLIENT]
576 try:
577 client.create_namespaced_custom_object(
578 group="cert-manager.io",
579 plural="certificates",
580 version="v1",
581 body=certificate_body,
582 namespace=namespace,
583 )
584 except ApiException as e:
585 info = json.loads(e.body)
586 if info.get("reason").lower() == "alreadyexists":
587 self.logger.warning("Certificate already exists: {}".format(e))
588 else:
589 raise e
590
591 async def delete_certificate(self, namespace, object_name):
592 client = self.clients[CUSTOM_OBJECT_CLIENT]
593 try:
594 client.delete_namespaced_custom_object(
595 group="cert-manager.io",
596 plural="certificates",
597 version="v1",
598 name=object_name,
599 namespace=namespace,
600 )
601 except ApiException as e:
602 info = json.loads(e.body)
603 if info.get("reason").lower() == "notfound":
604 self.logger.warning("Certificate already deleted: {}".format(e))
605 else:
606 raise e
Gabriel Cuba5f069332023-04-25 19:26:19 -0500607
608 @retry(
609 attempts=10,
610 delay=1,
611 fallback=Exception("Failed creating the namespace"),
612 )
Gabriel Cubad21509c2023-05-17 01:30:15 -0500613 async def create_namespace(self, name: str, labels: dict = None):
Gabriel Cuba5f069332023-04-25 19:26:19 -0500614 """
615 Create a namespace
616
617 :param: name: Name of the namespace to be created
Gabriel Cubad21509c2023-05-17 01:30:15 -0500618 :param: labels: Dictionary with labels for the new namespace
Gabriel Cuba5f069332023-04-25 19:26:19 -0500619
620 """
621 v1_core = self.clients[CORE_CLIENT]
Gabriel Cubad21509c2023-05-17 01:30:15 -0500622 metadata = V1ObjectMeta(name=name, labels=labels)
Gabriel Cuba5f069332023-04-25 19:26:19 -0500623 namespace = V1Namespace(
624 metadata=metadata,
625 )
626
627 try:
628 v1_core.create_namespace(namespace)
629 self.logger.debug("Namespace created: {}".format(name))
630 except ApiException as e:
631 info = json.loads(e.body)
632 if info.get("reason").lower() == "alreadyexists":
633 self.logger.warning("Namespace already exists: {}".format(e))
634 else:
635 raise e
636
637 @retry(
638 attempts=10,
639 delay=1,
640 fallback=Exception("Failed deleting the namespace"),
641 )
642 async def delete_namespace(self, name: str):
643 """
644 Delete a namespace
645
646 :param: name: Name of the namespace to be deleted
647
648 """
649 try:
650 self.clients[CORE_CLIENT].delete_namespace(name)
651 except ApiException as e:
652 if e.reason == "Not Found":
653 self.logger.warning("Namespace already deleted: {}".format(e))
garciadeblasc057eb32024-07-04 11:00:13 +0200654
655 def get_secrets(
656 self,
657 namespace: str,
658 field_selector: str = None,
659 ) -> typing.List[typing.Dict]:
660 """
661 Get Secret list from a namespace
662
663 :param: namespace: Kubernetes namespace
664 :param: field_selector: Kubernetes field selector
665
666 :return: List of the secrets matching the selectors specified
667 """
668 try:
669 v1_core = self.clients[CORE_CLIENT]
670 secrets = v1_core.list_namespaced_secret(
671 namespace=namespace,
672 field_selector=field_selector,
673 ).items
674 return secrets
675 except ApiException as e:
676 self.logger.error("Error calling get secrets: {}".format(e))
677 raise e
678
garciadeblasc1d37d02024-08-22 09:59:05 +0200679 def create_generic_object(
garciadeblasc057eb32024-07-04 11:00:13 +0200680 self,
681 api_group: str,
682 api_plural: str,
683 api_version: str,
684 namespace: str,
685 manifest_dict: dict,
686 ):
687 """
688 Creates generic object
689
690 :param: api_group: API Group
691 :param: api_plural: API Plural
692 :param: api_version: API Version
693 :param: namespace: Namespace
694 :param: manifest_dict: Dictionary with the content of the Kubernetes manifest
695
696 """
697 client = self.clients[CUSTOM_OBJECT_CLIENT]
698 try:
699 client.create_namespaced_custom_object(
700 group=api_group,
701 plural=api_plural,
702 version=api_version,
703 body=manifest_dict,
704 namespace=namespace,
705 )
706 except ApiException as e:
707 info = json.loads(e.body)
708 if info.get("reason").lower() == "alreadyexists":
709 self.logger.warning("Object already exists: {}".format(e))
710 else:
711 raise e
712
garciadeblasc1d37d02024-08-22 09:59:05 +0200713 def delete_generic_object(
garciadeblasc057eb32024-07-04 11:00:13 +0200714 self,
715 api_group: str,
716 api_plural: str,
717 api_version: str,
718 namespace: str,
719 name: str,
720 ):
721 """
722 Deletes generic object
723
724 :param: api_group: API Group
725 :param: api_plural: API Plural
726 :param: api_version: API Version
727 :param: namespace: Namespace
728 :param: name: Name of the object
729
730 """
731 client = self.clients[CUSTOM_OBJECT_CLIENT]
732 try:
733 client.delete_namespaced_custom_object(
734 group=api_group,
735 plural=api_plural,
736 version=api_version,
737 name=name,
738 namespace=namespace,
739 )
740 except ApiException as e:
741 info = json.loads(e.body)
742 if info.get("reason").lower() == "notfound":
743 self.logger.warning("Object already deleted: {}".format(e))
744 else:
745 raise e
746
747 async def get_generic_object(
748 self,
749 api_group: str,
750 api_plural: str,
751 api_version: str,
752 namespace: str,
753 name: str,
754 ):
755 """
756 Gets generic object
757
758 :param: api_group: API Group
759 :param: api_plural: API Plural
760 :param: api_version: API Version
761 :param: namespace: Namespace
762 :param: name: Name of the object
763
764 """
765 client = self.clients[CUSTOM_OBJECT_CLIENT]
766 try:
767 object_dict = client.list_namespaced_custom_object(
768 group=api_group,
769 plural=api_plural,
770 version=api_version,
771 namespace=namespace,
772 field_selector=f"metadata.name={name}",
773 )
774 if len(object_dict.get("items")) == 0:
775 return None
776 return object_dict.get("items")[0]
777 except ApiException as e:
778 info = json.loads(e.body)
779 if info.get("reason").lower() == "notfound":
780 self.logger.warning("Cannot get custom object: {}".format(e))
781 else:
782 raise e
783
784 async def list_generic_object(
785 self,
786 api_group: str,
787 api_plural: str,
788 api_version: str,
789 namespace: str,
790 ):
791 """
792 Lists all generic objects of the requested API group
793
794 :param: api_group: API Group
795 :param: api_plural: API Plural
796 :param: api_version: API Version
797 :param: namespace: Namespace
798
799 """
800 client = self.clients[CUSTOM_OBJECT_CLIENT]
801 try:
802 object_dict = client.list_namespaced_custom_object(
803 group=api_group,
804 plural=api_plural,
805 version=api_version,
806 namespace=namespace,
807 )
808 self.logger.debug(f"Object-list: {object_dict.get('items')}")
809 return object_dict.get("items")
810 except ApiException as e:
811 info = json.loads(e.body)
812 if info.get("reason").lower() == "notfound":
813 self.logger.warning(
814 "Cannot retrieve list of custom objects: {}".format(e)
815 )
816 else:
817 raise e
818
819 @retry(
820 attempts=10,
821 delay=1,
822 fallback=Exception("Failed creating the secret"),
823 )
824 async def create_secret_string(
825 self, name: str, string_data: str, namespace: str, secret_type: str
826 ):
827 """
828 Create secret with data
829
830 :param: name: Name of the secret
831 :param: string_data: String with data content
832 :param: namespace: Name of the namespace where the secret will be stored
833 :param: secret_type: Type of the secret, e.g., Opaque, kubernetes.io/service-account-token, kubernetes.io/tls
834
835 :return: None
836 """
837 v1_core = self.clients[CORE_CLIENT]
838 metadata = V1ObjectMeta(name=name, namespace=namespace)
839 secret = V1Secret(metadata=metadata, string_data=string_data, type=secret_type)
840 v1_core.create_namespaced_secret(namespace, secret)
841
842 @retry(
843 attempts=10,
844 delay=1,
845 fallback=Exception("Failed creating the pvc"),
846 )
847 async def create_pvc(self, name: str, namespace: str):
848 """
849 Create a namespace
850
851 :param: name: Name of the pvc to be created
852 :param: namespace: Name of the namespace where the pvc will be stored
853
854 """
855 try:
856 pvc = V1PersistentVolumeClaim(
857 api_version="v1",
858 kind="PersistentVolumeClaim",
859 metadata=V1ObjectMeta(name=name),
860 spec=V1PersistentVolumeClaimSpec(
861 access_modes=["ReadWriteOnce"],
862 resources=V1ResourceRequirements(requests={"storage": "100Mi"}),
863 ),
864 )
865 self.clients[CORE_CLIENT].create_namespaced_persistent_volume_claim(
866 namespace=namespace, body=pvc
867 )
868 except ApiException as e:
869 info = json.loads(e.body)
870 if info.get("reason").lower() == "alreadyexists":
871 self.logger.warning("PVC already exists: {}".format(e))
872 else:
873 raise e
874
875 @retry(
876 attempts=10,
877 delay=1,
878 fallback=Exception("Failed deleting the pvc"),
879 )
880 async def delete_pvc(self, name: str, namespace: str):
881 """
882 Create a namespace
883
884 :param: name: Name of the pvc to be deleted
885 :param: namespace: Namespace
886
887 """
888 self.clients[CORE_CLIENT].delete_namespaced_persistent_volume_claim(
889 name, namespace
890 )
891
892 def copy_file_to_pod(
893 self, namespace, pod_name, container_name, src_file, dest_path
894 ):
895 # Create an in-memory tar file containing the source file
896 tar_buffer = io.BytesIO()
897 with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
898 tar.add(src_file, arcname=dest_path.split("/")[-1])
899
900 tar_buffer.seek(0)
901
902 # Define the command to extract the tar file in the pod
903 exec_command = ["tar", "xvf", "-", "-C", dest_path.rsplit("/", 1)[0]]
904
905 # Execute the command
906 resp = stream(
907 self.clients[CORE_CLIENT].connect_get_namespaced_pod_exec,
908 pod_name,
909 namespace,
910 command=exec_command,
911 container=container_name,
912 stdin=True,
913 stderr=True,
914 stdout=True,
915 tty=False,
916 _preload_content=False,
917 )
918
919 # Write the tar data to the pod
920 resp.write_stdin(tar_buffer.read())
921 resp.close()
922
923 @retry(
924 attempts=10,
925 delay=1,
926 fallback=Exception("Failed creating the pvc"),
927 )
928 async def create_pvc_with_content(
garciadeblas3f7daf52024-09-24 14:41:40 +0200929 self, name: str, namespace: str, src_file: str, dest_filename: str
garciadeblasc057eb32024-07-04 11:00:13 +0200930 ):
931 """
932 Create a PVC with content
933
934 :param: name: Name of the pvc to be created
935 :param: namespace: Name of the namespace where the pvc will be stored
garciadeblas3f7daf52024-09-24 14:41:40 +0200936 :param: src_file: File to be copied
937 :param: filename: Name of the file in the destination folder
garciadeblasc057eb32024-07-04 11:00:13 +0200938 """
939 pod_name = f"copy-pod-{name}"
940 await self.create_pvc(name=name, namespace=namespace)
941 await self.create_copy_pod(name=pod_name, namespace=namespace, pvc_name=name)
942 self.copy_file_to_pod(
943 namespace=namespace,
944 pod_name=pod_name,
945 container_name="copy-container",
garciadeblas3f7daf52024-09-24 14:41:40 +0200946 src_file=src_file,
947 dest_path=f"/mnt/data/{dest_filename}",
garciadeblasc057eb32024-07-04 11:00:13 +0200948 )
949
950 @retry(
951 attempts=10,
952 delay=1,
953 fallback=Exception("Failed creating the pvc"),
954 )
955 async def create_copy_pod(self, name: str, namespace: str, pvc_name: str):
956 """
957 Create a pod to copy content into a PVC
958
959 :param: name: Name of the pod to be created
960 :param: namespace: Name of the namespace where the pod will be stored
961 :param: pvc_name: Name of the PVC that the pod will mount as a volume
962
963 """
964 pod = V1Pod(
965 api_version="v1",
966 kind="Pod",
967 metadata=client.V1ObjectMeta(name=name),
968 spec=V1PodSpec(
969 containers=[
970 V1Container(
971 name="copy-container",
972 image="busybox", # Imagen ligera para copiar archivos
973 command=["sleep", "3600"], # Mantén el contenedor en ejecución
974 volume_mounts=[
975 V1VolumeMount(mount_path="/mnt/data", name="my-storage")
976 ],
977 )
978 ],
979 volumes=[
980 V1Volume(
981 name="my-storage",
982 persistent_volume_claim=V1PersistentVolumeClaimVolumeSource(
983 claim_name=pvc_name
984 ),
985 )
986 ],
987 ),
988 )
989 # Create the pod
990 self.clients[CORE_CLIENT].create_namespaced_pod(namespace=namespace, body=pod)
991
992 @retry(
993 attempts=10,
994 delay=1,
995 fallback=Exception("Failed deleting the pod"),
996 )
997 async def delete_pod(self, name: str, namespace: str):
998 """
999 Create a namespace
1000
1001 :param: name: Name of the pod to be deleted
1002 :param: namespace: Namespace
1003
1004 """
1005 self.clients[CORE_CLIENT].delete_namespaced_pod(name, namespace)