Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

feat: Store authentication keys in a separate database #39

Merged
merged 1 commit into from
Feb 26, 2024
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ A Charmed Operator for SD-Core's Webui component for K8s, a configuration servic
```bash
juju deploy mongodb-k8s --trust --channel=6/beta
juju deploy sdcore-webui-k8s --trust --channel=edge
juju integrate mongodb-k8s sdcore-webui-k8s
juju integrate mongodb-k8s sdcore-webui-k8s:common_database
juju integrate mongodb-k8s sdcore-webui-k8s:auth_database
```

## Image
Expand Down
4 changes: 3 additions & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ storage:
minimum-size: 1M

requires:
database:
common_database:
interface: mongodb_client
auth_database:
interface: mongodb_client

provides:
Expand Down
174 changes: 113 additions & 61 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,24 @@
from subprocess import CalledProcessError, check_output
from typing import Optional

from charms.data_platform_libs.v0.data_interfaces import ( # type: ignore[import]
DatabaseRequires,
DatabaseRequiresEvent,
)
from charms.data_platform_libs.v0.data_interfaces import DatabaseRequires # type: ignore[import]
from charms.sdcore_webui_k8s.v0.sdcore_management import ( # type: ignore[import]
SdcoreManagementProvides,
)
from jinja2 import Environment, FileSystemLoader
from ops import ActiveStatus, BlockedStatus, RelationBrokenEvent, WaitingStatus
from ops.charm import CharmBase, EventBase
from ops.main import main
from ops.model import ActiveStatus, BlockedStatus, WaitingStatus
from ops.pebble import Layer

logger = logging.getLogger(__name__)

BASE_CONFIG_PATH = "/etc/webui"
CONFIG_FILE_NAME = "webuicfg.conf"
DATABASE_RELATION_NAME = "database"
DATABASE_NAME = "free5gc"
COMMON_DATABASE_RELATION_NAME = "common_database"
AUTH_DATABASE_RELATION_NAME = "auth_database"
AUTH_DATABASE_NAME = "authentication"
COMMON_DATABASE_NAME = "free5gc"
SDCORE_MANAGEMENT_RELATION_NAME = "sdcore-management"
GRPC_PORT = 9876
WEBUI_URL_PORT = 5000
Expand All @@ -46,21 +45,30 @@ def _get_pod_ip() -> Optional[str]:
return None


def render_config_file(database_name: str, database_url: str) -> str:
def render_config_file(
common_database_name: str,
common_database_url: str,
auth_database_name: str,
auth_database_url: str,
) -> str:
"""Renders webui configuration file based on Jinja template.

Args:
database_name: Database Name
database_url: Database URL.
common_database_name: Common Database Name
common_database_url: Common Database URL.
auth_database_name: Authentication Database Name
auth_database_url: Authentication Database URL.

Returns:
str: Content of the configuration file.
"""
jinja2_environment = Environment(loader=FileSystemLoader("src/templates/"))
template = jinja2_environment.get_template("webuicfg.conf.j2")
return template.render(
database_name=database_name,
database_url=database_url,
common_database_name=common_database_name,
common_database_url=common_database_url,
auth_database_name=auth_database_name,
auth_database_url=auth_database_url,
)


