From 12b35dabe4f89a987c9e65eaea2f596daa65898e Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Wed, 31 Jan 2024 13:20:52 +0000 Subject: [PATCH] add messages for incorrect rels to mongos --- .../mongodb/v0/config_server_interface.py | 7 +- lib/charms/mongodb/v1/shards_interface.py | 5 +- src/charm.py | 23 +++-- .../sharding_tests/test_sharding_relations.py | 88 ++++++++++++++++++- 4 files changed, 114 insertions(+), 9 deletions(-) diff --git a/lib/charms/mongodb/v0/config_server_interface.py b/lib/charms/mongodb/v0/config_server_interface.py index 70df7aa37..847c91267 100644 --- a/lib/charms/mongodb/v0/config_server_interface.py +++ b/lib/charms/mongodb/v0/config_server_interface.py @@ -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 +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus from config import Config @@ -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 = 5 +LIBPATCH = 6 class ClusterProvider(Object): @@ -73,6 +73,9 @@ def pass_hook_checks(self, event: EventBase) -> bool: logger.info( "Skipping %s. ShardingProvider is only be executed by config-server", type(event) ) + self.charm.unit.status = BlockedStatus( + "Relation to mongos not supported, config role must be config-server" + ) return False if not self.charm.unit.is_leader(): diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index b9a876aec..57a5451b0 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -51,7 +51,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 5 +LIBPATCH = 6 KEYFILE_KEY = "key-file" HOSTS_KEY = "host" OPERATOR_PASSWORD_KEY = MongoDBUser.get_password_key_name_for_user(OperatorUser.get_username()) @@ -520,6 +520,9 @@ def pass_hook_checks(self, event): logger.info("skipping %s is only be executed by shards", type(event)) return False + if not event.relation.app: + return False + mongos_hosts = event.relation.data[event.relation.app].get(HOSTS_KEY, None) if isinstance(event, RelationBrokenEvent) and not mongos_hosts: logger.info("Config-server relation never set up, no need to process broken event.") diff --git a/src/charm.py b/src/charm.py index 6c0477577..1095cecaf 100755 --- a/src/charm.py +++ b/src/charm.py @@ -529,11 +529,11 @@ def _on_storage_detaching(self, event: StorageDetachingEvent) -> None: logger.error("Failed to remove %s from replica set, error=%r", self.unit.name, e) def _on_update_status(self, event: UpdateStatusEvent): - # cannot have both legacy and new relations since they have different auth requirements - if self.client_relations._get_users_from_relations( - None, rel="obsolete" - ) and self.client_relations._get_users_from_relations(None): - self.unit.status = BlockedStatus("cannot have both legacy and new relations") + # user-made mistakes might result in other incorrect statues. Prioritise informing users of + # their mistake. + invalid_integration_status = self.get_invalid_integration_status() + if invalid_integration_status: + self.unit.status = invalid_integration_status return # no need to report on replica set status until initialised @@ -1353,6 +1353,19 @@ def _is_removing_last_replica(self) -> bool: """Returns True if the last replica (juju unit) is getting removed.""" return self.app.planned_units() == 0 and len(self._peers.units) == 0 + def get_invalid_integration_status(self) -> Optional[StatusBase]: + """Returns a status if an invalid integration is present.""" + if self.client_relations._get_users_from_relations( + None, rel="obsolete" + ) and self.client_relations._get_users_from_relations(None): + return BlockedStatus("cannot have both legacy and new relations") + + integrated_to_mongos = self.model.relations[Config.Relations.CLUSTER_RELATIONS_NAME] + if not self.is_role(Config.Role.CONFIG_SERVER) and integrated_to_mongos: + return BlockedStatus( + "Relation to mongos not supported, config role must be config-server" + ) + def get_status(self) -> StatusBase: """Returns the status with the highest priority from backups, sharding, and mongod. diff --git a/tests/integration/sharding_tests/test_sharding_relations.py b/tests/integration/sharding_tests/test_sharding_relations.py index bcfa3d2a8..9e1a0e123 100644 --- a/tests/integration/sharding_tests/test_sharding_relations.py +++ b/tests/integration/sharding_tests/test_sharding_relations.py @@ -11,6 +11,8 @@ REPLICATION_APP_NAME = "replication" APP_CHARM_NAME = "application" LEGACY_APP_CHARM_NAME = "legacy-application" +MONGOS_APP_NAME = "mongos" +MONGOS_HOST_APP_NAME = "application-host" SHARDING_COMPONENTS = [SHARD_ONE_APP_NAME, CONFIG_SERVER_ONE_APP_NAME] @@ -28,7 +30,11 @@ @pytest.mark.abort_on_fail async def test_build_and_deploy( - ops_test: OpsTest, application_charm, legacy_charm, database_charm + ops_test: OpsTest, + application_charm, + legacy_charm, + database_charm, + mongos_host_application_charm, ) -> None: """Build and deploy a sharded cluster.""" await ops_test.model.deploy( @@ -44,6 +50,17 @@ async def test_build_and_deploy( await ops_test.model.deploy( database_charm, config={"role": "shard"}, application_name=SHARD_ONE_APP_NAME ) + await ops_test.model.deploy( + MONGOS_APP_NAME, + channel="6/edge", + revision=3, + ) + # TODO: Future PR, once data integrator works with mongos charm deploy that charm instead of + # packing and deploying the charm in the application dir. + await ops_test.model.deploy( + mongos_host_application_charm, application_name=MONGOS_HOST_APP_NAME + ) + await ops_test.model.deploy(database_charm, application_name=REPLICATION_APP_NAME) await ops_test.model.deploy(application_charm, application_name=APP_CHARM_NAME) await ops_test.model.deploy(legacy_charm, application_name=LEGACY_APP_CHARM_NAME) @@ -59,6 +76,19 @@ async def test_build_and_deploy( timeout=TIMEOUT, ) + await ops_test.model.integrate( + f"{MONGOS_APP_NAME}", + f"{MONGOS_HOST_APP_NAME}", + ) + + await ops_test.model.wait_for_idle( + apps=[MONGOS_HOST_APP_NAME, MONGOS_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + raise_on_error=False, + ) + async def test_only_one_config_server_relation(ops_test: OpsTest) -> None: """Verify that a shard can only be related to one config server.""" @@ -218,3 +248,59 @@ async def test_replication_shard_relation(ops_test: OpsTest): raise_on_blocked=False, timeout=TIMEOUT, ) + + +async def test_replication_mongos_relation(ops_test: OpsTest) -> None: + """Verifies connecting a replica to a mongos router fails.""" + # attempt to add a replication deployment as a shard to the config server. + await ops_test.model.integrate( + f"{REPLICATION_APP_NAME}", + f"{MONGOS_APP_NAME}", + ) + + await ops_test.model.wait_for_idle( + apps=[REPLICATION_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) + + replication_unit = ops_test.model.applications[REPLICATION_APP_NAME].units[0] + assert ( + replication_unit.workload_status_message + == "Relation to mongos not supported, config role must be config-server" + ), "replica cannot be related to mongos." + + # clean up relations + await ops_test.model.applications[REPLICATION_APP_NAME].remove_relation( + f"{REPLICATION_APP_NAME}:cluster", + f"{SHARD_ONE_APP_NAME}:cluster", + ) + + +async def test_shard_mongos_relation(ops_test: OpsTest) -> None: + """Verifies connecting a shard to a mongos router fails.""" + # attempt to add a replication deployment as a shard to the config server. + await ops_test.model.integrate( + f"{SHARD_ONE_APP_NAME}", + f"{MONGOS_APP_NAME}", + ) + + await ops_test.model.wait_for_idle( + apps=[SHARD_ONE_APP_NAME], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + ) + + shard_unit = ops_test.model.applications[SHARD_ONE_APP_NAME].units[0] + assert ( + shard_unit.workload_status_message + == "Relation to mongos not supported, config role must be config-server" + ), "replica cannot be related to mongos." + + # clean up relations + await ops_test.model.applications[SHARD_ONE_APP_NAME].remove_relation( + f"{MONGOS_APP_NAME}:cluster", + f"{SHARD_ONE_APP_NAME}:cluster", + )