diff --git a/lib/charms/prometheus_k8s/v0/prometheus_scrape.py b/lib/charms/prometheus_k8s/v0/prometheus_scrape.py index df7d5b03..70b7f1ea 100644 --- a/lib/charms/prometheus_k8s/v0/prometheus_scrape.py +++ b/lib/charms/prometheus_k8s/v0/prometheus_scrape.py @@ -1,6 +1,8 @@ # Copyright 2021 Canonical Ltd. # See LICENSE file for licensing details. -"""## Overview. +"""Source code can be found on GitHub at canonical/observability-libs/lib/charms/observability_libs. + +## Overview This document explains how to integrate with the Prometheus charm for the purpose of providing a metrics endpoint to Prometheus. It @@ -11,6 +13,13 @@ shared between Prometheus charms and any other charm that intends to provide a scrape target for Prometheus. +## Dependencies + +Using this library requires you to fetch the juju_topology library from +[observability-libs](https://charmhub.io/observability-libs/libraries/juju_topology). + +`charmcraft fetch-lib charms.observability_libs.v0.juju_topology` + ## Provider Library Usage This Prometheus charm interacts with its scrape targets using its @@ -341,7 +350,7 @@ def _on_scrape_targets_changed(self, event): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 23 +LIBPATCH = 24 logger = logging.getLogger(__name__) @@ -1516,10 +1525,11 @@ def __init__( self.external_url = external_url events = self._charm.on[self._relation_name] - self.framework.observe(events.relation_joined, self._set_scrape_job_spec) self.framework.observe(events.relation_changed, self._on_relation_changed) if not refresh_event: + # FIXME remove once podspec charms are verified. + # `self._set_scrape_job_spec()` is called every re-init so this should not be needed. if len(self._charm.meta.containers) == 1: if "kubernetes" in self._charm.meta.series: # This is a podspec charm @@ -1544,10 +1554,16 @@ def __init__( for ev in refresh_event: self.framework.observe(ev, self._set_scrape_job_spec) - self.framework.observe(self._charm.on.upgrade_charm, self._set_scrape_job_spec) - - # If there is no leader during relation_joined we will still need to set alert rules. - self.framework.observe(self._charm.on.leader_elected, self._set_scrape_job_spec) + # Update relation data every reinit. If instead we used event hooks then observing only + # relation-joined would not be sufficient: + # - Would need to observe leader-elected, in case there was no leader during + # relation-joined. + # - If later related to an ingress provider, then would need to register and wait for + # update-status interval to elapse before changes would apply. + # - The ingerss-ready custom event is currently emitted prematurely and cannot be relied + # upon: https://github.com/canonical/traefik-k8s-operator/issues/78 + # NOTE We may still end up waiting for update-status before changes are applied. + self._set_scrape_job_spec() def _on_relation_changed(self, event): """Check for alert rule messages in the relation data before moving on.""" @@ -1563,9 +1579,7 @@ def _on_relation_changed(self, event): else: self.on.alert_rule_status_changed.emit(valid=valid, errors=errors) - self._set_scrape_job_spec(event) - - def _set_scrape_job_spec(self, event): + def _set_scrape_job_spec(self, _=None): """Ensure scrape target information is made available to prometheus. When a metrics provider charm is related to a prometheus charm, the @@ -1574,7 +1588,7 @@ def _set_scrape_job_spec(self, event): data. In addition, each of the consumer units also sets its own host address in Juju unit relation data. """ - self._set_unit_ip(event) + self._set_unit_ip() if not self._charm.unit.is_leader(): return @@ -1594,7 +1608,7 @@ def _set_scrape_job_spec(self, event): # that is written to the filesystem. relation.data[self._charm.app]["alert_rules"] = json.dumps(alert_rules_as_dict) - def _set_unit_ip(self, _): + def _set_unit_ip(self, _=None): """Set unit host address. Each time a metrics provider charm container is restarted it updates its own diff --git a/lib/charms/traefik_k8s/v1/ingress.py b/lib/charms/traefik_k8s/v1/ingress.py index fbf611ee..b01989a7 100644 --- a/lib/charms/traefik_k8s/v1/ingress.py +++ b/lib/charms/traefik_k8s/v1/ingress.py @@ -54,7 +54,7 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): import logging import socket import typing -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple, Union import yaml from ops.charm import CharmBase, RelationBrokenEvent, RelationEvent @@ -69,7 +69,7 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 3 +LIBPATCH = 4 DEFAULT_RELATION_NAME = "ingress" RELATION_INTERFACE = "ingress" @@ -97,6 +97,7 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): "name": {"type": "string"}, "host": {"type": "string"}, "port": {"type": "string"}, + "strip-prefix": {"type": "string"}, }, "required": ["model", "name", "host", "port"], } @@ -115,7 +116,11 @@ def _on_ingress_revoked(self, event: IngressPerAppRevokedEvent): from typing_extensions import TypedDict # py35 compat # Model of the data a unit implementing the requirer will need to provide. -RequirerData = TypedDict("RequirerData", {"model": str, "name": str, "host": str, "port": int}) +RequirerData = TypedDict( + "RequirerData", + {"model": str, "name": str, "host": str, "port": int, "strip-prefix": bool}, + total=False, +) # Provider ingress data model. ProviderIngressData = TypedDict("ProviderIngressData", {"url": str}) # Provider application databag model. @@ -221,12 +226,14 @@ def restore(self, snapshot: dict) -> None: class IngressPerAppDataProvidedEvent(_IPAEvent): """Event representing that ingress data has been provided for an app.""" - __args__ = ("name", "model", "port", "host") + __args__ = ("name", "model", "port", "host", "strip_prefix") + if typing.TYPE_CHECKING: name = None # type: str model = None # type: str port = None # type: int host = None # type: str + strip_prefix = False # type: bool class IngressPerAppDataRemovedEvent(RelationEvent): @@ -266,6 +273,7 @@ def _handle_relation(self, event): data["model"], data["port"], data["host"], + data.get("strip-prefix", False), ) def _handle_relation_broken(self, event): @@ -297,17 +305,14 @@ def _get_requirer_data(self, relation: Relation) -> RequirerData: return {} databag = relation.data[relation.app] - try: - remote_data = {k: databag[k] for k in ("model", "name", "host", "port")} - except KeyError as e: - # incomplete data / invalid data - log.debug("error {}; ignoring...".format(e)) - return {} - except TypeError as e: - raise DataValidationError("Error casting remote data: {}".format(e)) + remote_data = {} # type: Dict[str, Union[int, str]] + for k in ("port", "host", "model", "name", "mode", "strip-prefix"): + v = databag.get(k) + if v is not None: + remote_data[k] = v _validate_data(remote_data, INGRESS_REQUIRES_APP_SCHEMA) - remote_data["port"] = int(remote_data["port"]) + remote_data["strip-prefix"] = bool(remote_data.get("strip-prefix", False)) return remote_data def get_data(self, relation: Relation) -> RequirerData: @@ -408,6 +413,7 @@ def __init__( *, host: str = None, port: int = None, + strip_prefix: bool = False, ): """Constructor for IngressRequirer. @@ -422,6 +428,7 @@ def __init__( relation must be of interface type `ingress` and have "limit: 1") host: Hostname to be used by the ingress provider to address the requiring application; if unspecified, the default Kubernetes service name will be used. + strip_prefix: configure Traefik to strip the path prefix. Request Args: port: the port of the service @@ -429,6 +436,7 @@ def __init__( super().__init__(charm, relation_name) self.charm: CharmBase = charm self.relation_name = relation_name + self._strip_prefix = strip_prefix self._stored.set_default(current_url=None) @@ -501,6 +509,10 @@ def provide_ingress_requirements(self, *, host: str = None, port: int): "host": host, "port": str(port), } + + if self._strip_prefix: + data["strip_prefix"] = "true" + _validate_data(data, INGRESS_REQUIRES_APP_SCHEMA) self.relation.data[self.app].update(data)