Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DPE-2442] Add juju secrets support #242

Merged
merged 23 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 87 additions & 72 deletions lib/charms/mongodb/v0/mongodb_tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
from ops.framework import Object
from ops.model import ActiveStatus, MaintenanceStatus, Unit

from config import Config

APP_SCOPE = Config.Relations.APP_SCOPE
UNIT_SCOPE = Config.Relations.UNIT_SCOPE
Scopes = Config.Relations.Scopes


# The unique Charmhub library identifier, never change it
LIBID = "e02a50f0795e4dd292f58e93b4f493dd"

Expand All @@ -33,52 +40,57 @@
# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 5

LIBPATCH = 6

logger = logging.getLogger(__name__)
TLS_RELATION = "certificates"


class MongoDBTLS(Object):
"""In this class we manage client database relations."""

def __init__(self, charm, peer_relation, substrate="k8s"):
def __init__(self, charm, peer_relation, substrate):
"""Manager of MongoDB client relations."""
super().__init__(charm, "client-relations")
self.charm = charm
self.substrate = substrate
self.peer_relation = peer_relation
self.certs = TLSCertificatesRequiresV1(self.charm, TLS_RELATION)
self.certs = TLSCertificatesRequiresV1(self.charm, Config.TLS.TLS_PEER_RELATION)
self.framework.observe(
self.charm.on.set_tls_private_key_action, self._on_set_tls_private_key
)
self.framework.observe(
self.charm.on[TLS_RELATION].relation_joined, self._on_tls_relation_joined
self.charm.on[Config.TLS.TLS_PEER_RELATION].relation_joined,
self._on_tls_relation_joined,
)
self.framework.observe(
self.charm.on[TLS_RELATION].relation_broken, self._on_tls_relation_broken
self.charm.on[Config.TLS.TLS_PEER_RELATION].relation_broken,
self._on_tls_relation_broken,
)
self.framework.observe(self.certs.on.certificate_available, self._on_certificate_available)
self.framework.observe(self.certs.on.certificate_expiring, self._on_certificate_expiring)

def is_tls_enabled(self, scope: Scopes):
"""Returns a boolean indicating if TLS for a given `scope` is enabled."""
return self.charm.get_secret(scope, Config.TLS.SECRET_CERT_LABEL) is not None

def _on_set_tls_private_key(self, event: ActionEvent) -> None:
"""Set the TLS private key, which will be used for requesting the certificate."""
logger.debug("Request to set TLS private key received.")
try:
self._request_certificate("unit", event.params.get("external-key", None))
self._request_certificate(UNIT_SCOPE, event.params.get("external-key", None))

if not self.charm.unit.is_leader():
event.log(
"Only juju leader unit can set private key for the internal certificate. Skipping."
)
return

self._request_certificate("app", event.params.get("internal-key", None))
self._request_certificate(APP_SCOPE, event.params.get("internal-key", None))
logger.debug("Successfully set TLS private key.")
except ValueError as e:
event.fail(str(e))

def _request_certificate(self, scope: str, param: Optional[str]):
def _request_certificate(self, scope: Scopes, param: Optional[str]):
if param is None:
key = generate_private_key()
else:
Expand All @@ -92,11 +104,11 @@ def _request_certificate(self, scope: str, param: Optional[str]):
sans_ip=[str(self.charm.model.get_binding(self.peer_relation).network.bind_address)],
)

self.charm.set_secret(scope, "key", key.decode("utf-8"))
self.charm.set_secret(scope, "csr", csr.decode("utf-8"))
self.charm.set_secret(scope, "cert", None)
self.charm.set_secret(scope, Config.TLS.SECRET_KEY_LABEL, key.decode("utf-8"))
self.charm.set_secret(scope, Config.TLS.SECRET_CSR_LABEL, csr.decode("utf-8"))
self.charm.set_secret(scope, Config.TLS.SECRET_CERT_LABEL, None)

if self.charm.model.get_relation(TLS_RELATION):
if self.charm.model.get_relation(Config.TLS.TLS_PEER_RELATION):
self.certs.request_certificate_creation(certificate_signing_request=csr)

@staticmethod
Expand All @@ -117,63 +129,59 @@ def _parse_tls_file(raw_content: str) -> bytes:
def _on_tls_relation_joined(self, _: RelationJoinedEvent) -> None:
"""Request certificate when TLS relation joined."""
if self.charm.unit.is_leader():
self._request_certificate("app", None)
self._request_certificate(APP_SCOPE, None)

