Update relation data from configure_charm

This change adds a mechanism for charms to be able to update
relation data as part of configure_charm rather than responding
to an explicit relation event.

The reason for this is that pod can be recycled at any point
causing it to loose its defered events. So, if a relation
adapter method to send relation data contains a defer that
deferred event can be lost and when a pod comes back it is
never resent.

Closes-Bug: 2036188
Change-Id: I2e6b27cdc05e93ebb25a7859b3c039ae64d82d9d
This commit is contained in:
Liam Young 2023-09-15 10:04:59 +00:00
parent a7ac636d28
commit 92ec0fa3e8
2 changed files with 57 additions and 26 deletions

View File

@ -236,6 +236,14 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
"Not all relations are ready"
)
def update_relations(self):
"""Update relation data."""
for handler in self.relation_handlers:
try:
handler.update_relation_data()
except NotImplementedError:
logging.debug(f"send_requests not implemented for {handler}")
def configure_unit(self, event: ops.framework.EventBase) -> None:
"""Run configuration on this unit."""
self.check_leader_ready()
@ -270,6 +278,11 @@ class OSBaseOperatorCharm(ops.charm.CharmBase):
def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Catchall handler to configure charm services."""
with sunbeam_guard.guard(self, "Bootstrapping"):
# Publishing relation data may be dependent on something else (like
# receiving a piece of data from the leader). To cover that
# republish relation if the relation adapter has implemented an
# update method.
self.update_relations()
self.configure_unit(event)
self.configure_app(event)
self.bootstrap_status.set(ActiveStatus())

View File

@ -138,6 +138,10 @@ class RelationHandler(ops.charm.Object):
"""Pull together context for rendering templates."""
return self.interface_properties()
def update_relation_data(self):
"""Update relation outside of relation context."""
raise NotImplementedError
class IngressHandler(RelationHandler):
"""Base class to handle Ingress relations."""
@ -811,6 +815,7 @@ class TlsCertificatesHandler(RelationHandler):
) -> None:
"""Run constructor."""
self.sans = sans
self._private_key = None
super().__init__(charm, relation_name, callback_f, mandatory)
try:
self.store = self.PeerKeyStore(
@ -818,6 +823,7 @@ class TlsCertificatesHandler(RelationHandler):
)
except KeyError:
self.store = self.LocalDBKeyStore(charm._state)
self.setup_private_key()
def setup_event_handler(self) -> None:
"""Configure event handlers for tls relation."""
@ -831,7 +837,6 @@ class TlsCertificatesHandler(RelationHandler):
self.certificates = TLSCertificatesRequiresV1(
self.charm, "certificates"
)
self.framework.observe(self.charm.on.install, self._on_install)
self.framework.observe(
self.charm.on.certificates_relation_joined,
self._on_certificates_relation_joined,
@ -854,7 +859,8 @@ class TlsCertificatesHandler(RelationHandler):
)
return self.certificates
def _on_install(self, event: ops.framework.EventBase) -> None:
def setup_private_key(self) -> None:
"""Create and store private key if needed."""
# Lazy import to ensure this lib is only required if the charm
# has this relation.
from charms.tls_certificates_interface.v1.tls_certificates import (
@ -862,45 +868,65 @@ class TlsCertificatesHandler(RelationHandler):
)
if not self.store.store_ready():
event.defer()
logger.debug("Store not ready, cannot generate key")
return
if self.store.get_private_key():
# Secret already saved
logger.debug("Private key already present")
self._private_key = self.store.get_private_key()
private_key_secret_id = self.store.get_private_key()
private_key_secret = self.model.get_secret(
id=private_key_secret_id
)
self._private_key = private_key_secret.get_content().get(
"private-key"
)
return
private_key = generate_private_key()
self._private_key = generate_private_key()
private_key_secret = self.model.unit.add_secret(
{"private-key": private_key.decode()},
{"private-key": self._private_key.decode()},
label=f"{self.charm.model.unit}-private-key",
)
self.store.set_private_key(private_key_secret.id)
@property
def private_key(self):
"""Private key for certificates."""
logger.debug("Returning private key: {}".format(self._private_key))
return self._private_key
def update_relation_data(self):
"""Request certificates outside of relation context."""
self._request_certificates()
def _on_certificates_relation_joined(
self, event: ops.framework.EventBase
) -> None:
"""Request certificates in response to relation join event."""
self._request_certificates()
def _request_certificates(self):
"""Request certificates from remote provider."""
# Lazy import to ensure this lib is only required if the charm
# has this relation.
from charms.tls_certificates_interface.v1.tls_certificates import (
generate_csr,
)
if not self.store.store_ready():
event.defer()
if self.ready:
logger.debug("Certificate request already complete.")
return
private_key = None
private_key_secret_id = self.store.get_private_key()
if private_key_secret_id:
private_key_secret = self.model.get_secret(
id=private_key_secret_id
)
private_key = private_key_secret.get_content().get("private-key")
if self.private_key:
logger.debug("Private key found, requesting certificates")
else:
logger.debug("Cannot request certificates, private key not found")
return
csr = generate_csr(
private_key=private_key.encode(),
private_key=self.private_key.encode(),
subject=self.charm.model.unit.name.replace("/", "-"),
sans=self.sans,
)
@ -983,16 +1009,8 @@ class TlsCertificatesHandler(RelationHandler):
cert = certs["cert"]
ca_cert = certs["ca"] + "\n" + "\n".join(certs["chain"])
key = None
private_key_secret_id = self.store.get_private_key()
if private_key_secret_id:
private_key_secret = self.model.get_secret(
id=private_key_secret_id
)
key = private_key_secret.get_content().get("private-key")
ctxt = {
"key": key,
"key": self.private_key,
"cert": cert,
"ca_cert": ca_cert,
}