Skip to content

Commit

Permalink
Update ingress after cert is written (#534)
Browse files Browse the repository at this point in the history
  • Loading branch information
sed-i authored Sep 29, 2023
1 parent ad1f936 commit 748a0fc
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
```python
# ...
from charms.alertmanager_k8s.v0.alertmanager_dispatch import AlertmanagerConsumer
from charms.alertmanager_k8s.v1.alertmanager_dispatch import AlertmanagerConsumer
class SomeApplication(CharmBase):
def __init__(self, *args):
Expand All @@ -25,11 +25,11 @@ def __init__(self, *args):
```
"""
import logging
import socket
from typing import Callable, List, Optional, Set
from typing import Dict, Optional, Set
from urllib.parse import urlparse

import ops
import pydantic
from ops.charm import CharmBase, RelationEvent, RelationJoinedEvent, RelationRole
from ops.framework import EventBase, EventSource, Object, ObjectEvents
from ops.model import Relation
Expand All @@ -38,18 +38,48 @@ def __init__(self, *args):
LIBID = "37f1ca6f8fe84e3092ebbf6dc2885310"

# Increment this major API version when introducing breaking changes
LIBAPI = 0
LIBAPI = 1

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

PYDEPS = ["pydantic"]

# Set to match metadata.yaml
INTERFACE_NAME = "alertmanager_dispatch"

logger = logging.getLogger(__name__)


class _ProviderSchemaV0(pydantic.BaseModel):
# Currently, the provider splits the URL and the consumer merges. That's why we switched to v1.
public_address: str
scheme: str = "http"


class _ProviderSchemaV1(pydantic.BaseModel):
url: str

# The following are v0 fields that are continued to be populated for backwards compatibility.
# TODO: when we switch to pydantic 2+, use computed_field instead of the following fields, and
# also drop the __init__.
# https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.computed_field
public_address: Optional[str] # v0 relic
scheme: Optional[str] # v0 relic

def __init__(self, **kwargs):
super().__init__(**kwargs)

parsed = urlparse(kwargs["url"])
port = ":" + str(parsed.port) if parsed.port else ""
public_address = f"{parsed.hostname}{port}{parsed.path}"

# Derive v0 fields from v1 field
self.public_address = public_address
self.scheme = parsed.scheme


class ClusterChanged(EventBase):
"""Event raised when an alertmanager cluster is changed.
Expand Down Expand Up @@ -119,7 +149,7 @@ class AlertmanagerConsumer(RelationManagerBase):
A typical example of importing this library might be
```python
from charms.alertmanager_k8s.v0.alertmanager_dispatch import AlertmanagerConsumer
from charms.alertmanager_k8s.v1.alertmanager_dispatch import AlertmanagerConsumer
```
In your charm's `__init__` method:
Expand Down Expand Up @@ -150,7 +180,7 @@ class AlertmanagerConsumer(RelationManagerBase):

on = AlertmanagerConsumerEvents() # pyright: ignore

def __init__(self, charm: CharmBase, relation_name: str = "alerting"):
def __init__(self, charm: CharmBase, *, relation_name: str = "alerting"):
super().__init__(charm, relation_name, RelationRole.requires)

self.framework.observe(
Expand Down Expand Up @@ -195,33 +225,27 @@ def _on_relation_changed(self, event: ops.charm.RelationChangedEvent):
# inform consumer about the change
self.on.cluster_changed.emit() # pyright: ignore

def get_cluster_info(self) -> List[str]:
"""Returns a list of addresses of all the alertmanager units."""
def get_cluster_info(self) -> Set[str]:
"""Returns a list of URLs of all alertmanager units."""
if not (relation := self.charm.model.get_relation(self.name)):
return []

alertmanagers: List[str] = []
for unit in relation.units:
address = relation.data[unit].get("public_address")
if address:
alertmanagers.append(address)
return sorted(alertmanagers)

def get_cluster_info_with_scheme(self) -> List[str]:
"""Returns a list of URLs of all the alertmanager units."""
# FIXME: in v1 of the lib:
# - use a dict {"url": ...} so it's extendable
# - change return value to Set[str]
if not (relation := self.charm.model.get_relation(self.name)):
return []
return set()

alertmanagers: Set[str] = set()
for unit in relation.units:
address = relation.data[unit].get("public_address")
scheme = relation.data[unit].get("scheme", "http")
if address:
alertmanagers.add(f"{scheme}://{address}")
return sorted(alertmanagers)
if rel_data := relation.data[unit]:
try: # v1
data = _ProviderSchemaV1(**rel_data)
except pydantic.ValidationError as ev1:
try: # v0
data = _ProviderSchemaV0(**rel_data)
except pydantic.ValidationError as ev0:
logger.warning("Relation data failed validation for v1: %s", ev1)
logger.warning("Relation data failed validation for v0: %s", ev0)
else:
alertmanagers.add(f"{data.scheme}://{data.public_address}")
else:
alertmanagers.add(data.url)
return alertmanagers

def _on_relation_departed(self, _):
"""This hook notifies the charm that there may have been changes to the cluster."""
Expand All @@ -248,47 +272,40 @@ class AlertmanagerProvider(RelationManagerBase):
A typical example of importing this library might be
```python
from charms.alertmanager_k8s.v0.alertmanager_dispatch import AlertmanagerProvider
from charms.alertmanager_k8s.v1.alertmanager_dispatch import AlertmanagerProvider
```
In your charm's `__init__` method:
```python
self.alertmanager_provider = AlertmanagerProvider(self, self._relation_name, self._api_port)
self.alertmanager_provider = AlertmanagerProvider(
self, relation_name=self._relation_name, external_url=f"http://{socket.getfqdn()}:9093"
)
```
Then inform consumers on any update to alertmanager cluster data via
```python
self.alertmanager_provider.update_relation_data()
self.alertmanager_provider.update(external_url=self.ingress.url)
```
This provider auto-registers relation events on behalf of the main Alertmanager charm.
Arguments:
charm (CharmBase): consumer charm
relation_name (str): relation name (not interface name)
api_port (int): alertmanager server's api port; this is needed here to avoid accessing
charm constructs directly
Attributes:
charm (CharmBase): the Alertmanager charm
charm: consumer charm
external_url: URL for this unit's workload API endpoint
relation_name: relation name (not interface name)
"""

def __init__(
self,
charm,
relation_name: str = "alerting",
api_port: int = 9093, # TODO: breaking change: drop this arg
charm: CharmBase,
*,
external_url: Optional[Callable] = None, # TODO: breaking change: make this mandatory
external_url: str,
relation_name: str = "alerting",
):
# TODO: breaking change: force keyword-only args from relation_name onwards
super().__init__(charm, relation_name, RelationRole.provides)

# We don't need to worry about the literal "http" here because the external_url arg is set
# by the charm. TODO: drop it after external_url becomes a mandatory arg.
self._external_url = external_url or (lambda: f"http://{socket.getfqdn()}:{api_port}")
self._external_url = external_url

events = self.charm.on[self.name]

Expand All @@ -302,9 +319,9 @@ def _on_relation_joined(self, event: RelationJoinedEvent):
This is needed for consumers such as prometheus, which should be aware of all alertmanager
instances.
"""
self.update_relation_data(event)
self._update_relation_data(event)

def _generate_relation_data(self, relation: Relation):
def _generate_relation_data(self, relation: Relation) -> Dict[str, str]:
"""Helper function to generate relation data in the correct format.
Addresses are without scheme.
Expand All @@ -314,13 +331,10 @@ def _generate_relation_data(self, relation: Relation):
# deduplicate so that the config file only has one entry, but ideally the
# "alertmanagers.[].static_configs.targets" section in the prometheus config should list
# all units.
parsed = urlparse(self._external_url())
return {
"public_address": f"{parsed.hostname}:{parsed.port or 80}{parsed.path}",
"scheme": parsed.scheme,
}
data = _ProviderSchemaV1(url=self._external_url)
return data.dict()

def update_relation_data(self, event: Optional[RelationEvent] = None):
def _update_relation_data(self, event: Optional[RelationEvent] = None):
"""Helper function for updating relation data bags.
This function can be used in two different ways:
Expand All @@ -346,3 +360,8 @@ def update_relation_data(self, event: Optional[RelationEvent] = None):
event.relation.data[self.charm.unit].update(
self._generate_relation_data(event.relation)
)

def update(self, *, external_url: str):
"""Update data pertaining to this relation manager (similar args to __init__)."""
self._external_url = external_url
self._update_relation_data()
32 changes: 20 additions & 12 deletions lib/charms/tempo_k8s/v0/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,17 @@ def __init__(self, *args):
""" # noqa: W505
import json
import logging
from typing import TYPE_CHECKING, List, Literal, MutableMapping, Optional, Tuple, cast
from typing import (
TYPE_CHECKING,
Any,
Dict,
List,
Literal,
MutableMapping,
Optional,
Tuple,
cast,
)

import pydantic
from ops.charm import (
Expand All @@ -83,7 +93,7 @@ def __init__(self, *args):

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

PYDEPS = ["pydantic<2.0"]

Expand Down Expand Up @@ -457,7 +467,7 @@ def relations(self) -> List[Relation]:
return self._charm.model.relations[self._relation_name]

@property
def _relation(self) -> Relation:
def _relation(self) -> Optional[Relation]:
"""If this wraps a single endpoint, the relation bound to it, if any."""
if not self._is_single_endpoint:
objname = type(self).__name__
Expand All @@ -474,7 +484,7 @@ def is_ready(self, relation: Optional[Relation] = None):
"""Is this endpoint ready?"""
relation = relation or self._relation
if not relation:
logger.error(f"no relation on {self._relation_name}: tracing not ready")
logger.debug(f"no relation on {self._relation_name !r}: tracing not ready")
return False
if relation.data is None:
logger.error(f"relation data is None for {relation}")
Expand Down Expand Up @@ -502,7 +512,7 @@ def _on_tracing_relation_changed(self, event):
def _on_tracing_relation_broken(self, event: RelationBrokenEvent):
"""Notify the providers that the endpoint is broken."""
relation = event.relation
self.on.endpoint_removed.emit(relation)
self.on.endpoint_removed.emit(relation) # type: ignore

def get_all_endpoints(
self, relation: Optional[Relation] = None
Expand All @@ -512,7 +522,7 @@ def get_all_endpoints(
return
return TracingProviderAppData.load(relation.data[relation.app]) # type: ignore

def _get_ingester(self, relation: Relation, protocol: IngesterProtocol):
def _get_ingester(self, relation: Optional[Relation], protocol: IngesterProtocol):
ep = self.get_all_endpoints(relation)
if not ep:
return None
Expand All @@ -539,12 +549,10 @@ def tempo_endpoint(self, relation: Optional[Relation] = None) -> Optional[str]:
"""Ingester endpoint for the ``tempo`` protocol."""
return self._get_ingester(relation or self._relation, protocol="tempo")

@property
def jaeger_http_thrift_endpoint(self) -> Optional[str]:
def jaeger_http_thrift_endpoint(self, relation: Optional[Relation] = None) -> Optional[str]:
"""Ingester endpoint for the ``jaeger_http_thrift`` protocol."""
return self._get_ingester("jaeger_http_thrift")
return self._get_ingester(relation or self._relation, "jaeger_http_thrift")

@property
def jaeger_grpc_endpoint(self) -> Optional[str]:
def jaeger_grpc_endpoint(self, relation: Optional[Relation] = None) -> Optional[str]:
"""Ingester endpoint for the ``jaeger_grpc`` protocol."""
return self._get_ingester("jaeger_grpc")
return self._get_ingester(relation or self._relation, "jaeger_grpc")
18 changes: 10 additions & 8 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from urllib.parse import urlparse

import yaml
from charms.alertmanager_k8s.v0.alertmanager_dispatch import AlertmanagerConsumer
from charms.alertmanager_k8s.v1.alertmanager_dispatch import AlertmanagerConsumer
from charms.catalogue_k8s.v0.catalogue import CatalogueConsumer, CatalogueItem
from charms.grafana_k8s.v0.grafana_dashboard import GrafanaDashboardProvider
from charms.grafana_k8s.v0.grafana_source import GrafanaSourceProvider
Expand Down Expand Up @@ -386,10 +386,7 @@ def _on_k8s_patch_failed(self, event: K8sResourcePatchFailedEvent):
self.unit.status = BlockedStatus(cast(str, event.message))

def _on_server_cert_changed(self, _):
self.grafana_source_provider.update_source(self.external_url)
self.ingress.provide_ingress_requirements(
scheme=urlparse(self.internal_url).scheme, port=self._port
)
self._update_cert()
self._configure(_)

def _is_cert_available(self) -> bool:
Expand Down Expand Up @@ -492,9 +489,14 @@ def _configure(self, _):
self.unit.status = MaintenanceStatus("Configuring Prometheus")
return

if self._is_cert_available() and not self.container.exists(CERT_PATH):
if self._is_cert_available() and not self._is_tls_ready():
self._update_cert()

self.grafana_source_provider.update_source(self.external_url)
self.ingress.provide_ingress_requirements(
scheme=urlparse(self.internal_url).scheme, port=self._port
)

try:
# Need to reload if config or alerts changed.
# (Both functions need to run so cannot use the short-circuiting `or`.)
Expand Down Expand Up @@ -860,13 +862,13 @@ def _alerting_config(self) -> dict:
Returns:
a dictionary consisting of the alerting configuration for Prometheus.
"""
alertmanagers = self.alertmanager_consumer.get_cluster_info_with_scheme()
alertmanagers = self.alertmanager_consumer.get_cluster_info()
if not alertmanagers:
logger.debug("No alertmanagers available")
return {}

alerting_config: Dict[str, list] = PrometheusConfig.render_alertmanager_static_configs(
alertmanagers
list(alertmanagers)
)
return alerting_config

Expand Down

0 comments on commit 748a0fc

Please sign in to comment.