Skip to content

Commit b2c5820

Browse files
authored
Fix traefik lb being constantly re-created on controller restart (#104)
1 parent 997e107 commit b2c5820

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

lib/charms/observability_libs/v1/kubernetes_service_patch.py

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,15 @@ def setUp(self, *unused):
145145

146146
import logging
147147
from types import MethodType
148-
from typing import List, Literal, Optional, Union
148+
from typing import Any, List, Literal, Optional, Union
149149

150150
from lightkube import ApiError, Client # pyright: ignore
151151
from lightkube.core import exceptions
152152
from lightkube.models.core_v1 import ServicePort, ServiceSpec
153153
from lightkube.models.meta_v1 import ObjectMeta
154154
from lightkube.resources.core_v1 import Service
155155
from lightkube.types import PatchType
156+
from ops import UpgradeCharmEvent
156157
from ops.charm import CharmBase
157158
from ops.framework import BoundEvent, Object
158159

@@ -166,7 +167,7 @@ def setUp(self, *unused):
166167

167168
# Increment this PATCH version before using `charmcraft publish-lib` or reset
168169
# to 0 if you are raising the major API version
169-
LIBPATCH = 11
170+
LIBPATCH = 12
170171

171172
ServiceType = Literal["ClusterIP", "LoadBalancer"]
172173

@@ -225,9 +226,11 @@ def __init__(
225226
assert isinstance(self._patch, MethodType)
226227
# Ensure this patch is applied during the 'install' and 'upgrade-charm' events
227228
self.framework.observe(charm.on.install, self._patch)
228-
self.framework.observe(charm.on.upgrade_charm, self._patch)
229+
self.framework.observe(charm.on.upgrade_charm, self._on_upgrade_charm)
229230
self.framework.observe(charm.on.update_status, self._patch)
230-
self.framework.observe(charm.on.stop, self._remove_service)
231+
# Sometimes Juju doesn't clean-up a manually created LB service,
232+
# so we clean it up ourselves just in case.
233+
self.framework.observe(charm.on.remove, self._remove_service)
231234

232235
# apply user defined events
233236
if refresh_event:
@@ -356,6 +359,36 @@ def _is_patched(self, client: Client) -> bool:
356359
] # noqa: E501
357360
return expected_ports == fetched_ports
358361

362+
def _on_upgrade_charm(self, event: UpgradeCharmEvent):
363+
"""Handle the upgrade charm event."""
364+
# If a charm author changed the service type from LB to ClusterIP across an upgrade, we need to delete the previous LB.
365+
if self.service_type == "ClusterIP":
366+
367+
client = Client() # pyright: ignore
368+
369+
# Define a label selector to find services related to the app
370+
selector: dict[str, Any] = {"app.kubernetes.io/name": self._app}
371+
372+
# Check if any service of type LoadBalancer exists
373+
services = client.list(Service, namespace=self._namespace, labels=selector)
374+
for service in services:
375+
if (
376+
not service.metadata
377+
or not service.metadata.name
378+
or not service.spec
379+
or not service.spec.type
380+
):
381+
logger.warning(
382+
"Service patch: skipping resource with incomplete metadata: %s.", service
383+
)
384+
continue
385+
if service.spec.type == "LoadBalancer":
386+
client.delete(Service, service.metadata.name, namespace=self._namespace)
387+
logger.info(f"LoadBalancer service {service.metadata.name} deleted.")
388+
389+
# Continue the upgrade flow normally
390+
self._patch(event)
391+
359392
def _remove_service(self, _):
360393
"""Remove a Kubernetes service associated with this charm.
361394
@@ -372,13 +405,13 @@ def _remove_service(self, _):
372405

373406
try:
374407
client.delete(Service, self.service_name, namespace=self._namespace)
408+
logger.info("The patched k8s service '%s' was deleted.", self.service_name)
375409
except ApiError as e:
376410
if e.status.code == 404:
377411
# Service not found, so no action needed
378-
pass
379-
else:
380-
# Re-raise for other statuses
381-
raise
412+
return
413+
# Re-raise for other statuses
414+
raise
382415

383416
@property
384417
def _app(self) -> str:

tests/unit/test_kubernetes_service.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,14 @@ def test_given_initialized_charm_when_install_event_then_event_listener_is_attac
374374
charm.on.install.emit()
375375
self.assertEqual(patch.call_count, 1)
376376

377-
def test_given_initialized_charm_when_upgrade_event_then_event_listener_is_attached(self):
377+
@patch(f"{CL_PATH}._namespace", "test")
378+
@patch(f"{MOD_PATH}.Client")
379+
def test_given_initialized_charm_when_upgrade_event_then_event_listener_is_attached(
380+
self, client
381+
):
378382
charm = self.harness.charm
383+
client.return_value = client
384+
client.list.return_value = []
379385
with mock.patch(f"{CL_PATH}._patch") as patch:
380386
charm.on.upgrade_charm.emit()
381387
self.assertEqual(patch.call_count, 1)

0 commit comments

Comments
 (0)