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 5095] tests for check invalid relations #313

Merged
merged 9 commits into from
Aug 28, 2024
Merged
62 changes: 54 additions & 8 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
ModelError,
Relation,
RelationDataContent,
StatusBase,
Unit,
WaitingStatus,
)
Expand Down Expand Up @@ -465,6 +466,13 @@ def primary(self) -> str | None:
# END: properties

# BEGIN: generic helper methods
def get_cluster_mismatched_revision_status(self) -> Optional[StatusBase]:
"""Returns a Status if the cluster has mismatched revisions.

TODO implement this method as a part of sharding upgrades.
"""
return None

def remote_mongos_config(self, hosts) -> MongoConfiguration:
"""Generates a MongoConfiguration object for mongos in the deployment of MongoDB."""
# mongos that are part of the cluster have the same username and password, but different
Expand Down Expand Up @@ -508,14 +516,6 @@ def _compare_secret_ids(secret_id1: str, secret_id2: str) -> bool:
return pure_id1 == pure_id2
return False

def is_relation_feasible(self, rel_interface) -> bool:
"""Returns true if the proposed relation is feasible.

TODO implement this in a future PR as part of sharding
"""
logger.debug("checking if provided relation %s is feasible", rel_interface)
return True

# BEGIN: charm events
def _on_mongod_pebble_ready(self, event) -> None:
"""Configure MongoDB pebble layer specification."""
Expand Down Expand Up @@ -719,6 +719,12 @@ def _on_stop(self, event) -> None:
self.unit_peer_data["unit_departed"] = ""

def _on_update_status(self, event: UpdateStatusEvent):
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
# user-made mistakes might result in other incorrect statues. Prioritise informing users of
# their mistake.
if invalid_integration_status := self.status.get_invalid_integration_status():
self.status.set_and_share_status(invalid_integration_status)
return

# no need to report on replica set status until initialised
if not self.db_initialised:
return
Expand Down Expand Up @@ -1500,6 +1506,46 @@ 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 is_relation_feasible(self, rel_interface: str) -> bool:
"""Returns true if the proposed relation is feasible.

TODO add checks for version mismatch
"""
if self.is_sharding_component() and rel_interface in Config.Relations.DB_RELATIONS:
self.status.set_and_share_status(
BlockedStatus(f"Sharding roles do not support {rel_interface} interface.")
)
logger.error(
"Charm is in sharding role: %s. Does not support %s interface.",
rel_interface,
self.role,
)
return False

if (
not self.is_sharding_component()
and rel_interface == Config.Relations.SHARDING_RELATIONS_NAME
):
self.status.set_and_share_status(
BlockedStatus("sharding interface cannot be used by replicas")
)
logger.error(
"Charm is in sharding role: %s. Does not support %s interface.",
self.role,
rel_interface,
)
return False

if revision_mismatch_status := self.get_cluster_mismatched_revision_status():
self.status.set_and_share_status(revision_mismatch_status)
return False

return True

def is_sharding_component(self) -> bool:
"""Returns true if charm is running as a sharded component."""
return self.is_role(Config.Role.SHARD) or self.is_role(Config.Role.CONFIG_SERVER)

# END: helper functions

# BEGIN: static methods
Expand Down
1 change: 1 addition & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Relations:
SHARDING_RELATIONS_NAME = "sharding"
CONFIG_SERVER_RELATIONS_NAME = "config-server"
CLUSTER_RELATIONS_NAME = "cluster"
DB_RELATIONS = [NAME]
Scopes = Literal[APP_SCOPE, UNIT_SCOPE]

class Role:
Expand Down
206 changes: 205 additions & 1 deletion tests/integration/sharding_tests/test_sharding_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.
import pytest
from juju.errors import JujuAPIError
from pytest_operator.plugin import OpsTest

from ..helpers import METADATA, wait_for_mongodb_units_blocked
Expand All @@ -13,7 +14,7 @@
REPLICATION_APP_NAME = "replication"
APP_CHARM_NAME = "application"
LEGACY_APP_CHARM_NAME = "legacy-application"
MONGOS_APP_NAME = "mongos"
MONGOS_APP_NAME = "mongos-k8s"
MONGOS_HOST_APP_NAME = "application-host"

SHARDING_COMPONENTS = [SHARD_ONE_APP_NAME, CONFIG_SERVER_ONE_APP_NAME]
Expand All @@ -22,6 +23,8 @@
SHARD_REL_NAME = "sharding"
DATABASE_REL_NAME = "first-database"
LEGACY_RELATION_NAME = "obsolete"
RELATION_LIMIT_MESSAGE = 'cannot add relation "shard:sharding config-server-two:config-server": establishing a new relation for shard:sharding would exceed its maximum relation limit of 1'
TEST_APP_CHARM_PATH = "./tests/integration/relation_tests/application-charm"


@pytest.mark.group(1)
Expand All @@ -30,6 +33,14 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None:
"""Build and deploy a sharded cluster."""
database_charm = await ops_test.build_charm(".")
resources = {"mongodb-image": METADATA["resources"]["mongodb-image"]["upstream-source"]}
application_charm = await ops_test.build_charm(TEST_APP_CHARM_PATH)

await ops_test.model.deploy(application_charm, application_name=APP_CHARM_NAME)
await ops_test.model.deploy(
database_charm,
application_name=REPLICATION_APP_NAME,
resources=resources,
)

