Skip to content

Commit

Permalink
Fetch-lib. Fix for #315 (#327)
Browse files Browse the repository at this point in the history
* chore: update charm libraries

* fetch-lib

* Attempt at fixing potential startup errors

* Attempt at fixing potential startup errors

---------

Co-authored-by: Noctua <webops+observability-noctua-bot@canonical.com>
  • Loading branch information
sed-i and observability-noctua-bot committed Apr 26, 2024
1 parent 7bd93a1 commit 23c37dc
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 433 deletions.
50 changes: 34 additions & 16 deletions lib/charms/observability_libs/v0/cert_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,25 @@
import json
import socket
from itertools import filterfalse
from typing import List, Optional, Union
from typing import List, Optional, Union, cast

try:
from charms.tls_certificates_interface.v2.tls_certificates import ( # type: ignore
from charms.tls_certificates_interface.v3.tls_certificates import ( # type: ignore
AllCertificatesInvalidatedEvent,
CertificateAvailableEvent,
CertificateExpiringEvent,
CertificateInvalidatedEvent,
TLSCertificatesRequiresV2,
TLSCertificatesRequiresV3,
generate_csr,
generate_private_key,
)
except ImportError:
except ImportError as e:
raise ImportError(
"charms.tls_certificates_interface.v2.tls_certificates is missing; please get it through charmcraft fetch-lib"
)
"failed to import charms.tls_certificates_interface.v3.tls_certificates; "
"Either the library itself is missing (please get it through charmcraft fetch-lib) "
"or one of its dependencies is unmet."
) from e

import logging

from ops.charm import CharmBase, RelationBrokenEvent
Expand All @@ -64,7 +67,7 @@

LIBID = "b5cd5cd580f3428fa5f59a8876dcbe6a"
LIBAPI = 0
LIBPATCH = 9
LIBPATCH = 12


