diff --git a/src/charm.py b/src/charm.py index de768c1..73f574b 100755 --- a/src/charm.py +++ b/src/charm.py @@ -12,14 +12,14 @@ from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires # type: ignore[import] from charms.sdcore_nrf_k8s.v0.fiveg_nrf import NRFProvides # type: ignore[import] from charms.tls_certificates_interface.v3.tls_certificates import ( # type: ignore[import] - CertificateAvailableEvent, CertificateExpiringEvent, TLSCertificatesRequiresV3, generate_csr, generate_private_key, ) from jinja2 import Environment, FileSystemLoader # type: ignore[import] -from ops.charm import CharmBase, EventBase, RelationJoinedEvent +from ops.charm import CharmBase, RelationJoinedEvent +from ops.framework import EventBase from ops.main import main from ops.model import ActiveStatus, BlockedStatus, ModelError, WaitingStatus from ops.pebble import Layer @@ -110,67 +110,62 @@ def __init__(self, *args): self.framework.observe( self.on.fiveg_nrf_relation_joined, self._on_fiveg_nrf_relation_joined ) - self.framework.observe( - self.on.certificates_relation_created, self._on_certificates_relation_created - ) - self.framework.observe( - self.on.certificates_relation_joined, self._on_certificates_relation_joined - ) + self.framework.observe(self.on.certificates_relation_joined, self._configure_nrf) + self.framework.observe(self._certificates.on.certificate_available, self._configure_nrf) self.framework.observe( self.on.certificates_relation_broken, self._on_certificates_relation_broken ) - self.framework.observe( - self._certificates.on.certificate_available, self._on_certificate_available - ) self.framework.observe( self._certificates.on.certificate_expiring, self._on_certificate_expiring ) - def _configure_nrf(self, event: EventBase) -> None: - """Adds pebble layer and manages Juju unit status. - - Args: - event: Juju event - """ + def ready_to_configure(self) -> bool: + """Returns whether all preconditions are met to proceed with configuration.""" if not self._container.can_connect(): self.unit.status = WaitingStatus("Waiting for container to be ready") - event.defer() - return + return False for relation in [DATABASE_RELATION_NAME, "certificates"]: if not self._relation_created(relation): self.unit.status = BlockedStatus(f"Waiting for {relation} relation to be created") - return + return False if not self._database_is_available(): self.unit.status = WaitingStatus("Waiting for the database to be available") - return + return False if not self._get_database_uri(): self.unit.status = WaitingStatus("Waiting for database URI") - event.defer() - return - if not self._container.exists(path=BASE_CONFIG_PATH): + return False + if not self._container.exists(path=BASE_CONFIG_PATH) or not self._container.exists( + path=CERTS_DIR_PATH + ): self.unit.status = WaitingStatus("Waiting for storage to be attached") - event.defer() - return + return False if not _get_pod_ip(): self.unit.status = WaitingStatus("Waiting for pod IP address to be available") - event.defer() + return False + return True + + def _configure_nrf(self, event: EventBase) -> None: + """Adds pebble layer and manages Juju unit status. + + Args: + event: Juju event + """ + if not self.ready_to_configure(): return - if not self._certificate_is_stored(): + if not self._private_key_is_stored(): + self._generate_private_key() + if not self._csr_is_stored(): + self._request_new_certificate() + provider_certificate = self._get_current_provider_certificate() + if not provider_certificate: self.unit.status = WaitingStatus("Waiting for certificates to be stored") - event.defer() return - needs_restart = self._generate_config_file() - self._configure_workload(restart=needs_restart) + certificate_changed = self._update_certificate(provider_certificate=provider_certificate) + config_file_changed = self._generate_config_file() + self._configure_workload(restart=(config_file_changed or certificate_changed)) self._publish_nrf_info_for_all_requirers() self.unit.status = ActiveStatus() - def _on_certificates_relation_created(self, event: EventBase) -> None: - """Generates Private key.""" - if not self._container.can_connect(): - event.defer() - return - self._generate_private_key() - def _on_certificates_relation_broken(self, event: EventBase) -> None: """Deletes TLS related artifacts and reconfigures workload.""" if not self._container.can_connect(): @@ -181,33 +176,6 @@ def _on_certificates_relation_broken(self, event: EventBase) -> None: self._delete_certificate() self.unit.status = BlockedStatus("Waiting for certificates relation to be created") - def _on_certificates_relation_joined(self, event: EventBase) -> None: - """Generates CSR and requests new certificate.""" - if not self._container.can_connect(): - event.defer() - return - if not self._private_key_is_stored(): - event.defer() - return - if self._certificate_is_stored(): - return - - self._request_new_certificate() - - def _on_certificate_available(self, event: CertificateAvailableEvent) -> None: - """Pushes certificate to workload and configures workload.""" - if not self._container.can_connect(): - event.defer() - return - if not self._csr_is_stored(): - logger.warning("Certificate is available but no CSR is stored") - return - if event.certificate_signing_request != self._get_stored_csr(): - logger.debug("Stored CSR doesn't match one in certificate available event") - return - self._store_certificate(event.certificate) - self._configure_nrf(event) - def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None: """Requests new certificate.""" if not self._container.can_connect(): @@ -226,6 +194,31 @@ def _on_database_relation_broken(self, event: EventBase) -> None: """ self.unit.status = BlockedStatus("Waiting for database relation") + def _get_current_provider_certificate(self) -> str | None: + """Compares the current certificate request to what is in the interface. + + Returns The current valid provider certificate if present + """ + csr = self._get_stored_csr() + for provider_certificate in self._certificates.get_assigned_certificates(): + if provider_certificate.csr == csr: + return provider_certificate.certificate + return None + + def _update_certificate(self, provider_certificate) -> bool: + """Compares the provided certificate to what is stored. + + Returns True if the certificate was updated + """ + existing_certificate = ( + self._get_stored_certificate() if self._certificate_is_stored() else "" + ) + + if existing_certificate != provider_certificate: + self._store_certificate(certificate=provider_certificate) + return True + return False + def _generate_private_key(self) -> None: """Generates and stores private key.""" private_key = generate_private_key() diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 9177b13..7db9330 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -4,10 +4,11 @@ import unittest from unittest.mock import Mock, patch +from charms.tls_certificates_interface.v3.tls_certificates import ProviderCertificate from ops import testing from ops.model import ActiveStatus, BlockedStatus, WaitingStatus -from charm import NRFOperatorCharm # type: ignore[import] +from charm import NRFOperatorCharm DB_APPLICATION_NAME = "mongodb-k8s" BASE_CONFIG_PATH = "/etc/nrf" @@ -148,17 +149,18 @@ def test_given_storage_not_attached_when_pebble_ready_then_status_is_waiting( WaitingStatus("Waiting for storage to be attached"), ) + @patch("charm.generate_csr") @patch("charm.check_output") @patch("charm.generate_private_key") def test_given_certificates_not_stored_when_pebble_ready_then_status_is_waiting( - self, - patch_generate_private_key, - patch_check_output, + self, patch_generate_private_key, patch_check_output, patch_generate_csr ): self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) private_key = b"whatever key content" patch_generate_private_key.return_value = private_key + csr = b"whatever csr content" + patch_generate_csr.return_value = csr patch_check_output.return_value = b"1.1.1.1" self.harness.set_can_connect(container="nrf", val=True) self._create_database_relation_and_populate_data() @@ -169,12 +171,18 @@ def test_given_certificates_not_stored_when_pebble_ready_then_status_is_waiting( WaitingStatus("Waiting for certificates to be stored"), ) + @patch( + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 + ) + @patch("charm.generate_csr") @patch("charm.check_output") @patch("charm.generate_private_key") def test_given_database_info_and_storage_attached_and_certs_stored_when_pebble_ready_then_config_file_is_rendered_and_pushed( # noqa: E501 self, patch_generate_private_key, patch_check_output, + patch_generate_csr, + patch_get_assigned_certificates, ): self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) @@ -182,17 +190,20 @@ def test_given_database_info_and_storage_attached_and_certs_stored_when_pebble_r private_key = b"whatever key content" patch_generate_private_key.return_value = private_key patch_check_output.return_value = b"1.1.1.1" - csr = "Whatever CSR content" - (root / "support/TLS/nrf.csr").write_text(csr) - (root / f"etc/nrf/{CONFIG_FILE_NAME}").write_text("Dummy Content") certificate = "Whatever certificate content" - event = Mock() - event.certificate = certificate - event.certificate_signing_request = csr + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = csr.decode() + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] + (root / "support/TLS/nrf.csr").write_text(csr.decode()) + (root / f"etc/nrf/{CONFIG_FILE_NAME}").write_text("Dummy Content") self.harness.set_can_connect(container="nrf", val=True) self._create_database_relation_and_populate_data() self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME) - self.harness.charm._on_certificate_available(event=event) self.harness.container_pebble_ready(container_name="nrf") self.assertEqual(self.harness.model.unit.status, ActiveStatus("")) with open("tests/unit/expected_config/config.conf") as expected_config_file: @@ -223,17 +234,35 @@ def test_given_content_of_config_file_not_changed_when_pebble_ready_then_config_ (root / f"etc/nrf/{CONFIG_FILE_NAME}").stat().st_mtime, config_modification_time ) + @patch( + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 + ) + @patch("charm.generate_csr") @patch("charm.check_output") + @patch("charm.generate_private_key") def test_given_config_pushed_when_pebble_ready_then_pebble_plan_is_applied( self, + patch_generate_private_key, patch_check_output, + patch_generate_csr, + patch_get_assigned_certificates, ): - patch_check_output.return_value = b"1.1.1.1" self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) - certificate = "Whatever certificate content" root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.pem").write_text(certificate) + private_key = b"whatever key content" + patch_generate_private_key.return_value = private_key + patch_check_output.return_value = b"1.1.1.1" + certificate = "Whatever certificate content" + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = csr.decode() + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] + (root / "support/TLS/nrf.csr").write_text(csr.decode()) (root / f"etc/nrf/{CONFIG_FILE_NAME}").write_text( self._read_file("tests/unit/expected_config/config.conf").strip() ) @@ -265,17 +294,35 @@ def test_given_config_pushed_when_pebble_ready_then_pebble_plan_is_applied( self.assertEqual(expected_plan, updated_plan) + @patch( + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 + ) + @patch("charm.generate_csr") @patch("charm.check_output") + @patch("charm.generate_private_key") def test_given_database_relation_is_created_and_config_file_is_written_when_pebble_ready_then_status_is_active( # noqa: E501 self, + patch_generate_private_key, patch_check_output, + patch_generate_csr, + patch_get_assigned_certificates, ): - patch_check_output.return_value = b"1.1.1.1" self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) - certificate = "Whatever certificate content" root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.pem").write_text(certificate) + private_key = b"whatever key content" + patch_generate_private_key.return_value = private_key + patch_check_output.return_value = b"1.1.1.1" + certificate = "Whatever certificate content" + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = csr.decode() + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] + (root / "support/TLS/nrf.csr").write_text(csr.decode()) (root / f"etc/nrf/{CONFIG_FILE_NAME}").write_text( self._read_file("tests/unit/expected_config/config.conf").strip() ) @@ -316,27 +363,42 @@ def test_given_ip_not_available_when_pebble_ready_then_status_is_waiting( WaitingStatus("Waiting for pod IP address to be available"), ) + @patch( + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 + ) + @patch("charm.generate_csr") + @patch("charm.generate_private_key") @patch("charm.check_output") def test_given_https_nrf_url_and_service_is_running_when_fiveg_nrf_relation_joined_then_nrf_url_is_in_relation_databag( # noqa: E501 self, patch_check_output, + patch_generate_private_key, + patch_generate_csr, + patch_get_assigned_certificates, ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root("nrf") + private_key = b"whatever key content" + patch_generate_private_key.return_value = private_key patch_check_output.return_value = b"1.1.1.1" - self.harness.add_storage("config", attach=True) - self.harness.add_storage("certs", attach=True) certificate = "Whatever certificate content" - root = self.harness.get_filesystem_root("nrf") + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = csr.decode() + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] (root / "support/TLS/nrf.pem").write_text(certificate) (root / f"etc/nrf/{CONFIG_FILE_NAME}").write_text( self._read_file("tests/unit/expected_config/config.conf").strip() ) - self._create_database_relation_and_populate_data() self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME) - self.harness.set_can_connect(container="nrf", val=True) self.harness.container_pebble_ready("nrf") - relation_id = self.harness.add_relation( relation_name="fiveg_nrf", remote_app="nrf-requirer", @@ -347,16 +409,35 @@ def test_given_https_nrf_url_and_service_is_running_when_fiveg_nrf_relation_join ) self.assertEqual(relation_data["url"], "https://nrf:29510") + @patch( + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 + ) + @patch("charm.generate_csr") @patch("charm.check_output") + @patch("charm.generate_private_key") def test_service_starts_running_after_nrf_relation_joined_when_fiveg_pebble_ready_then_nrf_url_is_in_relation_databag( # noqa: E501 - self, patch_check_output + self, + patch_generate_private_key, + patch_check_output, + patch_generate_csr, + patch_get_assigned_certificates, ): - patch_check_output.return_value = b"1.1.1.1" self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) - certificate = "Whatever certificate content" root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.pem").write_text(certificate) + private_key = b"whatever key content" + patch_generate_private_key.return_value = private_key + patch_check_output.return_value = b"1.1.1.1" + certificate = "Whatever certificate content" + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = csr.decode() + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] + (root / "support/TLS/nrf.csr").write_text(csr.decode()) (root / f"etc/nrf/{CONFIG_FILE_NAME}").write_text( self._read_file("tests/unit/expected_config/config.conf").strip() ) @@ -393,19 +474,27 @@ def test_service_starts_running_after_nrf_relation_joined_when_fiveg_pebble_read self.assertEqual(relation_1_data["url"], "https://nrf:29510") self.assertEqual(relation_2_data["url"], "https://nrf:29510") + @patch("charm.generate_csr") + @patch("charm.check_output") @patch("charm.generate_private_key") def test_given_can_connect_when_on_certificates_relation_created_then_private_key_is_generated( - self, patch_generate_private_key + self, + patch_generate_private_key, + patch_check_output, + patch_generate_csr, ): private_key = b"whatever key content" self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) root = self.harness.get_filesystem_root("nrf") - self.harness.set_can_connect(container="nrf", val=True) patch_generate_private_key.return_value = private_key - - self.harness.charm._on_certificates_relation_created(event=Mock) - + patch_check_output.return_value = b"1.1.1.1" + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + self._create_database_relation_and_populate_data() + self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME) + self.harness.set_can_connect(container="nrf", val=True) + self.harness.container_pebble_ready("nrf") self.assertEqual((root / "support/TLS/nrf.key").read_text(), private_key.decode()) def test_given_certificates_are_stored_when_on_certificates_relation_broken_then_certificates_are_removed( # noqa: E501 @@ -450,98 +539,107 @@ def test_given_certificates_are_stored_when_on_certificates_relation_broken_then BlockedStatus(f"Waiting for {TLS_RELATION_NAME} relation to be created"), ) + @patch("charm.check_output") + @patch("charm.generate_private_key") @patch( "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.request_certificate_creation", # noqa: E501 new=Mock, ) @patch("charm.generate_csr") - def test_given_private_key_exists_when_on_certificates_relation_joined_then_csr_is_generated( - self, patch_generate_csr + def test_given_private_key_exists_when_pebble_ready_then_csr_is_generated( + self, + patch_generate_csr, + patch_generate_private_key, + patch_check_output, ): + self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) private_key = "whatever key content" + patch_generate_private_key.return_value = private_key + patch_check_output.return_value = b"1.1.1.1" root = self.harness.get_filesystem_root("nrf") (root / "support/TLS/nrf.key").write_text(private_key) csr = b"whatever csr content" patch_generate_csr.return_value = csr + self._create_database_relation_and_populate_data() + self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME) self.harness.set_can_connect(container="nrf", val=True) - self.harness.charm._on_certificates_relation_joined(event=Mock) + self.harness.container_pebble_ready(container_name="nrf") self.assertEqual((root / "support/TLS/nrf.csr").read_text(), csr.decode()) @patch( - "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.request_certificate_creation", # noqa: E501 + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 ) @patch("charm.generate_csr") - def test_given_private_key_exists_and_cert_not_yet_requested_when_on_certificates_relation_joined_then_cert_is_requested( # noqa: E501 + @patch("charm.check_output") + @patch("charm.generate_private_key") + def test_given_csr_matches_stored_one_when_certificate_available_then_certificate_is_pushed( self, + patch_generate_private_key, + patch_check_output, patch_generate_csr, - patch_request_certificate_creation, + patch_get_assigned_certificates, ): + self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) - private_key = "whatever key content" root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.key").write_text(private_key) + private_key = b"whatever key content" + patch_generate_private_key.return_value = private_key + patch_check_output.return_value = b"1.1.1.1" + certificate = "Whatever certificate content" csr = b"whatever csr content" patch_generate_csr.return_value = csr - self.harness.set_can_connect(container="nrf", val=True) + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = csr.decode() + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] + (root / "support/TLS/nrf.csr").write_text(csr.decode()) + self._create_database_relation_and_populate_data() + self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME) - self.harness.charm._on_certificates_relation_joined(event=Mock) + self.harness.set_can_connect(container="nrf", val=False) + self.harness.container_pebble_ready("nrf") - patch_request_certificate_creation.assert_called_with(certificate_signing_request=csr) + self.assertEqual((root / "support/TLS/nrf.pem").read_text(), certificate) @patch( - "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.request_certificate_creation", # noqa: E501 + "charms.tls_certificates_interface.v3.tls_certificates.TLSCertificatesRequiresV3.get_assigned_certificates", # noqa: E501 ) - def test_given_cert_already_stored_when_on_certificates_relation_joined_then_cert_is_not_requested( # noqa: E501 + @patch("charm.generate_csr") + @patch("charm.check_output") + @patch("charm.generate_private_key") + def test_given_csr_doesnt_match_stored_one_when_certificate_available_then_certificate_is_not_pushed( # noqa: E501 self, - patch_request_certificate_creation, + patch_generate_private_key, + patch_check_output, + patch_generate_csr, + patch_get_assigned_certificates, ): - self.harness.add_storage("certs", attach=True) - private_key = "whatever key content" - certificate = "Whatever certificate content" - root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.key").write_text(private_key) - (root / "support/TLS/nrf.pem").write_text(certificate) - self.harness.set_can_connect(container="nrf", val=True) - - self.harness.charm._on_certificates_relation_joined(event=Mock) - patch_request_certificate_creation.assert_not_called() - - def test_given_csr_matches_stored_one_when_certificate_available_then_certificate_is_pushed( - self, - ): + self.harness.add_storage("config", attach=True) self.harness.add_storage("certs", attach=True) - csr = "Whatever CSR content" - certificate = "Whatever certificate content" root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.csr").write_text(csr) - event = Mock() - event.certificate = certificate - event.certificate_signing_request = csr - self.harness.set_can_connect(container="nrf", val=True) - - self.harness.charm._on_certificate_available(event=event) - - self.assertEqual((root / "support/TLS/nrf.pem").read_text(), certificate) - - def test_given_csr_doesnt_match_stored_one_when_certificate_available_then_certificate_is_not_pushed( # noqa: E501 - self, - ): - self.harness.add_storage("certs", attach=True) - csr = "Stored CSR content" + private_key = b"whatever key content" + patch_generate_private_key.return_value = private_key + patch_check_output.return_value = b"1.1.1.1" certificate = "Whatever certificate content" - root = self.harness.get_filesystem_root("nrf") - (root / "support/TLS/nrf.csr").write_text(csr) - event = Mock() - event.certificate = certificate - event.certificate_signing_request = "Relation CSR content (different from stored one)" - self.harness.set_can_connect(container="nrf", val=True) - - self.harness.charm._on_certificate_available(event=event) - + csr = b"whatever csr content" + patch_generate_csr.return_value = csr + provider_certificate = Mock(ProviderCertificate) + provider_certificate.certificate = certificate + provider_certificate.csr = "This is a different CSR" + patch_get_assigned_certificates.return_value = [ + provider_certificate, + ] + (root / "support/TLS/nrf.csr").write_text(csr.decode()) + self._create_database_relation_and_populate_data() + self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME) + self.harness.set_can_connect(container="nrf", val=False) + self.harness.container_pebble_ready("nrf") with self.assertRaises(FileNotFoundError): (root / "support/TLS/nrf.pem").read_text()