Skip to content

Commit

Permalink
Merge pull request #56 from canonical/IAM-673-integrate-admin-ui-with…
Browse files Browse the repository at this point in the history
…-oathkeeper

feat: add oathkeeper-info interface
  • Loading branch information
natalian98 authored Feb 13, 2024
2 parents a279163 + 587e18c commit 1df6989
Show file tree
Hide file tree
Showing 7 changed files with 387 additions and 4 deletions.
161 changes: 161 additions & 0 deletions lib/charms/oathkeeper/v0/oathkeeper_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""Interface library for sharing oathkeeper info.
This library provides a Python API for both requesting and providing oathkeeper deployment info,
such as endpoints, namespace and ConfigMap details.
## Getting Started
To get started using the library, you need to fetch the library using `charmcraft`.
```shell
cd some-charm
charmcraft fetch-lib charms.oathkeeper.v0.oathkeeper_info
```
To use the library from the requirer side:
In the `metadata.yaml` of the charm, add the following:
```yaml
requires:
oathkeeper-info:
interface: oathkeeper_info
limit: 1
```
Then, to initialise the library:
```python
from charms.oathkeeper.v0.oathkeeper_info import OathkeeperInfoRequirer
Class SomeCharm(CharmBase):
def __init__(self, *args):
self.oathkeeper_info_relation = OathkeeperInfoRequirer(self)
self.framework.observe(self.on.some_event_emitted, self.some_event_function)
def some_event_function():
# fetch the relation info
if self.oathkeeper_info_relation.is_ready():
oathkeeper_data = self.oathkeeper_info_relation.get_oathkeeper_info()
```
"""

import logging
from typing import Dict, Optional

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

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

# 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

RELATION_NAME = "oathkeeper-info"
INTERFACE_NAME = "oathkeeper_info"
logger = logging.getLogger(__name__)


class OathkeeperInfoRelationCreatedEvent(EventBase):
"""Event to notify the charm that the relation is ready."""


class OathkeeperInfoProviderEvents(ObjectEvents):
"""Event descriptor for events raised by `OathkeeperInfoProvider`."""

ready = EventSource(OathkeeperInfoRelationCreatedEvent)


class OathkeeperInfoProvider(Object):
"""Provider side of the oathkeeper-info relation."""

on = OathkeeperInfoProviderEvents()

def __init__(self, charm: CharmBase, relation_name: str = RELATION_NAME):
super().__init__(charm, relation_name)

self._charm = charm
self._relation_name = relation_name

events = self._charm.on[relation_name]
self.framework.observe(events.relation_created, self._on_info_provider_relation_created)

def _on_info_provider_relation_created(self, event: RelationCreatedEvent) -> None:
self.on.ready.emit()

def send_info_relation_data(
self,
public_endpoint: str,
rules_configmap_name: str,
configmaps_namespace: str,
) -> None:
"""Updates relation with endpoints and configmaps info."""
if not self._charm.unit.is_leader():
return

relations = self.model.relations[self._relation_name]
info_databag = {
"public_endpoint": public_endpoint,
"rules_configmap_name": rules_configmap_name,
"configmaps_namespace": configmaps_namespace,
}

for relation in relations:
relation.data[self._charm.app].update(info_databag)


class OathkeeperInfoRelationError(Exception):
"""Base class for the relation exceptions."""

pass


class OathkeeperInfoRelationMissingError(OathkeeperInfoRelationError):
"""Raised when the relation is missing."""

def __init__(self) -> None:
self.message = "Missing oathkeeper-info relation with oathkeeper"
super().__init__(self.message)


class OathkeeperInfoRelationDataMissingError(OathkeeperInfoRelationError):
"""Raised when information is missing from the relation."""

def __init__(self, message: str) -> None:
self.message = message
super().__init__(self.message)


class OathkeeperInfoRequirer(Object):
"""Requirer side of the oathkeeper-info relation."""

def __init__(self, charm: CharmBase, relation_name: str = RELATION_NAME):
super().__init__(charm, relation_name)
self._charm = charm
self._relation_name = relation_name

def is_ready(self) -> bool:
"""Checks whether the relation data is ready.
Returns True when Oathkeeper shared the config; False otherwise.
"""
relation = self.model.get_relation(self._relation_name)
if not relation or not relation.app or not relation.data[relation.app]:
return False
return True

def get_oathkeeper_info(self) -> Optional[Dict]:
"""Get the oathkeeper info."""
info = self.model.relations[self._relation_name]
if len(info) == 0:
raise OathkeeperInfoRelationMissingError()

if not (app := info[0].app):
raise OathkeeperInfoRelationMissingError()

data = info[0].data[app]

if not data:
logger.info("No relation data available.")
raise OathkeeperInfoRelationDataMissingError("Missing relation data")

return data
4 changes: 4 additions & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ provides:
interface: auth_proxy
forward-auth:
interface: forward_auth
oathkeeper-info:
interface: oathkeeper_info
description: |
Provides oathkeeper deployment info to a related application
requires:
kratos-endpoint-info:
interface: kratos_endpoints
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ profile = "black"
max-line-length = 99
max-doc-length = 99
max-complexity = 10
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"]
exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv", "tests/unit/capture_events.py"]
select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore W503, E501 because using black creates errors with this
# Ignore D107 Missing docstring in __init__
Expand Down
61 changes: 59 additions & 2 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
ForwardAuthRelationRemovedEvent,
InvalidForwardAuthConfigEvent,
)
from charms.oathkeeper.v0.oathkeeper_info import (
OathkeeperInfoProvider,
OathkeeperInfoRelationCreatedEvent,
)
from charms.observability_libs.v0.cert_handler import CertChanged, CertHandler
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
from charms.traefik_k8s.v2.ingress import (
Expand All @@ -48,6 +52,7 @@
PebbleReadyEvent,
RelationChangedEvent,
RemoveEvent,
UpdateStatusEvent,
)
from ops.main import main
from ops.model import (
Expand Down Expand Up @@ -111,6 +116,7 @@ def __init__(self, *args):
relation_name=self._forward_auth_relation_name,
forward_auth_config=self._forward_auth_config,
)
self.info_provider = OathkeeperInfoProvider(self)

self.service_patcher = KubernetesServicePatch(
self, [("oathkeeper-api", OATHKEEPER_API_PORT)]
Expand Down Expand Up @@ -140,6 +146,7 @@ def __init__(self, *args):
self.framework.observe(self.on.oathkeeper_pebble_ready, self._on_oathkeeper_pebble_ready)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.update_status, self._on_update_status)
self.framework.observe(self.on.remove, self._on_remove)

self.framework.observe(
Expand All @@ -160,6 +167,10 @@ def __init__(self, *args):
self._on_forward_auth_relation_removed,
)

self.framework.observe(
self.info_provider.on.ready, self._on_oathkeeper_info_relation_ready
)

self.framework.observe(self.cert_handler.on.cert_changed, self._on_cert_changed)

self.framework.observe(self.on.list_rules_action, self._on_list_rules_action)
Expand Down Expand Up @@ -231,9 +242,21 @@ def _oathkeeper_service_is_running(self) -> bool:
return service.is_running()

@property
def _forward_auth_config(self) -> ForwardAuthConfig:
def _scheme(self) -> str:
scheme = "https" if self._is_tls_ready() and not self.config["dev"] else "http"
decisions_url = f"{scheme}://{self.app.name}.{self.model.name}.svc.cluster.local:{OATHKEEPER_API_PORT}/decisions"
return scheme

@property
def _public_endpoint(self) -> str:
public_endpoint = (
self.ingress.url
or f"{self._scheme}://{self.app.name}.{self.model.name}.svc.cluster.local:{OATHKEEPER_API_PORT}"
)
return public_endpoint

@property
def _forward_auth_config(self) -> ForwardAuthConfig:
decisions_url = f"{self._scheme}://{self.app.name}.{self.model.name}.svc.cluster.local:{OATHKEEPER_API_PORT}/decisions"
return ForwardAuthConfig(
decisions_address=decisions_url,
app_names=self.auth_proxy.get_app_names(),
Expand Down Expand Up @@ -292,6 +315,15 @@ def _update_config(self) -> None:
self.cert_handler.cert, self.cert_handler.key, self.cert_handler.ca
)

def _update_oathkeeper_info_relation_data(self, event: HookEvent) -> None:
logger.info("Sending oathkeeper info")

self.info_provider.send_info_relation_data(
public_endpoint=self._public_endpoint,
rules_configmap_name=self.access_rules_configmap.name,
configmaps_namespace=self.model.name,
)

def _get_kratos_endpoint_info(self, key: str) -> Optional[str]:
if not self.model.relations[self._kratos_relation_name]:
logger.info("Kratos relation not found")
Expand Down Expand Up @@ -392,6 +424,9 @@ def _on_oathkeeper_pebble_ready(self, event: PebbleReadyEvent) -> None:
def _on_config_changed(self, event: ConfigChangedEvent):
self.forward_auth.update_forward_auth_config(self._forward_auth_config)

def _on_update_status(self, event: UpdateStatusEvent) -> None:
self._update_oathkeeper_info_relation_data(event)

def _on_remove(self, event: RemoveEvent) -> None:
if not self.unit.is_leader():
return
Expand All @@ -401,14 +436,36 @@ def _on_remove(self, event: RemoveEvent) -> None:
def _on_kratos_relation_changed(self, event: RelationChangedEvent) -> None:
self._handle_status_update_config(event)

def _on_oathkeeper_info_relation_ready(
self, event: OathkeeperInfoRelationCreatedEvent
) -> None:
self._update_oathkeeper_info_relation_data(event)

if not self._container.can_connect():
logger.info(f"Cannot connect to Oathkeeper container. Deferring the {event} event.")
event.defer()
return

if not self._container.exists("/etc/config/access-rules/admin_ui_rules.json"):
# Create an empty configMap key for admin ui
# to make sure it will be enlisted in oathkeeper config
patch = {"data": {"admin_ui_rules.json": ""}}
self.access_rules_configmap.patch(patch=patch, cm_name="access-rules")

self._handle_status_update_config(event)

def _on_ingress_ready(self, event: IngressPerAppReadyEvent) -> None:
if self.unit.is_leader():
logger.info(f"This app's ingress URL: {event.url}")

self._update_oathkeeper_info_relation_data(event)

def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent) -> None:
if self.unit.is_leader():
logger.info("This app no longer has ingress")

self._update_oathkeeper_info_relation_data(event)

def _on_cert_changed(self, event: CertChanged) -> None:
if not self._container.can_connect():
logger.info(f"Cannot connect to Oathkeeper container. Deferring the {event} event.")
Expand Down
Loading

0 comments on commit 1df6989

Please sign in to comment.