diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 33e2ef2c..9a359b02 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -67,7 +67,17 @@ jobs: path-to-charm-directory: ${{ matrix.path }} integration-test: - name: Integration test charm | 3.5.3 + strategy: + fail-fast: false + matrix: + juju: + # This runs on all runs + - agent: 3.5.3 # renovate: juju-agent-pin-minor + allure_report: true + # This runs only on scheduled runs, DPW 21 specifics (scheduled + 3.6/X) + - snap_channel: 3.6/beta + allure_report: false + name: Integration test charm | ${{ matrix.juju.agent || matrix.juju.snap_channel }} needs: - lint - unit-test @@ -77,7 +87,8 @@ jobs: artifact-prefix: packed-charm-cache-true cloud: microk8s microk8s-snap-channel: 1.29-strict/stable - juju-agent-version: 3.5.3 # renovate: juju-agent-pin-minor - _beta_allure_report: true + juju-agent-version: ${{ matrix.juju.agent }} + juju-snap-channel: ${{ matrix.juju.snap_channel }} + _beta_allure_report: ${{ matrix.juju.allure_report }} permissions: contents: write # Needed for Allure Report beta diff --git a/lib/charms/mongodb/v0/config_server_interface.py b/lib/charms/mongodb/v0/config_server_interface.py index cdb733d9..44f5b26b 100644 --- a/lib/charms/mongodb/v0/config_server_interface.py +++ b/lib/charms/mongodb/v0/config_server_interface.py @@ -11,10 +11,11 @@ from charms.data_platform_libs.v0.data_interfaces import ( DatabaseProvides, + DatabaseRequestedEvent, DatabaseRequires, ) from charms.mongodb.v1.mongos import MongosConnection -from ops.charm import CharmBase, EventBase, RelationBrokenEvent +from ops.charm import CharmBase, EventBase, RelationBrokenEvent, RelationChangedEvent from ops.framework import Object from ops.model import ( ActiveStatus, @@ -42,7 +43,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 11 +LIBPATCH = 13 class ClusterProvider(Object): @@ -57,6 +58,9 @@ def __init__( self.database_provides = DatabaseProvides(self.charm, relation_name=self.relation_name) super().__init__(charm, self.relation_name) + self.framework.observe( + self.database_provides.on.database_requested, self._on_database_requested + ) self.framework.observe( charm.on[self.relation_name].relation_changed, self._on_relation_changed ) @@ -105,8 +109,14 @@ def is_valid_mongos_integration(self) -> bool: return True - def _on_relation_changed(self, event) -> None: - """Handles providing mongos with KeyFile and hosts.""" + def _on_database_requested(self, event: DatabaseRequestedEvent | RelationChangedEvent) -> None: + """Handles the database requested event. + + The first time secrets are written to relations should be on this event. + + Note: If secrets are written for the first time on other events we risk + the chance of writing secrets in plain sight. + """ if not self.pass_hook_checks(event): if not self.is_valid_mongos_integration(): self.charm.status.set_and_share_status( @@ -116,12 +126,9 @@ def _on_relation_changed(self, event) -> None: ) 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) - relation_data = { KEYFILE_KEY: self.charm.get_secret( Config.Relations.APP_SCOPE, Config.Secrets.SECRET_KEYFILE_NAME @@ -135,9 +142,20 @@ def _on_relation_changed(self, event) -> None: ) if int_tls_ca: relation_data[INT_TLS_CA_KEY] = int_tls_ca - self.database_provides.update_relation_data(event.relation.id, relation_data) + def _on_relation_changed(self, event: RelationChangedEvent) -> None: + """Handles providing mongos with KeyFile and hosts.""" + # First we need to ensure that the database requested event has run + # otherwise we risk the chance of writing secrets in plain sight. + if not self.database_provides.fetch_relation_field(event.relation.id, "database"): + logger.info("Database Requested has not run yet, skipping.") + event.defer() + return + + # TODO : This workflow is a fix until we have time for a better and complete fix (DPE-5513) + self._on_database_requested(event) + def _on_relation_broken(self, event) -> None: if self.charm.upgrade_in_progress: logger.warning( @@ -300,7 +318,8 @@ def _on_relation_changed(self, event) -> None: return self.charm.status.set_and_share_status(ActiveStatus()) - self.charm.mongos_intialised = True + if self.charm.unit.is_leader(): + self.charm.mongos_initialised = True def _on_relation_broken(self, event: RelationBrokenEvent) -> None: # Only relation_deparated events can check if scaling down diff --git a/lib/charms/mongodb/v0/mongo.py b/lib/charms/mongodb/v0/mongo.py index f8ef0e44..9ae3cd2d 100644 --- a/lib/charms/mongodb/v0/mongo.py +++ b/lib/charms/mongodb/v0/mongo.py @@ -31,7 +31,7 @@ class NotReadyError(PyMongoError): # 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 ADMIN_AUTH_SOURCE = "authSource=admin" SYSTEM_DBS = ("admin", "local", "config") @@ -40,6 +40,7 @@ class NotReadyError(PyMongoError): {"role": "userAdminAnyDatabase", "db": "admin"}, {"role": "readWriteAnyDatabase", "db": "admin"}, {"role": "userAdmin", "db": "admin"}, + {"role": "enableSharding", "db": "admin"}, ], "monitor": [ {"role": "explainRole", "db": "admin"}, @@ -127,7 +128,12 @@ def uri(self): def supported_roles(config: MongoConfiguration): """Return the supported roles for the given configuration.""" - return REGULAR_ROLES | {"default": [{"db": config.database, "role": "readWrite"}]} + return REGULAR_ROLES | { + "default": [ + {"db": config.database, "role": "readWrite"}, + {"db": config.database, "role": "enableSharding"}, + ] + } class MongoConnection: diff --git a/lib/charms/mongodb/v1/helpers.py b/lib/charms/mongodb/v1/helpers.py index 937786b8..1b2f1064 100644 --- a/lib/charms/mongodb/v1/helpers.py +++ b/lib/charms/mongodb/v1/helpers.py @@ -8,7 +8,7 @@ import secrets import string import subprocess -from typing import List +from typing import List, Mapping from charms.mongodb.v1.mongodb import MongoConfiguration from ops.model import ActiveStatus, MaintenanceStatus, StatusBase, WaitingStatus @@ -23,7 +23,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 8 +LIBPATCH = 12 # path to store mongodb ketFile KEY_FILE = "keyFile" @@ -89,7 +89,7 @@ def get_create_user_cmd(config: MongoConfiguration, mongo_path=MONGO_SHELL) -> L "mongodb://localhost/admin", "--quiet", "--eval", - "db.createUser({" + '"db.createUser({' f" user: '{config.username}'," " pwd: passwordPrompt()," " roles:[" @@ -99,7 +99,7 @@ def get_create_user_cmd(config: MongoConfiguration, mongo_path=MONGO_SHELL) -> L " ]," " mechanisms: ['SCRAM-SHA-256']," " passwordDigestor: 'server'," - "})", + '})"', ] @@ -118,7 +118,7 @@ def get_mongos_args( binding_ips = ( "--bind_ip_all" if external_connectivity - else f"--bind_ip {MONGODB_COMMON_DIR}/var/mongodb-27018.sock" + else f"--bind_ip {MONGODB_COMMON_DIR}/var/mongodb-27018.sock --filePermissions 0766" ) # mongos running on the config server communicates through localhost @@ -320,3 +320,25 @@ def add_args_to_env(var: str, args: str): with open(Config.ENV_VAR_PATH, "w") as service_file: service_file.writelines(env_vars) + + +def safe_exec( + command: list[str] | str, + env: Mapping[str, str] | None = None, + working_dir: str | None = None, +) -> str: + """Execs a command on the workload in a safe way.""" + try: + output = subprocess.check_output( + command, + stderr=subprocess.PIPE, + universal_newlines=True, + shell=isinstance(command, str), + env=env, + cwd=working_dir, + ) + logger.debug(f"{output=}") + return output + except subprocess.CalledProcessError as err: + logger.error(f"cmd failed - {err.cmd}, {err.stdout}, {err.stderr}") + raise diff --git a/lib/charms/mongodb/v1/mongodb_tls.py b/lib/charms/mongodb/v1/mongodb_tls.py index 266a202d..cfb0a739 100644 --- a/lib/charms/mongodb/v1/mongodb_tls.py +++ b/lib/charms/mongodb/v1/mongodb_tls.py @@ -12,7 +12,7 @@ import logging import re import socket -from typing import Dict, List, Optional, Tuple +from typing import Optional, Tuple from charms.tls_certificates_interface.v3.tls_certificates import ( CertificateAvailableEvent, diff --git a/lib/charms/mongos/v0/set_status.py b/lib/charms/mongos/v0/set_status.py index e70bedf0..3f57015e 100644 --- a/lib/charms/mongos/v0/set_status.py +++ b/lib/charms/mongos/v0/set_status.py @@ -61,6 +61,7 @@ def clear_status(self, status_to_clear): status_to_clear, self.charm.unit.status, ) + return # TODO: In the future compute the next highest priority status. self.charm.unit.status = ActiveStatus()