Skip to content

Commit

Permalink
Introduce show-proxied-endpoints action
Browse files Browse the repository at this point in the history
* Introduce the 'show-proxied-endpoints' action to expose the URLs
  provided by the applications related with traefik-k8s via the ingress
  and ingress-per-unit relation interfaces
* Updated the ingress library to expose on the Provider side which URLs
  are provided to which applications
* Updated the ingress_per_unit library to expose on the Provider side which URLs
  are provided to which units
  • Loading branch information
Michele Mancioppi committed Mar 2, 2022
1 parent ed8e2b8 commit 788a636
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 3 deletions.
3 changes: 3 additions & 0 deletions actions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
show-proxied-endpoints:
description: |
Returns a list of endpoints proxied by this Traefik proxy.
25 changes: 24 additions & 1 deletion lib/charms/traefik_k8s/v0/ingress.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _handle_ingress(self, event):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 1
LIBPATCH = 2

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -163,6 +163,29 @@ def is_failed(self, relation: Relation = None):
raise RelationDataMismatchError(relation, other_app)
return False

@property
def proxied_endpoints(self):
"""Returns the ingress settings provided to applications by this IngressPerAppProvider.
For example, when this IngressPerAppProvider has provided the
`http://foo.bar/my-model.my-app` URL to the my-app application, the returned dictionary
will be:
```
{
"my-app": {
"url": "http://foo.bar/my-model.my-app"
}
}
```
"""
return {
ingress_relation.app.name: self.unwrap(ingress_relation)[self.charm.app].get(
"ingress", {}
)
for ingress_relation in self.charm.model.relations[self.endpoint]
}


class IngressPerAppRequest:
"""A request for per-application ingress."""
Expand Down
28 changes: 27 additions & 1 deletion lib/charms/traefik_k8s/v0/ingress_per_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def _handle_ingress_per_unit(self, event):

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 2
LIBPATCH = 3

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -169,6 +169,32 @@ def is_failed(self, relation: Relation = None):
raise RelationDataMismatchError(relation, unit)
return False

@property
def proxied_endpoints(self):
"""Returns the ingress settings provided to units by this IngressPerUnitProvider.
For example, when this IngressPerUnitProvider has provided the
`http://foo.bar/my-model.my-app-1` and `http://foo.bar/my-model.my-app-2` URLs to
the two units of the my-app application, the returned dictionary will be:
```
{
"my-app/1": {
"url": "http://foo.bar/my-model.my-app-1"
},
"my-app/2": {
"url": "http://foo.bar/my-model.my-app-2"
}
}
```
"""
results = {}

for ingress_relation in self.charm.model.relations[self.endpoint]:
results.update(self.unwrap(ingress_relation)[self.charm.app].get("ingress", {}))

return results


class IngressRequest:
"""A request for per-unit ingress."""
Expand Down
18 changes: 18 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""Charm Traefik."""

import enum
import json
import logging
from typing import Optional, Tuple

Expand All @@ -16,6 +17,7 @@
from lightkube import Client
from lightkube.resources.core_v1 import Service
from ops.charm import (
ActionEvent,
CharmBase,
ConfigChangedEvent,
PebbleReadyEvent,
Expand Down Expand Up @@ -99,6 +101,22 @@ def __init__(self, *args):
self.framework.observe(self.ingress_per_unit.on.failed, self._handle_ingress_failure)
self.framework.observe(self.ingress_per_unit.on.broken, self._handle_ingress_broken)

# Action handlers
self.framework.observe(
self.on.show_proxied_endpoints_action, self._on_show_proxied_endpoints
)

def _on_show_proxied_endpoints(self, event: ActionEvent):
try:
result = {}
result.update(self.ingress_per_unit.proxied_endpoints)
result.update(self.ingress_per_app.proxied_endpoints)

event.set_results({"proxied-endpoints": json.dumps(result)})
except Exception as e:
logger.exception("Action 'show-proxied-endpoints' failed")
event.fail(str(e))

def _on_traefik_pebble_ready(self, _: PebbleReadyEvent):
# The the Traefik container comes up, e.g., after a pod churn, we
# ignore the unit status and start fresh.
Expand Down
63 changes: 62 additions & 1 deletion tests/unit/test_charm.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

import json
import unittest
from unittest.mock import patch
from unittest.mock import Mock, patch

import yaml
from ops.charm import ActionEvent
from ops.model import ActiveStatus, BlockedStatus, WaitingStatus
from ops.testing import Harness
from test_lib_helpers import MockIPARequirer, MockIPURequirer
Expand Down Expand Up @@ -373,3 +375,62 @@ def test_relation_broken(self):
raise Exception("The line above should fail")
except FileNotFoundError:
pass

@patch("charm._get_loadbalancer_status", lambda **unused: None)
@patch("charm.KubernetesServicePatch", lambda **unused: None)
def test_show_proxied_endpoints_action_no_relations(self):
self.harness.begin_with_initial_hooks()

action_event = Mock(spec=ActionEvent)
self.harness.charm._on_show_proxied_endpoints(action_event)
action_event.set_results.assert_called_once_with({"proxied-endpoints": "{}"})

@patch("charm._get_loadbalancer_status", lambda **unused: None)
@patch("charm.KubernetesServicePatch", lambda **unused: None)
def test_show_proxied_endpoints_action_only_ingress_per_app_relations(self):
self.harness.set_leader(True)
self.harness.update_config({"external_hostname": "testhostname"})
self.harness.begin_with_initial_hooks()

requirer = MockIPARequirer(self.harness)
requirer.relate()
requirer.request(host="10.0.0.1", port=3000)

self.harness.container_pebble_ready("traefik")

action_event = Mock(spec=ActionEvent)
self.harness.charm._on_show_proxied_endpoints(action_event)
action_event.set_results.assert_called_once_with(
{
"proxied-endpoints": json.dumps(
{"ingress-remote": {"url": "http://testhostname:80/test-model-ingress-remote"}}
)
}
)

@patch("charm._get_loadbalancer_status", lambda **unused: None)
@patch("charm.KubernetesServicePatch", lambda **unused: None)
def test_show_proxied_endpoints_action_only_ingress_per_unit_relations(self):
self.harness.set_leader(True)
self.harness.update_config({"external_hostname": "testhostname"})
self.harness.begin_with_initial_hooks()

requirer = MockIPURequirer(self.harness)
requirer.relate()
requirer.request(host="10.0.0.1", port=3000)

self.harness.container_pebble_ready("traefik")

action_event = Mock(spec=ActionEvent)
self.harness.charm._on_show_proxied_endpoints(action_event)
action_event.set_results.assert_called_once_with(
{
"proxied-endpoints": json.dumps(
{
"ingress-per-unit-remote/0": {
"url": "http://testhostname:80/test-model-ingress-per-unit-remote-0"
}
}
)
}
)

0 comments on commit 788a636

Please sign in to comment.