Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Redis Observer #213

Merged
merged 4 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .woke.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ ignore_files:
- tests/unit/test_synapse_workload.py
- docs/reference/integrations.md
rules:
# Ignore "master" - While https://github.com/canonical/redis-k8s-operator/pull/78
# is not merged
- name: master
# Ignore "grandfathered" used by SAML configuration.
- name: grandfathered
# Ignore "whitelist" used by Synapse configuration.
Expand Down
151 changes: 151 additions & 0 deletions lib/charms/redis_k8s/v0/redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Library for the redis relation.

This library contains the Requires and Provides classes for handling the
redis interface.

Import `RedisRequires` in your charm by adding the following to `src/charm.py`:
```
from charms.redis_k8s.v0.redis import RedisRequires
```
Define the following attributes in charm charm class for the library to be able to work with it
```
_stored = StoredState()
on = RedisRelationCharmEvents()
```
And then in your charm's `__init__` method:
```
# Make sure you set redis_relation in StoredState. Assuming you refer to this
# as `self._stored`:
self._stored.set_default(redis_relation={})
self.redis = RedisRequires(self, self._stored)
```
And then wherever you need to reference the relation data it will be available
in the property `relation_data`:
```
redis_host = self.redis.relation_data.get("hostname")
redis_port = self.redis.relation_data.get("port")
```
You will also need to add the following to `metadata.yaml`:
```
requires:
redis:
interface: redis
```
"""
import logging
import socket
from typing import Dict, Optional

from ops.charm import CharmEvents
from ops.framework import EventBase, EventSource, Object

# The unique Charmhub library identifier, never change it.
LIBID = "fe18a608cec5465fa5153e419abcad7b"

# 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 = 5

logger = logging.getLogger(__name__)

DEFAULT_REALTION_NAME = "redis"

class RedisRelationUpdatedEvent(EventBase):
"""An event for the redis relation having been updated."""


class RedisRelationCharmEvents(CharmEvents):
"""A class to carry custom charm events so requires can react to relation changes."""
redis_relation_updated = EventSource(RedisRelationUpdatedEvent)


class RedisRequires(Object):

def __init__(self, charm, _stored, relation_name: str = DEFAULT_REALTION_NAME):
"""A class implementing the redis requires relation."""
super().__init__(charm, relation_name)
self.framework.observe(charm.on[relation_name].relation_joined, self._on_relation_changed)
self.framework.observe(charm.on[relation_name].relation_changed, self._on_relation_changed)
self.framework.observe(charm.on[relation_name].relation_broken, self._on_relation_broken)
self._stored = _stored
self.charm = charm
self.relation_name = relation_name

def _on_relation_changed(self, event):
"""Handle the relation changed event."""
if not event.unit:
return

hostname = event.relation.data[event.unit].get("hostname")
port = event.relation.data[event.unit].get("port")
self._stored.redis_relation[event.relation.id] = {"hostname": hostname, "port": port}

# Trigger an event that our charm can react to.
self.charm.on.redis_relation_updated.emit()

def _on_relation_broken(self, event):
"""Handle the relation broken event."""
# Remove the unit data from local state.
self._stored.redis_relation.pop(event.relation.id, None)

# Trigger an event that our charm can react to.
self.charm.on.redis_relation_updated.emit()

@property
def relation_data(self) -> Optional[Dict[str, str]]:
"""Retrieve the relation data.

Returns:
Dict: dict containing the relation data.
"""
relation = self.model.get_relation(self.relation_name)
if not relation or not relation.units:
return None
unit = next(iter(relation.units))
return relation.data[unit]

@property
def url(self) -> Optional[str]:
"""Retrieve the Redis URL.