Expand All @@ -79,81 +87,116 @@ def __init__(self, *args):
return
self._container_name = self._service_name = "webui"
self._container = self.unit.get_container(self._container_name)
self._database = DatabaseRequires(
self._common_database = DatabaseRequires(
self,
relation_name=COMMON_DATABASE_RELATION_NAME,
database_name=COMMON_DATABASE_NAME,
extra_user_roles="admin",
)
self._auth_database = DatabaseRequires(
self,
relation_name=DATABASE_RELATION_NAME,
database_name=DATABASE_NAME,
relation_name=AUTH_DATABASE_RELATION_NAME,
database_name=AUTH_DATABASE_NAME,
extra_user_roles="admin",
)
self._sdcore_management = SdcoreManagementProvides(self, SDCORE_MANAGEMENT_RELATION_NAME)
self.unit.set_ports(GRPC_PORT, WEBUI_URL_PORT)

self.framework.observe(self.on.update_status, self._configure)
self.framework.observe(self.on.webui_pebble_ready, self._configure)
self.framework.observe(self.on.database_relation_joined, self._configure)
self.framework.observe(self.on.database_relation_broken, self._on_database_relation_broken)
self.framework.observe(self._database.on.database_created, self._configure_db)
self.framework.observe(self._database.on.endpoints_changed, self._configure_db)
self.framework.observe(self.on.webui_pebble_ready, self._configure_webui)
self.framework.observe(self.on.common_database_relation_joined, self._configure_webui)
self.framework.observe(
self.on.common_database_relation_broken, self._on_common_database_relation_broken
)
self.framework.observe(self.on.auth_database_relation_joined, self._configure_webui)
self.framework.observe(
self.on.auth_database_relation_broken, self._on_auth_database_relation_broken
)
self.framework.observe(self._common_database.on.database_created, self._configure_webui)
self.framework.observe(self._auth_database.on.database_created, self._configure_webui)
self.framework.observe(self._common_database.on.endpoints_changed, self._configure_webui)
self.framework.observe(self._auth_database.on.endpoints_changed, self._configure_webui)
self.framework.observe(
self.on.sdcore_management_relation_joined, self._publish_sdcore_management_url
)
# Handling config changed event to publish the new url if the unit reboots and gets new IP
self.framework.observe(self.on.config_changed, self._publish_sdcore_management_url)

def _configure(self, event: EventBase) -> None:
"""Juju event handler.
def _configure_webui(self, event: EventBase) -> None:
"""Main callback function of the Webui operator.

Sets unit status, writes configuration file adds pebble layer.
Handles config changes.
Manages pebble layer and Juju unit status.

Args:
event (EventBase): Juju event.
event: Juju event
"""
if not self._database_relation_is_created():
self.unit.status = BlockedStatus("Waiting for database relation to be created")
for relation in [COMMON_DATABASE_RELATION_NAME, AUTH_DATABASE_RELATION_NAME]:
if not self._relation_created(relation):
self.unit.status = BlockedStatus(f"Waiting for {relation} relation to be created")
return
if not self._common_database_is_available():
self.unit.status = WaitingStatus("Waiting for the common database to be available")
return
if not self._auth_database_is_available():
self.unit.status = WaitingStatus("Waiting for the auth database to be available")
return
if not self._container.can_connect():
self.unit.status = WaitingStatus("Waiting for container to be ready")
return
if not self._container.exists(path=BASE_CONFIG_PATH):
self.unit.status = WaitingStatus("Waiting for storage to be attached")
return
if not self._config_file_exists():
self.unit.status = WaitingStatus("Waiting for config file to be written")
return

config_file_content = render_config_file(
common_database_name=COMMON_DATABASE_NAME,
common_database_url=self._get_common_database_url(),
auth_database_name=AUTH_DATABASE_NAME,
auth_database_url=self._get_auth_database_url(),
)
self._write_config_file(content=config_file_content)
self._container.add_layer("webui", self._pebble_layer, combine=True)
self._container.replan()
self._container.restart(self._service_name)
self.unit.status = ActiveStatus()

def _configure_db(self, event: DatabaseRequiresEvent) -> None:
"""Handles database events (DatabaseCreatedEvent, DatabaseEndpointsChangedEvent).
def _get_common_database_url(self) -> str:
"""Returns the common database url.

Args:
event (DatabaseRequiresEvent): Juju event.
Returns:
str: The common database url.
"""
if not self._container.can_connect():
self.unit.status = WaitingStatus("Waiting for container to be ready")
event.defer()
return
if not self._container.exists(path=BASE_CONFIG_PATH):
self.unit.status = WaitingStatus("Waiting for storage to be attached")
event.defer()
return
self._write_database_information_in_config_file(event.uris)
self._configure(event)
if not self._common_database_is_available():
raise RuntimeError(f"Database `{COMMON_DATABASE_NAME}` is not available")
return self._common_database.fetch_relation_data()[self._common_database.relations[0].id][
"uris"
].split(",")[0]

def _write_database_information_in_config_file(self, uris: str) -> None:
"""Extracts the MongoDb URL from uris and writes it in the config file.
def _get_auth_database_url(self) -> str:
"""Returns the authentication database url.

Args:
uris (str): database connection URIs.
Returns:
str: The authentication database url.
"""
database_url = uris.split(",")[0]
config_file_content = render_config_file(
database_name=DATABASE_NAME, database_url=database_url
)
self._write_config_file(content=config_file_content)
if not self._auth_database_is_available():
raise RuntimeError(f"Database `{AUTH_DATABASE_NAME}` is not available")
return self._auth_database.fetch_relation_data()[self._auth_database.relations[0].id][
"uris"
].split(",")[0]

def _common_database_is_available(self) -> bool:
"""Returns whether common database relation is available.

Returns:
bool: Whether common database relation is available.
"""
return bool(self._common_database.is_resource_created())

def _auth_database_is_available(self) -> bool:
"""Returns whether authentication database relation is available.

Returns:
bool: Whether authentication database relation is available.
"""
return bool(self._auth_database.is_resource_created())

def _publish_sdcore_management_url(self, event: EventBase):
"""Sets the webui url in the sdcore management relation.
Expand All @@ -172,13 +215,25 @@ def _publish_sdcore_management_url(self, event: EventBase):
management_url=self._get_webui_endpoint_url(),
)

