Skip to content

Commit

Permalink
Update relation information on scale up\scale down (#144)
Browse files Browse the repository at this point in the history
* Update relation informaion on scale up\scale down

Add integration tests for releations

Fixes #111

* WIP: intergration tests

* Add integration tests for relations

* Fix lint issues

* Address review comments

* Reformat file

* Removed custom get_info_from_mongo_connection_string

methond in favor of from pymongo.uri_parser import parse_uri

* Removed extra blank lines
  • Loading branch information
dmitry-ratushnyy authored Jun 1, 2023
1 parent db32e77 commit b0b57d8
Show file tree
Hide file tree
Showing 12 changed files with 1,295 additions and 9 deletions.
18 changes: 18 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
UNIX_USER = "mongodb"
UNIX_GROUP = "mongodb"
MONGODB_EXPORTER_PORT = 9216
REL_NAME = "database"


class MongoDBCharm(CharmBase):
Expand Down Expand Up @@ -253,13 +254,30 @@ def _reconfigure(self, event) -> None:
event.defer()
return
mongo.add_replset_member(member)

# app relations should be made aware of the new set of hosts
self._update_app_relation_data(mongo.get_users())
except NotReadyError:
logger.info("Deferring reconfigure: another member doing sync right now")
event.defer()
except PyMongoError as e:
logger.info("Deferring reconfigure: error=%r", e)
event.defer()

def _update_app_relation_data(self, database_users):
"""Helper function to update application relation data."""
for relation in self.model.relations[REL_NAME]:
username = self.client_relations._get_username_from_relation_id(relation.id)
password = relation.data[self.app]["password"]
if username in database_users:
config = self.client_relations._get_config(username, password)
relation.data[self.app].update(
{
"endpoints": ",".join(config.hosts),
"uris": config.uri,
}
)

@property
def _mongodb_exporter_layer(self) -> Layer:
"""Returns a Pebble configuration layer for mongod."""
Expand Down
19 changes: 15 additions & 4 deletions tests/integration/ha_tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

import logging
import os
import string
import subprocess
Expand Down Expand Up @@ -34,9 +35,11 @@
TIMEOUT = 15 * 60
TEST_DB = "continuous_writes_database"
TEST_COLLECTION = "test_collection"
ANOTHER_DATABASE_APP_NAME = "another-database-a"
ANOTHER_DATABASE_APP_NAME = "another-database"
EXCLUDED_APPS = [ANOTHER_DATABASE_APP_NAME]

logger = logging.getLogger(__name__)

mongodb_charm, application_charm = None, None


Expand Down Expand Up @@ -311,17 +314,25 @@ async def mongod_ready(ops_test: OpsTest, unit: int) -> bool:
return True


async def get_replica_set_primary(ops_test: OpsTest, excluded: List[str] = []) -> Optional[Unit]:
async def get_replica_set_primary(
ops_test: OpsTest, excluded: List[str] = [], application_name=APP_NAME
) -> Optional[Unit]:
"""Returns the primary unit name based no the replica set host."""
with await get_mongo_client(ops_test, excluded) as client:
data = client.admin.command("replSetGetStatus")

unit_name = host_to_unit(primary_host(data))

if unit_name:
mongodb_name = await get_application_name(ops_test, APP_NAME)
mongodb_name = await get_application_name(ops_test, application_name)
for unit in ops_test.model.applications[mongodb_name].units:
logger.info(
f"Unit name: {unit.name}. Target unit name: {unit_name}, {unit.name == unit_name}"
)
if unit.name == unit_name:
return unit
logger.error(
f"Target unit name {unit_name} not found in {ops_test.model.applications[mongodb_name].units}"
)


async def count_primaries(ops_test: OpsTest) -> int:
Expand Down
23 changes: 18 additions & 5 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,19 @@ async def run_mongo_op(
mongo_uri: str = None,
suffix: str = "",
expecting_output: bool = True,
stringify: bool = True,
ignore_errors: bool = False,
) -> SimpleNamespace():
"""Runs provided MongoDB operation in a separate container."""
if mongo_uri is None:
mongo_uri = await mongodb_uri(ops_test)

mongo_cmd = f"mongo --quiet --eval 'JSON.stringify({mongo_op})' {mongo_uri}{suffix}"
if stringify:
mongo_cmd = f"mongo --quiet --eval 'JSON.stringify({mongo_op})' {mongo_uri}{suffix}"
else:
mongo_cmd = f"mongo --quiet --eval '{mongo_op}' {mongo_uri}{suffix}"

logger.info("Running mongo command: %r", mongo_cmd)
kubectl_cmd = (
"microk8s",
"kubectl",
Expand All @@ -127,6 +133,11 @@ async def run_mongo_op(
if ret_code != 0:
logger.error("code %r; stdout %r; stderr: %r", ret_code, stdout, stderr)
output.failed = True
output.data = {
"code": ret_code,
"stdout": stdout,
"stderr": stderr,
}
return output

output.succeeded = True
Expand All @@ -136,12 +147,14 @@ async def run_mongo_op(
except Exception:
logger.error(
"Could not serialize the output into json.{}{}".format(
f"\n\tOut: {stdout}" if stdout else "",
f"\n\tErr: {stderr}" if stderr else "",
f"\n\tSTDOUT:\n\t {stdout}" if stdout else "",
f"\n\tSTDERR:\n\t {stderr}" if stderr else "",
)
)
raise

if not ignore_errors:
raise
else:
output.data = stdout
return output


Expand Down
2 changes: 2 additions & 0 deletions tests/integration/relation_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
11 changes: 11 additions & 0 deletions tests/integration/relation_tests/application-charm/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

type: charm
bases:
- build-on:
- name: "ubuntu"
channel: "22.04"
run-on:
- name: "ubuntu"
channel: "22.04"
Loading

0 comments on commit b0b57d8

Please sign in to comment.