From 6574a0e0889b3cf360bb2cbb1cd778259d9f72f1 Mon Sep 17 00:00:00 2001 From: Telcobot Date: Sun, 15 Sep 2024 03:10:48 +0000 Subject: [PATCH] chore: update charm libraries --- .../v0/hugepages_volumes_patch.py | 276 ++++++++-------- .../kubernetes_charm_libraries/v0/multus.py | 297 +++++++++--------- lib/charms/loki_k8s/v1/loki_push_api.py | 85 ++++- lib/charms/sdcore_upf_k8s/v0/fiveg_n3.py | 14 +- lib/charms/sdcore_upf_k8s/v0/fiveg_n4.py | 16 +- 5 files changed, 393 insertions(+), 295 deletions(-) diff --git a/lib/charms/kubernetes_charm_libraries/v0/hugepages_volumes_patch.py b/lib/charms/kubernetes_charm_libraries/v0/hugepages_volumes_patch.py index c05b939a..c107f7e8 100644 --- a/lib/charms/kubernetes_charm_libraries/v0/hugepages_volumes_patch.py +++ b/lib/charms/kubernetes_charm_libraries/v0/hugepages_volumes_patch.py @@ -3,7 +3,7 @@ """Charm library used to manage HugePages volumes in Kubernetes charms. -- On a bound event (e.g., self.on.hugepages_volumes_config_changed), it will: +- Using the `configure` endpoint, it will: - Replace the volumes in the StatefulSet with the new requested ones - Replace the volume mounts in the container in the Pod with the new requested ones. - Replace the resource requirements in the container in the Pod with the new requested ones. @@ -18,27 +18,19 @@ ) -class K8sHugePagesVolumePatchChangedEvent(EventBase): - - -class K8sHugePagesVolumePatchChangedCharmEvents(CharmEvents): - hugepages_volumes_config_changed = EventSource(K8sHugePagesVolumePatchChangedEvent) - - class YourCharm(CharmBase): - on = K8sHugePagesVolumePatchChangedCharmEvents() - def __init__(self, *args): super().__init__(*args) self._kubernetes_volumes_patch = KubernetesHugePagesPatchCharmLib( - charm=self, - container_name=self._container_name, - hugepages_volumes_func=self._hugepages_volumes_func_from_config, - refresh_event=self.on.hugepages_volumes_config_changed, + statefulset_name=self.model.app.name, + container_name=self._bessd_container_name, + pod_name=self._pod_name, + hugepages_volumes=self._volumes_request_from_config(), ) + self.framework.observe(self.on.config_changed, self.on_config_changed) - def _hugepages_volumes_func_from_config(self) -> list[HugePagesVolume]: + def _volumes_request_from_config(self) -> list[HugePagesVolume]: return [ HugePagesVolume( mount_path="/dev/hugepages", @@ -47,12 +39,16 @@ def _hugepages_volumes_func_from_config(self) -> list[HugePagesVolume]: ) ] + def on_config_changed(self, event: ConfigChangedEvent): + self._kubernetes_volumes_patch.configure() +``` """ + import logging from dataclasses import dataclass -from typing import Callable, Iterable +from typing import Iterable, List -from lightkube import Client +from lightkube.core.client import Client from lightkube.core.exceptions import ApiError from lightkube.models.apps_v1 import StatefulSetSpec from lightkube.models.core_v1 import ( @@ -64,8 +60,6 @@ def _hugepages_volumes_func_from_config(self) -> list[HugePagesVolume]: ) from lightkube.resources.apps_v1 import StatefulSet from lightkube.resources.core_v1 import Pod -from ops.charm import CharmBase -from ops.framework import BoundEvent, Object # The unique Charmhub library identifier, never change it LIBID = "b4cf8e58c9f64b73b22083d3e8d0de8e" @@ -75,7 +69,7 @@ def _hugepages_volumes_func_from_config(self) -> list[HugePagesVolume]: # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 3 +LIBPATCH = 6 logger = logging.getLogger(__name__) @@ -105,7 +99,9 @@ def __init__(self, namespace: str): self.namespace = namespace @classmethod - def _get_container(cls, container_name: str, containers: Iterable[Container]) -> Container: + def _get_container( + cls, container_name: str, containers: Iterable[Container] + ) -> Container: """Find the container from the container list, assuming list is unique by name. Args: @@ -120,9 +116,13 @@ def _get_container(cls, container_name: str, containers: Iterable[Container]) -> Container: An instance of :class:`Container` whose name matches the given name. """ try: - return next(iter(filter(lambda ctr: ctr.name == container_name, containers))) + return next( + iter(filter(lambda ctr: ctr.name == container_name, containers)) + ) except StopIteration: - raise KubernetesHugePagesVolumesPatchError(f"Container `{container_name}` not found") + raise KubernetesHugePagesVolumesPatchError( + f"Container `{container_name}` not found" + ) def pod_is_patched( self, @@ -131,7 +131,7 @@ def pod_is_patched( requested_resources: ResourceRequirements, container_name: str, ) -> bool: - """Returns whether pod contains the given volumes mounts and resources. + """Return whether pod contains the given volumes mounts and resources. Args: pod_name: Pod name @@ -152,15 +152,17 @@ def pod_is_patched( if e.status.reason == "Unauthorized": logger.debug("kube-apiserver not ready yet") else: - raise KubernetesHugePagesVolumesPatchError(f"Pod `{pod_name}` not found") + raise KubernetesHugePagesVolumesPatchError( + f"Pod `{pod_name}` not found" + ) return False pod_has_volumemounts = self._pod_contains_requested_volumemounts( requested_volumemounts=requested_volumemounts, - containers=pod.spec.containers, # type: ignore[attr-defined] + containers=pod.spec.containers, # type: ignore[union-attr] container_name=container_name, ) pod_has_resources = self._pod_resources_are_set( - containers=pod.spec.containers, # type: ignore[attr-defined] + containers=pod.spec.containers, # type: ignore[union-attr] container_name=container_name, requested_resources=requested_resources, ) @@ -171,7 +173,7 @@ def statefulset_is_patched( statefulset_name: str, requested_volumes: Iterable[Volume], ) -> bool: - """Returns whether the statefulset contains the given volumes. + """Return whether the statefulset contains the given volumes. Args: statefulset_name: Statefulset name @@ -196,8 +198,10 @@ def statefulset_is_patched( f"Could not get statefulset `{statefulset_name}`" ) return False + if not statefulset.spec: + return False return self._statefulset_contains_requested_volumes( - statefulset_spec=statefulset.spec, # type: ignore[attr-defined] + statefulset_spec=statefulset.spec, requested_volumes=requested_volumes, ) @@ -206,7 +210,7 @@ def _statefulset_contains_requested_volumes( statefulset_spec: StatefulSetSpec, requested_volumes: Iterable[Volume], ) -> bool: - """Returns whether the StatefulSet contains the given volumes. + """Return whether the StatefulSet contains the given volumes. Args: statefulset_spec: StatefulSet spec @@ -215,13 +219,11 @@ def _statefulset_contains_requested_volumes( Returns: bool: Whether the StatefulSet contains the given volumes. """ - if not statefulset_spec.template.spec.volumes: # type: ignore[union-attr] + if not statefulset_spec.template.spec.volumes: # type: ignore[reportOptionalMemberAccess] return False return all( - [ - requested_volume in statefulset_spec.template.spec.volumes # type: ignore[union-attr] # noqa E501 - for requested_volume in requested_volumes - ] + requested_volume in statefulset_spec.template.spec.volumes # type: ignore[reportOptionalMemberAccess] + for requested_volume in requested_volumes ) def _pod_contains_requested_volumemounts( @@ -230,7 +232,7 @@ def _pod_contains_requested_volumemounts( container_name: str, requested_volumemounts: Iterable[VolumeMount], ) -> bool: - """Returns whether container spec contains the given volumemounts. + """Return whether container spec contains the given volumemounts. Args: containers: Iterable of Containers @@ -240,12 +242,12 @@ def _pod_contains_requested_volumemounts( Returns: bool: Whether container spec contains the given volumemounts. """ - container = self._get_container(container_name=container_name, containers=containers) + container = self._get_container( + container_name=container_name, containers=containers + ) return all( - [ - requested_volumemount in container.volumeMounts # type: ignore[operator] - for requested_volumemount in requested_volumemounts - ] + requested_volumemount in container.volumeMounts # type: ignore[reportOperatorIssue] + for requested_volumemount in requested_volumemounts ) def _pod_resources_are_set( @@ -254,7 +256,7 @@ def _pod_resources_are_set( container_name: str, requested_resources: ResourceRequirements, ) -> bool: - """Returns whether container spec contains the expected resources requests and limits. + """Return whether container spec contains the expected resources requests and limits. Args: containers: Iterable of Containers @@ -264,18 +266,20 @@ def _pod_resources_are_set( Returns: bool: whether container spec contains the expected resources requests and limits. """ - container = self._get_container(container_name=container_name, containers=containers) + container = self._get_container( + container_name=container_name, containers=containers + ) if requested_resources.limits: for limit, value in requested_resources.limits.items(): - if not container.resources.limits: # type: ignore[union-attr] + if not container.resources.limits: # type: ignore[reportOptionalMemberAccess] return False - if container.resources.limits.get(limit) != value: # type: ignore[union-attr] + if container.resources.limits.get(limit) != value: # type: ignore[reportOptionalMemberAccess] return False if requested_resources.requests: for request, value in requested_resources.requests.items(): - if not container.resources.requests: # type: ignore[union-attr] + if not container.resources.requests: # type: ignore[reportOptionalMemberAccess] return False - if container.resources.requests.get(request) != value: # type: ignore[union-attr] + if container.resources.requests.get(request) != value: # type: ignore[reportOptionalMemberAccess] return False return True @@ -287,7 +291,7 @@ def replace_statefulset( requested_resources: ResourceRequirements, container_name: str, ) -> None: - """Updates a StatefulSet and a container in its spec. + """Update a StatefulSet and a container in its spec. Raises: KubernetesHugePagesVolumesPatchError: If the user-provided statefulset name does @@ -308,11 +312,13 @@ def replace_statefulset( raise KubernetesHugePagesVolumesPatchError( f"Could not get statefulset `{statefulset_name}`" ) - containers: Iterable[Container] = statefulset.spec.template.spec.containers # type: ignore[attr-defined] # noqa: E501 - container = self._get_container(container_name=container_name, containers=containers) - container.volumeMounts = requested_volumemounts # type: ignore[assignment] + containers: Iterable[Container] = statefulset.spec.template.spec.containers # type: ignore[reportOptionalMemberAccess] + container = self._get_container( + container_name=container_name, containers=containers + ) + container.volumeMounts = requested_volumemounts # type: ignore[reportAttributeAccessIssue] container.resources = requested_resources - statefulset.spec.template.spec.volumes = requested_volumes # type: ignore[attr-defined] + statefulset.spec.template.spec.volumes = requested_volumes # type: ignore[reportOptionalMemberAccess] try: self.client.replace(obj=statefulset) except ApiError: @@ -322,7 +328,7 @@ def replace_statefulset( logger.info("Replaced `%s` statefulset", statefulset_name) def list_volumes(self, statefulset_name: str) -> list[Volume]: - """Lists current volumes in the given StatefulSet. + """List current volumes in the given StatefulSet. Args: statefulset_name: Statefulset name @@ -342,10 +348,12 @@ def list_volumes(self, statefulset_name: str) -> list[Volume]: raise KubernetesHugePagesVolumesPatchError( f"Could not get statefulset `{statefulset_name}`" ) - return statefulset.spec.template.spec.volumes # type: ignore[attr-defined] + return statefulset.spec.template.spec.volumes # type: ignore[reportOptionalMemberAccess] - def list_volumemounts(self, statefulset_name: str, container_name: str) -> list[VolumeMount]: - """Lists current volume mounts in the given container. + def list_volumemounts( + self, statefulset_name: str, container_name: str + ) -> list[VolumeMount]: + """List current volume mounts in the given container. Args: statefulset_name: Statefulset name @@ -366,14 +374,16 @@ def list_volumemounts(self, statefulset_name: str, container_name: str) -> list[ raise KubernetesHugePagesVolumesPatchError( f"Could not get statefulset `{statefulset_name}`" ) - containers: Iterable[Container] = statefulset.spec.template.spec.containers # type: ignore[attr-defined] # noqa: E501 - container = self._get_container(container_name=container_name, containers=containers) - return container.volumeMounts # type: ignore[return-value] + containers: Iterable[Container] = statefulset.spec.template.spec.containers # type: ignore[reportOptionalMemberAccess] + container = self._get_container( + container_name=container_name, containers=containers + ) + return container.volumeMounts if container.volumeMounts else [] def list_container_resources( self, statefulset_name: str, container_name: str ) -> ResourceRequirements: - """Returns resource requirements in the given container. + """Return resource requirements in the given container. Args: statefulset_name: Statefulset name @@ -394,43 +404,45 @@ def list_container_resources( raise KubernetesHugePagesVolumesPatchError( f"Could not get statefulset `{statefulset_name}`" ) - containers: Iterable[ - Container - ] = statefulset.spec.template.spec.containers # type: ignore[attr-defined] # noqa: E501 - container = self._get_container(container_name=container_name, containers=containers) + containers: Iterable[Container] = statefulset.spec.template.spec.containers # type: ignore[union-attr] + container = self._get_container( + container_name=container_name, containers=containers + ) return container.resources # type: ignore[return-value] -class KubernetesHugePagesPatchCharmLib(Object): +class KubernetesHugePagesPatchCharmLib: """Class to be instantiated by charms requiring changes in HugePages volumes.""" def __init__( self, - charm: CharmBase, - hugepages_volumes_func: Callable[[], Iterable[HugePagesVolume]], + hugepages_volumes: List[HugePagesVolume], + namespace: str, + statefulset_name: str, container_name: str, - refresh_event: BoundEvent, + pod_name: str, ): - """Constructor for the KubernetesHugePagesPatchCharmLib. + """Construct the KubernetesHugePagesPatchCharmLib. Args: - charm: Charm object - hugepages_volumes_func: A callable to a function returning a list of - `HugePagesVolume` to be created. + hugepages_volumes: list of `HugePagesVolume` to be created. + namespace: Namespace where the StatefulSet is located + statefulset_name: Statefulset name container_name: Container name - refresh_event: a bound event which will be observed to re-apply the patch. + pod_name: Pod name """ - super().__init__(charm, "kubernetes-requested-volumes") - self.kubernetes = KubernetesClient(namespace=self.model.name) - self.hugepages_volumes_func = hugepages_volumes_func + self.statefulset_name = statefulset_name + self.namespace = namespace + self.kubernetes = KubernetesClient(namespace=self.namespace) + self.hugepages_volumes = hugepages_volumes self.container_name = container_name - self.framework.observe(refresh_event, self._configure_requested_volumes) + self.pod_name = pod_name - def _configure_requested_volumes(self, _): - """Configures HugePages in the StatefulSet and container.""" + def configure(self): + """Configure HugePages in the StatefulSet and container.""" if not self.is_patched(): self.kubernetes.replace_statefulset( - statefulset_name=self.model.app.name, + statefulset_name=self.statefulset_name, container_name=self.container_name, requested_volumes=self._generate_volumes_to_be_replaced(), requested_volumemounts=self._generate_volumemounts_to_be_replaced(), @@ -442,7 +454,7 @@ def _pod_is_patched( requested_volumemounts: Iterable[VolumeMount], requested_resources: ResourceRequirements, ) -> bool: - """Returns whether pod contains given volume mounts and resource limits. + """Return whether pod contains given volume mounts and resource limits. If no HugePages volumeMount is requested, it returns whether other HugePages volumeMounts are present in the pod. @@ -456,31 +468,21 @@ def _pod_is_patched( """ if not requested_volumemounts: return not any( - [ - self._volumemount_is_hugepages(x) - for x in self.kubernetes.list_volumemounts( - statefulset_name=self.model.app.name, container_name=self.container_name - ) - ] + self._volumemount_is_hugepages(x) + for x in self.kubernetes.list_volumemounts( + statefulset_name=self.statefulset_name, + container_name=self.container_name, + ) ) return self.kubernetes.pod_is_patched( - pod_name=self._pod, + pod_name=self.pod_name, requested_volumemounts=requested_volumemounts, requested_resources=requested_resources, container_name=self.container_name, ) - @property - def _pod(self) -> str: - """Name of the unit's pod. - - Returns: - str: A string containing the name of the current unit's pod. - """ - return "-".join(self.model.unit.name.rsplit("/", 1)) - def _statefulset_is_patched(self, requested_volumes: Iterable[Volume]) -> bool: - """Returns whether statefulset contains requested volumes. + """Return whether statefulset contains requested volumes. If no HugePages volume is requested, it returns whether other HugePages volumes are present in the statefulset. @@ -493,20 +495,18 @@ def _statefulset_is_patched(self, requested_volumes: Iterable[Volume]) -> bool: """ if not requested_volumes: return not any( - [ - self._volume_is_hugepages(volume) - for volume in self.kubernetes.list_volumes( - statefulset_name=self.model.app.name - ) - ] + self._volume_is_hugepages(volume) + for volume in self.kubernetes.list_volumes( + statefulset_name=self.statefulset_name + ) ) return self.kubernetes.statefulset_is_patched( - statefulset_name=self.model.app.name, + statefulset_name=self.statefulset_name, requested_volumes=requested_volumes, ) def is_patched(self) -> bool: - """Returns whether statefulset and pod are patched. + """Return whether statefulset and pod are patched. Validates that the statefulset contains the appropriate volumes and that the pod also contains the appropriate volume mounts and @@ -518,7 +518,9 @@ def is_patched(self) -> bool: volumes = self._generate_volumes_from_requested_hugepage() statefulset_is_patched = self._statefulset_is_patched(volumes) volumemounts = self._generate_volumemounts_from_requested_hugepage() - resource_requirements = self._generate_resource_requirements_from_requested_hugepage() + resource_requirements = ( + self._generate_resource_requirements_from_requested_hugepage() + ) pod_is_patched = self._pod_is_patched( requested_volumemounts=volumemounts, requested_resources=resource_requirements, @@ -526,7 +528,7 @@ def is_patched(self) -> bool: return statefulset_is_patched and pod_is_patched def _generate_volumes_from_requested_hugepage(self) -> list[Volume]: - """Generates the list of required HugePages volumes. + """Generate the list of required HugePages volumes. Returns: list[Volume]: list of volumes to be set in the StatefulSet. @@ -534,13 +536,15 @@ def _generate_volumes_from_requested_hugepage(self) -> list[Volume]: return [ Volume( name=f"hugepages-{requested_hugepages.size.lower()}", - emptyDir=EmptyDirVolumeSource(medium=f"HugePages-{requested_hugepages.size}"), + emptyDir=EmptyDirVolumeSource( + medium=f"HugePages-{requested_hugepages.size}" + ), ) - for requested_hugepages in self.hugepages_volumes_func() + for requested_hugepages in self.hugepages_volumes ] def _generate_volumemounts_from_requested_hugepage(self) -> list[VolumeMount]: - """Generates the list of required HugePages volume mounts. + """Generate the list of required HugePages volume mounts. Returns: list[VolumeMount]: list of volume mounts to be set in the container. @@ -550,18 +554,20 @@ def _generate_volumemounts_from_requested_hugepage(self) -> list[VolumeMount]: name=f"hugepages-{requested_hugepages.size.lower()}", mountPath=requested_hugepages.mount_path, ) - for requested_hugepages in self.hugepages_volumes_func() + for requested_hugepages in self.hugepages_volumes ] - def _generate_resource_requirements_from_requested_hugepage(self) -> ResourceRequirements: - """Generates the required resource requirements for HugePages. + def _generate_resource_requirements_from_requested_hugepage( + self, + ) -> ResourceRequirements: + """Generate the required resource requirements for HugePages. Returns: ResourceRequirements: required resource requirements to be set in the container. """ limits = {} requests = {} - for hugepage in self.hugepages_volumes_func(): + for hugepage in self.hugepages_volumes: limits.update({f"hugepages-{hugepage.size}": hugepage.limit}) limits.update({"cpu": "2"}) requests.update({f"hugepages-{hugepage.size}": hugepage.limit}) @@ -573,17 +579,17 @@ def _generate_resource_requirements_from_requested_hugepage(self) -> ResourceReq @staticmethod def _volumemount_is_hugepages(volume_mount: VolumeMount) -> bool: - """Returns whether the specified volumeMount is HugePages.""" + """Return whether the specified volumeMount is HugePages.""" return volume_mount.name.startswith("hugepages") @staticmethod def _volume_is_hugepages(volume: Volume) -> bool: - """Returns whether the specified volume is HugePages.""" + """Return whether the specified volume is HugePages.""" return volume.name.startswith("hugepages") @staticmethod def _limit_or_request_is_hugepages(key: str) -> bool: - """Returns whether the specified limit or request regards HugePages.""" + """Return whether the specified limit or request regards HugePages.""" return key.startswith("hugepages") def _generate_volumes_to_be_replaced(self) -> list[Volume]: @@ -599,13 +605,15 @@ def _generate_volumes_to_be_replaced(self) -> list[Volume]: """ new_volumes = self._generate_volumes_from_requested_hugepage() current_volumes = self.kubernetes.list_volumes( - statefulset_name=self.model.app.name, + statefulset_name=self.statefulset_name, ) for current_volume in current_volumes: if not self._volume_is_hugepages(current_volume): new_volumes.append(current_volume) if not new_volumes: - logger.warning("StatefulSet `%s` will have no volumes", self.model.app.name) + logger.warning( + "StatefulSet `%s` will have no volumes", self.statefulset_name + ) return new_volumes def _generate_volumemounts_to_be_replaced(self) -> list[VolumeMount]: @@ -621,17 +629,21 @@ def _generate_volumemounts_to_be_replaced(self) -> list[VolumeMount]: """ new_volumemounts = self._generate_volumemounts_from_requested_hugepage() current_volumemounts = self.kubernetes.list_volumemounts( - statefulset_name=self.model.app.name, container_name=self.container_name + statefulset_name=self.statefulset_name, container_name=self.container_name ) for current_volumemount in current_volumemounts: if not self._volumemount_is_hugepages(current_volumemount): new_volumemounts.append(current_volumemount) if not new_volumemounts: - logger.warning("Container `%s` will have no volumeMounts", self.container_name) + logger.warning( + "Container `%s` will have no volumeMounts", self.container_name + ) return new_volumemounts - def _remove_hugepages_from_resource_requirements(self, resource_attribute: dict) -> dict: - """Removes HugePages-related keys from the given dictionary. + def _remove_hugepages_from_resource_requirements( + self, resource_attribute: dict + ) -> dict: + """Remove HugePages-related keys from the given dictionary. Args: resource_attribute: dictionary of resource requirements attribute (limits or requests) @@ -657,9 +669,11 @@ def _generate_resource_requirements_to_be_replaced(self) -> ResourceRequirements Returns: ResourceRequirements: new resource requirements to be replaced in the container. """ - additional_resources = self._generate_resource_requirements_from_requested_hugepage() + additional_resources = ( + self._generate_resource_requirements_from_requested_hugepage() + ) current_resources = self.kubernetes.list_container_resources( - statefulset_name=self.model.app.name, container_name=self.container_name + statefulset_name=self.statefulset_name, container_name=self.container_name ) new_limits = ( @@ -668,12 +682,16 @@ def _generate_resource_requirements_to_be_replaced(self) -> ResourceRequirements else {} ) new_requests = ( - self._remove_hugepages_from_resource_requirements(current_resources.requests) + self._remove_hugepages_from_resource_requirements( + current_resources.requests + ) if current_resources.requests else {} ) - new_limits = dict(new_limits.items() | additional_resources.limits.items()) # type: ignore[union-attr] # noqa E501 - new_requests = dict(new_requests.items() | additional_resources.requests.items()) # type: ignore[union-attr] # noqa E501 + new_limits = dict(new_limits.items() | additional_resources.limits.items()) # type: ignore[reportOptionalMemberAccess] + new_requests = dict( + new_requests.items() | additional_resources.requests.items() # type: ignore[reportOptionalMemberAccess] + ) new_resources = ResourceRequirements( limits=new_limits, requests=new_requests, claims=current_resources.claims ) diff --git a/lib/charms/kubernetes_charm_libraries/v0/multus.py b/lib/charms/kubernetes_charm_libraries/v0/multus.py index 25e83c26..40d0720e 100644 --- a/lib/charms/kubernetes_charm_libraries/v0/multus.py +++ b/lib/charms/kubernetes_charm_libraries/v0/multus.py @@ -3,90 +3,73 @@ """Charm Library used to leverage the Multus Kubernetes CNI in charms. -- On a BoundEvent (e.g. self.on.nad_config_changed which is originated from NadConfigChangedEvent), - it will: - - Configure the requested network attachment definitions - - Patch the statefulset with the necessary annotations for the container to have interfaces - that use those new network attachments. - - If an existing NAD config changed, it triggers pod restart to make the new config effective -- On charm removal, it will: - - Delete the created network attachment definitions - ## Usage ```python +from typing import List + from charms.kubernetes_charm_libraries.v0.multus import ( KubernetesMultusCharmLib, + NetworkAnnotation, NetworkAttachmentDefinition, - NetworkAnnotation ) - -class NadConfigChangedEvent(EventBase): - - -class KubernetesMultusCharmEvents(CharmEvents): - - nad_config_changed = EventSource(NadConfigChangedEvent) +from ops import RemoveEvent +from ops.charm import CharmBase +from ops.framework import EventBase +from ops.main import main class YourCharm(CharmBase): - on = KubernetesMultusCharmEvents() - def __init__(self, *args): super().__init__(*args) self._kubernetes_multus = KubernetesMultusCharmLib( - charm=self, - container_name=self._container_name, cap_net_admin=True, + namespace=self.model.name, + statefulset_name=self.model.app.name, + pod_name="-".join(self.model.unit.name.rsplit("/", 1)), + container_name=self._bessd_container_name, + network_annotations=self._generate_network_annotations(), + network_attachment_definitions=self._network_attachment_definitions_from_config(), privileged=True, - network_annotations=[ - NetworkAnnotation( - name=NETWORK_ATTACHMENT_DEFINITION_NAME, - interface=INTERFACE_NAME, - ) - ], - network_attachment_definitions_func=self._network_attachment_definitions_from_config, - refresh_event=self.on.nad_config_changed, ) - def _network_attachment_definitions_from_config(self) -> list[NetworkAttachmentDefinition]: + self.framework.observe(self.on.update_status, self._on_update_status) + + def _on_update_status(self, event: EventBase): + self._kubernetes_multus.configure() + + def _on_remove(self, _: RemoveEvent) -> None: + self._kubernetes_multus.remove() + + def _generate_network_annotations(self) -> List[NetworkAnnotation]: return [ - NetworkAttachmentDefinition( - metadata=ObjectMeta(name=NETWORK_ATTACHMENT_DEFINITION_NAME), - spec={ - "config": json.dumps( - { - "cniVersion": "0.3.1", - "type": "macvlan", - "ipam": { - "type": "static", - "routes": [ - { - "dst": self._get_upf_ip_address_from_config(), - "gw": self._get_upf_gateway_from_config(), - } - ], - "addresses": [ - { - "address": self._get_interface_ip_address_from_config(), - } - ], - }, - } - ) - }, + NetworkAnnotation( + name=ACCESS_NETWORK_ATTACHMENT_DEFINITION_NAME, + interface_name=ACCESS_INTERFACE_NAME, + bridge_name=ACCESS_INTERFACE_BRIDGE_NAME, + ), + NetworkAnnotation( + name=CORE_NETWORK_ATTACHMENT_DEFINITION_NAME, + interface_name=CORE_INTERFACE_NAME, + bridge_name=CORE_INTERFACE_BRIDGE_NAME, ), ] - def _on_config_changed(self, event: EventBase): - if not self.unit.is_leader(): - return - if self._get_invalid_configs(): - return - # Fire the NadConfigChangedEvent if the configs are valid. - self.on.nad_config_changed.emit() + def _network_attachment_definitions_from_config(self) -> List[NetworkAttachmentDefinition]: + return [ + NetworkAttachmentDefinition( + name=ACCESS_NETWORK_ATTACHMENT_DEFINITION_NAME, + cni_type="macvlan", + network_name=self.config["access_network_name"], + ), + NetworkAttachmentDefinition( + name=CORE_NETWORK_ATTACHMENT_DEFINITION_NAME, + cni_type="macvlan", + network_name=self.config["core_network_name"], + ), + ] ``` """ @@ -94,12 +77,15 @@ def _on_config_changed(self, event: EventBase): import logging from dataclasses import asdict, dataclass from json.decoder import JSONDecodeError -from typing import Callable, List, Optional, Union +from typing import List, Optional, Union import httpx -from lightkube import Client +from lightkube.core.client import Client from lightkube.core.exceptions import ApiError -from lightkube.generic_resource import GenericNamespacedResource, create_namespaced_resource +from lightkube.generic_resource import ( + GenericNamespacedResource, + create_namespaced_resource, +) from lightkube.models.apps_v1 import StatefulSetSpec from lightkube.models.core_v1 import ( Capabilities, @@ -112,8 +98,6 @@ def _on_config_changed(self, event: EventBase): from lightkube.resources.apps_v1 import StatefulSet from lightkube.resources.core_v1 import Pod from lightkube.types import PatchType -from ops.charm import CharmBase, RemoveEvent -from ops.framework import BoundEvent, Object # The unique Charmhub library identifier, never change it LIBID = "75283550e3474e7b8b5b7724d345e3c2" @@ -123,7 +107,7 @@ def _on_config_changed(self, event: EventBase): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 13 +LIBPATCH = 16 logger = logging.getLogger(__name__) @@ -136,11 +120,12 @@ def _on_config_changed(self, event: EventBase): ) -class NetworkAttachmentDefinition(_NetworkAttachmentDefinition): # type: ignore[valid-type, misc] +class NetworkAttachmentDefinition(_NetworkAttachmentDefinition): """Object to represent Kubernetes Multus NetworkAttachmentDefinition.""" def __eq__(self, other): """Validates equality between two NetworkAttachmentDefinitions object.""" + assert self.metadata return self.metadata.name == other.metadata.name and self.spec == other.spec @@ -225,7 +210,7 @@ def pod_is_ready( raise KubernetesMultusError(f"Pod {pod_name} not found") return False return self._pod_is_patched( - pod=pod, # type: ignore[arg-type] + pod=pod, network_annotations=network_annotations, container_name=container_name, cap_net_admin=cap_net_admin, @@ -243,6 +228,8 @@ def network_attachment_definition_is_created( Returns: bool: Whether the NetworkAttachmentDefinition is created """ + assert network_attachment_definition.metadata + assert network_attachment_definition.metadata.name try: existing_nad = self.client.get( res=NetworkAttachmentDefinition, @@ -281,15 +268,21 @@ def create_network_attachment_definition( Args: network_attachment_definition: NetworkAttachmentDefinition object """ + assert network_attachment_definition.metadata + assert network_attachment_definition.metadata.name try: - self.client.create(obj=network_attachment_definition, namespace=self.namespace) # type: ignore[call-overload] # noqa: E501 + self.client.create( # type: ignore[call-overload] + obj=network_attachment_definition, + namespace=self.namespace, + ) except ApiError: raise KubernetesMultusError( f"Could not create NetworkAttachmentDefinition " - f"{network_attachment_definition.metadata.name}" # type: ignore[union-attr] + f"{network_attachment_definition.metadata.name}" ) logger.info( - "NetworkAttachmentDefinition %s created", network_attachment_definition.metadata.name # type: ignore[union-attr] # noqa: E501, W505 + "NetworkAttachmentDefinition %s created", + network_attachment_definition.metadata.name, ) def list_network_attachment_definitions(self) -> list[NetworkAttachmentDefinition]: @@ -300,7 +293,9 @@ def list_network_attachment_definitions(self) -> list[NetworkAttachmentDefinitio """ try: return list( - self.client.list(res=NetworkAttachmentDefinition, namespace=self.namespace) + self.client.list( + res=NetworkAttachmentDefinition, namespace=self.namespace + ) ) except ApiError: raise KubernetesMultusError("Could not list NetworkAttachmentDefinitions") @@ -316,7 +311,9 @@ def delete_network_attachment_definition(self, name: str) -> None: res=NetworkAttachmentDefinition, name=name, namespace=self.namespace ) except ApiError: - raise KubernetesMultusError(f"Could not delete NetworkAttachmentDefinition {name}") + raise KubernetesMultusError( + f"Could not delete NetworkAttachmentDefinition {name}" + ) logger.info("NetworkAttachmentDefinition %s deleted", name) def patch_statefulset( @@ -340,7 +337,9 @@ def patch_statefulset( logger.info("No network annotations were provided") return try: - statefulset = self.client.get(res=StatefulSet, name=name, namespace=self.namespace) + statefulset = self.client.get( + res=StatefulSet, name=name, namespace=self.namespace + ) except ApiError: raise KubernetesMultusError(f"Could not get statefulset {name}") container = Container(name=container_name) @@ -356,8 +355,8 @@ def patch_statefulset( container.securityContext.privileged = True # type: ignore[union-attr] statefulset_delta = StatefulSet( spec=StatefulSetSpec( - selector=statefulset.spec.selector, # type: ignore[attr-defined] - serviceName=statefulset.spec.serviceName, # type: ignore[attr-defined] + selector=statefulset.spec.selector, # type: ignore[union-attr] + serviceName=statefulset.spec.serviceName, # type: ignore[union-attr] template=PodTemplateSpec( metadata=ObjectMeta( annotations={ @@ -398,7 +397,9 @@ def unpatch_statefulset( container_name: Container name """ try: - statefulset = self.client.get(res=StatefulSet, name=name, namespace=self.namespace) + statefulset = self.client.get( + res=StatefulSet, name=name, namespace=self.namespace + ) except ApiError: raise KubernetesMultusError(f"Could not get statefulset {name}") @@ -410,14 +411,16 @@ def unpatch_statefulset( ] ) ) - container.securityContext.privileged = False + container.securityContext.privileged = False # type: ignore[reportOptionalMemberAccess] statefulset_delta = StatefulSet( spec=StatefulSetSpec( - selector=statefulset.spec.selector, # type: ignore[attr-defined] - serviceName=statefulset.spec.serviceName, # type: ignore[attr-defined] + selector=statefulset.spec.selector, # type: ignore[union-attr] + serviceName=statefulset.spec.serviceName, # type: ignore[union-attr] template=PodTemplateSpec( metadata=ObjectMeta( - annotations={NetworkAnnotation.NETWORK_ANNOTATION_RESOURCE_KEY: "[]"} + annotations={ + NetworkAnnotation.NETWORK_ANNOTATION_RESOURCE_KEY: "[]" + } ), spec=PodSpec(containers=[container]), ), @@ -433,7 +436,9 @@ def unpatch_statefulset( field_manager=self.__class__.__name__, ) except ApiError: - raise KubernetesMultusError(f"Could not remove patches from statefulset {name}") + raise KubernetesMultusError( + f"Could not remove patches from statefulset {name}" + ) logger.info("Multus annotation removed from %s statefulset", name) def statefulset_is_patched( @@ -457,19 +462,23 @@ def statefulset_is_patched( bool: Whether the statefulset has the expected multus annotation. """ try: - statefulset = self.client.get(res=StatefulSet, name=name, namespace=self.namespace) + statefulset = self.client.get( + res=StatefulSet, name=name, namespace=self.namespace + ) except ApiError as e: if e.status.reason == "Unauthorized": logger.debug("kube-apiserver not ready yet") else: raise KubernetesMultusError(f"Could not get statefulset {name}") return False + if not statefulset.spec: + return False return self._pod_is_patched( container_name=container_name, cap_net_admin=cap_net_admin, privileged=privileged, network_annotations=network_annotations, - pod=statefulset.spec.template, # type: ignore[attr-defined] + pod=statefulset.spec.template, ) def _pod_is_patched( @@ -493,12 +502,12 @@ def _pod_is_patched( bool """ if not self._annotations_contains_multus_networks( - annotations=pod.metadata.annotations, # type: ignore[arg-type,union-attr] + annotations=pod.metadata.annotations, # type: ignore[reportOptionalMemberAccess] network_annotations=network_annotations, ): return False if not self._container_security_context_is_set( - containers=pod.spec.containers, # type: ignore[union-attr] + containers=pod.spec.containers, # type: ignore[reportOptionalMemberAccess] container_name=container_name, cap_net_admin=cap_net_admin, privileged=privileged, @@ -513,7 +522,9 @@ def _annotations_contains_multus_networks( if NetworkAnnotation.NETWORK_ANNOTATION_RESOURCE_KEY not in annotations: return False try: - if json.loads(annotations[NetworkAnnotation.NETWORK_ANNOTATION_RESOURCE_KEY]) != [ + if json.loads( + annotations[NetworkAnnotation.NETWORK_ANNOTATION_RESOURCE_KEY] + ) != [ network_annotation.dict() for network_annotation in network_annotations ]: return False @@ -541,9 +552,12 @@ def _container_security_context_is_set( """ for container in containers: if container.name == container_name: - if cap_net_admin and "NET_ADMIN" not in container.securityContext.capabilities.add: # type: ignore[operator,union-attr] # noqa E501 + if ( + cap_net_admin + and "NET_ADMIN" not in container.securityContext.capabilities.add # type: ignore[operator,union-attr] + ): return False - if privileged and not container.securityContext.privileged: # type: ignore[union-attr] # noqa E501 + if privileged and not container.securityContext.privileged: # type: ignore[union-attr] return False return True @@ -554,7 +568,11 @@ def multus_is_available(self) -> bool: bool: Whether Multus is enabled """ try: - list(self.client.list(res=NetworkAttachmentDefinition, namespace=self.namespace)) + list( + self.client.list( + res=NetworkAttachmentDefinition, namespace=self.namespace + ) + ) except httpx.HTTPStatusError as e: if e.response.status_code == 404: return False @@ -565,55 +583,49 @@ def multus_is_available(self) -> bool: return True -class KubernetesMultusCharmLib(Object): +class KubernetesMultusCharmLib: """Class to be instantiated by charms requiring Multus networking.""" def __init__( self, - charm: CharmBase, - network_attachment_definitions_func: Callable[[], list[NetworkAttachmentDefinition]], - network_annotations_func: Callable[[], list[NetworkAnnotation]], + network_attachment_definitions: List[NetworkAttachmentDefinition], + network_annotations: List[NetworkAnnotation], + namespace: str, + statefulset_name: str, + pod_name: str, container_name: str, - refresh_event: BoundEvent, cap_net_admin: bool = False, privileged: bool = False, ): """Constructor for the KubernetesMultusCharmLib. Args: - charm: Charm object - network_attachment_definitions_func: A callable to a function returning a list of - `NetworkAttachmentDefinition` to be created. - network_annotations_func: A callable to a function returning a list - of `NetworkAnnotation` to be added to the container. + network_attachment_definitions: list of `NetworkAttachmentDefinition` to be created. + network_annotations: List of `NetworkAnnotation` to be added to the container. + namespace: Kubernetes namespace + statefulset_name: Statefulset name + pod_name: Pod name container_name: Container name cap_net_admin: Container requires NET_ADMIN capability privileged: Container requires privileged security context - refresh_event: A BoundEvent which will be observed - to configure_multus (e.g. NadConfigChangedEvent). """ - super().__init__(charm, "kubernetes-multus") - self.kubernetes = KubernetesClient(namespace=self.model.name) - self.network_attachment_definitions_func = network_attachment_definitions_func - self.network_annotations_func = network_annotations_func + self.namespace = namespace + self.statefulset_name = statefulset_name + self.pod_name = pod_name + self.kubernetes = KubernetesClient(namespace=self.namespace) + self.network_attachment_definitions = network_attachment_definitions + self.network_annotations = network_annotations self.container_name = container_name self.cap_net_admin = cap_net_admin self.privileged = privileged - # Apply custom events - self.framework.observe(refresh_event, self._configure_multus) - self.framework.observe(charm.on.remove, self._on_remove) - def _configure_multus(self, event: BoundEvent) -> None: - """Creates network attachment definitions and patches statefulset. - - Args: - event: EventBase - """ + def configure(self) -> None: + """Creates network attachment definitions and patches statefulset.""" self._configure_network_attachment_definitions() if not self._statefulset_is_patched(): self.kubernetes.patch_statefulset( - name=self.model.app.name, - network_annotations=self.network_annotations_func(), + name=self.statefulset_name, + network_annotations=self.network_annotations, container_name=self.container_name, cap_net_admin=self.cap_net_admin, privileged=self.privileged, @@ -623,12 +635,12 @@ def _network_attachment_definition_created_by_charm( self, network_attachment_definition: NetworkAttachmentDefinition ) -> bool: """Returns whether a given NetworkAttachmentDefinitions was created by this charm.""" - labels = network_attachment_definition.metadata.labels + labels = network_attachment_definition.metadata.labels # type: ignore[reportOptionalMemberAccess] if not labels: return False if "app.juju.is/created-by" not in labels: return False - if labels["app.juju.is/created-by"] != self.model.app.name: + if labels["app.juju.is/created-by"] != self.statefulset_name: return False return True @@ -644,7 +656,7 @@ def _configure_network_attachment_definitions(self): 3. Detects the NAD config changes and triggers pod restart if any there is any modification in existing NADs """ - network_attachment_definitions_to_create = self.network_attachment_definitions_func() + network_attachment_definitions_to_create = self.network_attachment_definitions nad_config_changed = False for ( existing_network_attachment_definition @@ -656,6 +668,12 @@ def _configure_network_attachment_definitions(self): existing_network_attachment_definition not in network_attachment_definitions_to_create ): + if not existing_network_attachment_definition.metadata: + logger.warning("NetworkAttachmentDefinition has no metadata") + continue + if not existing_network_attachment_definition.metadata.name: + logger.warning("NetworkAttachmentDefinition has no name") + continue self.kubernetes.delete_network_attachment_definition( name=existing_network_attachment_definition.metadata.name ) @@ -664,7 +682,9 @@ def _configure_network_attachment_definitions(self): network_attachment_definitions_to_create.remove( existing_network_attachment_definition ) - for network_attachment_definition_to_create in network_attachment_definitions_to_create: + for ( + network_attachment_definition_to_create + ) in network_attachment_definitions_to_create: self.kubernetes.create_network_attachment_definition( network_attachment_definition=network_attachment_definition_to_create ) @@ -676,7 +696,7 @@ def _configure_network_attachment_definitions(self): def _network_attachment_definitions_are_created(self) -> bool: """Returns whether all network attachment definitions are created.""" - for network_attachment_definition in self.network_attachment_definitions_func(): + for network_attachment_definition in self.network_attachment_definitions: if not self.kubernetes.network_attachment_definition_is_created( network_attachment_definition=network_attachment_definition ): @@ -686,8 +706,8 @@ def _network_attachment_definitions_are_created(self) -> bool: def _statefulset_is_patched(self) -> bool: """Returns whether statefuset is patched with network annotations and capabilities.""" return self.kubernetes.statefulset_is_patched( - name=self.model.app.name, - network_annotations=self.network_annotations_func(), + name=self.statefulset_name, + network_annotations=self.network_annotations, container_name=self.container_name, cap_net_admin=self.cap_net_admin, privileged=self.privileged, @@ -696,8 +716,8 @@ def _statefulset_is_patched(self) -> bool: def _pod_is_ready(self) -> bool: """Returns whether pod is ready with network annotations and capabilities.""" return self.kubernetes.pod_is_ready( - pod_name=self._pod, - network_annotations=self.network_annotations_func(), + pod_name=self.pod_name, + network_annotations=self.network_annotations, container_name=self.container_name, cap_net_admin=self.cap_net_admin, privileged=self.privileged, @@ -718,26 +738,13 @@ def is_ready(self) -> bool: pod_is_ready = self._pod_is_ready() return nad_are_created and satefulset_is_patched and pod_is_ready - @property - def _pod(self) -> str: - """Name of the unit's pod. - - Returns: - str: A string containing the name of the current unit's pod. - """ - return "-".join(self.model.unit.name.rsplit("/", 1)) - - def _on_remove(self, event: RemoveEvent) -> None: - """Deletes network attachment definitions and removes patch. - - Args: - event: RemoveEvent - """ + def remove(self) -> None: + """Deletes network attachment definitions and removes patch.""" self.kubernetes.unpatch_statefulset( - name=self.model.app.name, + name=self.statefulset_name, container_name=self.container_name, ) - for network_attachment_definition in self.network_attachment_definitions_func(): + for network_attachment_definition in self.network_attachment_definitions: if self.kubernetes.network_attachment_definition_is_created( network_attachment_definition=network_attachment_definition ): @@ -747,7 +754,7 @@ def _on_remove(self, event: RemoveEvent) -> None: def delete_pod(self) -> None: """Delete the pod.""" - self.kubernetes.delete_pod(self._pod) + self.kubernetes.delete_pod(self.pod_name) def multus_is_available(self) -> bool: """Check whether Multus is enabled leveraging existence of NAD custom resource. diff --git a/lib/charms/loki_k8s/v1/loki_push_api.py b/lib/charms/loki_k8s/v1/loki_push_api.py index c3c1d086..d75cb7e1 100644 --- a/lib/charms/loki_k8s/v1/loki_push_api.py +++ b/lib/charms/loki_k8s/v1/loki_push_api.py @@ -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 @@ -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"] @@ -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): @@ -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__( @@ -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__( @@ -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 = {} @@ -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.") diff --git a/lib/charms/sdcore_upf_k8s/v0/fiveg_n3.py b/lib/charms/sdcore_upf_k8s/v0/fiveg_n3.py index 0c974bba..41fdeaa7 100644 --- a/lib/charms/sdcore_upf_k8s/v0/fiveg_n3.py +++ b/lib/charms/sdcore_upf_k8s/v0/fiveg_n3.py @@ -80,7 +80,7 @@ def _on_fiveg_n3_available(self, event): import logging -from interface_tester.schema_base import DataBagSchema # type: ignore[import] +from interface_tester.schema_base import DataBagSchema from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, RelationJoinedEvent from ops.framework import EventBase, EventSource, Object from pydantic import BaseModel, Field, IPvAnyAddress, ValidationError @@ -93,7 +93,7 @@ def _on_fiveg_n3_available(self, event): # 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", "pytest-interface-tester"] @@ -125,7 +125,7 @@ class ProviderAppData(BaseModel): class ProviderSchema(DataBagSchema): """Provider schema for fiveg_n3.""" - app: ProviderAppData + app_data: ProviderAppData def data_matches_provider_schema(data: dict) -> bool: @@ -138,7 +138,7 @@ def data_matches_provider_schema(data: dict) -> bool: bool: True if data matches provider schema, False otherwise. """ try: - ProviderSchema(app=data) + ProviderSchema(app_data=ProviderAppData(**data)) return True except ValidationError as e: logger.error("Invalid data: %s", e) @@ -173,7 +173,7 @@ class N3ProviderCharmEvents(CharmEvents): class N3Provides(Object): """Class to be instantiated by provider of the `fiveg_n3`.""" - on = N3ProviderCharmEvents() + on = N3ProviderCharmEvents() # type: ignore def __init__(self, charm: CharmBase, relation_name: str): """Observe relation joined event. @@ -238,7 +238,7 @@ class N3RequirerCharmEvents(CharmEvents): class N3Requires(Object): """Class to be instantiated by requirer of the `fiveg_n3`.""" - on = N3RequirerCharmEvents() + on = N3RequirerCharmEvents() # type: ignore def __init__(self, charm: CharmBase, relation_name: str): """Observe relation joined and relation changed events. @@ -260,6 +260,6 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: event (RelationChangedEvent): Juju event """ relation_data = event.relation.data - upf_ip_address = relation_data[event.app].get("upf_ip_address") # type: ignore[index] + upf_ip_address = relation_data[event.app].get("upf_ip_address") if upf_ip_address: self.on.fiveg_n3_available.emit(upf_ip_address=upf_ip_address) diff --git a/lib/charms/sdcore_upf_k8s/v0/fiveg_n4.py b/lib/charms/sdcore_upf_k8s/v0/fiveg_n4.py index 6d56937c..ce00d86d 100644 --- a/lib/charms/sdcore_upf_k8s/v0/fiveg_n4.py +++ b/lib/charms/sdcore_upf_k8s/v0/fiveg_n4.py @@ -85,7 +85,7 @@ def _on_fiveg_n4_available(self, event): import logging -from interface_tester.schema_base import DataBagSchema # type: ignore[import] +from interface_tester.schema_base import DataBagSchema from ops.charm import CharmBase, CharmEvents, RelationChangedEvent, RelationJoinedEvent from ops.framework import EventBase, EventSource, Object from pydantic import BaseModel, Field, ValidationError @@ -98,7 +98,7 @@ def _on_fiveg_n4_available(self, event): # 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", "pytest-interface-tester"] @@ -138,7 +138,7 @@ class FivegN4ProviderAppData(BaseModel): class ProviderSchema(DataBagSchema): """Provider schema for fiveg_n4.""" - app: FivegN4ProviderAppData + app_data: FivegN4ProviderAppData def data_matches_provider_schema(data: dict) -> bool: @@ -151,7 +151,7 @@ def data_matches_provider_schema(data: dict) -> bool: bool: True if data matches provider schema, False otherwise. """ try: - ProviderSchema(app=data) + ProviderSchema(app_data=FivegN4ProviderAppData(**data)) return True except ValidationError as e: logger.error("Invalid data: %s", e) @@ -186,7 +186,7 @@ class N4ProviderCharmEvents(CharmEvents): class N4Provides(Object): """Class to be instantiated by provider of the `fiveg_n4`.""" - on = N4ProviderCharmEvents() + on = N4ProviderCharmEvents() # type: ignore def __init__(self, charm: CharmBase, relation_name: str): """Observe relation joined event. @@ -262,7 +262,7 @@ class N4RequirerCharmEvents(CharmEvents): class N4Requires(Object): """Class to be instantiated by requirer of the `fiveg_n4`.""" - on = N4RequirerCharmEvents() + on = N4RequirerCharmEvents() # type: ignore def __init__(self, charm: CharmBase, relation_name: str): """Observe relation joined and relation changed events. @@ -284,7 +284,7 @@ def _on_relation_changed(self, event: RelationChangedEvent) -> None: event (RelationChangedEvent): Juju event """ relation_data = event.relation.data - upf_hostname = relation_data[event.app].get("upf_hostname") # type: ignore[index] - upf_port = relation_data[event.app].get("upf_port") # type: ignore[index] + upf_hostname = relation_data[event.app].get("upf_hostname") + upf_port = relation_data[event.app].get("upf_port") if upf_hostname and upf_port: self.on.fiveg_n4_available.emit(upf_hostname=upf_hostname, upf_port=upf_port)