Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch-lib. Fix for #315 #327

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading