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-3293] mongos charm supports external connections via data-integrator charm #19

Merged
merged 14 commits into from
Feb 23, 2024
459 changes: 357 additions & 102 deletions lib/charms/data_platform_libs/v0/data_interfaces.py

Large diffs are not rendered by default.

43 changes: 33 additions & 10 deletions lib/charms/mongodb/v0/config_server_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from charms.mongodb.v1.mongos import MongosConnection
from ops.charm import CharmBase, EventBase, RelationBrokenEvent
from ops.framework import Object
from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus

from config import Config

Expand All @@ -35,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 = 4
LIBPATCH = 7


class ClusterProvider(Object):
Expand Down Expand Up @@ -64,25 +64,40 @@ def __init__(

def pass_hook_checks(self, event: EventBase) -> bool:
"""Runs the pre-hooks checks for ClusterProvider, returns True if all pass."""
if not self.charm.is_role(Config.Role.CONFIG_SERVER):
if not self.charm.db_initialised:
logger.info("Deferring %s. db is not initialised.", type(event))
event.defer()
return False

if not self.is_valid_mongos_integration():
logger.info(
"Skipping %s. ShardingProvider is only be executed by config-server", type(event)
"Skipping %s. ClusterProvider is only be executed by config-server", type(event)
)
return False

if not self.charm.unit.is_leader():
return False

if not self.charm.db_initialised:
logger.info("Deferring %s. db is not initialised.", type(event))
event.defer()
return True

def is_valid_mongos_integration(self) -> bool:
"""Returns true if the integration to mongos is valid."""
is_integrated_to_mongos = len(
Mehdi-Bendriss marked this conversation as resolved.
Show resolved Hide resolved
self.charm.model.relations[Config.Relations.CLUSTER_RELATIONS_NAME]
)

if not self.charm.is_role(Config.Role.CONFIG_SERVER) and is_integrated_to_mongos:
return False

return True

def _on_relation_changed(self, event) -> None:
"""Handles providing mongos with KeyFile and hosts."""
if not self.pass_hook_checks(event):
if not self.is_valid_mongos_integration():
self.charm.unit.status = BlockedStatus(
"Relation to mongos not supported, config role must be config-server"
)
logger.info("Skipping relation joined event: hook checks did not pass")
return

Expand Down Expand Up @@ -208,7 +223,6 @@ def _on_relation_changed(self, event) -> None:
event.relation.id, CONFIG_SERVER_DB_KEY
)
if not key_file_contents or not config_server_db:
event.defer()
self.charm.unit.status = WaitingStatus("Waiting for secrets from config-server")
return

Expand Down Expand Up @@ -261,7 +275,13 @@ def _on_relation_broken(self, event: RelationBrokenEvent) -> None:

def is_mongos_running(self) -> bool:
"""Returns true if mongos service is running."""
with MongosConnection(None, f"mongodb://{MONGOS_SOCKET_URI_FMT}") as mongo:
connection_uri = f"mongodb://{self.charm.get_mongos_host()}"

# when running internally, connections through Unix Domain sockets do not need port.
if self.charm.is_external_client:
connection_uri = connection_uri + f":{Config.MONGOS_PORT}"

with MongosConnection(None, connection_uri) as mongo:
return mongo.is_ready

def update_config_server_db(self, config_server_db) -> bool:
Expand All @@ -271,7 +291,10 @@ def update_config_server_db(self, config_server_db) -> bool:

mongos_config = self.charm.mongos_config
mongos_start_args = get_mongos_args(
mongos_config, snap_install=True, config_server_db=config_server_db
mongos_config,
snap_install=True,
config_server_db=config_server_db,
external_connectivity=self.charm.is_external_client,
)
add_args_to_env("MONGOS_ARGS", mongos_start_args)
return True
Expand Down
28 changes: 21 additions & 7 deletions lib/charms/mongodb/v1/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Simple functions, which can be used in both K8s and VM charms."""

MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
import json
Expand Down Expand Up @@ -29,7 +30,7 @@

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

# path to store mongodb ketFile
KEY_FILE = "keyFile"
Expand All @@ -44,11 +45,22 @@
MONGO_SHELL = "charmed-mongodb.mongosh"

DATA_DIR = "/var/lib/mongodb"
LOG_DIR = "/var/log/mongodb"
LOG_TO_SYSLOG = True
CONF_DIR = "/etc/mongod"
MONGODB_LOG_FILENAME = "mongodb.log"
logger = logging.getLogger(__name__)


def _get_logging_options(snap_install: bool) -> str:
# TODO sending logs to syslog until we have a separate mount point for logs
if LOG_TO_SYSLOG:
return ""
# in k8s the default logging options that are used for the vm charm are ignored and logs are
# the output of the container. To enable logging to a file it must be set explicitly
return f"--logpath={LOG_DIR}/{MONGODB_LOG_FILENAME}" if snap_install else ""


# noinspection GrazieInspection
def get_create_user_cmd(config: MongoDBConfiguration, mongo_path=MONGO_SHELL) -> List[str]:
"""Creates initial admin user for MongoDB.
Expand Down Expand Up @@ -84,6 +96,7 @@ def get_mongos_args(
config,
snap_install: bool = False,
config_server_db: str = None,
external_connectivity: bool = True,
) -> str:
"""Returns the arguments used for starting mongos on a config-server side application.

Expand All @@ -92,9 +105,9 @@ def get_mongos_args(
"""
# suborinate charm which provides its own config_server_db, should only use unix domain socket
binding_ips = (
f"--bind_ip {MONGODB_COMMON_DIR}/var/mongodb-27018.sock"
if config_server_db
else "--bind_ip_all"
"--bind_ip_all"
if external_connectivity
else f"--bind_ip {MONGODB_COMMON_DIR}/var/mongodb-27018.sock"
)

# mongos running on the config server communicates through localhost
Expand Down Expand Up @@ -130,9 +143,7 @@ def get_mongod_args(
"""
full_data_dir = f"{MONGODB_COMMON_DIR}{DATA_DIR}" if snap_install else DATA_DIR
full_conf_dir = f"{MONGODB_SNAP_DATA_DIR}{CONF_DIR}" if snap_install else CONF_DIR
# in k8s the default logging options that are used for the vm charm are ignored and logs are
# the output of the container. To enable logging to a file it must be set explicitly
logging_options = "" if snap_install else f"--logpath={full_data_dir}/{MONGODB_LOG_FILENAME}"
logging_options = _get_logging_options(snap_install)
cmd = [
# bind to localhost and external interfaces
"--bind_ip_all",
Expand All @@ -143,6 +154,8 @@ def get_mongod_args(
# for simplicity we run the mongod daemon on shards, configsvrs, and replicas on the same
# port
f"--port={Config.MONGODB_PORT}",
"--auditDestination=syslog", # TODO sending logs to syslog until we have a separate mount point for logs
f"--auditFormat={Config.AuditLog.FORMAT}",
logging_options,
]
if auth:
Expand All @@ -164,6 +177,7 @@ def get_mongod_args(
f"--tlsCertificateKeyFile={full_conf_dir}/{TLS_EXT_PEM_FILE}",
# allow non-TLS connections
"--tlsMode=preferTLS",
"--tlsDisabledProtocols=TLS1_0,TLS1_1",
]
)

Expand Down
54 changes: 21 additions & 33 deletions lib/charms/mongos/v0/mongos_client_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ops.charm import CharmBase
from charms.data_platform_libs.v0.data_interfaces import (
DatabaseProvides,
DatabaseRequires,
DatabaseRequestedEvent,
)

from charms.mongodb.v1.mongos import MongosConfiguration
Expand All @@ -20,6 +20,7 @@
DATABASE_KEY = "database"
USER_ROLES_KEY = "extra-user-roles"
MONGOS_RELATION_NAME = "mongos_proxy"
EXTERNAL_CONNECTIVITY_TAG = "external-node-connectivity"

# TODO - the below LIBID, LIBAPI, and LIBPATCH are not valid and were made manually. These will be
# created automatically once the charm has been published. The charm has not yet been published
Expand All @@ -34,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 = 1
LIBPATCH = 2

"""Library to manage the relation for the application between mongos and the deployed application.
In short, this relation ensure that:
Expand Down Expand Up @@ -92,9 +93,20 @@ def _on_relation_changed(self, event) -> None:
if not self.charm.unit.is_leader():
return

relation_data = event.relation.data[event.app]
new_database_name = relation_data.get(DATABASE_KEY, self.charm.database)
new_extra_user_roles = relation_data.get(USER_ROLES_KEY, self.charm.extra_user_roles)
new_database_name = (
self.database_provides.fetch_relation_field(event.relation.id, DATABASE_KEY)
or self.charm.database
)
new_extra_user_roles = (
self.database_provides.fetch_relation_field(event.relation.id, USER_ROLES_KEY)
or self.charm.extra_user_roles
)
external_connectivity = (
self.database_provides.fetch_relation_field(
event.relation.id, EXTERNAL_CONNECTIVITY_TAG
)
== "true"
)

if new_database_name != self.charm.database:
self.charm.set_database(new_database_name)
Expand All @@ -105,6 +117,10 @@ def _on_relation_changed(self, event) -> None:

self.charm.set_user_roles(new_extra_user_roles)

self.charm.set_external_connectivity(external_connectivity)
if external_connectivity:
self.charm.open_mongos_port()
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved

def remove_connection_info(self) -> None:
"""Sends the URI to the related parent application"""
logger.info("Removing connection information from host application.")
Expand All @@ -123,31 +139,3 @@ def update_connection_info(self, config: MongosConfiguration) -> None:
relation.id,
config.uri,
)


