@@ -145,14 +145,15 @@ def setUp(self, *unused):
145145
146146import logging
147147from types import MethodType
148- from typing import List , Literal , Optional , Union
148+ from typing import Any , List , Literal , Optional , Union
149149
150150from lightkube import ApiError , Client # pyright: ignore
151151from lightkube .core import exceptions
152152from lightkube .models .core_v1 import ServicePort , ServiceSpec
153153from lightkube .models .meta_v1 import ObjectMeta
154154from lightkube .resources .core_v1 import Service
155155from lightkube .types import PatchType
156+ from ops import UpgradeCharmEvent
156157from ops .charm import CharmBase
157158from 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
171172ServiceType = 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 :
0 commit comments