self._request_certificate("unit", None)
self._request_certificate(UNIT_SCOPE, None)

def _on_tls_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Disable TLS when TLS relation broken."""
logger.debug("Disabling external TLS for unit: %s", self.charm.unit.name)
self.charm.set_secret("unit", "ca", None)
self.charm.set_secret("unit", "cert", None)
self.charm.set_secret("unit", "chain", None)
self.charm.set_secret(UNIT_SCOPE, Config.TLS.SECRET_CA_LABEL, None)
self.charm.set_secret(UNIT_SCOPE, Config.TLS.SECRET_CERT_LABEL, None)
self.charm.set_secret(UNIT_SCOPE, Config.TLS.SECRET_CHAIN_LABEL, None)

if self.charm.unit.is_leader():
logger.debug("Disabling internal TLS")
self.charm.set_secret("app", "ca", None)
self.charm.set_secret("app", "cert", None)
self.charm.set_secret("app", "chain", None)
if self.charm.get_secret("app", "cert"):
self.charm.set_secret(APP_SCOPE, Config.TLS.SECRET_CA_LABEL, None)
self.charm.set_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL, None)
self.charm.set_secret(APP_SCOPE, Config.TLS.SECRET_CHAIN_LABEL, None)

if self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL):
logger.debug(
"Defer until the leader deletes the internal TLS certificate to avoid second restart."
)
event.defer()
return

logger.debug("Restarting mongod with TLS disabled.")
if self.substrate == "vm":
self.charm.unit.status = MaintenanceStatus("disabling TLS")
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()
else:
self.charm.on_mongod_pebble_ready(event)
logger.info("Restarting mongod with TLS disabled.")
self.charm.unit.status = MaintenanceStatus("disabling TLS")
self.charm.delete_tls_certificate_from_workload()
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()

def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
"""Enable TLS when TLS certificate available."""
if (
event.certificate_signing_request.rstrip()
== self.charm.get_secret("unit", "csr").rstrip()
):
unit_csr = self.charm.get_secret(UNIT_SCOPE, Config.TLS.SECRET_CSR_LABEL)
app_csr = self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CSR_LABEL)

if unit_csr and event.certificate_signing_request.rstrip() == unit_csr.rstrip():
logger.debug("The external TLS certificate available.")
scope = "unit" # external crs
elif (
event.certificate_signing_request.rstrip()
== self.charm.get_secret("app", "csr").rstrip()
):
scope = UNIT_SCOPE # external crs
elif app_csr and event.certificate_signing_request.rstrip() == app_csr.rstrip():
logger.debug("The internal TLS certificate available.")
scope = "app" # internal crs
scope = APP_SCOPE # internal crs
else:
logger.error("An unknown certificate available.")
logger.error("An unknown certificate is available -- ignoring.")
return

old_cert = self.charm.get_secret(scope, "cert")
renewal = old_cert and old_cert != event.certificate

if scope == "unit" or (scope == "app" and self.charm.unit.is_leader()):
if scope == UNIT_SCOPE or (scope == APP_SCOPE and self.charm.unit.is_leader()):
self.charm.set_secret(
scope, "chain", "\n".join(event.chain) if event.chain is not None else None
scope,
Config.TLS.SECRET_CHAIN_LABEL,
"\n".join(event.chain) if event.chain is not None else None,
)
self.charm.set_secret(scope, "cert", event.certificate)
self.charm.set_secret(scope, "ca", event.ca)
self.charm.set_secret(scope, Config.TLS.SECRET_CERT_LABEL, event.certificate)
self.charm.set_secret(scope, Config.TLS.SECRET_CA_LABEL, event.ca)

if self._waiting_for_certs():
logger.debug(
Expand All @@ -182,46 +190,48 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
event.defer()
return

if renewal and self.substrate == "k8s":
self.charm.unit.get_container("mongod").stop("mongod")
dmitry-ratushnyy marked this conversation as resolved.
Show resolved Hide resolved
logger.info("Restarting mongod with TLS enabled.")

logger.debug("Restarting mongod with TLS enabled.")
if self.substrate == "vm":
self.charm._push_tls_certificate_to_workload()
self.charm.unit.status = MaintenanceStatus("enabling TLS")
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()
else:
self.charm.on_mongod_pebble_ready(event)
self.charm.delete_tls_certificate_from_workload()
self.charm.push_tls_certificate_to_workload()
self.charm.unit.status = MaintenanceStatus("enabling TLS")
self.charm.restart_mongod_service()
self.charm.unit.status = ActiveStatus()

def _waiting_for_certs(self):
"""Returns a boolean indicating whether additional certs are needed."""
if not self.charm.get_secret("app", "cert"):
if not self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL):
logger.debug("Waiting for application certificate.")
return True
if not self.charm.get_secret("unit", "cert"):
if not self.charm.get_secret(UNIT_SCOPE, Config.TLS.SECRET_CERT_LABEL):
logger.debug("Waiting for application certificate.")
return True

return False

def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
"""Request the new certificate when old certificate is expiring."""
if event.certificate.rstrip() == self.charm.get_secret("unit", "cert").rstrip():
if (
event.certificate.rstrip()
== self.charm.get_secret(UNIT_SCOPE, Config.TLS.SECRET_CERT_LABEL).rstrip()
):
logger.debug("The external TLS certificate expiring.")
scope = "unit" # external cert
elif event.certificate.rstrip() == self.charm.get_secret("app", "cert").rstrip():
scope = UNIT_SCOPE # external cert
elif (
event.certificate.rstrip()
== self.charm.get_secret(APP_SCOPE, Config.TLS.SECRET_CERT_LABEL).rstrip()
):
logger.debug("The internal TLS certificate expiring.")
if not self.charm.unit.is_leader():
return
scope = "app" # internal cert
scope = APP_SCOPE # internal cert
else:
logger.error("An unknown certificate expiring.")
return

logger.debug("Generating a new Certificate Signing Request.")
key = self.charm.get_secret(scope, "key").encode("utf-8")
old_csr = self.charm.get_secret(scope, "csr").encode("utf-8")
key = self.charm.get_secret(scope, Config.TLS.SECRET_KEY_LABEL).encode("utf-8")
old_csr = self.charm.get_secret(scope, Config.TLS.SECRET_CSR_LABEL).encode("utf-8")
new_csr = generate_csr(
private_key=key,
subject=self.get_host(self.charm.unit),
Expand All @@ -236,7 +246,7 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
new_certificate_signing_request=new_csr,
)

self.charm.set_secret(scope, "csr", new_csr.decode("utf-8"))
self.charm.set_secret(scope, Config.TLS.SECRET_CSR_LABEL, new_csr.decode("utf-8"))

def _get_sans(self) -> List[str]:
"""Create a list of DNS names for a MongoDB unit.
Expand All @@ -252,19 +262,24 @@ def _get_sans(self) -> List[str]:
str(self.charm.model.get_binding(self.peer_relation).network.bind_address),
]