def is_ip_address(value: str) -> bool:
Expand Down Expand Up @@ -129,7 +132,7 @@ def __init__(
self.peer_relation_name = peer_relation_name
self.certificates_relation_name = certificates_relation_name

self.certificates = TLSCertificatesRequiresV2(self.charm, self.certificates_relation_name)
self.certificates = TLSCertificatesRequiresV3(self.charm, self.certificates_relation_name)

self.framework.observe(
self.charm.on.config_changed,
Expand Down Expand Up @@ -237,6 +240,13 @@ def _generate_csr(
This method intentionally does not emit any events, leave it for caller's responsibility.
"""
# if we are in a relation-broken hook, we might not have a relation to publish the csr to.
if not self.charm.model.get_relation(self.certificates_relation_name):
logger.warning(
f"No {self.certificates_relation_name!r} relation found. " f"Cannot generate csr."
)
return

# At this point, assuming "peer joined" and "certificates joined" have already fired
# (caller must guard) so we must have a private_key entry in relation data at our disposal.
# Otherwise, traceback -> debug.
Expand Down Expand Up @@ -279,7 +289,7 @@ def _generate_csr(
if clear_cert:
self._ca_cert = ""
self._server_cert = ""
self._chain = []
self._chain = ""

def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
"""Get the certificate from the event and store it in a peer relation.
Expand All @@ -301,7 +311,7 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
if event_csr == self._csr:
self._ca_cert = event.ca
self._server_cert = event.certificate
self._chain = event.chain
self._chain = event.chain_as_pem()
self.on.cert_changed.emit() # pyright: ignore

@property
Expand Down Expand Up @@ -372,21 +382,29 @@ def _server_cert(self, value: str):
rel.data[self.charm.unit].update({"certificate": value})

@property
def _chain(self) -> List[str]:
def _chain(self) -> str:
if self._peer_relation:
if chain := self._peer_relation.data[self.charm.unit].get("chain", []):
return json.loads(chain)
return []
if chain := self._peer_relation.data[self.charm.unit].get("chain", ""):
chain = json.loads(chain)

# In a previous version of this lib, chain used to be a list.
# Convert the List[str] to str, per
# https://github.com/canonical/tls-certificates-interface/pull/141
if isinstance(chain, list):
chain = "\n\n".join(reversed(chain))

return cast(str, chain)
return ""

@_chain.setter
def _chain(self, value: List[str]):
def _chain(self, value: str):
# Caller must guard. We want the setter to fail loudly. Failure must have a side effect.
rel = self._peer_relation
assert rel is not None # For type checker
rel.data[self.charm.unit].update({"chain": json.dumps(value)})

@property
def chain(self) -> List[str]:
def chain(self) -> str:
"""Return the ca chain."""
return self._chain

Expand Down
17 changes: 8 additions & 9 deletions lib/charms/prometheus_k8s/v0/prometheus_scrape.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,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 = 44
LIBPATCH = 46

PYDEPS = ["cosl"]

Expand Down Expand Up @@ -521,8 +521,8 @@ def expand_wildcard_targets_into_individual_jobs(
# for such a target. Therefore labeling with Juju topology, excluding the
# unit name.
non_wildcard_static_config["labels"] = {
**non_wildcard_static_config.get("labels", {}),
**topology.label_matcher_dict,
**non_wildcard_static_config.get("labels", {}),
}

non_wildcard_static_configs.append(non_wildcard_static_config)
Expand All @@ -547,9 +547,9 @@ def expand_wildcard_targets_into_individual_jobs(
if topology:
# Add topology labels
modified_static_config["labels"] = {
**modified_static_config.get("labels", {}),
**topology.label_matcher_dict,
**{"juju_unit": unit_name},
**modified_static_config.get("labels", {}),
}

# Instance relabeling for topology should be last in order.
Expand Down Expand Up @@ -1537,12 +1537,11 @@ def set_scrape_job_spec(self, _=None):
relation.data[self._charm.app]["scrape_metadata"] = json.dumps(self._scrape_metadata)
relation.data[self._charm.app]["scrape_jobs"] = json.dumps(self._scrape_jobs)

if alert_rules_as_dict:
# Update relation data with the string representation of the rule file.
# Juju topology is already included in the "scrape_metadata" field above.
# The consumer side of the relation uses this information to name the rules file
# that is written to the filesystem.
relation.data[self._charm.app]["alert_rules"] = json.dumps(alert_rules_as_dict)
# Update relation data with the string representation of the rule file.
# Juju topology is already included in the "scrape_metadata" field above.
# The consumer side of the relation uses this information to name the rules file
# that is written to the filesystem.
relation.data[self._charm.app]["alert_rules"] = json.dumps(alert_rules_as_dict)

def _set_unit_ip(self, _=None):
"""Set unit host address.
Expand Down
74 changes: 49 additions & 25 deletions lib/charms/tempo_k8s/v2/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,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 = 2
LIBPATCH = 3

PYDEPS = ["pydantic"]

Expand All @@ -117,15 +117,13 @@ def __init__(self, *args):
"zipkin",
"kafka",
"opencensus",
"tempo", # legacy, renamed to tempo_http
"tempo_http",
"tempo_grpc",
"otlp_grpc",
"otlp_http",
"jaeger_grpc",
# "jaeger_grpc",
"jaeger_thrift_compact",
"jaeger_thrift_http",
"jaeger_http_thrift", # legacy, renamed to jaeger_thrift_http
"jaeger_thrift_binary",
]

Expand Down Expand Up @@ -302,11 +300,14 @@ class TracingProviderAppData(DatabagModel): # noqa: D101
"""Application databag model for the tracing provider."""

host: str
"""Server hostname."""
"""Server hostname (local fqdn)."""

receivers: List[Receiver]
"""Enabled receivers and ports at which they are listening."""

external_url: Optional[str] = None
"""Server url. If an ingress is present, it will be the ingress address."""


class TracingRequirerAppData(DatabagModel): # noqa: D101
"""Application databag model for the tracing requirer."""
Expand Down Expand Up @@ -492,13 +493,16 @@ def __init__(
self,
charm: CharmBase,
host: str,
external_url: Optional[str] = None,
relation_name: str = DEFAULT_RELATION_NAME,
):
"""Initialize.
Args:
charm: a `CharmBase` instance that manages this instance of the Tempo service.
host: address of the node hosting the tempo server.
external_url: external address of the node hosting the tempo server,
if an ingress is present.
relation_name: an optional string name of the relation between `charm`
and the Tempo charmed service. The default is "tracing".
Expand All @@ -519,6 +523,7 @@ def __init__(
super().__init__(charm, relation_name + "tracing-provider-v2")
self._charm = charm
self._host = host
self._external_url = external_url
self._relation_name = relation_name
self.framework.observe(
self._charm.on[relation_name].relation_joined, self._on_relation_event
Expand Down Expand Up @@ -585,6 +590,7 @@ def publish_receivers(self, receivers: Sequence[RawReceiver]):
try:
TracingProviderAppData(
host=self._host,
external_url=self._external_url,
receivers=[
Receiver(port=port, protocol=protocol) for protocol, port in receivers
],
Expand Down Expand Up @@ -612,16 +618,17 @@ class EndpointRemovedEvent(RelationBrokenEvent):
class EndpointChangedEvent(_AutoSnapshotEvent):
"""Event representing a change in one of the receiver endpoints."""

__args__ = ("host", "_ingesters")
__args__ = ("host", "external_url", "_receivers")

if TYPE_CHECKING:
host = "" # type: str
_ingesters = [] # type: List[dict]
external_url = "" # type: str
_receivers = [] # type: List[dict]

@property
def receivers(self) -> List[Receiver]:
"""Cast receivers back from dict."""
return [Receiver(**i) for i in self._ingesters]
return [Receiver(**i) for i in self._receivers]


class TracingEndpointRequirerEvents(CharmEvents):
Expand Down Expand Up @@ -776,7 +783,9 @@ def _on_tracing_relation_changed(self, event):
return

data = TracingProviderAppData.load(relation.data[relation.app])
self.on.endpoint_changed.emit(relation, data.host, [i.dict() for i in data.receivers]) # type: ignore
self.on.endpoint_changed.emit( # type: ignore
relation, data.host, data.external_url, [i.dict() for i in data.receivers]
)

def _on_tracing_relation_broken(self, event: RelationBrokenEvent):
"""Notify the providers that the endpoint is broken."""
Expand All @@ -787,28 +796,43 @@ def get_all_endpoints(
self, relation: Optional[Relation] = None
) -> Optional[TracingProviderAppData]:
"""Unmarshalled relation data."""
if not self.is_ready(relation or self._relation):
relation = relation or self._relation
if not self.is_ready(relation):
return
return TracingProviderAppData.load(relation.data[relation.app]) # type: ignore

def _get_endpoint(
self, relation: Optional[Relation], protocol: ReceiverProtocol, ssl: bool = False
):
ep = self.get_all_endpoints(relation)
if not ep:
self, relation: Optional[Relation], protocol: ReceiverProtocol
) -> Optional[str]:
app_data = self.get_all_endpoints(relation)
if not app_data:
return None
try:
receiver: Receiver = next(filter(lambda i: i.protocol == protocol, ep.receivers))
if receiver.protocol in ["otlp_grpc", "jaeger_grpc"]:
if ssl:
logger.warning("unused ssl argument - was the right protocol called?")
return f"{ep.host}:{receiver.port}"
if ssl:
return f"https://{ep.host}:{receiver.port}"
return f"http://{ep.host}:{receiver.port}"
except StopIteration:
receivers: List[Receiver] = list(
filter(lambda i: i.protocol == protocol, app_data.receivers)
)
if not receivers:
logger.error(f"no receiver found with protocol={protocol!r}")
return None
return
if len(receivers) > 1:
logger.error(
f"too many receivers with protocol={protocol!r}; using first one. Found: {receivers}"
)
return

receiver = receivers[0]
# if there's an external_url argument (v2.5+), use that. Otherwise, we use the tempo local fqdn
if app_data.external_url:
url = app_data.external_url
else:
# FIXME: if we don't get an external url but only a
# hostname, we don't know what scheme we need to be using. ASSUME HTTP
url = f"http://{app_data.host}:{receiver.port}"

if receiver.protocol.endswith("grpc"):
# TCP protocols don't want an http/https scheme prefix
url = url.split("://")[1]

return url

def get_endpoint(
self, protocol: ReceiverProtocol, relation: Optional[Relation] = None
Expand Down
Loading

0 comments on commit 23c37dc

Please sign in to comment.