Skip to content

Commit

Permalink
chore: bump libraries (#290)
Browse files Browse the repository at this point in the history
* Bump libraries using charmcraft fetch-lib.
* Check for major versions using noctua charm libraries check --major. If there is one, then commit a do-not-merge-message.txt file.

Ref canonical/bundle-kubeflow#1184
  • Loading branch information
orfeas-k authored Jan 15, 2025
1 parent e4e844e commit 080e5ac
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 147 deletions.
85 changes: 79 additions & 6 deletions charms/kserve-controller/lib/charms/loki_k8s/v1/loki_push_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,25 @@ def _alert_rules_error(self, event):
Units of consumer charm send their alert rules over app relation data using the `alert_rules`
key.
## Charm logging
The `charms.loki_k8s.v0.charm_logging` library can be used in conjunction with this one to configure python's
logging module to forward all logs to Loki via the loki-push-api interface.
```python
from lib.charms.loki_k8s.v0.charm_logging import log_charm
from lib.charms.loki_k8s.v1.loki_push_api import charm_logging_config, LokiPushApiConsumer
@log_charm(logging_endpoint="my_endpoints", server_cert="cert_path")
class MyCharm(...):
_cert_path = "/path/to/cert/on/charm/container.crt"
def __init__(self, ...):
self.logging = LokiPushApiConsumer(...)
self.my_endpoints, self.cert_path = charm_logging_config(
self.logging, self._cert_path)
```
Do this, and all charm logs will be forwarded to Loki as soon as a relation is formed.
"""

import json
Expand Down Expand Up @@ -527,7 +546,7 @@ def _alert_rules_error(self, event):

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

PYDEPS = ["cosl"]

Expand Down Expand Up @@ -577,7 +596,11 @@ def _alert_rules_error(self, event):
GRPC_LISTEN_PORT_START = 9095 # odd start port


class RelationNotFoundError(ValueError):
class LokiPushApiError(Exception):
"""Base class for errors raised by this module."""


class RelationNotFoundError(LokiPushApiError):
"""Raised if there is no relation with the given name."""

def __init__(self, relation_name: str):
Expand All @@ -587,7 +610,7 @@ def __init__(self, relation_name: str):
super().__init__(self.message)


class RelationInterfaceMismatchError(Exception):
class RelationInterfaceMismatchError(LokiPushApiError):
"""Raised if the relation with the given name has a different interface."""

def __init__(
Expand All @@ -607,7 +630,7 @@ def __init__(
super().__init__(self.message)


class RelationRoleMismatchError(Exception):
class RelationRoleMismatchError(LokiPushApiError):
"""Raised if the relation with the given name has a different direction."""

def __init__(
Expand Down Expand Up @@ -2555,14 +2578,18 @@ def _on_pebble_ready(self, event: PebbleReadyEvent):

self._update_endpoints(event.workload, loki_endpoints)

def _update_logging(self, _):
def _update_logging(self, event: RelationEvent):
"""Update the log forwarding to match the active Loki endpoints."""
if not (loki_endpoints := self._retrieve_endpoints_from_relation()):
logger.warning("No Loki endpoints available")
return

for container in self._charm.unit.containers.values():
self._update_endpoints(container, loki_endpoints)
if container.can_connect():
self._update_endpoints(container, loki_endpoints)
# else: `_update_endpoints` will be called on pebble-ready anyway.

self._handle_alert_rules(event.relation)

def _retrieve_endpoints_from_relation(self) -> dict:
loki_endpoints = {}
Expand Down Expand Up @@ -2748,3 +2775,49 @@ def _exec(self, cmd) -> str:
result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE)
output = result.stdout.decode("utf-8").strip()
return output


def charm_logging_config(
endpoint_requirer: LokiPushApiConsumer, cert_path: Optional[Union[Path, str]]
) -> Tuple[Optional[List[str]], Optional[str]]:
"""Utility function to determine the charm_logging config you will likely want.
If no endpoint is provided:
disable charm logging.
If https endpoint is provided but cert_path is not found on disk:
disable charm logging.
If https endpoint is provided and cert_path is None:
ERROR
Else:
proceed with charm logging (with or without tls, as appropriate)
Args:
endpoint_requirer: an instance of LokiPushApiConsumer.
cert_path: a path where a cert is stored.
Returns:
A tuple with (optionally) the values of the endpoints and the certificate path.
Raises:
LokiPushApiError: if some endpoint are http and others https.
"""
endpoints = [ep["url"] for ep in endpoint_requirer.loki_endpoints]
if not endpoints:
return None, None

https = tuple(endpoint.startswith("https://") for endpoint in endpoints)

if all(https): # all endpoints are https
if cert_path is None:
raise LokiPushApiError("Cannot send logs to https endpoints without a certificate.")
if not Path(cert_path).exists():
# if endpoints is https BUT we don't have a server_cert yet:
# disable charm logging until we do to prevent tls errors
return None, None
return endpoints, str(cert_path)

if all(not x for x in https): # all endpoints are http
return endpoints, None

# if there's a disagreement, that's very weird:
raise LokiPushApiError("Some endpoints are http, some others are https. That's not good.")
Original file line number Diff line number Diff line change
@@ -1,146 +1,13 @@
# Copyright 2021 Canonical Ltd.
# See LICENSE file for licensing details.

"""# KubernetesServicePatch Library.
This library is designed to enable developers to more simply patch the Kubernetes Service created
by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a
service named after the application in the namespace (named after the Juju model). This service by
default contains a "placeholder" port, which is 65535/TCP.
When modifying the default set of resources managed by Juju, one must consider the lifecycle of the
charm. In this case, any modifications to the default service (created during deployment), will be
overwritten during a charm upgrade.
When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm`
events which applies the patch to the cluster. This should ensure that the service ports are
correct throughout the charm's life.
The constructor simply takes a reference to the parent charm, and a list of
[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the
service. For information regarding the `lightkube` `ServicePort` model, please visit the
`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport).
Optionally, a name of the service (in case service name needs to be patched as well), labels,
selectors, and annotations can be provided as keyword arguments.
## Getting Started
To get started using the library, you just need to fetch the library using `charmcraft`. **Note
that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.**
```shell
cd some-charm
charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch
cat << EOF >> requirements.txt
lightkube
lightkube-models
EOF
```
Then, to initialise the library:
For `ClusterIP` services:
```python
# ...
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from lightkube.models.core_v1 import ServicePort
class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
port = ServicePort(443, name=f"{self.app.name}")
self.service_patcher = KubernetesServicePatch(self, [port])
# ...
```
For `LoadBalancer`/`NodePort` services:
```python
# ...
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from lightkube.models.core_v1 import ServicePort
class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666)
self.service_patcher = KubernetesServicePatch(
self, [port], "LoadBalancer"
)
# ...
```
Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"`
```python
# ...
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from lightkube.models.core_v1 import ServicePort
class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP")
udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP")
sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP")
self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp])
# ...
```
Bound with custom events by providing `refresh_event` argument:
For example, you would like to have a configurable port in your charm and want to apply
service patch every time charm config is changed.
```python
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from lightkube.models.core_v1 import ServicePort
class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}")
self.service_patcher = KubernetesServicePatch(
self,
[port],
refresh_event=self.on.config_changed
)
# ...
```
Creating a new k8s lb service instead of patching the one created by juju
Service name is optional. If not provided, it defaults to {app_name}-lb.
If provided and equal to app_name, it also defaults to {app_name}-lb to prevent conflicts with the Juju default service.
```python
from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch
from lightkube.models.core_v1 import ServicePort
class SomeCharm(CharmBase):
def __init__(self, *args):
# ...
port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}")
self.service_patcher = KubernetesServicePatch(
self,
[port],
service_type="LoadBalancer",
service_name="application-lb"
)
# ...
```
Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library
does not try to make any API calls, or open any files during testing that are unlikely to be
present, and could break your tests. The easiest way to do this is during your test `setUp`:
```python
# ...
@patch("charm.KubernetesServicePatch", lambda x, y: None)
def setUp(self, *unused):
self.harness = Harness(SomeCharm)
# ...
```
"""# [DEPRECATED!] KubernetesServicePatch Library.
The `kubernetes_service_patch` library is DEPRECATED and will be removed in October 2025.
For patching the Kubernetes service created by Juju during the deployment of a charm,
`ops.Unit.set_ports` functionality should be used instead.
"""

import logging
Expand All @@ -167,7 +34,7 @@ def setUp(self, *unused):

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

ServiceType = Literal["ClusterIP", "LoadBalancer"]

Expand Down Expand Up @@ -205,6 +72,11 @@ def __init__(
will be observed to re-apply the patch (e.g. on port change).
The `install` and `upgrade-charm` events would be observed regardless.
"""
logger.warning(
"The ``kubernetes_service_patch v1`` library is DEPRECATED and will be removed "
"in October 2025. For patching the Kubernetes service created by Juju during "
"the deployment of a charm, ``ops.Unit.set_ports`` functionality should be used instead."
)
super().__init__(charm, "kubernetes-service-patch")
self.charm = charm
self.service_name = service_name or self._app
Expand Down

0 comments on commit 080e5ac

Please sign in to comment.