def get_tls_files(self, scope: str) -> Tuple[Optional[str], Optional[str]]:
def get_tls_files(self, scope: Scopes) -> Tuple[Optional[str], Optional[str]]:
"""Prepare TLS files in special MongoDB way.

MongoDB needs two files:
— CA file should have a full chain.
— PEM file should have private key and certificate without certificate chain.
"""
ca = self.charm.get_secret(scope, "ca")
chain = self.charm.get_secret(scope, "chain")
if not self.is_tls_enabled(scope):
logging.debug(f"TLS disabled for {scope}")
return None, None
logging.debug(f"TLS *enabled* for {scope}, fetching data for CA and PEM files ")

ca = self.charm.get_secret(scope, Config.TLS.SECRET_CA_LABEL)
chain = self.charm.get_secret(scope, Config.TLS.SECRET_CHAIN_LABEL)
ca_file = chain if chain else ca

key = self.charm.get_secret(scope, "key")
cert = self.charm.get_secret(scope, "cert")
key = self.charm.get_secret(scope, Config.TLS.SECRET_KEY_LABEL)
cert = self.charm.get_secret(scope, Config.TLS.SECRET_CERT_LABEL)
pem_file = key
if cert:
pem_file = key + "\n" + cert if key else cert
Expand All @@ -276,4 +291,4 @@ def get_host(self, unit: Unit):
if self.substrate == "vm":
return self.charm._unit_ip(unit)
else:
return self.charm.get_hostname_by_unit(unit.name)
return self.charm.get_hostname_for_unit(unit)
Loading