-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lib: add the glauth-auxiliary library
- Loading branch information
1 parent
3446b02
commit d8a9878
Showing
1 changed file
with
275 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
# Copyright 2023 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
|
||
"""# Juju Charm Library for the `glauth_auxiliary` Juju Interface. | ||
This juju charm library contains the Provider and Requirer classes for handling | ||
the `glauth_auxiliary` interface. | ||
## Requirer Charm | ||
The requirer charm is expected to: | ||
- Listen to the custom juju event `AuxiliaryReadyEvent` to consume the | ||
auxiliary data from the integration | ||
- Listen to the custom juju event `AuxiliaryUnavailableEvent` to handle the | ||
situation when the auxiliary integration is broken | ||
```python | ||
from charms.glauth_utils.v0.glauth_auxiliary import ( | ||
AuxiliaryRequirer, | ||
AuxiliaryReadyEvent, | ||
) | ||
class RequirerCharm(CharmBase): | ||
# Auxiliary requirer charm that integrates with an auxiliary provider charm. | ||
def __init__(self, *args): | ||
super().__init__(*args) | ||
self.auxiliary_requirer = AuxiliaryRequirer(self) | ||
self.framework.observe( | ||
self.auxiliary_requirer.on.auxiliary_ready, | ||
self._on_auxiliary_ready, | ||
) | ||
self.framework.observe( | ||
self.auxiliary_requirer.on.auxiliary_unavailable, | ||
self._on_auxiliary_unavailable, | ||
) | ||
def _on_auxiliary_ready(self, event: AuxiliaryReadyEvent) -> None: | ||
# Consume the auxiliary data | ||
auxiliary_data = self.auxiliary_requirer.consume_auxiliary_relation_data( | ||
event.relation.id, | ||
) | ||
def _on_auxiliary_unavailable(self, event: AuxiliaryUnavailableEvent) -> None: | ||
# Handle the situation where the auxiliary integration is broken | ||
... | ||
``` | ||
As shown above, the library offers custom juju event to handle the specific | ||
situation, which are listed below: | ||
- auxiliary_ready: event emitted when the auxiliary data is ready for | ||
requirer charm to use. | ||
- auxiliary_unavailable: event emitted when the auxiliary integration is broken. | ||
Additionally, the requirer charmed operator needs to declare the `auxiliary` | ||
interface in the `metadata.yaml`: | ||
```yaml | ||
requires: | ||
glauth-auxiliary: | ||
interface: glauth_auxiliary | ||
limit: 1 | ||
``` | ||
## Provider Charm | ||
The provider charm is expected to: | ||
- Listen to the custom juju event `AuxiliaryRequestedEvent` to provide the | ||
auxiliary data in the integration | ||
```python | ||
from charms.glauth_utils.v0.glauth_auxiliary import ( | ||
AuxiliaryProvider, | ||
AuxiliaryRequestedEvent, | ||
) | ||
class ProviderCharm(CharmBase): | ||
# Auxiliary provider charm. | ||
def __init__(self, *args): | ||
super().__init__(*args) | ||
self.auxiliary_provider = AuxiliaryProvider(self) | ||
self.framework.observe( | ||
self.auxiliary_provider.on.auxiliary_requested, | ||
self._on_auxiliary_requested, | ||
) | ||
def _on_auxiliary_requested(self, event: AuxiliaryRequestedEvent) -> None: | ||
# Prepare the auxiliary data | ||
auxiliary_data = ... | ||
# Update the integration data | ||
self.auxiliary_provider.update_relation_app_data( | ||
relation.id, | ||
auxiliary_data, | ||
) | ||
``` | ||
As shown above, the library offers custom juju event to handle the specific | ||
situation, which are listed below: | ||
- auxiliary_requested: event emitted when the requirer charm integrates with | ||
the provider charm | ||
""" | ||
|
||
from functools import wraps | ||
from typing import Any, Callable, Optional, Union | ||
|
||
from ops.charm import ( | ||
CharmBase, | ||
RelationBrokenEvent, | ||
RelationChangedEvent, | ||
RelationCreatedEvent, | ||
RelationEvent, | ||
) | ||
from ops.framework import EventSource, Object, ObjectEvents | ||
from pydantic import BaseModel, ConfigDict | ||
|
||
# The unique Charmhub library identifier, never change it | ||
LIBID = "8c3a907cf23345ea8be7fccfe15b2cf7" | ||
|
||
# Increment this major API version when introducing breaking changes | ||
LIBAPI = 0 | ||
|
||
# Increment this PATCH version before using `charmcraft publish-lib` or reset | ||
# to 0 if you are raising the major API version | ||
LIBPATCH = 1 | ||
|
||
PYDEPS = ["pydantic~=2.5.3"] | ||
|
||
DEFAULT_RELATION_NAME = "glauth-auxiliary" | ||
|
||
|
||
def leader_unit(func: Callable) -> Callable: | ||
@wraps(func) | ||
def wrapper( | ||
obj: Union["AuxiliaryProvider", "AuxiliaryRequirer"], | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> Any: | ||
if not obj.unit.is_leader(): | ||
return None | ||
|
||
return func(obj, *args, **kwargs) | ||
|
||
return wrapper | ||
|
||
|
||
class AuxiliaryData(BaseModel): | ||
model_config = ConfigDict(frozen=True) | ||
|
||
database: str | ||
endpoint: str | ||
username: str | ||
password: str | ||
|
||
|
||
class AuxiliaryRequestedEvent(RelationEvent): | ||
"""An event emitted when the auxiliary integration is built.""" | ||
|
||
|
||
class AuxiliaryReadyEvent(RelationEvent): | ||
"""An event emitted when the auxiliary data is ready.""" | ||
|
||
|
||
class AuxiliaryUnavailableEvent(RelationEvent): | ||
"""An event emitted when the auxiliary integration is unavailable.""" | ||
|
||
|
||
class AuxiliaryProviderEvents(ObjectEvents): | ||
auxiliary_requested = EventSource(AuxiliaryRequestedEvent) | ||
|
||
|
||
class AuxiliaryRequirerEvents(ObjectEvents): | ||
auxiliary_ready = EventSource(AuxiliaryReadyEvent) | ||
auxiliary_unavailable = EventSource(AuxiliaryUnavailableEvent) | ||
|
||
|
||
class AuxiliaryProvider(Object): | ||
on = AuxiliaryProviderEvents() | ||
|
||
def __init__( | ||
self, | ||
charm: CharmBase, | ||
relation_name: str = DEFAULT_RELATION_NAME, | ||
) -> None: | ||
super().__init__(charm, relation_name) | ||
|
||
self.charm = charm | ||
self.app = charm.app | ||
self.unit = charm.unit | ||
self._relation_name = relation_name | ||
|
||
self.framework.observe( | ||
self.charm.on[self._relation_name].relation_created, | ||
self._on_relation_created, | ||
) | ||
|
||
@leader_unit | ||
def _on_relation_created(self, event: RelationCreatedEvent) -> None: | ||
"""Handle the event emitted when an auxiliary integration is created.""" | ||
self.on.auxiliary_requested.emit(event.relation) | ||
|
||
@leader_unit | ||
def update_relation_app_data( | ||
self, /, data: AuxiliaryData, relation_id: Optional[int] = None | ||
) -> None: | ||
"""An API for the provider charm to provide the auxiliary data.""" | ||
if not (relations := self.charm.model.relations.get(self._relation_name)): | ||
return | ||
|
||
if relation_id is not None: | ||
relations = [relation for relation in relations if relation.id == relation_id] | ||
|
||
for relation in relations: | ||
relation.data[self.app].update(data.model_dump()) | ||
|
||
|
||
class AuxiliaryRequirer(Object): | ||
on = AuxiliaryRequirerEvents() | ||
|
||
def __init__( | ||
self, | ||
charm: CharmBase, | ||
relation_name: str = DEFAULT_RELATION_NAME, | ||
) -> None: | ||
super().__init__(charm, relation_name) | ||
|
||
self.charm = charm | ||
self.app = charm.app | ||
self.unit = charm.unit | ||
self._relation_name = relation_name | ||
|
||
self.framework.observe( | ||
self.charm.on[self._relation_name].relation_changed, | ||
self._on_relation_changed, | ||
) | ||
self.framework.observe( | ||
self.charm.on[self._relation_name].relation_broken, | ||
self._on_auxiliary_relation_broken, | ||
) | ||
|
||
@leader_unit | ||
def _on_relation_changed(self, event: RelationChangedEvent) -> None: | ||
"""Handle the event emitted when auxiliary data is ready.""" | ||
if not event.relation.data.get(event.relation.app): | ||
return | ||
|
||
self.on.auxiliary_ready.emit(event.relation) | ||
|
||
def _on_auxiliary_relation_broken(self, event: RelationBrokenEvent) -> None: | ||
"""Handle the event emitted when the auxiliary integration is broken.""" | ||
self.on.auxiliary_unavailable.emit(event.relation) | ||
|
||
def consume_auxiliary_relation_data( | ||
self, | ||
/, | ||
relation_id: Optional[int] = None, | ||
) -> Optional[AuxiliaryData]: | ||
"""An API for the requirer charm to consume the auxiliary data.""" | ||
if not (relation := self.charm.model.get_relation(self._relation_name, relation_id)): | ||
return None | ||
|
||
if not (auxiliary_data := relation.data.get(relation.app)): | ||
return None | ||
|
||
return AuxiliaryData(**auxiliary_data) if auxiliary_data else None |