Skip to content

Commit

Permalink
Merge branch '6/edge' into status-reporting-shard-side
Browse files Browse the repository at this point in the history
  • Loading branch information
MiaAltieri committed Oct 26, 2023
2 parents 2ed1580 + b62aba9 commit 739cad0
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 141 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
- tls-integration
- backup-integration
- metric-integration
- sharding-integration
name: ${{ matrix.tox-environments }}
needs:
- lint
Expand Down
2 changes: 2 additions & 0 deletions 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.
89 changes: 89 additions & 0 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

import json
from pathlib import Path
from typing import Dict, Optional

import ops
import yaml
Expand Down Expand Up @@ -100,3 +102,90 @@ async def set_password(
)
action = await action.wait()
return action.results


async def get_application_relation_data(
ops_test: OpsTest,
application_name: str,
relation_name: str,
key: str,
relation_id: str = None,
relation_alias: str = None,
) -> Optional[str]:
"""Get relation data for an application.
Args:
ops_test: The ops test framework instance
application_name: The name of the application
relation_name: name of the relation to get connection data from
key: key of data to be retrieved
relation_id: id of the relation to get connection data from
relation_alias: alias of the relation (like a connection name)
to get connection data from
Returns:
the that that was requested or None
if no data in the relation
Raises:
ValueError if it's not possible to get application unit data
or if there is no data for the particular relation endpoint
and/or alias.
"""
unit_name = f"{application_name}/0"
raw_data = (await ops_test.juju("show-unit", unit_name))[1]

if not raw_data:
raise ValueError(f"no unit info could be grabbed for {unit_name}")
data = yaml.safe_load(raw_data)

# Filter the data based on the relation name.
relation_data = [v for v in data[unit_name]["relation-info"] if v["endpoint"] == relation_name]

if relation_id:
# Filter the data based on the relation id.
relation_data = [v for v in relation_data if v["relation-id"] == relation_id]

if relation_alias:
# Filter the data based on the cluster/relation alias.
relation_data = [
v
for v in relation_data
if json.loads(v["application-data"]["data"])["alias"] == relation_alias
]

if len(relation_data) == 0:
raise ValueError(
f"no relation data could be grabbed on relation with endpoint {relation_name} and alias {relation_alias}"
)

return relation_data[0]["application-data"].get(key)


async def get_secret_id(ops_test, app_or_unit: Optional[str] = None) -> str:
"""Retrieve secert ID for an app or unit."""
complete_command = "list-secrets"

prefix = ""
if app_or_unit:
if app_or_unit[-1].isdigit():
# it's a unit
app_or_unit = "-".join(app_or_unit.split("/"))
prefix = "unit-"
else:
prefix = "application-"
complete_command += f" --owner {prefix}{app_or_unit}"

_, stdout, _ = await ops_test.juju(*complete_command.split())
output_lines_split = [line.split() for line in stdout.split("\n")]
if app_or_unit:
return [line[0] for line in output_lines_split if app_or_unit in line][0]
else:
return output_lines_split[1][0]


async def get_secret_content(ops_test, secret_id) -> Dict[str, str]:
"""Retrieve contents of a Juju Secret."""
secret_id = secret_id.split("/")[-1]
complete_command = f"show-secret {secret_id} --reveal --format=json"
_, stdout, _ = await ops_test.juju(*complete_command.split())
data = json.loads(stdout)
return data[secret_id]["content"]["Data"]
59 changes: 2 additions & 57 deletions tests/integration/relation_tests/new_relations/helpers.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,13 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.

import json
from typing import Optional

import yaml
from pytest_operator.plugin import OpsTest
from tenacity import RetryError, Retrying, stop_after_delay, wait_fixed


async def get_application_relation_data(
ops_test: OpsTest,
application_name: str,
relation_name: str,
key: str,
relation_id: str = None,
relation_alias: str = None,
) -> Optional[str]:
"""Get relation data for an application.
Args:
ops_test: The ops test framework instance
application_name: The name of the application
relation_name: name of the relation to get connection data from
key: key of data to be retrieved
relation_id: id of the relation to get connection data from
relation_alias: alias of the relation (like a connection name)
to get connection data from
Returns:
the that that was requested or None
if no data in the relation
Raises:
ValueError if it's not possible to get application unit data
or if there is no data for the particular relation endpoint
and/or alias.
"""
unit_name = f"{application_name}/0"
raw_data = (await ops_test.juju("show-unit", unit_name))[1]

if not raw_data:
raise ValueError(f"no unit info could be grabbed for {unit_name}")
data = yaml.safe_load(raw_data)

# Filter the data based on the relation name.
relation_data = [v for v in data[unit_name]["relation-info"] if v["endpoint"] == relation_name]

if relation_id:
# Filter the data based on the relation id.
relation_data = [v for v in relation_data if v["relation-id"] == relation_id]

if relation_alias:
# Filter the data based on the cluster/relation alias.
relation_data = [
v
for v in relation_data
if json.loads(v["application-data"]["data"])["alias"] == relation_alias
]

if len(relation_data) == 0:
raise ValueError(
f"no relation data could be grabbed on relation with endpoint {relation_name} and alias {relation_alias}"
)

return relation_data[0]["application-data"].get(key)
from tests.integration.helpers import get_application_relation_data


