Skip to content

Commit

Permalink
Use one relation for cert transfer v0 and v1
Browse files Browse the repository at this point in the history
  • Loading branch information
saltiyazan committed Dec 5, 2024
1 parent 5434be6 commit 3d34917
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"]

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]
Expand Down
9 changes: 0 additions & 9 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 29 additions & 16 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 3d34917

Please sign in to comment.