def _on_database_relation_broken(self, event: EventBase) -> None:
"""Event handler for database relation broken.
def _on_common_database_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Event handler for common database relation broken.

Args:
event: Juju event
event: Juju relation broken event
"""
self.unit.status = BlockedStatus("Waiting for database relation")
if not self.model.relations[COMMON_DATABASE_RELATION_NAME]:
gatici marked this conversation as resolved.
Show resolved Hide resolved
self.unit.status = BlockedStatus(
f"Waiting for {COMMON_DATABASE_RELATION_NAME} relation"
)

def _on_auth_database_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Event handler for auth database relation broken.

Args:
event: Juju relation broken event
"""
if not self.model.relations[AUTH_DATABASE_RELATION_NAME]:
self.unit.status = BlockedStatus(f"Waiting for {AUTH_DATABASE_RELATION_NAME} relation")

def _write_config_file(self, content: str) -> None:
"""Writes configuration file based on provided content.
Expand All @@ -193,9 +248,6 @@ def _config_file_exists(self) -> bool:
"""Returns whether the configuration file exists."""
return bool(self._container.exists(f"{BASE_CONFIG_PATH}/{CONFIG_FILE_NAME}"))

def _database_relation_is_created(self) -> bool:
return self._relation_created(DATABASE_RELATION_NAME)

def _relation_created(self, relation_name: str) -> bool:
"""Returns whether a given Juju relation was crated.

Expand Down
6 changes: 4 additions & 2 deletions src/templates/webuicfg.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ configuration:
enabled: true
syncUrl: ""
mongodb:
name: {{ database_name }}
url: {{ database_url }}
name: {{ common_database_name }}
url: {{ common_database_url }}
authKeysDbName: {{ auth_database_name }}
authUrl: {{ auth_database_url }}
spec-compliant-sdf: false
info:
description: WebUI initial local configuration
Expand Down
4 changes: 2 additions & 2 deletions terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ module "sdcore-webui-k8s" {
Create the integrations, for instance:

```text
resource "juju_integration" "webui-db" {
resource "juju_integration" "webui-common-db" {
model = var.model_name

application {
name = module.webui.app_name
endpoint = module.webui.database_endpoint
endpoint = module.webui.common_database_endpoint
}

application {
Expand Down
11 changes: 8 additions & 3 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ output "app_name" {

# Required integration endpoints

output "database_endpoint" {
description = "Name of the endpoint to integrate with MongoDB using mongodb_client interface."
value = "database"
output "common_database_endpoint" {
description = "Name of the endpoint to integrate with MongoDB for common database using mongodb_client interface."
value = "common_database"
}

output "auth_database_endpoint" {
description = "Name of the endpoint to integrate with MongoDB for authentication database using mongodb_client interface."
value = "auth_database"
}

# Provided integration endpoints
Expand Down
16 changes: 13 additions & 3 deletions tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
APP_NAME = METADATA["name"]
DATABASE_APP_NAME = "mongodb-k8s"
COMMON_DATABASE_RELATION_NAME = "common_database"
AUTH_DATABASE_RELATION_NAME = "auth_database"


async def _deploy_database(ops_test):
Expand Down Expand Up @@ -63,8 +65,11 @@ async def test_relate_and_wait_for_active_status(
ops_test,
build_and_deploy,
):
await ops_test.model.add_relation(
relation1=f"{APP_NAME}:database", relation2=f"{DATABASE_APP_NAME}"
await ops_test.model.integrate(
relation1=f"{APP_NAME}:{COMMON_DATABASE_RELATION_NAME}", relation2=f"{DATABASE_APP_NAME}"
)
await ops_test.model.integrate(
relation1=f"{APP_NAME}:{AUTH_DATABASE_RELATION_NAME}", relation2=f"{DATABASE_APP_NAME}"
)
await ops_test.model.wait_for_idle(
apps=[APP_NAME],
Expand All @@ -90,7 +95,12 @@ async def test_remove_database_and_wait_for_blocked_status(ops_test: OpsTest, bu
async def test_restore_database_and_wait_for_active_status(ops_test: OpsTest, build_and_deploy):
assert ops_test.model
await _deploy_database(ops_test)
await ops_test.model.integrate(relation1=APP_NAME, relation2=DATABASE_APP_NAME)
await ops_test.model.integrate(
relation1=f"{APP_NAME}:{COMMON_DATABASE_RELATION_NAME}", relation2=DATABASE_APP_NAME
)
await ops_test.model.integrate(
relation1=f"{APP_NAME}:{AUTH_DATABASE_RELATION_NAME}", relation2=DATABASE_APP_NAME
)
await ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", timeout=1000)


Expand Down
2 changes: 2 additions & 0 deletions tests/unit/expected_webui_cfg.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ configuration:
mongodb:
name: free5gc
url: 1.9.11.4:1234
authKeysDbName: authentication
authUrl: 1.9.11.4:1234
spec-compliant-sdf: false
info:
description: WebUI initial local configuration
Expand Down
Loading
Loading