Skip to content

Commit

Permalink
feat: TLS events + update operator interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Gu1nness committed Nov 26, 2024
1 parent 0ef8f75 commit b869026
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 8 deletions.
17 changes: 17 additions & 0 deletions single_kernel_mongo/core/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@
from ops.model import Unit

from single_kernel_mongo.config.literals import CharmRole
from single_kernel_mongo.managers.mongo import MongoManager
from single_kernel_mongo.state.charm_state import CharmState

if TYPE_CHECKING:
from single_kernel_mongo.abstract_charm import AbstractMongoCharm
from single_kernel_mongo.managers.tls import TLSManager


class OperatorProtocol(ABC, Object):
"""Protocol for a charm operator."""

charm: AbstractMongoCharm
name: ClassVar[CharmRole]
tls_manager: TLSManager
state: CharmState
mongo_manager: MongoManager

def on_install(self) -> None:
"""Handles the install event."""
Expand Down Expand Up @@ -70,3 +76,14 @@ def on_relation_departed(self, departing_unit: Unit | None) -> None:
def on_stop(self) -> None:
"""Handles the stop event."""
...

def start_charm_services(self) -> None:
"""Starts the relevant services."""
...

def stop_charm_services(self) -> None:
"""Stop the relevant services."""
...

def restart_charm_services(self) -> None:
"""Restart the relevant services with updated config."""
4 changes: 2 additions & 2 deletions single_kernel_mongo/events/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ def __init__(self, dependent: MongoDBOperator):
self.manager = self.dependent.backup_manager
self.charm: AbstractMongoCharm = dependent.charm
self.relation_name = ExternalRequirerRelations.S3_CREDENTIALS
self.s3_client = S3Requirer(self.charm, self.relation_name)
self.s3_client = S3Requirer(self.charm, self.relation_name.value)

