From 3d34917eb957d05a5db30207c7bddfb85049373f Mon Sep 17 00:00:00 2001 From: yazansalti Date: Wed, 4 Dec 2024 13:52:50 +0400 Subject: [PATCH] Use one relation for cert transfer v0 and v1 --- .../v0/certificate_transfer.py | 11 ++++- .../v1/certificate_transfer.py | 15 +++++-- metadata.yaml | 9 ---- src/charm.py | 45 ++++++++++++------- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py b/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py index caa6e228..72cc9a26 100644 --- a/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py +++ b/lib/charms/certificate_transfer_interface/v0/certificate_transfer.py @@ -101,6 +101,7 @@ def _on_certificate_removed(self, event: CertificateRemovedEvent): from typing import List, Mapping from jsonschema import exceptions, validate # type: ignore[import-untyped] +from ops import Relation from ops.charm import CharmBase, CharmEvents, RelationBrokenEvent, RelationChangedEvent from ops.framework import EventBase, EventSource, Handle, Object @@ -112,7 +113,7 @@ def _on_certificate_removed(self, event: CertificateRemovedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 8 +LIBPATCH = 9 PYDEPS = ["jsonschema"] @@ -391,3 +392,11 @@ def _on_relation_broken(self, event: RelationBrokenEvent) -> None: None """ self.on.certificate_removed.emit(relation_id=event.relation.id) + + def is_ready(self, relation: Relation) -> bool: + """Check if the relation is ready by checking that it has valid relation data.""" + relation_data = _load_relation_data(relation.data[relation.app]) + if not self._relation_data_is_valid(relation_data): + logger.warning("Provider relation data did not pass JSON Schema validation: ") + return False + return True diff --git a/lib/charms/certificate_transfer_interface/v1/certificate_transfer.py b/lib/charms/certificate_transfer_interface/v1/certificate_transfer.py index 5213484a..10b974fc 100644 --- a/lib/charms/certificate_transfer_interface/v1/certificate_transfer.py +++ b/lib/charms/certificate_transfer_interface/v1/certificate_transfer.py @@ -115,7 +115,7 @@ def _on_certificates_removed(self, event: CertificatesRemovedEvent): # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 1 +LIBPATCH = 2 logger = logging.getLogger(__name__) @@ -212,7 +212,7 @@ class CertificateTransferProvides(Object): """Certificate Transfer provider class to be instantiated by charms sending certificates.""" def __init__(self, charm: CharmBase, relationship_name: str): - super().__init__(charm, relationship_name) + super().__init__(charm, relationship_name + "_v1") self.charm = charm self.relationship_name = relationship_name @@ -376,7 +376,7 @@ def __init__( charm: Charm object relationship_name: Juju relation name """ - super().__init__(charm, relationship_name) + super().__init__(charm, relationship_name + "_v1") self.relationship_name = relationship_name self.charm = charm self.framework.observe( @@ -428,6 +428,15 @@ def get_all_certificates(self, relation_id: Optional[int] = None) -> Set[str]: result = result.union(data) return result + def is_ready(self, relation: Relation) -> bool: + """Check if the relation is ready by checking that it has valid relation data.""" + databag = relation.data[relation.app] + try: + ProviderApplicationData().load(databag) + return True + except DataValidationError: + return False + def _get_relation_data(self, relation: Relation) -> Set[str]: """Get the given relation data.""" databag = relation.data[relation.app] diff --git a/metadata.yaml b/metadata.yaml index 1e8b3c80..cc03e975 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -100,15 +100,6 @@ requires: Receive a CA cert for traefik to trust. This relation can be used with a local CA to obtain the CA cert that was used to sign proxied endpoints. - This is meant for applications that use the certificate-transfer-interface v0. - receive-ca-cert-v1: - interface: certificate_transfer - description: | - Receive a CA certs for traefik to trust. - This relation can be used with a local CA to obtain the CA certs that was used to sign proxied - endpoints. - This is meant for applications that use the certificate-transfer-interface v1. - # Must limit the relation count to 1 due to # https://github.com/canonical/certificate-transfer-interface/issues/6 limit: 1 diff --git a/src/charm.py b/src/charm.py index 011e9c4e..9cc10dea 100755 --- a/src/charm.py +++ b/src/charm.py @@ -100,6 +100,7 @@ logger = logging.getLogger(__name__) _TRAEFIK_CONTAINER_NAME = "traefik" +_RECV_CA_CERT_RELATION_NAME = "receive-ca-cert" PYDANTIC_IS_V1 = int(pydantic.version.VERSION.split(".")[0]) < 2 @@ -166,8 +167,8 @@ def __init__(self, *args): sans=sans, ) - self.recv_ca_cert_v0 = CertificateTransferRequiresV0(self, "receive-ca-cert") - self.recv_ca_cert_v1 = CertificateTransferRequiresV1(self, "receive-ca-cert-v1") + self.recv_ca_cert_v0 = CertificateTransferRequiresV0(self, _RECV_CA_CERT_RELATION_NAME) + self.recv_ca_cert_v1 = CertificateTransferRequiresV1(self, _RECV_CA_CERT_RELATION_NAME) # FIXME: Do not move these lower. They must exist before `_tcp_ports` is called. The # better long-term solution is to allow dynamic modification of the object, and to try @@ -301,6 +302,7 @@ def __init__(self, *args): self.recv_ca_cert_v1.on.certificates_removed, # pyright: ignore self._on_recv_ca_cert_removed, ) + observe(self.forward_auth.on.auth_config_changed, self._on_forward_auth_config_changed) observe(self.forward_auth.on.auth_config_removed, self._on_forward_auth_config_removed) @@ -396,20 +398,23 @@ def _update_received_ca_certs( if event and isinstance(event, CertificateTransferAvailableEventV0): cas.append(CA(event.ca, uid=event.relation_id)) else: - for relation in self.model.relations.get(self.recv_ca_cert_v0.relationship_name, []): - # For some reason, relation.units includes our unit and app. Need to exclude them. - for unit in set(relation.units).difference([self.app, self.unit]): - # Note: this nested loop handles the case of multi-unit CA, each unit providing - # a different ca cert, but that is not currently supported by the lib itself. - if ca := relation.data[unit].get("ca"): - cas.append(CA(ca, uid=relation.id)) - for relation in self.model.relations.get(self.recv_ca_cert_v1.relationship_name, []): - # add index to relation id to avoid conflicts in case of multiple CAs per relation - cas.extend( - CA(ca, uid=f"{relation.id}-{i}") - for i, ca in enumerate(self.recv_ca_cert_v1.get_all_certificates(relation.id)) - ) - + for relation in self.model.relations.get(_RECV_CA_CERT_RELATION_NAME, []): + recv_ca_cert_requirer = self._recv_ca_cert_requirer_from_relation(relation) + if recv_ca_cert_requirer is self.recv_ca_cert_v0: + # For some reason, relation.units includes our unit and app. Need to exclude them. + for unit in set(relation.units).difference([self.app, self.unit]): + # Note: this nested loop handles the case of multi-unit CA, each unit providing + # a different ca cert, but that is not currently supported by the lib itself. + if ca := relation.data[unit].get("ca"): + cas.append(CA(ca, uid=relation.id)) + elif recv_ca_cert_requirer is self.recv_ca_cert_v1: + # add index to relation id to avoid conflicts in case of multiple CAs per relation + cas.extend( + CA(ca, uid=f"{relation.id}-{i}") + for i, ca in enumerate( + self.recv_ca_cert_v1.get_all_certificates(relation.id) + ) + ) self.traefik.add_cas(cas) def _on_recv_ca_cert_removed( @@ -1132,6 +1137,14 @@ def _provider_from_relation(self, relation: Relation): return self.traefik_route raise RuntimeError(f"Invalid relation type: {relation_type} ({relation.name})") + def _recv_ca_cert_requirer_from_relation(self, relation: Relation): + """Returns the correct CertificateTransferRequirer based on a relation.""" + if self.recv_ca_cert_v0.is_ready(relation): + return self.recv_ca_cert_v0 + if self.recv_ca_cert_v1.is_ready(relation): + return self.recv_ca_cert_v1 + return None + @property def _external_host(self) -> Optional[str]: """Determine the external address for the ingress gateway.