class MongosRequirer(Object):
"""Manage relations between the mongos router and the application on the application side."""

def __init__(
self,
charm: CharmBase,
database_name: str,
extra_user_roles: str,
relation_name: str = MONGOS_RELATION_NAME,
) -> None:
"""Constructor for MongosRequirer object."""
self.relation_name = relation_name
self.charm = charm

if not database_name:
database_name = f"{self.charm.app}"

self.database_requires = DatabaseRequires(
self.charm,
relation_name=self.relation_name,
database_name=database_name,
extra_user_roles=extra_user_roles,
)

super().__init__(charm, self.relation_name)
# TODO Future PRs handle relation broken
juditnovak marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ requires:
mongos_proxy:
interface: mongos_client
scope: container
limit: 1
cluster:
interface: config-server
limit: 1
50 changes: 34 additions & 16 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
from charms.operator_libs_linux.v1 import snap
from pathlib import Path

from typing import Set, List, Optional, Dict

from charms.mongodb.v0.mongodb_secrets import SecretCache
from charms.mongos.v0.mongos_client_interface import MongosProvider
from typing import Set, List, Optional, Dict
from charms.mongodb.v0.mongodb_secrets import generate_secret_label
from charms.mongodb.v1.mongos import MongosConfiguration
from charms.mongodb.v0.config_server_interface import ClusterRequirer
Expand Down Expand Up @@ -39,6 +40,7 @@
CONFIG_ARG = "--configdb"
USER_ROLES_TAG = "extra-user-roles"
DATABASE_TAG = "database"
EXTERNAL_CONNECTIVITY_TAG = "external-connectivity"


class MongosOperatorCharm(ops.CharmBase):
Expand Down Expand Up @@ -286,6 +288,12 @@ def set_database(self, database: str) -> None:
config_server_rel.id, {DATABASE_TAG: database}
)

def set_external_connectivity(self, external_connectivity: bool) -> None:
"""Sets the connectivity type for mongos."""
self.app_peer_data[EXTERNAL_CONNECTIVITY_TAG] = json.dumps(
external_connectivity
)

def check_relation_broken_or_scale_down(self, event: RelationDepartedEvent) -> None:
"""Checks relation departed event is the result of removed relation or scale down.

Expand Down Expand Up @@ -338,14 +346,37 @@ def proceed_on_broken_event(self, event) -> bool:

return True

def get_mongos_host(self) -> str:
"""Returns the host for mongos as a str.

The host for mongos can be either the Unix Domain Socket or an IP address depending on how
the client wishes to connect to mongos (inside Juju or outside).
"""
if self.is_external_client:
return self._unit_ip
return Config.MONGOS_SOCKET_URI_FMT

@staticmethod
def _generate_relation_departed_key(rel_id: int) -> str:
"""Generates the relation departed key for a specified relation id."""
return f"relation_{rel_id}_departed"

def open_mongos_port(self) -> None:
"""Opens the mongos port for TCP connections."""
self.unit.open_port("tcp", Config.MONGOS_PORT)

# END: helper functions

# BEGIN: properties
@property
def _unit_ip(self) -> str:
"""Returns the ip address of the unit."""
return str(self.model.get_binding(Config.Relations.PEERS).network.bind_address)

@property
def is_external_client(self) -> Optional[str]:
"""Returns the database requested by the hosting application of the subordinate charm."""
return json.loads(self.app_peer_data.get(EXTERNAL_CONNECTIVITY_TAG))

@property
def database(self) -> Optional[str]:
Expand All @@ -370,12 +401,8 @@ def extra_user_roles(self) -> Set[str]:
@property
def mongos_config(self) -> MongosConfiguration:
"""Generates a MongoDBConfiguration object for mongos in the deployment of MongoDB."""
# TODO future PR - use ip addresses for hosts for data-integrator as that charm will not
# communicate to mongos via the Unix Domain Socket.
hosts = [Config.MONGOS_SOCKET_URI_FMT]
# mongos using Unix Domain Socket to communicate do not use port, Future PR - use port
# when suborinate charm of data-integrator.
port = None
hosts = [self.get_mongos_host()]
port = Config.MONGOS_PORT if self.is_external_client else None

return MongosConfiguration(
database=self.database,
Expand All @@ -397,15 +424,6 @@ def _peers(self) -> Optional[Relation]:
"""
return self.model.get_relation(Config.Relations.PEERS)

@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 unit_peer_data(self) -> Dict:
"""Unit peer relation data object."""
Expand Down
Loading
Loading