await ops_test.model.deploy(
database_charm,
Expand All @@ -50,16 +61,209 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None:
application_name=SHARD_ONE_APP_NAME,
)

# Will be enabled after DPE-5040 is done
# await ops_test.model.deploy(
# MONGOS_APP_NAME,
# channel="6/edge",
# )
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved

await ops_test.model.deploy(S3_APP_NAME, channel="edge")

await ops_test.model.wait_for_idle(
apps=[
APP_CHARM_NAME,
S3_APP_NAME,
REPLICATION_APP_NAME,
CONFIG_SERVER_ONE_APP_NAME,
CONFIG_SERVER_TWO_APP_NAME,
SHARD_ONE_APP_NAME,
],
idle_period=20,
raise_on_blocked=False,
raise_on_error=False, # TODO: remove raise_on_error when we move to juju 3.5 (DPE-4996)
)


@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_only_one_config_server_relation(ops_test: OpsTest) -> None:
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
"""Verify that a shard can only be related to one config server."""
await ops_test.model.integrate(
f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_ONE_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

with pytest.raises(JujuAPIError) as juju_error:
MiaAltieri marked this conversation as resolved.
Show resolved Hide resolved
await ops_test.model.integrate(
f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_TWO_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

assert (
juju_error.value.args[0] == RELATION_LIMIT_MESSAGE
), "Shard can relate to multiple config servers."

# clean up relation
await ops_test.model.applications[SHARD_ONE_APP_NAME].remove_relation(
f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_ONE_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

await ops_test.model.wait_for_idle(
apps=[CONFIG_SERVER_ONE_APP_NAME, SHARD_ONE_APP_NAME],
idle_period=20,
raise_on_blocked=False,
)


@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_cannot_use_db_relation(ops_test: OpsTest) -> None:
"""Verify that sharding components cannot use the DB relation."""
for sharded_component in SHARDING_COMPONENTS:
await ops_test.model.integrate(f"{APP_CHARM_NAME}:{DATABASE_REL_NAME}", sharded_component)

for sharded_component in SHARDING_COMPONENTS:
await wait_for_mongodb_units_blocked(
ops_test,
sharded_component,
status="Sharding roles do not support database interface.",
timeout=300,
)

# clean up relations
for sharded_component in SHARDING_COMPONENTS:
await ops_test.model.applications[sharded_component].remove_relation(
f"{APP_CHARM_NAME}:{DATABASE_REL_NAME}",
sharded_component,
)

await ops_test.model.wait_for_idle(
apps=SHARDING_COMPONENTS,
idle_period=20,
raise_on_blocked=False,
)


@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_replication_config_server_relation(ops_test: OpsTest):
"""Verifies that using a replica as a shard fails."""
# attempt to add a replication deployment as a shard to the config server.
await ops_test.model.integrate(
f"{REPLICATION_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_ONE_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

await wait_for_mongodb_units_blocked(
ops_test,
REPLICATION_APP_NAME,
status="sharding interface cannot be used by replicas",
timeout=300,
)

# clean up relations
await ops_test.model.applications[REPLICATION_APP_NAME].remove_relation(
f"{REPLICATION_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_ONE_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

await ops_test.model.wait_for_idle(
apps=[REPLICATION_APP_NAME],
idle_period=20,
raise_on_blocked=False,
)


@pytest.mark.group(1)
@pytest.mark.abort_on_fail
async def test_replication_shard_relation(ops_test: OpsTest):
"""Verifies that using a replica as a config-server fails."""
# attempt to add a shard to a replication deployment as a config server.
await ops_test.model.integrate(
f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}",
f"{REPLICATION_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

await wait_for_mongodb_units_blocked(
ops_test,
REPLICATION_APP_NAME,
status="sharding interface cannot be used by replicas",
timeout=300,
)

# clean up relation
await ops_test.model.applications[REPLICATION_APP_NAME].remove_relation(
f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}",
f"{REPLICATION_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

await ops_test.model.wait_for_idle(
apps=[REPLICATION_APP_NAME],
idle_period=20,
raise_on_blocked=False,
)


@pytest.mark.skip("Will be enabled after DPE-5040 is done")
@pytest.mark.group(1)
@pytest.mark.abort_on_fail
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 wait_for_mongodb_units_blocked(
ops_test,
REPLICATION_APP_NAME,
status="Relation to mongos not supported, config role must be config-server",
timeout=300,
)

# clean up relations
await ops_test.model.applications[REPLICATION_APP_NAME].remove_relation(
f"{REPLICATION_APP_NAME}:cluster",
f"{MONGOS_APP_NAME}:cluster",
)

await ops_test.model.wait_for_idle(
apps=[REPLICATION_APP_NAME, MONGOS_APP_NAME],
idle_period=20,
raise_on_blocked=False,
)


@pytest.mark.skip("Will be enabled after DPE-5040 is done")
@pytest.mark.group(1)
@pytest.mark.abort_on_fail
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 wait_for_mongodb_units_blocked(
ops_test,
SHARD_ONE_APP_NAME,
status="Relation to mongos not supported, config role must be config-server",
timeout=300,
)

# 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",
)

await ops_test.model.wait_for_idle(
apps=[SHARD_ONE_APP_NAME, MONGOS_APP_NAME],
idle_period=20,
raise_on_blocked=False,
)


Expand Down
Loading