async def verify_application_data(
Expand Down
45 changes: 45 additions & 0 deletions tests/integration/sharding_tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
from urllib.parse import quote_plus

from pymongo import MongoClient
from pytest_operator.plugin import OpsTest

from ..helpers import get_password

MONGOS_PORT = 27018
MONGOD_PORT = 27017


async def generate_mongodb_client(ops_test: OpsTest, app_name: str, mongos: bool):
"""Returns a MongoDB client for mongos/mongod."""
hosts = [unit.public_address for unit in ops_test.model.applications[app_name].units]
password = await get_password(ops_test, app_name)
port = MONGOS_PORT if mongos else MONGOD_PORT
hosts = [f"{host}:{port}" for host in hosts]
hosts = ",".join(hosts)
auth_source = ""
database = "admin"

return MongoClient(
f"mongodb://operator:"
f"{quote_plus(password)}@"
f"{hosts}/{quote_plus(database)}?"
f"{auth_source}"
)


def write_data_to_mongodb(client, db_name, coll_name, content) -> None:
"""Writes data to the provided collection and database."""
db = client[db_name]
horses_collection = db[coll_name]
horses_collection.insert_one(content)


def verify_data_mongodb(client, db_name, coll_name, key, value) -> bool:
"""Checks a key/value pair for a provided collection and database."""
db = client[db_name]
test_collection = db[coll_name]
query = test_collection.find({}, {key: 1})
return query[0][key] == value
120 changes: 120 additions & 0 deletions tests/integration/sharding_tests/test_sharding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
# See LICENSE file for licensing details.
import pytest
from pytest_operator.plugin import OpsTest

from .helpers import generate_mongodb_client, verify_data_mongodb, write_data_to_mongodb

SHARD_ONE_APP_NAME = "shard-one"
SHARD_TWO_APP_NAME = "shard-two"
CONFIG_SERVER_APP_NAME = "config-server-one"
SHARD_REL_NAME = "sharding"
CONFIG_SERVER_REL_NAME = "config-server"
MONGODB_KEYFILE_PATH = "/var/snap/charmed-mongodb/current/etc/mongod/keyFile"
TIMEOUT = 15 * 60


@pytest.mark.abort_on_fail
async def test_build_and_deploy(ops_test: OpsTest) -> None:
"""Build and deploy a sharded cluster."""
my_charm = await ops_test.build_charm(".")
await ops_test.model.deploy(
my_charm,
num_units=2,
config={"role": "config-server"},
application_name=CONFIG_SERVER_APP_NAME,
)
await ops_test.model.deploy(
my_charm, num_units=2, config={"role": "shard"}, application_name=SHARD_ONE_APP_NAME
)
await ops_test.model.deploy(
my_charm, num_units=2, config={"role": "shard"}, application_name=SHARD_TWO_APP_NAME
)

async with ops_test.fast_forward():
await ops_test.model.wait_for_idle(
apps=[CONFIG_SERVER_APP_NAME, SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME],
idle_period=20,
raise_on_blocked=False,
timeout=TIMEOUT,
)

# TODO Future PR: assert that CONFIG_SERVER_APP_NAME, SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME
# are blocked waiting for relaitons


@pytest.mark.abort_on_fail
async def test_cluster_active(ops_test: OpsTest) -> None:
"""Tests the integration of cluster components works without error."""
await ops_test.model.integrate(
f"{SHARD_ONE_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)
await ops_test.model.integrate(
f"{SHARD_TWO_APP_NAME}:{SHARD_REL_NAME}",
f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}",
)

async with ops_test.fast_forward():
await ops_test.model.wait_for_idle(
apps=[CONFIG_SERVER_APP_NAME, SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME],
idle_period=20,
status="active",
timeout=TIMEOUT,
)

# TODO Future PR: assert that CONFIG_SERVER_APP_NAME, SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME
# have the correct active statuses.


async def test_sharding(ops_test: OpsTest) -> None:
"""Tests writing data to mongos gets propagated to shards."""
# write data to mongos on both shards.
mongos_client = await generate_mongodb_client(
ops_test, app_name=CONFIG_SERVER_APP_NAME, mongos=True
)

# write data to shard one
write_data_to_mongodb(
mongos_client,
db_name="animals_database_1",
coll_name="horses",
content={"horse-breed": "unicorn", "real": True},
)
mongos_client.admin.command("movePrimary", "animals_database_1", to=SHARD_ONE_APP_NAME)

# write data to shard two
write_data_to_mongodb(
mongos_client,
db_name="animals_database_2",
coll_name="horses",
content={"horse-breed": "pegasus", "real": True},
)
mongos_client.admin.command("movePrimary", "animals_database_2", to=SHARD_TWO_APP_NAME)

# log into shard 1 verify data
shard_one_client = await generate_mongodb_client(
ops_test, app_name=SHARD_ONE_APP_NAME, mongos=False
)
has_correct_data = verify_data_mongodb(
shard_one_client,
db_name="animals_database_1",
coll_name="horses",
key="horse-breed",
value="unicorn",
)
assert has_correct_data, "data not written to shard-one"

# log into shard 2 verify data
shard_two_client = await generate_mongodb_client(
ops_test, app_name=SHARD_TWO_APP_NAME, mongos=False
)
has_correct_data = verify_data_mongodb(
shard_two_client,
db_name="animals_database_2",
coll_name="horses",
key="horse-breed",
value="pegasus",
)
assert has_correct_data, "data not written to shard-two"
Loading

0 comments on commit 739cad0

Please sign in to comment.