Returns:
str: the Redis URL.
"""
relation_data = self.relation_data
if not relation_data:
return None
redis_host = relation_data.get("hostname")
redis_port = relation_data.get("port")
return f"redis://{redis_host}:{redis_port}"


class RedisProvides(Object):
def __init__(self, charm, port):
"""A class implementing the redis provides relation."""
super().__init__(charm, DEFAULT_REALTION_NAME)
self.framework.observe(charm.on.redis_relation_changed, self._on_relation_changed)
self._port = port
self._charm = charm

def _on_relation_changed(self, event):
"""Handle the relation changed event."""
event.relation.data[self.model.unit]["hostname"] = self._get_master_ip()
event.relation.data[self.model.unit]["port"] = str(self._port)
# The reactive Redis charm also exposes 'password'. When tackling
# https://github.com/canonical/redis-k8s/issues/7 add 'password'
# field so that it matches the exposed interface information from it.
# event.relation.data[self.unit]['password'] = ''

def _bind_address(self, event):
"""Convenience function for getting the unit address."""
relation = self.model.get_relation(event.relation.name, event.relation.id)
if address := self.model.get_binding(relation).network.bind_address:
return address
return self.app.name

def _get_master_ip(self) -> str:
"""Gets the ip of the current redis master."""
amandahla marked this conversation as resolved.
Show resolved Hide resolved
return socket.gethostbyname(self._charm.current_master)
4 changes: 4 additions & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ requires:
interface: s3
limit: 1
optional: true
redis:
interface: redis
limit: 1
optional: true

peers:
synapse-peers:
Expand Down
4 changes: 2 additions & 2 deletions src-docs/charm.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Charm for Synapse on kubernetes.
## <kbd>class</kbd> `SynapseCharm`
Charm the service.

<a href="../src/charm.py#L41"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm.py#L42"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down Expand Up @@ -69,7 +69,7 @@ Unit that this execution is responsible for.

---

<a href="../src/charm.py#L99"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm.py#L101"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `change_config`

Expand Down
42 changes: 42 additions & 0 deletions src-docs/redis_observer.py.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!-- markdownlint-disable -->

<a href="../src/redis_observer.py#L0"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

# <kbd>module</kbd> `redis_observer.py`
The Redis agent relation observer.



---

## <kbd>class</kbd> `RedisObserver`
The Redis relation observer.

Attrs: on: listen to Redis events. _stored: stored state.

<a href="../src/redis_observer.py#L26"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

```python
__init__(charm: CharmBase)
```

Initialize the observer and register event handlers.



**Args:**

- <b>`charm`</b>: The parent charm to attach the observer to.


---

#### <kbd>property</kbd> model

Shortcut for more simple access the model.




2 changes: 2 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from mjolnir import Mjolnir
from observability import Observability
from pebble import PebbleService, PebbleServiceError
from redis_observer import RedisObserver
from saml_observer import SAMLObserver
from smtp_observer import SMTPObserver
from user import User
Expand All @@ -49,6 +50,7 @@ def __init__(self, *args: typing.Any) -> None:
self._database = DatabaseObserver(self)
self._saml = SAMLObserver(self)
self._smtp = SMTPObserver(self)
self._redis = RedisObserver(self)
try:
self._charm_state = CharmState.from_charm(
charm=self,
Expand Down
43 changes: 43 additions & 0 deletions src/redis_observer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""The Redis agent relation observer."""

import logging

from charms.redis_k8s.v0.redis import RedisRelationCharmEvents, RedisRequires
from ops.charm import CharmBase, HookEvent
from ops.framework import Object, StoredState

logger = logging.getLogger(__name__)


class RedisObserver(Object):
"""The Redis relation observer.

Attrs:
on: listen to Redis events.
_stored: stored state.
"""

on = RedisRelationCharmEvents()
_stored = StoredState()

def __init__(self, charm: CharmBase):
"""Initialize the observer and register event handlers.

Args:
charm: The parent charm to attach the observer to.
"""
super().__init__(charm, "redis-observer")
self._charm = charm
self._stored.set_default(
redis_relation={},
)
self._stored.set_default(redis_relation={})
self.redis = RedisRequires(self._charm, self._stored)
self.framework.observe(self.on.redis_relation_updated, self._on_redis_relation_updated)

def _on_redis_relation_updated(self, _: HookEvent) -> None:
"""Handle redis relation updated event."""
logger.info("Redis relation changed.")
Loading