Skip to content

Commit

Permalink
Add update method (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
sed-i authored Oct 4, 2023
1 parent c10a6f0 commit 87578f3
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 6 deletions.
13 changes: 9 additions & 4 deletions charm/lib/charms/catalogue_k8s/v0/catalogue.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ipaddress
import logging
import socket
import warnings
from typing import List, Optional, Union

from ops.charm import CharmBase
Expand All @@ -21,10 +22,7 @@


class CatalogueItem:
"""`CatalogueItem` represents an application entry sent to a catalogue.
The icon is an iconify mdi string; see https://icon-sets.iconify.design/mdi.
"""
"""`CatalogueItem` represents an application entry sent to a catalogue."""

def __init__(self, name: str, url: str, icon: str, description: str = ""):
self.name = name
Expand Down Expand Up @@ -57,6 +55,13 @@ def __init__(

self._register_refresh_event(refresh_event)

warnings.warn(
"charms.catalogue_k8s.v0.catalogue is deprecated. "
"Use charms.catalogue_k8s.v1.catalogue instead. "
"For more details, see https://github.com/canonical/catalogue-k8s-operator/issues/41.",
category=DeprecationWarning,
)

def _register_refresh_event(
self, refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None
):
Expand Down
164 changes: 164 additions & 0 deletions charm/lib/charms/catalogue_k8s/v1/catalogue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Copyright 2021 Canonical Ltd.
# See LICENSE file for licensing details.

"""Charm for providing services catalogues to bundles or sets of charms."""

import ipaddress
import logging
import socket
from typing import Optional

from ops.charm import CharmBase
from ops.framework import EventBase, EventSource, Object, ObjectEvents

LIBID = "fa28b361293b46668bcd1f209ada6983"
LIBAPI = 1
LIBPATCH = 0

DEFAULT_RELATION_NAME = "catalogue"

logger = logging.getLogger(__name__)


class CatalogueItem:
"""`CatalogueItem` represents an application entry sent to a catalogue.
The icon is an iconify mdi string; see https://icon-sets.iconify.design/mdi.
"""

def __init__(self, name: str, url: str, icon: str, description: str = ""):
self.name = name
self.url = url
self.icon = icon
self.description = description


class CatalogueConsumer(Object):
"""`CatalogueConsumer` is used to send over a `CatalogueItem`."""

def __init__(
self,
charm,
relation_name: str = DEFAULT_RELATION_NAME,
item: Optional[CatalogueItem] = None,
):
super().__init__(charm, relation_name)
self._charm = charm
self._relation_name = relation_name
self._item = item

events = self._charm.on[self._relation_name]
self.framework.observe(events.relation_joined, self._on_relation_changed)
self.framework.observe(events.relation_broken, self._on_relation_changed)
self.framework.observe(events.relation_changed, self._on_relation_changed)
self.framework.observe(events.relation_departed, self._on_relation_changed)
self.framework.observe(events.relation_created, self._on_relation_changed)

def _on_relation_changed(self, _):
self._update_relation_data()

def _update_relation_data(self):
if not self._charm.unit.is_leader():
return

if not self._item:
return

for relation in self._charm.model.relations[self._relation_name]:
relation.data[self._charm.model.app]["name"] = self._item.name
relation.data[self._charm.model.app]["description"] = self._item.description
relation.data[self._charm.model.app]["url"] = self.unit_address(relation)
relation.data[self._charm.model.app]["icon"] = self._item.icon

def update_item(self, item: CatalogueItem):
"""Update the catalogue item."""
self._item = item
self._update_relation_data()

def unit_address(self, relation):
"""The unit address of the consumer, on which it is reachable.
Requires ingress to be connected for it to be routable.
"""
if self._item and self._item.url:
return self._item.url

unit_ip = str(self._charm.model.get_binding(relation).network.bind_address)
if self._is_valid_unit_address(unit_ip):
return unit_ip

return socket.getfqdn()

def _is_valid_unit_address(self, address: str) -> bool:
"""Validate a unit address.
At present only IP address validation is supported, but
this may be extended to DNS addresses also, as needed.
Args:
address: a string representing a unit address
"""
try:
_ = ipaddress.ip_address(address)
except ValueError:
return False

return True


class CatalogueItemsChangedEvent(EventBase):
"""Event emitted when the catalogue entries change."""

def __init__(self, handle, items):
super().__init__(handle)
self.items = items

def snapshot(self):
"""Save catalogue entries information."""
return {"items": self.items}

def restore(self, snapshot):
"""Restore catalogue entries information."""
self.items = snapshot["items"]


class CatalogueEvents(ObjectEvents):
"""Events raised by `CatalogueConsumer`."""

items_changed = EventSource(CatalogueItemsChangedEvent)


class CatalogueProvider(Object):
"""`CatalogueProvider` is the side of the relation that serves the actual service catalogue."""

on = CatalogueEvents() # pyright: ignore

def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME):
super().__init__(charm, relation_name)
self._charm = charm
self._relation_name = relation_name
events = self._charm.on[self._relation_name]
self.framework.observe(events.relation_changed, self._on_relation_changed)
self.framework.observe(events.relation_joined, self._on_relation_changed)
self.framework.observe(events.relation_departed, self._on_relation_changed)
self.framework.observe(events.relation_broken, self._on_relation_broken)

def _on_relation_broken(self, event):
self.on.items_changed.emit(items=self.items) # pyright: ignore

def _on_relation_changed(self, event):
self.on.items_changed.emit(items=self.items) # pyright: ignore

@property
def items(self):
"""A list of apps sent over relation data."""
return [
{
"name": relation.data[relation.app].get("name", ""),
"url": relation.data[relation.app].get("url", ""),
"icon": relation.data[relation.app].get("icon", ""),
"description": relation.data[relation.app].get("description", ""),
}
for relation in self._charm.model.relations[self._relation_name]
if relation.app and relation.units
]
2 changes: 1 addition & 1 deletion charm/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import socket
from urllib.parse import urlparse

from charms.catalogue_k8s.v0.catalogue import (
from charms.catalogue_k8s.v1.catalogue import (
CatalogueItemsChangedEvent,
CatalogueProvider,
)
Expand Down
2 changes: 1 addition & 1 deletion charm/tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from unittest.mock import patch

from charm import CatalogueCharm
from charms.catalogue_k8s.v0.catalogue import DEFAULT_RELATION_NAME
from charms.catalogue_k8s.v1.catalogue import DEFAULT_RELATION_NAME
from ops.model import ActiveStatus
from ops.testing import Harness

Expand Down

0 comments on commit 87578f3

Please sign in to comment.