From 27b06dbb541794acab4491e929361aea43083a6d Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Wed, 5 Feb 2025 15:49:48 +0530 Subject: [PATCH] Remove deprecated kubernetes_service_patch library Remove deprecated kubernetes_service_patch library. Add new class in ops_sunbeam to manage new kubernetes service of type LoadBalancer. The service name should be -lb. Use the class in ovn-relay-k8s and designate-bind-k8s to create new service of type LoadBalancer. Update open ports in default service definition for ovn-relay-k8s and designate-bind-k8s. Remove library kubernetes_service_patch from external libs and remove any references. Change-Id: Ic62af0aeaf9f5502d5d7792ed0b182b9a83c2b72 --- charms/designate-bind-k8s/.sunbeam-build.yaml | 1 - charms/designate-bind-k8s/requirements.txt | 1 + charms/designate-bind-k8s/src/charm.py | 27 +- .../tests/unit/test_bind_charm.py | 4 +- .../openstack-hypervisor/.sunbeam-build.yaml | 1 - charms/ovn-relay-k8s/.sunbeam-build.yaml | 1 - charms/ovn-relay-k8s/requirements.txt | 1 + charms/ovn-relay-k8s/src/charm.py | 16 +- .../tests/unit/test_ovn_relay_charm.py | 4 +- charms/tempest-k8s/.sunbeam-build.yaml | 1 - .../v1/kubernetes_service_patch.py | 341 ------------------ .../lib/charms/observability_libs/v1/py.typed | 0 .../ops_sunbeam/k8s_resource_handlers.py | 145 ++++++++ test-requirements.txt | 2 + tests/all-k8s/smoke.yaml.j2 | 2 +- tests/misc/smoke.yaml.j2 | 2 +- 16 files changed, 179 insertions(+), 370 deletions(-) delete mode 100644 libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py delete mode 100644 libs/external/lib/charms/observability_libs/v1/py.typed create mode 100644 ops-sunbeam/ops_sunbeam/k8s_resource_handlers.py diff --git a/charms/designate-bind-k8s/.sunbeam-build.yaml b/charms/designate-bind-k8s/.sunbeam-build.yaml index 5ae09e95..230695be 100644 --- a/charms/designate-bind-k8s/.sunbeam-build.yaml +++ b/charms/designate-bind-k8s/.sunbeam-build.yaml @@ -1,5 +1,4 @@ external-libraries: - - charms.observability_libs.v1.kubernetes_service_patch - charms.loki_k8s.v1.loki_push_api - charms.tempo_k8s.v2.tracing - charms.tempo_k8s.v1.charm_tracing diff --git a/charms/designate-bind-k8s/requirements.txt b/charms/designate-bind-k8s/requirements.txt index ccdfbaf8..92353403 100644 --- a/charms/designate-bind-k8s/requirements.txt +++ b/charms/designate-bind-k8s/requirements.txt @@ -2,6 +2,7 @@ ops jinja2 lightkube lightkube-models +lightkube-extensions @ git+https://github.com/canonical/lightkube-extensions.git@main # From ops_sunbeam tenacity diff --git a/charms/designate-bind-k8s/src/charm.py b/charms/designate-bind-k8s/src/charm.py index 84281fbd..eb1cba5a 100755 --- a/charms/designate-bind-k8s/src/charm.py +++ b/charms/designate-bind-k8s/src/charm.py @@ -33,7 +33,6 @@ from typing import ( ) import charms.designate_bind_k8s.v0.bind_rndc as bind_rndc -import charms.observability_libs.v1.kubernetes_service_patch as kubernetes_service_patch import lightkube.models.core_v1 as core_v1 import ops import ops.charm @@ -45,6 +44,9 @@ import ops_sunbeam.tracing as sunbeam_tracing from ops.framework import ( StoredState, ) +from ops_sunbeam.k8s_resource_handlers import ( + KubernetesLoadBalancerHandler, +) logger = logging.getLogger(__name__) @@ -201,18 +203,21 @@ class BindOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): def __init__(self, *args): super().__init__(*args) self.framework.observe(self.on.secret_rotate, self._on_secret_rotate) - self.service = kubernetes_service_patch.KubernetesServicePatch( + + service_ports = [ + core_v1.ServicePort( + 53, appProtocol="domain", name="bind", protocol="UDP" + ), + core_v1.ServicePort( + 953, appProtocol="rndc", name="rndc", protocol="TCP" + ), + ] + self.lb_handler = KubernetesLoadBalancerHandler( self, - ports=[ - core_v1.ServicePort( - 53, appProtocol="domain", name="bind", protocol="UDP" - ), - core_v1.ServicePort( - 953, appProtocol="rndc", name="rndc", protocol="TCP" - ), - ], - service_type="LoadBalancer", + service_ports, + refresh_event=[self.on.install], ) + self.unit.set_ports(53, 953) def _on_secret_rotate(self, event: ops.SecretRotateEvent): """Handle secret rotate event.""" diff --git a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py index 5bfb0074..65ca63e7 100644 --- a/charms/designate-bind-k8s/tests/unit/test_bind_charm.py +++ b/charms/designate-bind-k8s/tests/unit/test_bind_charm.py @@ -41,9 +41,7 @@ class _BindTestOperatorCharm(charm.BindOperatorCharm): class TestBindOperatorCharm(test_utils.CharmTestCase): """Test charm.""" - PATCHES = [ - "kubernetes_service_patch", - ] + PATCHES = [] def setUp(self): """Test setup.""" diff --git a/charms/openstack-hypervisor/.sunbeam-build.yaml b/charms/openstack-hypervisor/.sunbeam-build.yaml index 5d84fb07..95a0d4f0 100644 --- a/charms/openstack-hypervisor/.sunbeam-build.yaml +++ b/charms/openstack-hypervisor/.sunbeam-build.yaml @@ -1,7 +1,6 @@ external-libraries: - charms.data_platform_libs.v0.data_interfaces - charms.grafana_agent.v0.cos_agent - - charms.observability_libs.v1.kubernetes_service_patch - charms.operator_libs_linux.v2.snap - charms.rabbitmq_k8s.v0.rabbitmq - charms.traefik_k8s.v2.ingress diff --git a/charms/ovn-relay-k8s/.sunbeam-build.yaml b/charms/ovn-relay-k8s/.sunbeam-build.yaml index b7d23719..1b330e21 100644 --- a/charms/ovn-relay-k8s/.sunbeam-build.yaml +++ b/charms/ovn-relay-k8s/.sunbeam-build.yaml @@ -1,6 +1,5 @@ external-libraries: - charms.tls_certificates_interface.v3.tls_certificates - - charms.observability_libs.v1.kubernetes_service_patch - charms.loki_k8s.v1.loki_push_api - charms.tempo_k8s.v2.tracing - charms.tempo_k8s.v1.charm_tracing diff --git a/charms/ovn-relay-k8s/requirements.txt b/charms/ovn-relay-k8s/requirements.txt index ddbedec1..e4d5cd64 100644 --- a/charms/ovn-relay-k8s/requirements.txt +++ b/charms/ovn-relay-k8s/requirements.txt @@ -9,6 +9,7 @@ jinja2 jsonschema lightkube lightkube-models +lightkube-extensions @ git+https://github.com/canonical/lightkube-extensions.git@main ops # From ops_sunbeam diff --git a/charms/ovn-relay-k8s/src/charm.py b/charms/ovn-relay-k8s/src/charm.py index b21689fb..9afbd970 100755 --- a/charms/ovn-relay-k8s/src/charm.py +++ b/charms/ovn-relay-k8s/src/charm.py @@ -44,12 +44,12 @@ import ops_sunbeam.ovn.container_handlers as ovn_chandlers import ops_sunbeam.ovn.relation_handlers as ovn_relation_handlers import ops_sunbeam.relation_handlers as sunbeam_rhandlers import ops_sunbeam.tracing as sunbeam_tracing -from charms.observability_libs.v1.kubernetes_service_patch import ( - KubernetesServicePatch, -) from lightkube.models.core_v1 import ( ServicePort, ) +from ops_sunbeam.k8s_resource_handlers import ( + KubernetesLoadBalancerHandler, +) logger = logging.getLogger(__name__) @@ -95,11 +95,15 @@ class OVNRelayOperatorCharm(ovn_charm.OSBaseOVNOperatorCharm): def __init__(self, framework): super().__init__(framework) - self.service_patcher = KubernetesServicePatch( + + service_ports = [ServicePort(6642, name="southbound")] + self.lb_handler = KubernetesLoadBalancerHandler( self, - [ServicePort(6642, name="southbound")], - service_type="LoadBalancer", + service_ports, + refresh_event=[self.on.install], ) + self.unit.set_ports(6642) + self.framework.observe( self.on.get_southbound_db_url_action, self._get_southbound_db_url_action, diff --git a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py index 475e4d55..3d4e1617 100644 --- a/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py +++ b/charms/ovn-relay-k8s/tests/unit/test_ovn_relay_charm.py @@ -36,9 +36,7 @@ class _OVNRelayOperatorCharm(charm.OVNRelayOperatorCharm): class TestOVNRelayOperatorCharm(test_utils.CharmTestCase): """Test OVN relay.""" - PATCHES = [ - "KubernetesServicePatch", - ] + PATCHES = [] def setUp(self): """Setup OVN relay tests.""" diff --git a/charms/tempest-k8s/.sunbeam-build.yaml b/charms/tempest-k8s/.sunbeam-build.yaml index ebba1f29..a627d37f 100644 --- a/charms/tempest-k8s/.sunbeam-build.yaml +++ b/charms/tempest-k8s/.sunbeam-build.yaml @@ -1,5 +1,4 @@ external-libraries: - - charms.observability_libs.v1.kubernetes_service_patch - charms.grafana_k8s.v0.grafana_dashboard - charms.loki_k8s.v1.loki_push_api - charms.certificate_transfer_interface.v0.certificate_transfer diff --git a/libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 2cce729e..00000000 --- a/libs/external/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# KubernetesServicePatch Library. - -This library is designed to enable developers to more simply patch the Kubernetes Service created -by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a -service named after the application in the namespace (named after the Juju model). This service by -default contains a "placeholder" port, which is 65536/TCP. - -When modifying the default set of resources managed by Juju, one must consider the lifecycle of the -charm. In this case, any modifications to the default service (created during deployment), will be -overwritten during a charm upgrade. - -When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm` -events which applies the patch to the cluster. This should ensure that the service ports are -correct throughout the charm's life. - -The constructor simply takes a reference to the parent charm, and a list of -[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the -service. For information regarding the `lightkube` `ServicePort` model, please visit the -`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport). - -Optionally, a name of the service (in case service name needs to be patched as well), labels, -selectors, and annotations can be provided as keyword arguments. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. **Note -that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.** - -```shell -cd some-charm -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -cat << EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For `ClusterIP` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch(self, [port]) - # ... -``` - -For `LoadBalancer`/`NodePort` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666) - self.service_patcher = KubernetesServicePatch( - self, [port], "LoadBalancer" - ) - # ... -``` - -Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"` - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP") - udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP") - sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP") - self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp]) - # ... -``` - -Bound with custom events by providing `refresh_event` argument: -For example, you would like to have a configurable port in your charm and want to apply -service patch every time charm config is changed. - -```python -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch( - self, - [port], - refresh_event=self.on.config_changed - ) - # ... -``` - -Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library -does not try to make any API calls, or open any files during testing that are unlikely to be -present, and could break your tests. The easiest way to do this is during your test `setUp`: - -```python -# ... - -@patch("charm.KubernetesServicePatch", lambda x, y: None) -def setUp(self, *unused): - self.harness = Harness(SomeCharm) - # ... -``` -""" - -import logging -from types import MethodType -from typing import List, Literal, Optional, Union - -from lightkube import ApiError, Client # pyright: ignore -from lightkube.core import exceptions -from lightkube.models.core_v1 import ServicePort, ServiceSpec -from lightkube.models.meta_v1 import ObjectMeta -from lightkube.resources.core_v1 import Service -from lightkube.types import PatchType -from ops.charm import CharmBase -from ops.framework import BoundEvent, Object - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0042f86d0a874435adef581806cddbbb" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 9 - -ServiceType = Literal["ClusterIP", "LoadBalancer"] - - -class KubernetesServicePatch(Object): - """A utility for patching the Kubernetes service set up by Juju.""" - - def __init__( - self, - charm: CharmBase, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - *, - refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, - ): - """Constructor for KubernetesServicePatch. - - Args: - charm: the charm that is instantiating the library. - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - refresh_event: an optional bound event or list of bound events which - will be observed to re-apply the patch (e.g. on port change). - The `install` and `upgrade-charm` events would be observed regardless. - """ - super().__init__(charm, "kubernetes-service-patch") - self.charm = charm - self.service_name = service_name if service_name else self._app - self.service = self._service_object( - ports, - service_name, - service_type, - additional_labels, - additional_selectors, - additional_annotations, - ) - - # Make mypy type checking happy that self._patch is a method - assert isinstance(self._patch, MethodType) - # Ensure this patch is applied during the 'install' and 'upgrade-charm' events - self.framework.observe(charm.on.install, self._patch) - self.framework.observe(charm.on.upgrade_charm, self._patch) - self.framework.observe(charm.on.update_status, self._patch) - - # apply user defined events - if refresh_event: - if not isinstance(refresh_event, list): - refresh_event = [refresh_event] - - for evt in refresh_event: - self.framework.observe(evt, self._patch) - - def _service_object( - self, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - ) -> Service: - """Creates a valid Service representation. - - Args: - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - - Returns: - Service: A valid representation of a Kubernetes Service with the correct ports. - """ - if not service_name: - service_name = self._app - labels = {"app.kubernetes.io/name": self._app} - if additional_labels: - labels.update(additional_labels) - selector = {"app.kubernetes.io/name": self._app} - if additional_selectors: - selector.update(additional_selectors) - return Service( - apiVersion="v1", - kind="Service", - metadata=ObjectMeta( - namespace=self._namespace, - name=service_name, - labels=labels, - annotations=additional_annotations, # type: ignore[arg-type] - ), - spec=ServiceSpec( - selector=selector, - ports=ports, - type=service_type, - ), - ) - - def _patch(self, _) -> None: - """Patch the Kubernetes service created by Juju to map the correct port. - - Raises: - PatchFailed: if patching fails due to lack of permissions, or otherwise. - """ - try: - client = Client() # pyright: ignore - except exceptions.ConfigError as e: - logger.warning("Error creating k8s client: %s", e) - return - - try: - if self._is_patched(client): - return - if self.service_name != self._app: - self._delete_and_create_service(client) - client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) - except ApiError as e: - if e.status.code == 403: - logger.error("Kubernetes service patch failed: `juju trust` this application.") - else: - logger.error("Kubernetes service patch failed: %s", str(e)) - else: - logger.info("Kubernetes service '%s' patched successfully", self._app) - - def _delete_and_create_service(self, client: Client): - service = client.get(Service, self._app, namespace=self._namespace) - service.metadata.name = self.service_name # type: ignore[attr-defined] - service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 - client.delete(Service, self._app, namespace=self._namespace) - client.create(service) - - def is_patched(self) -> bool: - """Reports if the service patch has been applied. - - Returns: - bool: A boolean indicating if the service patch has been applied. - """ - client = Client() # pyright: ignore - return self._is_patched(client) - - def _is_patched(self, client: Client) -> bool: - # Get the relevant service from the cluster - try: - service = client.get(Service, name=self.service_name, namespace=self._namespace) - except ApiError as e: - if e.status.code == 404 and self.service_name != self._app: - return False - logger.error("Kubernetes service get failed: %s", str(e)) - raise - - # Construct a list of expected ports, should the patch be applied - expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] # type: ignore[attr-defined] - # Construct a list in the same manner, using the fetched service - fetched_ports = [ - (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] - ] # noqa: E501 - return expected_ports == fetched_ports - - @property - def _app(self) -> str: - """Name of the current Juju application. - - Returns: - str: A string containing the name of the current Juju application. - """ - return self.charm.app.name - - @property - def _namespace(self) -> str: - """The Kubernetes namespace we're running in. - - Returns: - str: A string containing the name of the current Kubernetes namespace. - """ - with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: - return f.read().strip() diff --git a/libs/external/lib/charms/observability_libs/v1/py.typed b/libs/external/lib/charms/observability_libs/v1/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/ops-sunbeam/ops_sunbeam/k8s_resource_handlers.py b/ops-sunbeam/ops_sunbeam/k8s_resource_handlers.py new file mode 100644 index 00000000..5c2c2666 --- /dev/null +++ b/ops-sunbeam/ops_sunbeam/k8s_resource_handlers.py @@ -0,0 +1,145 @@ +# Copyright 2025 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Handles management of kubernetes resources.""" + +import logging + +import ops_sunbeam.tracing as sunbeam_tracing +from lightkube.core.client import ( + Client, +) +from lightkube.models.core_v1 import ( + ServicePort, + ServiceSpec, +) +from lightkube.models.meta_v1 import ( + ObjectMeta, +) +from lightkube.resources.core_v1 import ( + Service, +) +from lightkube_extensions.batch import ( # type: ignore[import-untyped, import-not-found] + KubernetesResourceManager, + create_charm_default_labels, +) +from ops.framework import ( + BoundEvent, + Object, +) +from ops_sunbeam.charm import ( + OSBaseOperatorCharmK8S, +) + +logger = logging.getLogger(__name__) + + +@sunbeam_tracing.trace_type +class KubernetesLoadBalancerHandler(Object): + """Manage Kubernetes LB services. + + Creates a new Kubernetes service of type Loadbalancer + with name as {app.name}-lb. Patch the service on + events defined by the charm. + Remove the kubernetes service on removal of application + or the last unit. + """ + + def __init__( + self, + charm: OSBaseOperatorCharmK8S, + service_ports: list[ServicePort], + refresh_event: list[BoundEvent] | None = None, + ): + super().__init__(charm, "kubernetes-lb-handler") + self.charm = charm + self._service_ports = service_ports + self._lb_label = f"{self.charm.app.name}-lb" + + self._lightkube_client = None + self._lightkube_field_manager: str = self.charm.app.name + self._lb_name: str = f"{self.charm.app.name}-lb" + + # apply user defined events + if refresh_event: + if not isinstance(refresh_event, list): + refresh_event = [refresh_event] + + for evt in refresh_event: + self.framework.observe(evt, self._reconcile_lb) + + # Remove service if the last unit is removed + self.framework.observe(charm.on.remove, self._on_remove) + + @property + def lightkube_client(self): + """Returns a lightkube client configured for this charm.""" + if self._lightkube_client is None: + self._lightkube_client = Client( + namespace=self.charm.model.name, + field_manager=self._lightkube_field_manager, + ) + return self._lightkube_client + + def _get_lb_resource_manager(self): + return KubernetesResourceManager( + labels=create_charm_default_labels( + self.charm.app.name, + self.charm.model.name, + scope=self._lb_label, + ), + resource_types={Service}, + lightkube_client=self.lightkube_client, + logger=logger, + ) + + def _construct_lb(self) -> Service: + return Service( + metadata=ObjectMeta( + name=f"{self._lb_name}", + namespace=self.charm.model.name, + labels={"app.kubernetes.io/name": self.charm.app.name}, + ), + spec=ServiceSpec( + ports=self._service_ports, + selector={"app.kubernetes.io/name": self.charm.app.name}, + type="LoadBalancer", + ), + ) + + def _reconcile_lb(self, _) -> None: + """Reconcile the LoadBalancer's state.""" + if not self.charm.unit.is_leader(): + return + + klm = self._get_lb_resource_manager() + resources_list = [self._construct_lb()] + logger.info( + f"Patching k8s loadbalancer service object {self._lb_name}" + ) + klm.reconcile(resources_list) + + def _on_remove(self, _) -> None: + if not self.charm.unit.is_leader(): + return + + # juju scale down on kubernetes charms removes non-leader units. + # So removal of leader unit can be considered as application is + # getting destroyed or all the units are removed. Remove the + # service in this case. + logger.info( + f"Removing k8s loadbalancer service object {self._lb_name}" + ) + klm = self._get_lb_resource_manager() + klm.delete() diff --git a/test-requirements.txt b/test-requirements.txt index a2a4d5a4..bc5cf010 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -23,3 +23,5 @@ types-PyYAML # types stub for mypy # due to https://github.com/corpusops/croniter/issues/144 # we might need to find an alternative for croniter project croniter==5.0.1 # tempest-k8s + +lightkube-extensions @ git+https://github.com/canonical/lightkube-extensions.git@main # ovn-relay-k8s,designate-bind-k8s diff --git a/tests/all-k8s/smoke.yaml.j2 b/tests/all-k8s/smoke.yaml.j2 index a4b0c298..62e23983 100644 --- a/tests/all-k8s/smoke.yaml.j2 +++ b/tests/all-k8s/smoke.yaml.j2 @@ -331,7 +331,7 @@ applications: {% endif -%} base: ubuntu@24.04 scale: 1 - trust: false + trust: true resources: designate-bind-image: ubuntu/bind9:9.18-22.04_beta designate: diff --git a/tests/misc/smoke.yaml.j2 b/tests/misc/smoke.yaml.j2 index 6adf244d..e628b4e9 100644 --- a/tests/misc/smoke.yaml.j2 +++ b/tests/misc/smoke.yaml.j2 @@ -55,7 +55,7 @@ applications: {% endif -%} base: ubuntu@24.04 scale: 1 - trust: false + trust: true resources: designate-bind-image: ubuntu/bind9:9.18-22.04_beta designate: