Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[DPE-2995] Receive mongos user #10

Merged
merged 13 commits into from
Dec 13, 2023
2,272 changes: 2,272 additions & 0 deletions lib/charms/data_platform_libs/v0/data_interfaces.py

Large diffs are not rendered by default.

69 changes: 58 additions & 11 deletions lib/charms/mongodb/v0/config_server_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
This class handles the sharing of secrets between sharded components, adding shards, and removing
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
shards.
"""
import json
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file copied from mongodb charm

import logging

from charms.data_platform_libs.v0.data_interfaces import (
DatabaseProvides,
DatabaseRequires,
)
from charms.mongodb.v1.helpers import add_args_to_env, get_mongos_args
from charms.mongodb.v1.mongos import MongosConnection
from ops.charm import CharmBase, EventBase
Expand All @@ -32,7 +35,7 @@

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 2
LIBPATCH = 1


class ClusterProvider(Object):
Expand All @@ -44,10 +47,11 @@ def __init__(
"""Constructor for ShardingProvider object."""
self.relation_name = relation_name
self.charm = charm
self.database_provides = DatabaseProvides(self.charm, relation_name=self.relation_name)

super().__init__(charm, self.relation_name)
self.framework.observe(
charm.on[self.relation_name].relation_joined, self._on_relation_joined
charm.on[self.relation_name].relation_changed, self._on_relation_changed
)

# TODO Future PRs handle scale down
Expand All @@ -71,16 +75,19 @@ def pass_hook_checks(self, event: EventBase) -> bool:

return True

def _on_relation_joined(self, event) -> None:
def _on_relation_changed(self, event) -> None:
"""Handles providing mongos with KeyFile and hosts."""
if not self.pass_hook_checks(event):
logger.info("Skipping relation joined event: hook checks did not pass")
return

config_server_db = self.generate_config_server_db()

# create user and set secrets for mongos relation
self.charm.client_relations.oversee_users(None, None)

# TODO Future PR, use secrets
self._update_relation_data(
self.update_relation_data(
event.relation.id,
{
KEYFILE_KEY: self.charm.get_secret(
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -90,7 +97,7 @@ def _on_relation_joined(self, event) -> None:
},
)

def _update_relation_data(self, relation_id: int, data: dict) -> None:
def update_relation_data(self, relation_id: int, data: dict) -> None:
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
"""Updates a set of key-value pairs in the relation.

This function writes in the application data bag, therefore, only the leader unit can call
Expand All @@ -102,9 +109,7 @@ def _update_relation_data(self, relation_id: int, data: dict) -> None:
that should be updated in the relation.
"""
if self.charm.unit.is_leader():
relation = self.charm.model.get_relation(self.relation_name, relation_id)
if relation:
relation.data[self.charm.model.app].update(data)
self.database_provides.update_relation_data(relation_id, data)

def generate_config_server_db(self) -> str:
"""Generates the config server database for mongos to connect to."""
Expand All @@ -126,13 +131,38 @@ def __init__(
"""Constructor for ShardingProvider object."""
self.relation_name = relation_name
self.charm = charm
self.database_requires = DatabaseRequires(
self.charm,
relation_name=self.relation_name,
database_name=self.charm.database,
extra_user_roles=self.charm.extra_user_roles,
)

super().__init__(charm, self.relation_name)
self.framework.observe(
charm.on[self.relation_name].relation_created, self._on_relation_created_event
)
self.framework.observe(
charm.on[self.relation_name].relation_changed, self._on_relation_changed
)
# TODO Future PRs handle scale down

def _on_relation_created_event(self, event):
"""Sets database and extra user roles in the relation."""
if not self.charm.unit.is_leader():
return

if not self.charm.database:
logger.info("Waiting for database from application")
event.defer()
return

rel_data = {"database": self.charm.database}
if self.charm.extra_user_roles:
rel_data["extra-user-roles"] = str(self.charm.extra_user_roles)

MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
self.update_relation_data(event.relation.id, rel_data)

def _on_relation_changed(self, event) -> None:
"""Starts/restarts monogs with config server information."""
relation_data = event.relation.data[event.app]
Expand Down Expand Up @@ -162,9 +192,11 @@ def _on_relation_changed(self, event) -> None:
event.defer()
return

# TODO: Follow up PR. Add a user for mongos once it has been started
self.charm.share_uri()
self.charm.unit.status = ActiveStatus()

# BEGIN: helper functions

def is_mongos_running(self) -> bool:
"""Returns true if mongos service is running."""
with MongosConnection(None, f"mongodb://{MONGOS_SOCKET_URI_FMT}") as mongo:
Expand All @@ -180,7 +212,6 @@ def update_config_server_db(self, config_server_db) -> bool:
mongos_config, snap_install=True, config_server_db=config_server_db
)
add_args_to_env("MONGOS_ARGS", mongos_start_args)
self.charm.unit_peer_data["config_server_db"] = json.dumps(config_server_db)
return True

def update_keyfile(self, key_file_contents: str) -> bool:
Expand All @@ -202,3 +233,19 @@ def update_keyfile(self, key_file_contents: str) -> bool:
)

return True

def update_relation_data(self, relation_id: int, data: dict) -> None:
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
"""Updates a set of key-value pairs in the relation.

This function writes in the application data bag, therefore, only the leader unit can call
it.

Args:
relation_id: the identifier for a particular relation.
data: dict containing the key-value pairs
that should be updated in the relation.
"""
if self.charm.unit.is_leader():
self.database_requires.update_relation_data(relation_id, data)

# END: helper functions
3 changes: 2 additions & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ peers:

requires:
mongos_proxy:
interface: mongodb_client
interface: mongos_client
scope: container
cluster:
interface: config-server
limit: 1
125 changes: 102 additions & 23 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# See LICENSE file for licensing details.
import os
import pwd
import json
from charms.mongodb.v1.helpers import copy_licenses_to_unit, KEY_FILE
from charms.operator_libs_linux.v1 import snap
from pathlib import Path
Expand Down Expand Up @@ -37,6 +36,11 @@
UNIT_SCOPE = Config.Relations.UNIT_SCOPE
ROOT_USER_GID = 0
MONGO_USER = "snap_daemon"
ENV_VAR_PATH = "/etc/environment"
MONGOS_VAR = "MONGOS_ARGS"
CONFIG_ARG = "--configdb"
USER_ROLES_TAG = "extra-user-roles"
DATABASE_TAG = "database"


class MongosOperatorCharm(ops.CharmBase):
Expand Down Expand Up @@ -102,11 +106,6 @@ def _install_snap_packages(self, packages: List[str]) -> None:
)
raise

@property
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to properties section of file

def mongos_config(self) -> MongosConfiguration:
"""Generates a MongoDBConfiguration object for mongos in the deployment of MongoDB."""
return self._get_mongos_config_for_user(OperatorUser, set(Config.MONGOS_SOCKET))

def _get_mongos_config_for_user(
self, user: MongoDBUser, hosts: Set[str]
) -> MongosConfiguration:
Expand All @@ -121,15 +120,6 @@ def _get_mongos_config_for_user(
tls_internal=None, # Future PR will support TLS
)

@property
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to properties section of file

def _peers(self) -> Optional[Relation]:
"""Fetch the peer relation.

Returns:
An `ops.model.Relation` object representing the peer relation.
"""
return self.model.get_relation(Config.Relations.PEERS)

def get_secret(self, scope: str, key: str) -> Optional[str]:
"""Get secret from the secret storage."""
label = generate_secret_label(self, scope)
Expand Down Expand Up @@ -242,6 +232,79 @@ def restart_mongos_service(self) -> None:
self.stop_mongos_service()
self.start_mongos_service()

def share_uri(self) -> None:
"""Future PR - generate URI and give it to related app"""
# TODO future PR - generate different URI for data-integrator as that charm will not
# communicate to mongos via the Unix Domain Socket.

def set_user_role(self, roles: List[str]):
juditnovak marked this conversation as resolved.
Show resolved Hide resolved
"""Updates the roles for the mongos user."""
roles = roles.join(",")
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
self.app_peer_data[USER_ROLES_TAG] = roles

if len(self.model.relations[Config.Relations.CLUSTER_RELATIONS_NAME]) == 0:
return

# a mongos shard can only be related to one config server
config_server_rel = self.model.relations[
Config.Relations.CLUSTER_RELATIONS_NAME
][0]
self.cluster.update_relation_data(config_server_rel.id, {USER_ROLES_TAG: roles})

def set_database(self, database: str):
"""Updates the database requested for the mongos user."""
self.app_peer_data[DATABASE_TAG] = database
juditnovak marked this conversation as resolved.
Show resolved Hide resolved

if len(self.model.relations[Config.Relations.CLUSTER_RELATIONS_NAME]) == 0:
return

# a mongos shard can only be related to one config server
config_server_rel = self.model.relations[
Config.Relations.CLUSTER_RELATIONS_NAME
][0]
self.cluster.update_relation_data(
config_server_rel.id, {DATABASE_TAG: database}
)

# TODO future PR add function to set database field
# END: helper functions

# BEGIN: properties

@property
def database(self) -> Optional[str]:
"""Returns the database requested by the hosting application of the subordinate charm."""
if not self._peers:
logger.info("Peer relation not joined yet.")
# TODO future PR implement relation interface between host application mongos and use
# host application name in generation of db name.
return "mongos-database"

return self.app_peer_data.get("database", "mongos-database")
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved

@property
def extra_user_roles(self) -> Set[str]:
"""Returns the user roles requested by the hosting application of the subordinate charm."""
if not self._peers:
logger.info("Peer relation not joined yet.")
return None

return self.app_peer_data.get(USER_ROLES_TAG, "default")

@property
def mongos_config(self) -> MongosConfiguration:
"""Generates a MongoDBConfiguration object for mongos in the deployment of MongoDB."""
return self._get_mongos_config_for_user(OperatorUser, set(Config.MONGOS_SOCKET))

@property
def _peers(self) -> Optional[Relation]:
"""Fetch the peer relation.

Returns:
An `ops.model.Relation` object representing the peer relation.
"""
return self.model.get_relation(Config.Relations.PEERS)

@property
def _peers(self) -> Optional[Relation]:
"""Fetch the peer relation.
Expand All @@ -257,18 +320,34 @@ def unit_peer_data(self) -> Dict:
return self._peers.data[self.unit]

@property
def config_server_db(self):
"""Fetch current the config server database that this unit is connected to.
def app_peer_data(self) -> Dict:
"""App peer relation data object."""
return self._peers.data[self.app]

Returns:
A list of hosts addresses (strings).
"""
if "config_server_db" not in self.unit_peer_data:
@property
def config_server_db(self) -> str:
"""Fetch current the config server database that this unit is connected to."""

env_var = Path(ENV_VAR_PATH)
if not env_var.is_file():
logger.info("no environment variable file")
return ""

return json.loads(self.unit_peer_data.get("config_server_db"))
with open(ENV_VAR_PATH, "r") as file:
env_vars = file.read()

# END: helper functions
for env_var in env_vars.split("\n"):
if MONGOS_VAR not in env_var:
continue
if CONFIG_ARG not in env_var:
return ""
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved

# parse config db variable
return env_var.split(CONFIG_ARG)[1].strip().split(" ")[0]

return ""

# END: properties


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/application/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ series:

provides:
mongos_proxy:
interface: mongodb_client
interface: mongos_client
Loading
Loading