self.framework.observe(
self.charm.on[self.relation_name].relation_joined,
self.charm.on[self.relation_name.value].relation_joined,
self._on_s3_relation_joined,
)
self.framework.observe(
Expand Down
171 changes: 171 additions & 0 deletions single_kernel_mongo/events/tls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

"""Manager for handling TLS events."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from ops.charm import ActionEvent, RelationBrokenEvent, RelationJoinedEvent
from ops.framework import Object

from single_kernel_mongo.config.relations import ExternalRequirerRelations
from single_kernel_mongo.core.operator import OperatorProtocol
from single_kernel_mongo.core.structured_config import MongoDBRoles
from single_kernel_mongo.exceptions import UnknownCertificateExpiringError
from single_kernel_mongo.lib.charms.tls_certificates_interface.v3.tls_certificates import (
CertificateAvailableEvent,
CertificateExpiringEvent,
TLSCertificatesRequiresV3,
)
from single_kernel_mongo.utils.event_helpers import (
fail_action_with_error_log,
)

if TYPE_CHECKING:
from single_kernel_mongo.abstract_charm import AbstractMongoCharm


logger = logging.getLogger(__name__)


class TLSEventsHandler(Object):
"""Event Handler for managing TLS events."""

def __init__(self, dependent: OperatorProtocol):
super().__init__(parent=dependent, key="tls")
self.dependent = dependent
self.manager = self.dependent.tls_manager
self.charm: AbstractMongoCharm = dependent.charm
self.relation_name = ExternalRequirerRelations.TLS.value
self.certs_client = TLSCertificatesRequiresV3(self.charm, self.relation_name)

self.framework.observe(
self.charm.on.set_tls_private_key_action, self._on_set_tls_private_key
)
self.framework.observe(
self.charm.on[self.relation_name].relation_joined,
self._on_tls_relation_joined,
)
self.framework.observe(
self.charm.on[self.relation_name].relation_broken,
self._on_tls_relation_broken,
)
self.framework.observe(
self.certs_client.on.certificate_available, self._on_certificate_available
)
self.framework.observe(
self.certs_client.on.certificate_expiring, self._on_certificate_expiring
)

def _on_set_tls_private_key(self, event: ActionEvent) -> None:
"""Set the TLS private key which will be used for requesting the certificates."""
logger.debug("Request to set TLS private key received.")
if (
self.manager.state.is_role(MongoDBRoles.MONGOS)
and not self.manager.state.config_server_name is not None
):
logger.info(
"mongos is not running (not integrated to config-server) deferring renewal of certificates."
)
event.fail("Mongos cannot set TLS keys until integrated to config-server.")
return
if self.manager.state.upgrade_in_progress:
fail_action_with_error_log(
logger,
event,
"set-tls-private-key",
"Setting TLS keys during an upgrade is not supported.",
)
return
try:
for internal in (True, False):
param = "internal-key" if internal else "external-key"
key = event.params.get(param, None)
csr = self.manager.generate_certificate_request(key, internal=internal)
self.certs_client.request_certificate_creation(certificate_signing_request=csr)
self.manager.set_waiting_for_cert_to_update(internal=internal, waiting=True)
except ValueError as e:
event.fail(str(e))

def _on_tls_relation_joined(self, event: RelationJoinedEvent) -> None:
"""Handler for relation joined."""
if (
self.manager.state.is_role(MongoDBRoles.MONGOS)
and not self.manager.state.config_server_name is not None
):
logger.info(
"mongos is not running (not integrated to config-server) deferring renewal of certificates."
)
event.defer()
return
if self.manager.state.upgrade_in_progress:
logger.warning(
"Enabling TLS is not supported during an upgrade. The charm may be in a broken, unrecoverable state."
)
event.defer()
return

for internal in (True, False):
csr = self.manager.generate_certificate_request(None, internal=internal)
self.certs_client.request_certificate_creation(certificate_signing_request=csr)
self.manager.set_waiting_for_cert_to_update(internal=internal, waiting=True)

def _on_tls_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Handler for relation joined."""
if not self.manager.state.db_initialised:
logger.info(f"Deferring {str(type(event))}. db is not initialised.")
event.defer()
return

if self.manager.state.upgrade_in_progress:
logger.warning(
"Disabling TLS is not supported during an upgrade. The charm may be in a broken, unrecoverable state."
)
logger.debug("Disabling external and internal TLS for unit: %s", self.charm.unit.name)
self.manager.disable_certificates_for_unit()

def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
"""...."""
if (
self.manager.state.is_role(MongoDBRoles.MONGOS)
and not self.manager.state.config_server_name is not None
):
logger.info(
"mongos is not running (not integrated to config-server) deferring renewal of certificates."
)
event.defer()
return
if self.manager.state.upgrade_in_progress:
logger.warning(
"Enabling TLS is not supported during an upgrade. The charm may be in a broken, unrecoverable state."
)
event.defer()
return
self.manager.set_certificates(
event.certificate_signing_request, event.chain, event.certificate, event.ca
)
self.manager.enable_certificates_for_unit()

def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
"""...."""
if (
self.manager.state.is_role(MongoDBRoles.MONGOS)
and not self.manager.state.config_server_name is not None
):
logger.info(
"mongos is not running (not integrated to config-server) deferring renewal of certificates."
)
event.defer()
return
try:
old_csr, new_csr = self.manager.renew_expiring_certificate(event.certificate)
self.certs_client.request_certificate_renewal(
old_certificate_signing_request=old_csr,
new_certificate_signing_request=new_csr,
)
except UnknownCertificateExpiringError:
logger.debug("An unknown certificate is expiring.")
4 changes: 4 additions & 0 deletions single_kernel_mongo/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,7 @@ class InvalidPBMStatusError(Exception):

class InvalidArgumentForActionError(Exception):
"""Raised when arguments for an action are invalid."""


class UnknownCertificateExpiringError(Exception):
"""Raised when an unknown certificate is expiring."""
14 changes: 12 additions & 2 deletions single_kernel_mongo/managers/mongodb_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def __init__(self, charm: AbstractMongoCharm):
self.define_workloads_and_config_managers(container)

self.backup_manager = BackupManager(self.charm, self.substrate, self.state, container)
self.tls_manager = TLSManager(self.charm, self.workload, self.state, self.substrate)
self.tls_manager = TLSManager(self, self.workload, self.state, self.substrate)
self.mongo_manager = MongoManager(self.charm, self.workload, self.state, self.substrate)

self.backup_events = BackupEventsHandler(self)
Expand Down Expand Up @@ -484,18 +484,28 @@ def open_ports(self) -> None:
logger.exception(f"Failed to open port: {e}")
raise

@override
def start_charm_services(self):
"""Start the relevant services."""
self.workload.start()
if self.state.is_role(MongoDBRoles.CONFIG_SERVER):
self.mongos_workload.start()

@override
def stop_charm_services(self):
"""Start the relevant services."""
"""Stop the relevant services."""
self.workload.stop()
if self.state.is_role(MongoDBRoles.CONFIG_SERVER):
self.mongos_workload.stop()

@override
def restart_charm_services(self):
"""Restarts the charm services with updated config."""
self.stop_charm_services()
self.config_manager.set_environment()
self.mongos_config_manager.set_environment()
self.start_charm_services()

def instantiate_keyfile(self):
"""Instantiate the keyfile."""
if not (keyfile := self.state.app_peer_data.keyfile):
Expand Down
43 changes: 39 additions & 4 deletions single_kernel_mongo/managers/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
from cryptography.hazmat.backends import default_backend

from single_kernel_mongo.config.literals import Scope, Substrates
from single_kernel_mongo.core.operator import OperatorProtocol
from single_kernel_mongo.core.structured_config import MongoDBRoles
from single_kernel_mongo.exceptions import (
UnknownCertificateExpiringError,
WorkloadServiceError,
)
from single_kernel_mongo.lib.charms.tls_certificates_interface.v3.tls_certificates import (
generate_csr,
generate_private_key,
Expand All @@ -36,7 +41,7 @@
from single_kernel_mongo.workload.mongodb_workload import MongoDBWorkload

if TYPE_CHECKING:
from single_kernel_mongo.abstract_charm import AbstractMongoCharm
pass


class Sans(TypedDict):
Expand All @@ -54,17 +59,18 @@ class TLSManager:

def __init__(
self,
charm: AbstractMongoCharm,
dependent: OperatorProtocol,
workload: MongoDBWorkload,
state: CharmState,
substrate: Substrates,
) -> None:
self.charm = charm
self.dependent = dependent
self.charm = dependent.charm
self.workload = workload
self.state = state
self.substrate = substrate

def generate_certificate_request(self, param: str | None, internal: bool):
def generate_certificate_request(self, param: str | None, internal: bool) -> bytes:
"""Generate a TLS Certificate request."""
key: bytes
if param is None:
Expand All @@ -87,6 +93,7 @@ def generate_certificate_request(self, param: str | None, internal: bool):
label = "int" if internal else "ext"

self.state.unit_peer_data.update({f"{label}_certs_subject": self._get_subject_name()})
return csr

def generate_new_csr(self, internal: bool) -> tuple[bytes, bytes]:
"""Requests the renewal of a certificate.
Expand Down Expand Up @@ -205,6 +212,22 @@ def disable_certificates_for_unit(self):
self.workload.restart()
self.charm.status_manager.to_active(None)

def enable_certificates_for_unit(self):
"""Enables the new certificates for this unit."""
self.delete_certificates_from_workload()
self.push_tls_files_to_workload()
self.charm.status_manager.to_maintenance("enabling TLS")
try:
self.dependent.restart_charm_services()
except WorkloadServiceError as e:
logger.error("An exception occurred when starting mongod agent, error: %s.", str(e))
self.charm.status_manager.to_blocked("couldn't start MongoDB")
return
if not self.dependent.mongo_manager.mongod_ready():
self.charm.status_manager.to_waiting("waiting for MongoDB to start")
else:
self.charm.status_manager.to_active(None)

def delete_certificates_from_workload(self):
"""Deletes the certificates from the workload."""
logger.info("Deleting TLS certificate from VM")
Expand Down Expand Up @@ -254,6 +277,18 @@ def set_certificates(
self.set_tls_secret(internal, SECRET_CA_LABEL, ca)
self.set_waiting_for_cert_to_update(internal=internal, waiting=False)

def renew_expiring_certificate(self, certificate: str) -> tuple[bytes, bytes]:
"""Renew the expiring certificate."""
for internal in (False, True):
charm_cert = self.get_tls_secret(internal=internal, label_name=SECRET_CERT_LABEL) or ""
if certificate.rstrip() == charm_cert.rstrip():
logger.debug(
f"The {'internal' if internal else 'external'} TLS certificate is expiring."
)
logger.debug("Generating a new Certificate Signing Request.")
return self.generate_new_csr(internal)
raise UnknownCertificateExpiringError

def set_waiting_for_cert_to_update(self, internal: bool, waiting: bool) -> None:
"""Sets the databag."""
scope = "int" if internal else "ext"
Expand Down

0 comments on commit b869026

Please sign in to comment.