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

Enforce TLS Integration #39

Merged
merged 2 commits into from
Sep 27, 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
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@ Charmed Operator for the SD-Core Network Repository Function (NRF).
```bash
juju deploy sdcore-nrf --trust --channel=edge
juju deploy mongodb-k8s --trust --channel=5/edge
juju integrate sdcore-nrf:database mongodb-k8s
```

## Optional
juju deploy self-signed-certificates --channel=beta

```bash
juju deploy self-signed-certificates
juju integrate sdcore-nrf:database mongodb-k8s
juju integrate self-signed-certificates:certificates sdcore-nrf:certificates
```

Expand Down
21 changes: 13 additions & 8 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ def _configure_nrf(self, event: EventBase) -> None:
self.unit.status = WaitingStatus("Waiting for container to be ready")
event.defer()
return
if not self._relation_created(DATABASE_RELATION_NAME):
self.unit.status = BlockedStatus("Waiting for database relation to be created")
return
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
if not self._database_is_available():
self.unit.status = WaitingStatus("Waiting for the database to be available")
return
Expand All @@ -156,6 +157,10 @@ def _configure_nrf(self, event: EventBase) -> None:
self.unit.status = WaitingStatus("Waiting for pod IP address to be available")
event.defer()
return
if not self._certificate_is_stored():
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)
self._publish_nrf_info_for_all_requirers()
Expand All @@ -176,7 +181,7 @@ def _on_certificates_relation_broken(self, event: EventBase) -> None:
self._delete_private_key()
self._delete_csr()
self._delete_certificate()
self._configure_nrf(event)
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."""
Expand Down Expand Up @@ -309,7 +314,7 @@ def _generate_config_file(self) -> bool:
nrf_ip=_get_pod_ip(), # type: ignore[arg-type]
database_name=DATABASE_NAME,
nrf_sbi_port=NRF_SBI_PORT,
scheme="https" if self._certificate_is_stored() else "http",
scheme="https",
)
if not self._config_file_content_matches(content=content):
self._push_config_file(
Expand Down Expand Up @@ -462,10 +467,10 @@ def _nrf_service_is_running(self) -> bool:
return False
return service.is_running()

def _get_nrf_url(self) -> str:
@staticmethod
def _get_nrf_url() -> str:
"""Returns NRF URL."""
scheme = "https" if self._certificate_is_stored() else "http"
return f"{scheme}://nrf:{NRF_SBI_PORT}"
return f"https://nrf:{NRF_SBI_PORT}"


if __name__ == "__main__":
Expand Down
22 changes: 21 additions & 1 deletion tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async def deploy_self_signed_certificates(ops_test):
await ops_test.model.deploy(
TLS_APPLICATION_NAME,
application_name=TLS_APPLICATION_NAME,
channel="edge",
channel="beta",
)


Expand Down Expand Up @@ -73,3 +73,23 @@ async def test_given_charm_is_deployed_when_relate_to_mongo_and_certificates_the
relation1=f"{APP_NAME}:certificates", relation2=f"{TLS_APPLICATION_NAME}:certificates"
)
await ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", timeout=1000)


@pytest.mark.abort_on_fail
async def test_remove_tls_and_wait_for_blocked_status(ops_test, build_and_deploy):
await ops_test.model.remove_application(TLS_APPLICATION_NAME, block_until_done=True) # type: ignore[union-attr] # noqa: E501
await ops_test.model.wait_for_idle(apps=[APP_NAME], status="blocked", timeout=60) # type: ignore[union-attr] # noqa: E501


@pytest.mark.abort_on_fail
async def test_restore_tls_and_wait_for_active_status(ops_test, build_and_deploy):
await ops_test.model.deploy( # type: ignore[union-attr]
TLS_APPLICATION_NAME,
application_name=TLS_APPLICATION_NAME,
channel="beta",
trust=True,
)
await ops_test.model.add_relation( # type: ignore[union-attr]
relation1=APP_NAME, relation2=TLS_APPLICATION_NAME
)
await ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", timeout=1000) # type: ignore[union-attr] # noqa: E501
2 changes: 1 addition & 1 deletion tests/unit/expected_config/config.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ configuration:
bindingIPv4: 0.0.0.0
port: 29510
registerIPv4: 1.1.1.1
scheme: http
scheme: https
serviceNameList:
- nnrf-nfm
- nnrf-disc
Expand Down
117 changes: 81 additions & 36 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
DB_APPLICATION_NAME = "mongodb-k8s"
BASE_CONFIG_PATH = "/etc/nrf"
CONFIG_FILE_NAME = "nrfcfg.yaml"
TLS_APPLICATION_NAME = "self-signed-certificates"
TLS_RELATION_NAME = "certificates"


class TestCharm(unittest.TestCase):
Expand Down Expand Up @@ -77,10 +79,22 @@ def test_given_database_relation_not_created_when_pebble_ready_then_status_is_bl
BlockedStatus("Waiting for database relation to be created"),
)

def test_given_certificates_relation_not_created_when_pebble_ready_then_status_is_blocked(
self,
):
self.harness.container_pebble_ready(container_name="nrf")
self._create_database_relation()

self.assertEqual(
self.harness.model.unit.status,
BlockedStatus(f"Waiting for {TLS_RELATION_NAME} relation to be created"),
)

def test_given_database_not_available_when_pebble_ready_then_status_is_waiting(
self,
):
self._create_database_relation()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)
self.harness.container_pebble_ready(container_name="nrf")
self.assertEqual(
self.harness.model.unit.status,
Expand All @@ -94,6 +108,7 @@ def test_given_database_information_not_available_when_pebble_ready_then_status_
):
patch_is_resource_created.return_value = True
self._create_database_relation()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)
self.harness.container_pebble_ready(container_name="nrf")
self.assertEqual(
self.harness.model.unit.status,
Expand All @@ -104,28 +119,63 @@ def test_given_storage_not_attached_when_pebble_ready_then_status_is_waiting(
self,
):
self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)
self.harness.container_pebble_ready(container_name="nrf")

self.assertEqual(
self.harness.model.unit.status,
WaitingStatus("Waiting for storage to be attached"),
)

@patch("ops.model.Container.exists")
@patch("ops.model.Container.push")
@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,
patch_push,
patch_exists,
):
private_key = b"whatever key content"
patch_generate_private_key.return_value = private_key
patch_check_output.return_value = b"1.1.1.1"
patch_exists.side_effect = [True, False, True, False]
self.harness.set_can_connect(container="nrf", val=True)
self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)
self.harness.container_pebble_ready("nrf")
self.assertEqual(
self.harness.model.unit.status,
WaitingStatus("Waiting for certificates to be stored"),
)

@patch("ops.model.Container.exists")
@patch("ops.model.Container.push")
@patch("ops.model.Container.pull")
@patch("charm.check_output")
def test_given_database_info_and_storage_attached_when_pebble_ready_then_config_file_is_rendered_and_pushed( # noqa: E501
@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_pull,
patch_push,
patch_exists,
):
private_key = b"whatever key content"
patch_generate_private_key.return_value = private_key
patch_check_output.return_value = b"1.1.1.1"
patch_pull.return_value = StringIO("dummy")
patch_exists.side_effect = [True, False, False]
csr = "Whatever CSR content"
certificate = "Whatever certificate content"
event = Mock()
event.certificate = certificate
event.certificate_signing_request = csr
patch_pull.side_effect = [StringIO(csr), StringIO("Dummy Content")]
patch_exists.side_effect = [True, True, False, False]
self._database_is_available()
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")
with open("tests/unit/expected_config/config.conf") as expected_config_file:
expected_content = expected_config_file.read()
Expand Down Expand Up @@ -172,6 +222,7 @@ def test_given_config_pushed_when_pebble_ready_then_pebble_plan_is_applied(
patch_exists.return_value = True

self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)

self.harness.container_pebble_ready(container_name="nrf")

Expand Down Expand Up @@ -214,8 +265,8 @@ def test_given_database_relation_is_created_and_config_file_is_written_when_pebb
patch_exists.return_value = True

self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)

self.harness.container_pebble_ready(container_name="nrf")
self.harness.container_pebble_ready("nrf")

self.assertEqual(self.harness.model.unit.status, ActiveStatus())
Expand All @@ -238,6 +289,7 @@ def test_given_ip_not_available_when_pebble_ready_then_status_is_waiting(
patch_exists.return_value = True

self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)

self.harness.container_pebble_ready(container_name="nrf")
self.harness.container_pebble_ready("nrf")
Expand All @@ -247,34 +299,6 @@ def test_given_ip_not_available_when_pebble_ready_then_status_is_waiting(
WaitingStatus("Waiting for pod IP address to be available"),
)

@patch("ops.model.Container.push", new=Mock)
@patch("ops.model.Container.pull")
@patch("ops.model.Container.exists")
@patch("charm.check_output")
def test_given_http_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_exists, patch_pull
):
patch_check_output.return_value = b"1.1.1.1"
patch_exists.side_effect = [True, False, False, False]
patch_pull.return_value = StringIO(
self._read_file("tests/unit/expected_config/config.conf").strip()
)

self._database_is_available()

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",
)
self.harness.add_relation_unit(relation_id=relation_id, remote_unit_name="nrf-requirer/0")
relation_data = self.harness.get_relation_data(
relation_id=relation_id, app_or_unit=self.harness.charm.app.name
)
self.assertEqual(relation_data["url"], "http://nrf:29510")

@patch("ops.model.Container.push", new=Mock)
@patch("ops.model.Container.pull")
@patch("ops.model.Container.exists")
Expand All @@ -289,6 +313,7 @@ def test_given_https_nrf_url_and_service_is_running_when_fiveg_nrf_relation_join
)

self._database_is_available()
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")
Expand All @@ -311,7 +336,7 @@ def test_service_starts_running_after_nrf_relation_joined_when_fiveg_pebble_read
self, patch_check_output, patch_exists, patch_pull
):
patch_check_output.return_value = b"1.1.1.1"
patch_exists.side_effect = [True, False, False, False]
patch_exists.side_effect = [True, True, False, False, False]
patch_pull.side_effect = [
StringIO(self._read_file("tests/unit/expected_config/config.conf").strip()),
StringIO(self._read_file("tests/unit/expected_config/config.conf").strip()),
Expand All @@ -336,6 +361,7 @@ def test_service_starts_running_after_nrf_relation_joined_when_fiveg_pebble_read
)

self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)

self.harness.container_pebble_ready("nrf")

Expand All @@ -345,8 +371,8 @@ def test_service_starts_running_after_nrf_relation_joined_when_fiveg_pebble_read
relation_2_data = self.harness.get_relation_data(
relation_id=relation_2_id, app_or_unit=self.harness.charm.app.name
)
self.assertEqual(relation_1_data["url"], "http://nrf:29510")
self.assertEqual(relation_2_data["url"], "http://nrf:29510")
self.assertEqual(relation_1_data["url"], "https://nrf:29510")
self.assertEqual(relation_2_data["url"], "https://nrf:29510")

@patch("charm.generate_private_key")
@patch("ops.model.Container.push")
Expand Down Expand Up @@ -375,6 +401,25 @@ def test_given_certificates_are_stored_when_on_certificates_relation_broken_then
patch_remove_path.assert_any_call(path="/support/TLS/nrf.key")
patch_remove_path.assert_any_call(path="/support/TLS/nrf.csr")

@patch("ops.model.Container.push")
@patch("ops.model.Container.remove_path")
@patch("ops.model.Container.exists")
def test_given_certificates_are_stored_when_on_certificates_relation_broken_then_status_is_blocked( # noqa: E501
self,
patch_exists,
patch_remove_path,
patch_push,
):
patch_exists.return_value = True
self.harness.set_can_connect(container="nrf", val=True)
self._database_is_available()
self.harness.add_relation(relation_name=TLS_RELATION_NAME, remote_app=TLS_APPLICATION_NAME)
self.harness.charm._on_certificates_relation_broken(event=Mock())
self.assertEqual(
self.harness.charm.unit.status,
BlockedStatus(f"Waiting for {TLS_RELATION_NAME} relation to be created"),
)

@patch(
"charms.tls_certificates_interface.v2.tls_certificates.TLSCertificatesRequiresV2.request_certificate_creation", # noqa: E501
new=Mock,
Expand Down