[ops-sunbeam] Allow post-init to throw status exceptions

When the setup of relation handlers throws an ops sunbeam status
exception, the charm is put to error while this is a supported patterns
for developping charms. The reason is that the exception is not thrown
from within a guard. But it is reasonable, for example, for
`OSBaseOperatorAPICharm.internal_url` to raise a WaitingExceptionError
instead of returning None.

Change-Id: Ide137421308733784b6aca7e247eb3e13485d2ff
Signed-off-by: Guillaume Boutry <guillaume.boutry@canonical.com>
This commit is contained in:
Guillaume Boutry
2025-02-24 11:06:54 +01:00
parent 4d4b4a41b0
commit 99e69fdc9d
2 changed files with 36 additions and 10 deletions

View File

@@ -25,6 +25,9 @@ from typing import (
) )
import ops_sunbeam.tracing as sunbeam_tracing import ops_sunbeam.tracing as sunbeam_tracing
from ops_sunbeam.guard import (
BaseStatusExceptionError,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from ops_sunbeam.charm import ( from ops_sunbeam.charm import (
@@ -101,5 +104,20 @@ class PostInitMeta(type):
def __call__(cls, *args, **kw): def __call__(cls, *args, **kw):
"""Call __post_init__ after __init__.""" """Call __post_init__ after __init__."""
instance = super().__call__(*args, **kw) instance = super().__call__(*args, **kw)
instance.__post_init__() try:
instance.__post_init__()
except BaseStatusExceptionError as e:
# Allow post init to raise an ops_sunbeam status
# exception without causing the charm to error.
# This status will be collected and set on the
# unit.
# import here to avoid circular import
from ops_sunbeam.charm import (
OSBaseOperatorCharm,
)
if isinstance(instance, OSBaseOperatorCharm):
instance.status.set(e.to_status())
else:
raise e
return instance return instance

View File

@@ -20,9 +20,11 @@ from contextlib import (
contextmanager, contextmanager,
) )
from ops.model import ( from ops import (
ActiveStatus,
BlockedStatus, BlockedStatus,
MaintenanceStatus, MaintenanceStatus,
StatusBase,
WaitingStatus, WaitingStatus,
) )
@@ -43,27 +45,33 @@ class GuardExceptionError(Exception):
class BaseStatusExceptionError(Exception): class BaseStatusExceptionError(Exception):
"""Charm is blocked.""" """Charm is blocked."""
def __init__(self, msg): status_type: type[StatusBase] = ActiveStatus
def __init__(self, msg: str):
super().__init__(msg)
self.msg = msg self.msg = msg
super().__init__(self.msg)
def to_status(self):
"""Convert the exception to an ops status."""
return self.status_type(self.msg)
class BlockedExceptionError(BaseStatusExceptionError): class BlockedExceptionError(BaseStatusExceptionError):
"""Charm is blocked.""" """Charm is blocked."""
pass status_type = BlockedStatus
class MaintenanceExceptionError(BaseStatusExceptionError): class MaintenanceExceptionError(BaseStatusExceptionError):
"""Charm is performing maintenance.""" """Charm is performing maintenance."""
pass status_type = MaintenanceStatus
class WaitingExceptionError(BaseStatusExceptionError): class WaitingExceptionError(BaseStatusExceptionError):
"""Charm is waiting.""" """Charm is waiting."""
pass status_type = WaitingStatus
@contextmanager @contextmanager
@@ -103,19 +111,19 @@ def guard(
logger.warning( logger.warning(
"Charm is blocked in section '%s' due to '%s'", section, str(e) "Charm is blocked in section '%s' due to '%s'", section, str(e)
) )
charm.status.set(BlockedStatus(e.msg)) charm.status.set(e.to_status())
except WaitingExceptionError as e: except WaitingExceptionError as e:
logger.warning( logger.warning(
"Charm is waiting in section '%s' due to '%s'", section, str(e) "Charm is waiting in section '%s' due to '%s'", section, str(e)
) )
charm.status.set(WaitingStatus(e.msg)) charm.status.set(e.to_status())
except MaintenanceExceptionError as e: except MaintenanceExceptionError as e:
logger.warning( logger.warning(
"Charm performing maintenance in section '%s' due to '%s'", "Charm performing maintenance in section '%s' due to '%s'",
section, section,
str(e), str(e),
) )
charm.status.set(MaintenanceStatus(e.msg)) charm.status.set(e.to_status())
except Exception as e: except Exception as e:
# something else went wrong # something else went wrong
if handle_exception: if handle_exception: