diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index a87fb15..eac6dc2 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -27,7 +27,7 @@ jobs: integration-test: uses: canonical/sdcore-github-workflows/.github/workflows/integration-test.yaml@main with: - charm-file-name: "sdcore-pcf_ubuntu-22.04-amd64.charm" + charm-file-name: "sdcore-pcf-k8s_ubuntu-22.04-amd64.charm" publish-charm: name: Publish Charm @@ -39,5 +39,5 @@ jobs: if: ${{ github.ref_name == 'main' }} uses: canonical/sdcore-github-workflows/.github/workflows/publish-charm.yaml@main with: - charm-file-name: "sdcore-pcf_ubuntu-22.04-amd64.charm" + charm-file-name: "sdcore-pcf-k8s_ubuntu-22.04-amd64.charm" secrets: inherit diff --git a/README.md b/README.md index 4ef4ec3..00be913 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,24 @@ -
- ONF Icon -
-
-
- CharmHub Badge - - .github/workflows/publish-charm.yaml - -
-
-

SD-Core PCF Operator

-
- -A Charmed Operator for SD-Core's Policy Control Function (PCF) component. +# SD-Core PCF Operator (k8s) +[![CharmHub Badge](https://charmhub.io/sdcore-pcf-k8s/badge.svg)](https://charmhub.io/sdcore-pcf-k8s) + +A Charmed Operator for SD-Core's Policy Control Function (PCF) component for K8s. ## Usage ```bash juju deploy mongodb-k8s --channel 6/beta --trust -juju deploy sdcore-nrf --channel edge --trust -juju deploy sdcore-pcf --channel edge --trust +juju deploy sdcore-nrf-k8s --channel edge +juju deploy sdcore-pcf-k8s --channel edge + juju deploy self-signed-certificates --channel=beta -juju integrate sdcore-pcf mongodb-k8s -juju integrate sdcore-nrf self-signed-certificates -juju integrate sdcore-pcf:fiveg_nrf sdcore-nrf -juju integrate sdcore-pcf:certificates self-signed-certificates:certificates +juju integrate sdcore-pcf-k8s mongodb-k8s +juju integrate sdcore-nrf-k8s self-signed-certificates +juju integrate sdcore-pcf-k8s:fiveg_nrf sdcore-nrf-k8s:fiveg_nrf +juju integrate sdcore-pcf-k8s:certificates self-signed-certificates:certificates ``` ## Image **pcf**: `ghcr.io/canonical/sdcore-pcf:1.3` + diff --git a/lib/charms/observability_libs/v1/kubernetes_service_patch.py b/lib/charms/observability_libs/v1/kubernetes_service_patch.py deleted file mode 100644 index 64dd13c..0000000 --- a/lib/charms/observability_libs/v1/kubernetes_service_patch.py +++ /dev/null @@ -1,341 +0,0 @@ -# Copyright 2021 Canonical Ltd. -# See LICENSE file for licensing details. - -"""# KubernetesServicePatch Library. - -This library is designed to enable developers to more simply patch the Kubernetes Service created -by Juju during the deployment of a sidecar charm. When sidecar charms are deployed, Juju creates a -service named after the application in the namespace (named after the Juju model). This service by -default contains a "placeholder" port, which is 65536/TCP. - -When modifying the default set of resources managed by Juju, one must consider the lifecycle of the -charm. In this case, any modifications to the default service (created during deployment), will be -overwritten during a charm upgrade. - -When initialised, this library binds a handler to the parent charm's `install` and `upgrade_charm` -events which applies the patch to the cluster. This should ensure that the service ports are -correct throughout the charm's life. - -The constructor simply takes a reference to the parent charm, and a list of -[`lightkube`](https://github.com/gtsystem/lightkube) ServicePorts that each define a port for the -service. For information regarding the `lightkube` `ServicePort` model, please visit the -`lightkube` [docs](https://gtsystem.github.io/lightkube-models/1.23/models/core_v1/#serviceport). - -Optionally, a name of the service (in case service name needs to be patched as well), labels, -selectors, and annotations can be provided as keyword arguments. - -## Getting Started - -To get started using the library, you just need to fetch the library using `charmcraft`. **Note -that you also need to add `lightkube` and `lightkube-models` to your charm's `requirements.txt`.** - -```shell -cd some-charm -charmcraft fetch-lib charms.observability_libs.v1.kubernetes_service_patch -cat << EOF >> requirements.txt -lightkube -lightkube-models -EOF -``` - -Then, to initialise the library: - -For `ClusterIP` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch(self, [port]) - # ... -``` - -For `LoadBalancer`/`NodePort` services: - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(443, name=f"{self.app.name}", targetPort=443, nodePort=30666) - self.service_patcher = KubernetesServicePatch( - self, [port], "LoadBalancer" - ) - # ... -``` - -Port protocols can also be specified. Valid protocols are `"TCP"`, `"UDP"`, and `"SCTP"` - -```python -# ... -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - tcp = ServicePort(443, name=f"{self.app.name}-tcp", protocol="TCP") - udp = ServicePort(443, name=f"{self.app.name}-udp", protocol="UDP") - sctp = ServicePort(443, name=f"{self.app.name}-sctp", protocol="SCTP") - self.service_patcher = KubernetesServicePatch(self, [tcp, udp, sctp]) - # ... -``` - -Bound with custom events by providing `refresh_event` argument: -For example, you would like to have a configurable port in your charm and want to apply -service patch every time charm config is changed. - -```python -from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch -from lightkube.models.core_v1 import ServicePort - -class SomeCharm(CharmBase): - def __init__(self, *args): - # ... - port = ServicePort(int(self.config["charm-config-port"]), name=f"{self.app.name}") - self.service_patcher = KubernetesServicePatch( - self, - [port], - refresh_event=self.on.config_changed - ) - # ... -``` - -Additionally, you may wish to use mocks in your charm's unit testing to ensure that the library -does not try to make any API calls, or open any files during testing that are unlikely to be -present, and could break your tests. The easiest way to do this is during your test `setUp`: - -```python -# ... - -@patch("charm.KubernetesServicePatch", lambda x, y: None) -def setUp(self, *unused): - self.harness = Harness(SomeCharm) - # ... -``` -""" - -import logging -from types import MethodType -from typing import List, Literal, Optional, Union - -from lightkube import ApiError, Client -from lightkube.core import exceptions -from lightkube.models.core_v1 import ServicePort, ServiceSpec -from lightkube.models.meta_v1 import ObjectMeta -from lightkube.resources.core_v1 import Service -from lightkube.types import PatchType -from ops.charm import CharmBase -from ops.framework import BoundEvent, Object - -logger = logging.getLogger(__name__) - -# The unique Charmhub library identifier, never change it -LIBID = "0042f86d0a874435adef581806cddbbb" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 7 - -ServiceType = Literal["ClusterIP", "LoadBalancer"] - - -class KubernetesServicePatch(Object): - """A utility for patching the Kubernetes service set up by Juju.""" - - def __init__( - self, - charm: CharmBase, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - *, - refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, - ): - """Constructor for KubernetesServicePatch. - - Args: - charm: the charm that is instantiating the library. - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - refresh_event: an optional bound event or list of bound events which - will be observed to re-apply the patch (e.g. on port change). - The `install` and `upgrade-charm` events would be observed regardless. - """ - super().__init__(charm, "kubernetes-service-patch") - self.charm = charm - self.service_name = service_name if service_name else self._app - self.service = self._service_object( - ports, - service_name, - service_type, - additional_labels, - additional_selectors, - additional_annotations, - ) - - # Make mypy type checking happy that self._patch is a method - assert isinstance(self._patch, MethodType) - # Ensure this patch is applied during the 'install' and 'upgrade-charm' events - self.framework.observe(charm.on.install, self._patch) - self.framework.observe(charm.on.upgrade_charm, self._patch) - self.framework.observe(charm.on.update_status, self._patch) - - # apply user defined events - if refresh_event: - if not isinstance(refresh_event, list): - refresh_event = [refresh_event] - - for evt in refresh_event: - self.framework.observe(evt, self._patch) - - def _service_object( - self, - ports: List[ServicePort], - service_name: Optional[str] = None, - service_type: ServiceType = "ClusterIP", - additional_labels: Optional[dict] = None, - additional_selectors: Optional[dict] = None, - additional_annotations: Optional[dict] = None, - ) -> Service: - """Creates a valid Service representation. - - Args: - ports: a list of ServicePorts - service_name: allows setting custom name to the patched service. If none given, - application name will be used. - service_type: desired type of K8s service. Default value is in line with ServiceSpec's - default value. - additional_labels: Labels to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_selectors: Selectors to be added to the kubernetes service (by default only - "app.kubernetes.io/name" is set to the service name) - additional_annotations: Annotations to be added to the kubernetes service. - - Returns: - Service: A valid representation of a Kubernetes Service with the correct ports. - """ - if not service_name: - service_name = self._app - labels = {"app.kubernetes.io/name": self._app} - if additional_labels: - labels.update(additional_labels) - selector = {"app.kubernetes.io/name": self._app} - if additional_selectors: - selector.update(additional_selectors) - return Service( - apiVersion="v1", - kind="Service", - metadata=ObjectMeta( - namespace=self._namespace, - name=service_name, - labels=labels, - annotations=additional_annotations, # type: ignore[arg-type] - ), - spec=ServiceSpec( - selector=selector, - ports=ports, - type=service_type, - ), - ) - - def _patch(self, _) -> None: - """Patch the Kubernetes service created by Juju to map the correct port. - - Raises: - PatchFailed: if patching fails due to lack of permissions, or otherwise. - """ - try: - client = Client() - except exceptions.ConfigError as e: - logger.warning("Error creating k8s client: %s", e) - return - - try: - if self._is_patched(client): - return - if self.service_name != self._app: - self._delete_and_create_service(client) - client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) - except ApiError as e: - if e.status.code == 403: - logger.error("Kubernetes service patch failed: `juju trust` this application.") - else: - logger.error("Kubernetes service patch failed: %s", str(e)) - else: - logger.info("Kubernetes service '%s' patched successfully", self._app) - - def _delete_and_create_service(self, client: Client): - service = client.get(Service, self._app, namespace=self._namespace) - service.metadata.name = self.service_name # type: ignore[attr-defined] - service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 - client.delete(Service, self._app, namespace=self._namespace) - client.create(service) - - def is_patched(self) -> bool: - """Reports if the service patch has been applied. - - Returns: - bool: A boolean indicating if the service patch has been applied. - """ - client = Client() - return self._is_patched(client) - - def _is_patched(self, client: Client) -> bool: - # Get the relevant service from the cluster - try: - service = client.get(Service, name=self.service_name, namespace=self._namespace) - except ApiError as e: - if e.status.code == 404 and self.service_name != self._app: - return False - logger.error("Kubernetes service get failed: %s", str(e)) - raise - - # Construct a list of expected ports, should the patch be applied - expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] - # Construct a list in the same manner, using the fetched service - fetched_ports = [ - (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] - ] # noqa: E501 - return expected_ports == fetched_ports - - @property - def _app(self) -> str: - """Name of the current Juju application. - - Returns: - str: A string containing the name of the current Juju application. - """ - return self.charm.app.name - - @property - def _namespace(self) -> str: - """The Kubernetes namespace we're running in. - - Returns: - str: A string containing the name of the current Kubernetes namespace. - """ - with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: - return f.read().strip() diff --git a/lib/charms/sdcore_nrf/v0/fiveg_nrf.py b/lib/charms/sdcore_nrf_k8s/v0/fiveg_nrf.py similarity index 97% rename from lib/charms/sdcore_nrf/v0/fiveg_nrf.py rename to lib/charms/sdcore_nrf_k8s/v0/fiveg_nrf.py index 2a35036..8342e0a 100644 --- a/lib/charms/sdcore_nrf/v0/fiveg_nrf.py +++ b/lib/charms/sdcore_nrf_k8s/v0/fiveg_nrf.py @@ -13,7 +13,7 @@ From a charm directory, fetch the library using `charmcraft`: ```shell -charmcraft fetch-lib charms.sdcore_nrf.v0.fiveg_nrf +charmcraft fetch-lib charms.sdcore_nrf_k8s.v0.fiveg_nrf ``` Add the following libraries to the charm's `requirements.txt` file: @@ -28,7 +28,7 @@ from ops.charm import CharmBase from ops.main import main -from charms.sdcore_nrf.v0.fiveg_nrf import NRFAvailableEvent, NRFRequires +from charms.sdcore_nrf_k8s.v0.fiveg_nrf import NRFAvailableEvent, NRFRequires logger = logging.getLogger(__name__) @@ -58,7 +58,7 @@ def _on_nrf_available(self, event: NRFAvailableEvent): from ops.charm import CharmBase, RelationJoinedEvent from ops.main import main -from charms.sdcore_nrf.v0.fiveg_nrf import NRFProvides +from charms.sdcore_nrf_k8s.v0.fiveg_nrf import NRFProvides class DummyFiveGNRFProviderCharm(CharmBase): @@ -103,14 +103,14 @@ def _on_nrf_url_changed( from pydantic import AnyHttpUrl, BaseModel, Field, ValidationError # The unique Charmhub library identifier, never change it -LIBID = "cd132a12c2b34243bfd2bae8d08c32d6" +LIBID = "14746bb6f8d34accbeac27ea50ff4715" # Increment this major API version when introducing breaking changes LIBAPI = 0 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 6 +LIBPATCH = 1 PYDEPS = ["pydantic", "pytest-interface-tester"] diff --git a/metadata.yaml b/metadata.yaml index 7fdc52b..ca8cdf0 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,12 +1,12 @@ -name: sdcore-pcf +name: sdcore-pcf-k8s -display-name: SD-Core PCF +display-name: SD-Core PCF K8s summary: A Charmed Operator for SD-Core's PCF component. description: | A Charmed Operator for SD-Core's Policy Control Function (PCF) component. -website: https://charmhub.io/sdcore-pcf -source: https://github.com/canonical/sdcore-pcf-operator -issues: https://github.com/canonical/sdcore-pcf-operator/issues +website: https://charmhub.io/sdcore-pcf-k8s +source: https://github.com/canonical/sdcore-pcf-k8s-operator +issues: https://github.com/canonical/sdcore-pcf-k8s-operator/issues containers: pcf: diff --git a/requirements.txt b/requirements.txt index 0e404db..31ea8fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ jinja2 -lightkube -lightkube-models ops pydantic pytest-interface-tester diff --git a/src/charm.py b/src/charm.py index 74b1783..8548964 100755 --- a/src/charm.py +++ b/src/charm.py @@ -2,7 +2,7 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -"""Charmed operator for the SD-Core's PCF service.""" +"""Charmed operator for the SD-Core's PCF service for K8s.""" import logging from ipaddress import IPv4Address @@ -10,10 +10,7 @@ from typing import Optional from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires # type: ignore[import] -from charms.observability_libs.v1.kubernetes_service_patch import ( # type: ignore[import] # noqa: E501 - KubernetesServicePatch, -) -from charms.sdcore_nrf.v0.fiveg_nrf import NRFRequires # type: ignore[import] +from charms.sdcore_nrf_k8s.v0.fiveg_nrf import NRFRequires # type: ignore[import] from charms.tls_certificates_interface.v2.tls_certificates import ( # type: ignore[import] CertificateAvailableEvent, CertificateExpiringEvent, @@ -22,7 +19,6 @@ generate_private_key, ) from jinja2 import Environment, FileSystemLoader -from lightkube.models.core_v1 import ServicePort from ops.charm import CharmBase from ops.framework import EventBase from ops.main import main @@ -46,7 +42,7 @@ class PCFOperatorCharm(CharmBase): - """Main class to describe Juju event handling for the 5G PCF operator.""" + """Main class to describe Juju event handling for the 5G PCF operator for K8s.""" def __init__(self, *args): super().__init__(*args) @@ -59,12 +55,10 @@ def __init__(self, *args): self, relation_name="database", database_name=DATABASE_NAME ) self._nrf_requires = NRFRequires(charm=self, relation_name=NRF_RELATION_NAME) - self._service_patcher = KubernetesServicePatch( - charm=self, - ports=[ServicePort(name="sbi", port=PCF_SBI_PORT)], - ) + self.unit.set_ports(PCF_SBI_PORT) self._certificates = TLSCertificatesRequiresV2(self, "certificates") self.framework.observe(self.on.database_relation_joined, self._configure_sdcore_pcf) + self.framework.observe(self.on.database_relation_broken, self._on_database_relation_broken) self.framework.observe(self._database.on.database_created, self._configure_sdcore_pcf) self.framework.observe(self.on.fiveg_nrf_relation_joined, self._configure_sdcore_pcf) self.framework.observe(self._nrf_requires.on.nrf_available, self._configure_sdcore_pcf) @@ -133,6 +127,14 @@ def _on_nrf_broken(self, event: EventBase) -> None: """ self.unit.status = BlockedStatus("Waiting for fiveg_nrf relation") + def _on_database_relation_broken(self, event: EventBase) -> None: + """Event handler for database relation broken. + + Args: + event: Juju event + """ + self.unit.status = BlockedStatus("Waiting for database relation") + def _on_certificates_relation_created(self, event: EventBase) -> None: """Generates Private key. @@ -170,6 +172,9 @@ def _on_certificates_relation_joined(self, event: EventBase) -> None: 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: diff --git a/test-requirements.txt b/test-requirements.txt index 6b5ceb7..c433a2f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,9 +3,11 @@ coverage[toml] flake8-docstrings flake8-builtins isort +macaroonbakery==1.3.4 # https://protobuf.dev/news/2022-05-06/#python-updates mypy pep8-naming pyproject-flake8 pytest +pytest-asyncio==0.21.1 pytest-operator types-PyYAML diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index fa0745e..6aba2f0 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -14,7 +14,7 @@ METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) APPLICATION_NAME = METADATA["name"] -NRF_APP_NAME = "sdcore-nrf" +NRF_APP_NAME = "sdcore-nrf-k8s" DATABASE_APP_NAME = "mongodb-k8s" TLS_PROVIDER_NAME = "self-signed-certificates" @@ -136,3 +136,29 @@ async def test_restore_tls_and_wait_for_active_status(ops_test: OpsTest, build_a ) await ops_test.model.integrate(relation1=APPLICATION_NAME, relation2=TLS_PROVIDER_NAME) await ops_test.model.wait_for_idle(apps=[APPLICATION_NAME], status="active", timeout=1000) + + +@pytest.mark.skip( + reason="Bug in MongoDB: https://github.com/canonical/mongodb-k8s-operator/issues/218" +) +@pytest.mark.abort_on_fail +async def test_remove_database_and_wait_for_blocked_status(ops_test: OpsTest, build_and_deploy): + assert ops_test.model + await ops_test.model.remove_application(DATABASE_APP_NAME, block_until_done=True) + await ops_test.model.wait_for_idle(apps=[APPLICATION_NAME], status="blocked", timeout=60) + + +@pytest.mark.skip( + reason="Bug in MongoDB: https://github.com/canonical/mongodb-k8s-operator/issues/218" +) +@pytest.mark.abort_on_fail +async def test_restore_database_and_wait_for_active_status(ops_test: OpsTest, build_and_deploy): + assert ops_test.model + await ops_test.model.deploy( + DATABASE_APP_NAME, + application_name=DATABASE_APP_NAME, + channel="5/edge", + trust=True, + ) + await ops_test.model.integrate(relation1=APPLICATION_NAME, relation2=DATABASE_APP_NAME) + await ops_test.model.wait_for_idle(apps=[APPLICATION_NAME], status="active", timeout=1000) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index bbab6df..0b171c5 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -3,7 +3,6 @@ import logging import unittest -from io import StringIO from unittest.mock import Mock, PropertyMock, patch import yaml @@ -11,7 +10,6 @@ from ops.model import ActiveStatus, BlockedStatus, WaitingStatus from charm import ( - BASE_CONFIG_PATH, CONFIG_FILE_NAME, DATABASE_RELATION_NAME, NRF_RELATION_NAME, @@ -26,10 +24,6 @@ class TestCharm(unittest.TestCase): - @patch( - "charm.KubernetesServicePatch", - lambda charm, ports: None, - ) def setUp(self): self.maxDiff = None self.namespace = "whatever" @@ -82,7 +76,7 @@ def _create_database_relation(self) -> int: ) return relation_id - def _database_is_available(self) -> str: + def _create_database_relation_and_populate_data(self) -> int: database_url = "http://1.1.1.1" database_username = "banana" database_password = "pizza" @@ -96,7 +90,7 @@ def _database_is_available(self) -> str: "uris": "".join([database_url]), }, ) - return database_url + return database_relation_id def _create_nrf_relation(self) -> int: """Creates NRF relation. @@ -161,24 +155,26 @@ def test_given_container_can_connect_and_certificates_relation_is_not_created_wh ) @patch("charm.check_output") - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) - @patch("ops.Container.push") + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) @patch("ops.model.Container.restart") def test_given_pcf_charm_in_active_state_when_nrf_relation_breaks_then_status_is_blocked( - self, _, __, patched_nrf_url, patch_exists, patch_pull, patch_check_output + self, _, patched_nrf_url, patch_check_output ): + self.harness.add_storage(storage_name="config", attach=True) + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root("pcf") + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) pod_ip = "1.1.1.1" patch_check_output.return_value = pod_ip.encode() - patch_pull.return_value = StringIO("super different config file content") self.harness.set_can_connect(container=self.container_name, val=True) patched_nrf_url.return_value = VALID_NRF_URL nrf_relation_id = self._create_nrf_relation() - self._database_is_available() - self.harness.charm._storage_is_attached = Mock(return_value=True) - patch_exists.return_value = [True, False] - self.harness.container_pebble_ready("pcf") + self._create_database_relation_and_populate_data() + self.harness.add_relation( + relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" + ) + self.harness.container_pebble_ready(self.container_name) self.harness.remove_relation(nrf_relation_id) @@ -187,11 +183,39 @@ def test_given_pcf_charm_in_active_state_when_nrf_relation_breaks_then_status_is BlockedStatus("Waiting for fiveg_nrf relation"), ) - @patch("ops.model.Container.push") + @patch("charm.check_output") + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) + @patch("ops.model.Container.restart") + def test_given_pcf_charm_in_active_state_when_database_relation_breaks_then_status_is_blocked( + self, _, patched_nrf_url, patch_check_output + ): + self.harness.add_storage(storage_name="config", attach=True) + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) + pod_ip = "1.1.1.1" + patch_check_output.return_value = pod_ip.encode() + self.harness.set_can_connect(container=self.container_name, val=True) + patched_nrf_url.return_value = VALID_NRF_URL + self._create_nrf_relation() + database_relation_id = self._create_database_relation_and_populate_data() + self.harness.add_relation( + relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" + ) + self.harness.container_pebble_ready(self.container_name) + + self.harness.remove_relation(database_relation_id) + + self.assertEqual( + self.harness.model.unit.status, + BlockedStatus("Waiting for database relation"), + ) + def test_given_container_can_connect_and_database_relation_is_not_available_when_configure_sdcore_pcf_then_status_is_waiting( # noqa: E501 self, - patch_push, ): + self.harness.add_storage(storage_name="certs", attach=True) self.harness.set_can_connect(container=self.container_name, val=True) self._create_database_relation() self._create_nrf_relation() @@ -205,13 +229,12 @@ def test_given_container_can_connect_and_database_relation_is_not_available_when WaitingStatus("Waiting for `database` relation to be available"), ) - @patch("ops.model.Container.push") def test_given_container_can_connect_and_fiveg_nrf_relation_is_not_available_when_configure_sdcore_pcf_then_status_is_waiting( # noqa: E501 self, - patch_push, ): + self.harness.add_storage(storage_name="certs", attach=True) self.harness.set_can_connect(container=self.container_name, val=True) - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" @@ -224,21 +247,19 @@ def test_given_container_can_connect_and_fiveg_nrf_relation_is_not_available_whe WaitingStatus("Waiting for NRF endpoint to be available"), ) - @patch("ops.model.Container.push") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) def test_given_container_storage_is_not_attached_when_configure_sdcore_pcf_then_status_is_waiting( # noqa: E501 self, patched_nrf_url, - patch_push, ): + self.harness.add_storage(storage_name="certs", attach=True) self.harness.set_can_connect(container=self.container_name, val=True) patched_nrf_url.return_value = VALID_NRF_URL - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.charm._storage_is_attached = Mock(return_value=False) self.harness.charm._configure_sdcore_pcf(event=Mock()) @@ -247,23 +268,21 @@ def test_given_container_storage_is_not_attached_when_configure_sdcore_pcf_then_ ) @patch("charm.check_output") - @patch("ops.model.Container.push") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) def test_given_certificate_is_not_stored_when_configure_sdcore_pcf_then_status_is_waiting( # noqa: E501 self, patched_nrf_url, - patch_push, patch_check_output, ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) self.harness.set_can_connect(container=self.container_name, val=True) patched_nrf_url.return_value = VALID_NRF_URL - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.charm._storage_is_attached = Mock(return_value=True) - self.harness.charm._certificate_is_stored = Mock(return_value=False) patch_check_output.return_value = b"1.1.1.1" self.harness.charm._configure_sdcore_pcf(event=Mock()) @@ -273,50 +292,51 @@ def test_given_certificate_is_not_stored_when_configure_sdcore_pcf_then_status_i ) @patch("charm.check_output") - @patch("ops.model.Container.exists") - @patch("ops.Container.push") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) - @patch("ops.model.Container.pull") + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) def test_given_config_file_is_not_written_when_configure_sdcore_pcf_is_called_then_config_file_is_written_with_expected_content( # noqa: E501 - self, _, patched_nrf_url, patch_push, patch_exists, patch_check_output + self, patched_nrf_url, patch_check_output ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) pod_ip = "1.1.1.1" patch_check_output.return_value = pod_ip.encode() self.harness.set_can_connect(container=self.container_name, val=True) patched_nrf_url.return_value = VALID_NRF_URL - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.charm._certificate_is_stored = Mock(return_value=True) - patch_exists.side_effect = [True, False, False] expected_config_file_content = self._read_file(EXPECTED_CONFIG_FILE_PATH) self.harness.charm._configure_sdcore_pcf(event=Mock()) - patch_push.assert_called_with( - path=f"{BASE_CONFIG_PATH}/{CONFIG_FILE_NAME}", - source=expected_config_file_content.strip(), + self.assertEqual( + (root / f"etc/pcf/{CONFIG_FILE_NAME}").read_text(), + expected_config_file_content.strip(), ) @patch("charm.check_output") - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") - @patch("ops.Container.push") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) def test_given_config_file_is_written_and_is_not_changed_when_configure_sdcore_pcf_is_called_then_config_file_is_not_written( # noqa: E501 - self, patched_nrf_url, patch_push, patch_exists, patch_pull, patch_check_output + self, patched_nrf_url, patch_check_output ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) + (root / f"etc/pcf/{CONFIG_FILE_NAME}").write_text( + self._read_file("tests/unit/expected_pcfcfg.yaml").strip() + ) + config_modification_time = (root / f"etc/pcf/{CONFIG_FILE_NAME}").stat().st_mtime pod_ip = "1.1.1.1" patch_check_output.return_value = pod_ip.encode() - patch_pull.side_effect = [ - StringIO(self._read_file(EXPECTED_CONFIG_FILE_PATH)), - StringIO(self._read_file(EXPECTED_CONFIG_FILE_PATH)), - ] patched_nrf_url.return_value = VALID_NRF_URL - patch_exists.return_value = False - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" @@ -325,59 +345,58 @@ def test_given_config_file_is_written_and_is_not_changed_when_configure_sdcore_p self.harness.container_pebble_ready("pcf") - patch_push.assert_not_called() + self.assertEqual( + (root / f"etc/pcf/{CONFIG_FILE_NAME}").stat().st_mtime, config_modification_time + ) - @patch("ops.model.Container.pull") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) - @patch("ops.model.Container.exists") + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) @patch("charm.check_output") - @patch("ops.model.Container.push") def test_given_config_file_exists_and_is_changed_when_configure_pcf_then_config_file_is_updated( # noqa: E501 self, - patch_push, patch_check_output, - patch_exists, patch_nrf_url, - patch_pull, ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) + (root / f"etc/pcf/{CONFIG_FILE_NAME}").write_text("super different config file content") pod_ip = "1.1.1.1" patch_check_output.return_value = pod_ip.encode() - patch_pull.return_value = StringIO("super different config file content") - patch_exists.side_effect = [True, False, False] patch_nrf_url.return_value = VALID_NRF_URL - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.charm._certificate_is_stored = Mock(return_value=True) self.harness.set_can_connect(container="pcf", val=True) self.harness.charm._configure_sdcore_pcf(event=Mock()) - patch_push.assert_called_with( - path="/etc/pcf/pcfcfg.yaml", - source=self._read_file("tests/unit/expected_pcfcfg.yaml"), + expected_content = self._read_file("tests/unit/expected_pcfcfg.yaml") + self.assertEqual( + (root / f"etc/pcf/{CONFIG_FILE_NAME}").read_text(), expected_content.strip() ) - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url") + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url") @patch("charm.check_output") - @patch("ops.model.Container.exists") - @patch("ops.model.Container.push") - @patch("ops.model.Container.pull") def test_given_config_files_and_relations_are_created_when_configure_sdcore_pcf_is_called_then_expected_plan_is_applied( # noqa: E501 - self, _, __, patch_exists, patch_check_output, patch_nrf_url + self, patch_check_output, patch_nrf_url ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) pod_ip = "1.1.1.1" patch_check_output.return_value = pod_ip.encode() patch_nrf_url.return_value = VALID_NRF_URL - patch_exists.side_effect = [True, True, False] - self._database_is_available() + self._create_database_relation_and_populate_data() self._create_nrf_relation() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.charm._certificate_is_stored = Mock(return_value=True) self.harness.set_can_connect(container=self.container_name, val=True) self.harness.charm._configure_sdcore_pcf(event=Mock()) @@ -403,27 +422,26 @@ def test_given_config_files_and_relations_are_created_when_configure_sdcore_pcf_ self.assertEqual(expected_plan, updated_plan) @patch("charm.check_output") - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) - @patch("ops.Container.push") + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) @patch("ops.model.Container.restart") def test_given_config_file_is_written_when_configure_sdcore_pcf_is_called_then_status_is_active( # noqa: E501 - self, _, __, patched_nrf_url, patch_exists, patch_pull, patch_check_output + self, _, patched_nrf_url, patch_check_output ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) + (root / f"etc/pcf/{CONFIG_FILE_NAME}").write_text("super different config file content") pod_ip = "1.1.1.1" patch_check_output.return_value = pod_ip.encode() - patch_pull.return_value = StringIO("super different config file content") self.harness.set_can_connect(container=self.container_name, val=True) patched_nrf_url.return_value = VALID_NRF_URL self._create_nrf_relation() - self._database_is_available() + self._create_database_relation_and_populate_data() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.charm._certificate_is_stored = Mock(return_value=True) - self.harness.charm._storage_is_attached = Mock(return_value=True) - patch_exists.return_value = [True, False] self.harness.charm._configure_sdcore_pcf(event=Mock()) @@ -431,22 +449,22 @@ def test_given_config_file_is_written_when_configure_sdcore_pcf_is_called_then_s @patch("ops.model.Container.restart", new=Mock) @patch("charm.check_output") - @patch("ops.model.Container.pull", new=Mock) - @patch("ops.model.Container.exists") - @patch("ops.Container.push", new=Mock) - @patch("charms.sdcore_nrf.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) + @patch("charms.sdcore_nrf_k8s.v0.fiveg_nrf.NRFRequires.nrf_url", new_callable=PropertyMock) def test_given_ip_not_available_when_configure_then_status_is_waiting( - self, _, patch_exists, patch_check_output + self, _, patch_check_output ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) patch_check_output.return_value = "".encode() self._create_nrf_relation() - self._database_is_available() - self.harness.charm._storage_is_attached = Mock(return_value=True) - patch_exists.return_value = [True, False] + self._create_database_relation_and_populate_data() self.harness.add_relation( relation_name=TLS_RELATION_NAME, remote_app="tls-certificates-operator" ) - self.harness.container_pebble_ready(container_name="pcf") + self.harness.container_pebble_ready(container_name=self.container_name) self.assertEqual( self.harness.model.unit.status, @@ -454,89 +472,108 @@ def test_given_ip_not_available_when_configure_then_status_is_waiting( ) @patch("charm.generate_private_key") - @patch("ops.model.Container.push") def test_given_can_connect_when_on_certificates_relation_created_then_private_key_is_generated( - self, patch_push, patch_generate_private_key + self, patch_generate_private_key ): + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.add_storage(storage_name="config", attach=True) + root = self.harness.get_filesystem_root(self.container_name) private_key = b"whatever key content" - self.harness.set_can_connect(container="pcf", val=True) + self.harness.set_can_connect(container=self.container_name, val=True) patch_generate_private_key.return_value = private_key self.harness.charm._on_certificates_relation_created(event=Mock) - patch_push.assert_called_with(path="/support/TLS/pcf.key", source=private_key.decode()) + self.assertEqual((root / "support/TLS/pcf.key").read_text(), private_key.decode()) - @patch("ops.model.Container.remove_path") - @patch("ops.model.Container.exists") def test_given_certificates_are_stored_when_on_certificates_relation_broken_then_certificates_are_removed( # noqa: E501 - self, patch_exists, patch_remove_path + self, ): - patch_exists.return_value = True - self.harness.set_can_connect(container="pcf", val=True) + self.harness.add_storage(storage_name="certs", attach=True) + self.harness.set_can_connect(container=self.container_name, val=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" + csr = "Whatever CSR content" + certificate = "Whatever certificate content" + (root / "support/TLS/pcf.key").write_text(private_key) + (root / "support/TLS/pcf.csr").write_text(csr) + (root / "support/TLS/pcf.pem").write_text(certificate) self.harness.charm._on_certificates_relation_broken(event=Mock) - patch_remove_path.assert_any_call(path="/support/TLS/pcf.pem") - patch_remove_path.assert_any_call(path="/support/TLS/pcf.key") - patch_remove_path.assert_any_call(path="/support/TLS/pcf.csr") + with self.assertRaises(FileNotFoundError): + (root / "support/TLS/pcf.pem").read_text() + (root / "support/TLS/pcf.key").read_text() + (root / "support/TLS/pcf.csr").read_text() @patch( "charms.tls_certificates_interface.v2.tls_certificates.TLSCertificatesRequiresV2.request_certificate_creation", # noqa: E501 new=Mock, ) - @patch("ops.model.Container.push") @patch("charm.generate_csr") - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") def test_given_private_key_exists_when_on_certificates_relation_joined_then_csr_is_generated( - self, patch_exists, patch_pull, patch_generate_csr, patch_push + self, patch_generate_csr ): + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" + (root / "support/TLS/pcf.key").write_text(private_key) csr = b"whatever csr content" patch_generate_csr.return_value = csr - patch_pull.return_value = StringIO("private key content") - patch_exists.return_value = True - self.harness.set_can_connect(container="pcf", val=True) + self.harness.set_can_connect(container=self.container_name, val=True) self.harness.charm._on_certificates_relation_joined(event=Mock) - patch_push.assert_called_with(path="/support/TLS/pcf.csr", source=csr.decode()) + self.assertEqual((root / "support/TLS/pcf.csr").read_text(), csr.decode()) @patch( "charms.tls_certificates_interface.v2.tls_certificates.TLSCertificatesRequiresV2.request_certificate_creation", # noqa: E501 ) - @patch("ops.model.Container.push", new=Mock) @patch("charm.generate_csr") - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") - def test_given_private_key_exists_when_on_certificates_relation_joined_then_cert_is_requested( + def test_given_private_key_exists_and_cert_not_yet_requested_when_on_certificates_relation_joined_then_cert_is_requested( # noqa: E501 self, - patch_exists, - patch_pull, patch_generate_csr, patch_request_certificate_creation, ): + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" + (root / "support/TLS/pcf.key").write_text(private_key) csr = b"whatever csr content" patch_generate_csr.return_value = csr - patch_pull.return_value = StringIO("private key content") - patch_exists.return_value = True self.harness.set_can_connect(container="pcf", val=True) self.harness.charm._on_certificates_relation_joined(event=Mock) patch_request_certificate_creation.assert_called_with(certificate_signing_request=csr) - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") - @patch("ops.model.Container.push") + @patch( + "charms.tls_certificates_interface.v2.tls_certificates.TLSCertificatesRequiresV2.request_certificate_creation", # noqa: E501 + ) + def test_given_cert_already_stored_when_on_certificates_relation_joined_then_cert_is_not_requested( # noqa: E501 + self, patch_request_certificate_creation + ): + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" + (root / "support/TLS/pcf.key").write_text(private_key) + certificate = "whatever certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) + self.harness.set_can_connect(container="pcf", 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, - patch_push, - patch_exists, - patch_pull, ): + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" csr = "Whatever CSR content" - patch_pull.return_value = StringIO(csr) - patch_exists.return_value = True + (root / "support/TLS/pcf.key").write_text(private_key) + (root / "support/TLS/pcf.csr").write_text(csr) certificate = "Whatever certificate content" event = Mock() event.certificate = certificate @@ -545,19 +582,17 @@ def test_given_csr_matches_stored_one_when_certificate_available_then_certificat self.harness.charm._on_certificate_available(event=event) - patch_push.assert_called_with(path="/support/TLS/pcf.pem", source=certificate) + self.assertEqual((root / "support/TLS/pcf.pem").read_text(), certificate) - @patch("ops.model.Container.pull") - @patch("ops.model.Container.exists") - @patch("ops.model.Container.push") def test_given_csr_doesnt_match_stored_one_when_certificate_available_then_certificate_is_not_pushed( # noqa: E501 self, - patch_push, - patch_exists, - patch_pull, ): - patch_pull.return_value = StringIO("Stored CSR content") - patch_exists.return_value = True + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" + csr = "Stored CSR content" + (root / "support/TLS/pcf.key").write_text(private_key) + (root / "support/TLS/pcf.csr").write_text(csr) certificate = "Whatever certificate content" event = Mock() event.certificate = certificate @@ -566,19 +601,21 @@ def test_given_csr_doesnt_match_stored_one_when_certificate_available_then_certi self.harness.charm._on_certificate_available(event=event) - patch_push.assert_not_called() + with self.assertRaises(FileNotFoundError): + (root / "support/TLS/pcf.pem").read_text() @patch( "charms.tls_certificates_interface.v2.tls_certificates.TLSCertificatesRequiresV2.request_certificate_creation", # noqa: E501 ) - @patch("ops.model.Container.push", new=Mock) @patch("charm.generate_csr") - @patch("ops.model.Container.pull") def test_given_certificate_does_not_match_stored_one_when_certificate_expiring_then_certificate_is_not_requested( # noqa: E501 - self, patch_pull, patch_generate_csr, patch_request_certificate_creation + self, patch_generate_csr, patch_request_certificate_creation ): + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + certificate = "Stored certificate content" + (root / "support/TLS/pcf.pem").write_text(certificate) event = Mock() - patch_pull.return_value = StringIO("Stored certificate content") event.certificate = "Relation certificate content (different from stored)" csr = b"whatever csr content" patch_generate_csr.return_value = csr @@ -591,16 +628,18 @@ def test_given_certificate_does_not_match_stored_one_when_certificate_expiring_t @patch( "charms.tls_certificates_interface.v2.tls_certificates.TLSCertificatesRequiresV2.request_certificate_creation", # noqa: E501 ) - @patch("ops.model.Container.push", new=Mock) @patch("charm.generate_csr") - @patch("ops.model.Container.pull") def test_given_certificate_matches_stored_one_when_certificate_expiring_then_certificate_is_requested( # noqa: E501 - self, patch_pull, patch_generate_csr, patch_request_certificate_creation + self, patch_generate_csr, patch_request_certificate_creation ): + self.harness.add_storage(storage_name="certs", attach=True) + root = self.harness.get_filesystem_root(self.container_name) + private_key = "whatever key content" certificate = "whatever certificate content" + (root / "support/TLS/pcf.key").write_text(private_key) + (root / "support/TLS/pcf.pem").write_text(certificate) event = Mock() event.certificate = certificate - patch_pull.return_value = StringIO(certificate) csr = b"whatever csr content" patch_generate_csr.return_value = csr self.harness.set_can_connect(container="pcf", val=True)