From 29be59e35c3b170b468220730273a0b3a9ea760d Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 9 Apr 2026 10:53:59 +0200 Subject: [PATCH 01/35] wip --- .github/workflows/release_tests.yaml | 209 +++++++++ .../src/continuous_reads.py | 76 +++ tests/integration/mongodb/release/conftest.py | 33 ++ .../mongodb/release/test_release.py | 310 ++++++++++++ .../mongodb/release/test_sharding_release.py | 442 ++++++++++++++++++ .../mongos/release/test_release.py | 0 .../mongodb/lxd/test_release.py/task.yaml | 8 + .../microk8s/test_release.py/task.yaml | 8 + .../mongos/lxd/test_release.py/task.yaml | 8 + .../mongos/microk8s/test_release.py/task.yaml | 8 + 10 files changed, 1102 insertions(+) create mode 100644 .github/workflows/release_tests.yaml create mode 100644 tests/integration/applications/continuous_write_charm/src/continuous_reads.py create mode 100644 tests/integration/mongodb/release/conftest.py create mode 100644 tests/integration/mongodb/release/test_release.py create mode 100644 tests/integration/mongodb/release/test_sharding_release.py create mode 100644 tests/integration/mongos/release/test_release.py create mode 100644 tests/spread/release/mongodb/lxd/test_release.py/task.yaml create mode 100644 tests/spread/release/mongodb/microk8s/test_release.py/task.yaml create mode 100644 tests/spread/release/mongos/lxd/test_release.py/task.yaml create mode 100644 tests/spread/release/mongos/microk8s/test_release.py/task.yaml diff --git a/.github/workflows/release_tests.yaml b/.github/workflows/release_tests.yaml new file mode 100644 index 0000000000..71895ce873 --- /dev/null +++ b/.github/workflows/release_tests.yaml @@ -0,0 +1,209 @@ +on: + workflow_call: + inputs: + mongodb-revision: + description: | + MongoDB revisions to target (ex: '{"amd64": "161", "arm64": "162"}' + required: true + type: string + mongos-revision: + description: | + Mongos revisions to target (ex: '{"amd64": "161", "arm64": "162"}' + required: true + type: string + charm: + description: | + The charm type we're targetting. + + One of mongodb or mongos or "..." (default, both charms). + type: string + required: false + default: "..." + backend: + description: | + Backend to target. + + Either lxd or microk8s, or "..." (default, all backends). + required: false + type: string + default: "..." + workflow_dispatch: + inputs: + mongodb-revision: + description: | + MongoDB revision to target + required: true + type: number + charm: + description: | + The charm type we're targetting. + + One of mongodb or mongos or "..." (default, both charms). + type: string + required: false + default: "..." + mongos-revision: + description: | + MongoDB revision to target + required: true + type: number + backend: + description: | + Backend to target. + + Either lxd or microk8s, or "..." (default, all backends). + required: false + type: string + default: "..." + +jobs: + collect-release-tests: + name: Collect release test spread jobs + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up environment + run: | + sudo snap install go --classic + go install github.com/canonical/spread/cmd/spread@latest + pipx install tox poetry + - name: Install deps + run: | + sudo apt update + sudo apt install -y build-essential python3-dev libldap-dev libsasl2-dev + - name: Collect spread jobs + id: collect-jobs + shell: python + run: | + import json + import pathlib + import os + import subprocess + backend = "${{ inputs.backend }}" + charm_type = "${{ inputs.charm }} + pattern = f"github-ci:...:tests/spread/release/{charm_type}/{backend}/" + spread_jobs = ( + subprocess.run( + [pathlib.Path.home() / "go/bin/spread", "-list", pattern], + capture_output=True, + check=True, + text=True, + ) + .stdout.strip() + .split("\n") + ) + jobs = [] + for job in spread_jobs: + # Example `job`: "github-ci:ubuntu-24.04:tests/spread/release/microk8s/test_release.py" + _, runner, task = job.split(":") + # Remove arm jobs in regular testing. + # Example: "test_charm.py" + task = task.removeprefix("tests/spread/") + if "arm64" in runner: + architecture = "arm64" + else: + architecture = "amd64" + # Example: "test_charm.py | amd64" + name = f"{task} | {architecture}" + # ":" character not valid in GitHub Actions artifact + name_in_artifact = f"{task.replace('/', '-')}-{architecture}" + jobs.append({ + "spread_job": job, + "name": name, + "name_in_artifact": name_in_artifact, + "runner": runner, + }) + output = f"jobs={json.dumps(jobs)}" + print(output) + with open(os.environ["GITHUB_OUTPUT"], "a") as file: + file.write(output) + outputs: + jobs: ${{ steps.collect-jobs.outputs.jobs }} + + release-test: + strategy: + fail-fast: false + matrix: + job: ${{ fromJSON(needs.collect-integration-tests.outputs.jobs) }} + name: ${{ matrix.job.name }} + needs: + - collect-integration-tests + runs-on: ${{ matrix.job.runner }} + timeout-minutes: 230 # Sum of steps `timeout-minutes` + 5 + steps: + - name: (IS hosted) Disk usage + timeout-minutes: 1 + if: ${{ contains(matrix.job.runner, 'self-hosted') }} + run: df --human-readable + - name: Checkout + timeout-minutes: 3 + uses: actions/checkout@v6 + - name: Setup python 3.12 + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Set up environment + timeout-minutes: 5 + run: sudo snap install charmcraft --classic + # TODO: remove when https://github.com/canonical/charmcraft/issues/2105 and + # https://github.com/canonical/charmcraft/issues/2130 fixed + - run: | + sudo snap install go --classic + go install github.com/canonical/spread/cmd/spread@latest + - name: Run spread job + timeout-minutes: 180 + id: spread + # TODO: replace with `charmcraft test` when + # https://github.com/canonical/charmcraft/issues/2105 and + # https://github.com/canonical/charmcraft/issues/2130 fixed + run: ~/go/bin/spread -vv -artifacts=artifacts '${{ matrix.job.spread_job }}' + env: + MONGODB_REVISION: ${{ inputs.mongodb-revision }} + MONGOS_REVISION: ${{ inputs.mongos-revision }} + AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} + AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }} + GCP_ACCESS_KEY: ${{ secrets.GCP_ACCESS_KEY }} + GCP_SECRET_KEY: ${{ secrets.GCP_SECRET_KEY }} + GCS_SERVICE_ACCOUNT: ${{ secrets.GCS_SERVICE_ACCOUNT }} + - timeout-minutes: 1 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + run: snap list + - name: Select model + timeout-minutes: 1 + if: ${{ (success() || (failure() && steps.spread.outcome == 'failure')) }} + id: juju-switch + run: | + # sudo needed since spread runs scripts as root + # "testing" is default model created by concierge + sudo juju switch testing + mkdir ~/logs/ + - name: juju status + timeout-minutes: 1 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + run: sudo juju status --color --relations | tee ~/logs/juju-status.txt + - name: juju status (YAML) + timeout-minutes: 1 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + run: sudo juju status --format yaml --color --relations --storage | tee ~/logs/juju-status-yaml.txt + - name: juju debug-log + timeout-minutes: 3 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + run: sudo juju debug-log --color --replay --no-tail | tee ~/logs/juju-debug-log.txt + - name: jhack tail + timeout-minutes: 3 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + run: sudo jhack tail --printer raw --replay --no-watch | tee ~/logs/jhack-tail.txt + - name: Upload logs + timeout-minutes: 5 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + uses: actions/upload-artifact@v4 + with: + name: logs-integration-test-${{ matrix.job.name_in_artifact }} + path: ~/logs/ + if-no-files-found: error + - name: Disk usage + timeout-minutes: 1 + if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} + run: df --human-readable diff --git a/tests/integration/applications/continuous_write_charm/src/continuous_reads.py b/tests/integration/applications/continuous_write_charm/src/continuous_reads.py new file mode 100644 index 0000000000..02bb23f273 --- /dev/null +++ b/tests/integration/applications/continuous_write_charm/src/continuous_reads.py @@ -0,0 +1,76 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +"""This file is meant to run in the background continuously reading data from MongoDB.""" + +import json +import random +import signal +import sys + +import math +from pymongo import MongoClient +from pymongo.errors import PyMongoError +from pymongo.write_concern import WriteConcern + +DEFAULT_DB_NAME = "continuous_writes_database" +DEFAULT_COLL_NAME = "continuous_writes_collection" + +run = True + + +def sigterm_handler(_signo, _stack_frame): + global run + run = False + +def n_read_filename(db_name: str, coll_name: str) -> str: + return f"n_read_value-{db_name}-{coll_name}" + + +def continous_reads( + connection_string: str, + db_name: str, + coll_name: str, +): + failed_reads = [] + reads = 0 + while run: + client = MongoClient( + connection_string, + socketTimeoutMS=5000, + ) + db = client[db_name] + test_collection = db[coll_name] + try: + if (rand:= random.random()) < 0.3: + # run some basic sampling + test_collection.aggregate([{"$sample": {"size": 10}}, {"$sort": {"number": 1}}]) + elif rand < 0.6: + n_docs = test_collection.count_documents() + # get one single sample + test_collection.aggregate([{"$skip": math.floor(n_docs * random.random())}, {"$limit": 1}]) + else: + n_docs = test_collection.count_documents() + test_collection.find({"number": {"$lte": math.floor(n_docs /2)}}) + except PyMongoError as err: + failed_reads.append(str(err)) + continue + finally: + client.close() + + reads += 1 + + with open(n_read_filename(db_name, coll_name), "w") as fd: + json.dump({"reads": reads, "failed_reads": failed_reads}, fd) + + +def main(): + connection_string = sys.argv[1] + db_name = DEFAULT_DB_NAME if len(sys.argv) < 3 else sys.argv[2] + coll_name = DEFAULT_COLL_NAME if len(sys.argv) < 4 else sys.argv[3] + continous_reads(connection_string, db_name, coll_name) + + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, sigterm_handler) + main() diff --git a/tests/integration/mongodb/release/conftest.py b/tests/integration/mongodb/release/conftest.py new file mode 100644 index 0000000000..945b683a1c --- /dev/null +++ b/tests/integration/mongodb/release/conftest.py @@ -0,0 +1,33 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +from collections.abc import AsyncGenerator +from logging import getLogger +from typing import Any + +import pytest +from juju.model import Model +from kubernetes.config.config_exception import ConfigException +from pytest_operator.plugin import OpsTest + +TIMEOUT = 15 * 60 + +logger = getLogger(__name__) + + +@pytest.fixture(scope="module") +async def kubernetes_model(ops_test: OpsTest, architecture: str) -> AsyncGenerator[Model, Any]: + try: + k8s_cloud = await ops_test.add_k8s(skip_storage=False) + logger.warning(f"created cloud {k8s_cloud}") + except (ConfigException, TypeError): + pytest.fail("No Kubernetes config found to add-k8s") + # deploy the glauth-k8s charm + kubernetes_model = await ops_test.track_model( + "secondary", cloud_name=k8s_cloud, keep=ops_test.ModelKeep.NEVER + ) + logger.warning(f"Created model {kubernetes_model.name}") + + yield kubernetes_model + + await ops_test.forget_model(alias="secondary", timeout=TIMEOUT, allow_failure=True) diff --git a/tests/integration/mongodb/release/test_release.py b/tests/integration/mongodb/release/test_release.py new file mode 100644 index 0000000000..ba2715f3a6 --- /dev/null +++ b/tests/integration/mongodb/release/test_release.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +import asyncio +from logging import getLogger + +import pytest +from pytest_operator.plugin import OpsTest +from tenacity import RetryError, Retrying +from tenacity.stop import stop_after_delay +from tenacity.wait import wait_fixed + +from tests.integration.helpers.backups import S3_APP_NAME, CloudConfigs, count_logical_backups +from tests.integration.helpers.common import ( + CONTINUOUS_WRITE_APPLICATION, + CONTINUOUS_WRITE_APPLICATION_BIS, + DATA_INTEGRATOR_APP_NAME, + DEFAULT_COLLECTION_NAME, + DEFAULT_DATABASE_NAME, + DEPLOYMENT_TIMEOUT, + READER_APPLICATION, + TIMEOUT, + UNIT_IDS, + count_writes, + deploy_application, + deploy_charm, + execute_on_mongod, + find_unit, + get_app_name, + relate_mongodb_and_application, + start_continous_writes, + start_continuous_reads, + stop_continous_writes, + stop_continuous_reads, +) +from tests.integration.helpers.ldap import ( + LDAP_CERT_OFFER, + LDAP_OFFER, + apply_ldif, + consume_glauth_offers, + create_mongodb_user_roles, + deploy_glauth, + generate_mongodb_ldap_client, +) +from tests.integration.helpers.tls import ( + TLS_CERTIFICATES_APP_NAME, + TLS_CERTIFICATES_BASE, + TLS_CERTIFICATES_CHANNEL, + integrate_apps_with_tls, +) +from tests.integration.helpers.types import Substrate + +logger = getLogger(__name__) + +SECOND_DB_NAME = f"{DEFAULT_DATABASE_NAME}_bis" +SECOND_COLL_NAME = f"{DEFAULT_COLLECTION_NAME}_bis" + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy( + ops_test: OpsTest, + mongodb_charm_name: str, + application_path: str, + substrate: Substrate, + mongodb_revision: int, + base_app_name: str, + kubernetes_model: str, +): + """Build and deploy one unit of MongoDB.""" + tls_config = {"ca-common-name": "MongoDB release CA"} + # it is possible for users to provide their own cluster for testing. Hence check if there + # is a pre-existing cluster. + asyncio.gather( + deploy_charm( + ops_test=ops_test, + revision=mongodb_revision, + charm=mongodb_charm_name, + substrate=substrate, + app_name=base_app_name, + num_units=len(UNIT_IDS), + ), + deploy_application( + ops_test, application_path=application_path, app_name=CONTINUOUS_WRITE_APPLICATION + ), + ops_test.model.deploy( + TLS_CERTIFICATES_APP_NAME, + channel=TLS_CERTIFICATES_CHANNEL, + config=tls_config, + base=TLS_CERTIFICATES_BASE, + ), + ops_test.model.deploy( + DATA_INTEGRATOR_APP_NAME, + channel="latest/stable", + series="noble", + config={"database-name": "test-database"}, + ), + deploy_glauth(ops_test, kubernetes_model), + ) + + # Consume the offers exposed by glauth + await consume_glauth_offers(ops_test, kubernetes_model) + + # Apply the LDIF file on glauth-utils to create users and groups + await apply_ldif(ops_test, kubernetes_model, "ldap_entries.ldif") + + await ops_test.model.wait_for_idle( + apps=[base_app_name, TLS_CERTIFICATES_APP_NAME], timeout=DEPLOYMENT_TIMEOUT, status="active" + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_with_tls( + ops_test: OpsTest, +): + """Tests that we can integrate with TLS without losing data.""" + app_name = await get_app_name(ops_test) + await integrate_apps_with_tls(ops_test, applications=[app_name]) + + await ops_test.model.wait_for_idle( + apps=[app_name, TLS_CERTIFICATES_APP_NAME], status="active", timeout=1000, idle_period=60 + ) + + await relate_mongodb_and_application(ops_test, app_name, CONTINUOUS_WRITE_APPLICATION) + await start_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) + + +async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): + app_name = await get_app_name(ops_test) + + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{app_name}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{app_name}:ldap-certificate-transfer" + ) + # Create the roles on MongoDB + await create_mongodb_user_roles( + ops_test, + substrate, + app_name, + role_name="ou=superheroes,ou=users,dc=glauth,dc=com", + db=DEFAULT_DATABASE_NAME, + ) + + await ops_test.model.wait_for_idle(apps=[app_name], status="active", timeout=TIMEOUT) + + +@pytest.mark.abort_on_fail +async def test_integrate_second_client(ops_test: OpsTest, application_path: str): + app_name = await get_app_name(ops_test) + + await deploy_application( + ops_test, + application_path=application_path, + app_name=CONTINUOUS_WRITE_APPLICATION_BIS, + database_name=SECOND_DB_NAME, + ) + await relate_mongodb_and_application(ops_test, app_name, CONTINUOUS_WRITE_APPLICATION_BIS) + await start_continous_writes( + ops_test, + CONTINUOUS_WRITE_APPLICATION_BIS, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_third_client(ops_test: OpsTest, application_path: str): + app_name = await get_app_name(ops_test) + + await deploy_application( + ops_test, + application_path=application_path, + app_name=READER_APPLICATION, + database_name=DEFAULT_DATABASE_NAME, + ) + await relate_mongodb_and_application(ops_test, app_name, READER_APPLICATION) + + await start_continuous_reads( + ops_test, + READER_APPLICATION, + db_name=DEFAULT_DATABASE_NAME, + coll_name=DEFAULT_COLLECTION_NAME, + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs): + app_name = await get_app_name(ops_test) + + # deploy the s3 integrator charm + await ops_test.model.deploy(S3_APP_NAME, channel="1/edge") + await ops_test.model.wait_for_idle(apps=[S3_APP_NAME], timeout=DEPLOYMENT_TIMEOUT) + + configuration_parameters, credentials = cloud_configs["AWS"] + s3_integrator_unit = ops_test.model.applications[S3_APP_NAME].units[0] + + # apply new configuration options + await ops_test.model.applications[S3_APP_NAME].set_config(configuration_parameters) + action = await s3_integrator_unit.run_action(action_name="sync-s3-credentials", **credentials) + await action.wait() + + await ops_test.model.wait_for_idle( + apps=[S3_APP_NAME, app_name], status="active", timeout=TIMEOUT + ) + + leader_unit = await find_unit(ops_test, leader=True, app_name=app_name) + action = await leader_unit.run_action(action_name="create-backup") + backup_result = await action.wait() + + logger.info(f"Create backup result {backup_result.results=}") + assert "backup started" in backup_result.results["backup-status"], "backup didn't start" + try: + for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(5)): + with attempt: + backups = await count_logical_backups(leader_unit) + assert backups == 1 + except RetryError: + assert backups == 1, "Backup not created." + + +@pytest.mark.abort_on_fail +async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): + app_name = await get_app_name(ops_test) + + first_reported_writes = await stop_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) + second_reported_writes = await stop_continous_writes( + ops_test, + CONTINUOUS_WRITE_APPLICATION_BIS, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + leader_unit = await find_unit(ops_test, leader=True, app_name=app_name) + # count total writes + first_number_writes = await count_writes(ops_test, substrate, app_name, leader_unit) + second_number_writes = await count_writes( + ops_test, + substrate, + app_name, + leader_unit, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + assert first_number_writes == first_reported_writes + assert second_number_writes == second_reported_writes + + # find most recent backup id and restore + action = await leader_unit.run_action(action_name="list-backups") + list_result = await action.wait() + list_result = list_result.results["backups"] + most_recent_backup = list_result.split("\n")[-1] + + backup_id = most_recent_backup.split()[0] + + action = await leader_unit.run_action(action_name="restore", **{"backup-id": backup_id}) + restore = await action.wait() + logger.info(f"Restore backup result {restore.results=}") + assert restore.results["restore-status"] == "restore started", "restore not successful" + + with ops_test.fast_forward("60s"): + (await ops_test.model.wait_for_idle(apps=[app_name], status="active", idle_period=15),) + + +@pytest.mark.abort_on_fail +async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): + """Checks that the LDAP user can write to the DB. + + This checks both authentication and authorisation. + """ + app_name = await get_app_name(ops_test) + + # We create a client which should be able to write + uri = await generate_mongodb_ldap_client( + ops_test, + substrate, + app_name, + database="superdb", + username="cn=johndoe,ou=superheroes,ou=users,dc=glauth,dc=com", + password="dogood", + ) + + result = await execute_on_mongod( + ops_test, app_name, substrate, uri, "db.test.insertOne({number: 1})" + ) + assert result.succeeded, "Failed to insert value with LDAP client" + + result = await execute_on_mongod( + ops_test, app_name, substrate, uri, "db.test.findOne({number: 1})" + ) + assert result.succeeded, "Failed to read value with LDAP client" + + result = await execute_on_mongod( + ops_test, + app_name, + substrate, + uri, + f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", + ) + assert result.succeeded, "Failed to read value with LDAP client" + + +@pytest.mark.abort_on_fail +async def test_valid_reads(ops_test: OpsTest): + reads, failed_reads = await stop_continuous_reads( + ops_test, + READER_APPLICATION, + db_name=DEFAULT_DATABASE_NAME, + coll_name=DEFAULT_COLLECTION_NAME, + ) + assert reads > 1000 + assert len(failed_reads) == 0 diff --git a/tests/integration/mongodb/release/test_sharding_release.py b/tests/integration/mongodb/release/test_sharding_release.py new file mode 100644 index 0000000000..56313f3f8f --- /dev/null +++ b/tests/integration/mongodb/release/test_sharding_release.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +import asyncio +from logging import getLogger + +import pytest +from pytest_operator.plugin import OpsTest +from tenacity import RetryError, Retrying +from tenacity.stop import stop_after_delay +from tenacity.wait import wait_fixed + +from tests.integration.helpers.backups import S3_APP_NAME, CloudConfigs, count_logical_backups +from tests.integration.helpers.common import ( + CONTINUOUS_WRITE_APPLICATION, + CONTINUOUS_WRITE_APPLICATION_BIS, + DEFAULT_COLLECTION_NAME, + DEFAULT_DATABASE_NAME, + DEPLOYMENT_TIMEOUT, + MONGOS_APP_NAME, + TIMEOUT, + UNIT_IDS, + count_writes, + deploy_application, + deploy_charm, + execute_on_mongod, + find_unit, + get_app_name, + start_continous_writes, + stop_continous_writes, + wait_for_mongodb_units_blocked, +) +from tests.integration.helpers.ldap import ( + LDAP_CERT_OFFER, + LDAP_OFFER, + apply_ldif, + consume_glauth_offers, + create_mongodb_user_roles, + deploy_glauth, + generate_mongodb_ldap_client, +) +from tests.integration.helpers.sharding import ( + CONFIG_SERVER_APP_NAME, + CONFIG_SERVER_REL_NAME, + SHARD_ONE_APP_NAME, + SHARD_REL_NAME, + SHARD_THREE_APP_NAME, + SHARD_TWO_APP_NAME, +) +from tests.integration.helpers.tls import ( + TLS_CERTIFICATES_APP_NAME, + TLS_CERTIFICATES_BASE, + TLS_CERTIFICATES_CHANNEL, + integrate_apps_with_tls, +) +from tests.integration.helpers.types import Substrate + +logger = getLogger(__name__) + +SECOND_DB_NAME = f"{DEFAULT_DATABASE_NAME}_bis" +SECOND_COLL_NAME = f"{DEFAULT_COLLECTION_NAME}_bis" + +MONGOS_BIS_APP_NAME = f"{MONGOS_APP_NAME}-bis" + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy( + ops_test: OpsTest, + mongodb_charm_name: str, + mongos_charm_name: str, + application_path: str, + substrate: Substrate, + mongodb_revision: int, + mongos_revision: int, + kubernetes_model: str, +): + """Build and deploy one unit of MongoDB.""" + tls_config = {"ca-common-name": "MongoDB release CA"} + # it is possible for users to provide their own cluster for testing. Hence check if there + # is a pre-existing cluster. + asyncio.gather( + deploy_charm( + ops_test=ops_test, + revision=mongodb_revision, + charm=mongodb_charm_name, + substrate=substrate, + app_name=CONFIG_SERVER_APP_NAME, + num_units=len(UNIT_IDS), + config={"role": "config-server"}, + ), + deploy_charm( + ops_test=ops_test, + revision=mongodb_revision, + charm=mongodb_charm_name, + substrate=substrate, + app_name=SHARD_ONE_APP_NAME, + num_units=len(UNIT_IDS), + config={"role": "shard"}, + ), + deploy_charm( + ops_test=ops_test, + revision=mongodb_revision, + charm=mongodb_charm_name, + substrate=substrate, + app_name=SHARD_TWO_APP_NAME, + num_units=len(UNIT_IDS), + config={"role": "shard"}, + ), + deploy_charm( + ops_test=ops_test, + revision=mongos_revision, + charm=mongos_charm_name, + substrate=substrate, + app_name=MONGOS_APP_NAME, + num_units=(1 if substrate == "microk8s" else 0), + ), + deploy_charm( + ops_test=ops_test, + revision=mongos_revision, + charm=mongos_charm_name, + substrate=substrate, + app_name=MONGOS_BIS_APP_NAME, + num_units=(1 if substrate == "microk8s" else 0), + ), + ops_test.model.deploy( + TLS_CERTIFICATES_APP_NAME, + channel=TLS_CERTIFICATES_CHANNEL, + config=tls_config, + base=TLS_CERTIFICATES_BASE, + ), + deploy_application( + ops_test, application_path=application_path, app_name=CONTINUOUS_WRITE_APPLICATION + ), + deploy_glauth(ops_test, kubernetes_model), + ) + + # Consume the offers exposed by glauth + await consume_glauth_offers(ops_test, kubernetes_model) + + # Apply the LDIF file on glauth-utils to create users and groups + await apply_ldif(ops_test, kubernetes_model, "ldap_entries.ldif") + + await ops_test.model.wait_for_idle( + apps=[TLS_CERTIFICATES_APP_NAME], timeout=DEPLOYMENT_TIMEOUT, status="active" + ) + # verify that Charmed MongoDB is blocked and reports incorrect credentials + await wait_for_mongodb_units_blocked(ops_test, substrate, CONFIG_SERVER_APP_NAME, timeout=300) + await wait_for_mongodb_units_blocked(ops_test, substrate, SHARD_ONE_APP_NAME, timeout=300) + await wait_for_mongodb_units_blocked(ops_test, substrate, SHARD_TWO_APP_NAME, timeout=300) + + 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}", + ) + await ops_test.model.wait_for_idle( + apps=[ + CONFIG_SERVER_APP_NAME, + SHARD_ONE_APP_NAME, + SHARD_TWO_APP_NAME, + ], + idle_period=15, + status="active", + timeout=TIMEOUT, + raise_on_error=False, + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_with_tls( + ops_test: OpsTest, +): + """Tests that we can integrate with TLS without losing data.""" + await ops_test.model.integrate( + f"{MONGOS_APP_NAME}", + f"{CONFIG_SERVER_APP_NAME}", + ) + await ops_test.model.wait_for_idle( + apps=[ + CONTINUOUS_WRITE_APPLICATION, + MONGOS_APP_NAME, + SHARD_ONE_APP_NAME, + CONFIG_SERVER_APP_NAME, + ], + idle_period=20, + timeout=TIMEOUT, + ) + + await integrate_apps_with_tls( + ops_test, + applications=[ + CONFIG_SERVER_APP_NAME, + SHARD_ONE_APP_NAME, + SHARD_TWO_APP_NAME, + MONGOS_APP_NAME, + ], + ) + await ops_test.model.wait_for_idle( + apps=[ + CONFIG_SERVER_APP_NAME, + SHARD_ONE_APP_NAME, + SHARD_TWO_APP_NAME, + TLS_CERTIFICATES_APP_NAME, + ], + status="active", + timeout=1000, + idle_period=60, + ) + + await ops_test.model.integrate( + f"{MONGOS_APP_NAME}", + f"{CONTINUOUS_WRITE_APPLICATION}", + ) + + await ops_test.model.wait_for_idle( + apps=[ + CONTINUOUS_WRITE_APPLICATION, + MONGOS_APP_NAME, + SHARD_ONE_APP_NAME, + SHARD_TWO_APP_NAME, + CONFIG_SERVER_APP_NAME, + ], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + raise_on_error=False, + ) + + await ops_test.model.integrate(CONTINUOUS_WRITE_APPLICATION, TLS_CERTIFICATES_APP_NAME) + + await ops_test.model.wait_for_idle( + apps=[ + CONTINUOUS_WRITE_APPLICATION, + MONGOS_APP_NAME, + SHARD_ONE_APP_NAME, + SHARD_TWO_APP_NAME, + CONFIG_SERVER_APP_NAME, + ], + idle_period=20, + raise_on_blocked=False, + timeout=TIMEOUT, + raise_on_error=False, + ) + + await start_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) + + +async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{CONFIG_SERVER_APP_NAME}:ldap") + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_APP_NAME}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{CONFIG_SERVER_APP_NAME}:ldap-certificate-transfer" + ) + # Create the roles on MongoDB + await create_mongodb_user_roles( + ops_test, + substrate, + CONFIG_SERVER_APP_NAME, + role_name="ou=superheroes,ou=users,dc=glauth,dc=com", + db=DEFAULT_DATABASE_NAME, + ) + + await ops_test.model.wait_for_idle( + apps=[CONFIG_SERVER_APP_NAME], status="active", timeout=TIMEOUT + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_second_client(ops_test: OpsTest, application_path: str): + app_name = await get_app_name(ops_test) + + await deploy_application( + ops_test, + application_path=application_path, + app_name=CONTINUOUS_WRITE_APPLICATION_BIS, + database_name=SECOND_DB_NAME, + ) + await ops_test.model.integrate( + f"{MONGOS_BIS_APP_NAME}", + f"{CONTINUOUS_WRITE_APPLICATION_BIS}", + ) + await integrate_apps_with_tls( + ops_test, + applications=[ + MONGOS_BIS_APP_NAME, + ], + ) + await ops_test.model.integrate( + f"{TLS_CERTIFICATES_APP_NAME}", + f"{CONTINUOUS_WRITE_APPLICATION_BIS}", + ) + + await start_continous_writes( + ops_test, + CONTINUOUS_WRITE_APPLICATION_BIS, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_third_shard( + ops_test: OpsTest, substrate: Substrate, mongodb_charm_name: str, mongodb_revision: int | None +): + await deploy_charm( + ops_test=ops_test, + revision=mongodb_revision, + charm=mongodb_charm_name, + substrate=substrate, + app_name=SHARD_THREE_APP_NAME, + num_units=len(UNIT_IDS), + config={"role": "shard"}, + ) + await wait_for_mongodb_units_blocked(ops_test, substrate, SHARD_THREE_APP_NAME, timeout=300) + await integrate_apps_with_tls( + ops_test, + applications=[SHARD_THREE_APP_NAME], + ) + + await ops_test.model.integrate( + f"{SHARD_THREE_APP_NAME}:{SHARD_REL_NAME}", + f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}", + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs): + # deploy the s3 integrator charm + await ops_test.model.deploy(S3_APP_NAME, channel="1/edge") + await ops_test.model.wait_for_idle(apps=[S3_APP_NAME], timeout=DEPLOYMENT_TIMEOUT) + + configuration_parameters, credentials = cloud_configs["AWS"] + s3_integrator_unit = ops_test.model.applications[S3_APP_NAME].units[0] + + # apply new configuration options + await ops_test.model.applications[S3_APP_NAME].set_config(configuration_parameters) + action = await s3_integrator_unit.run_action(action_name="sync-s3-credentials", **credentials) + await action.wait() + + await ops_test.model.wait_for_idle( + apps=[S3_APP_NAME, CONFIG_SERVER_APP_NAME], status="active", timeout=TIMEOUT + ) + + leader_unit = await find_unit(ops_test, leader=True, app_name=CONFIG_SERVER_APP_NAME) + action = await leader_unit.run_action(action_name="create-backup") + backup_result = await action.wait() + + logger.info(f"Create backup result {backup_result.results=}") + assert "backup started" in backup_result.results["backup-status"], "backup didn't start" + try: + for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(5)): + with attempt: + backups = await count_logical_backups(leader_unit) + assert backups == 1 + except RetryError: + assert backups == 1, "Backup not created." + + +@pytest.mark.abort_on_fail +async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): + first_reported_writes = await stop_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) + second_reported_writes = await stop_continous_writes( + ops_test, + CONTINUOUS_WRITE_APPLICATION_BIS, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + leader_unit = await find_unit(ops_test, leader=True, app_name=CONFIG_SERVER_APP_NAME) + # count total writes + first_number_writes = await count_writes( + ops_test, substrate, CONFIG_SERVER_APP_NAME, leader_unit + ) + second_number_writes = await count_writes( + ops_test, + substrate, + CONFIG_SERVER_APP_NAME, + leader_unit, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + assert first_number_writes == first_reported_writes + assert second_number_writes == second_reported_writes + + # find most recent backup id and restore + action = await leader_unit.run_action(action_name="list-backups") + list_result = await action.wait() + list_result = list_result.results["backups"] + most_recent_backup = list_result.split("\n")[-1] + + backup_id = most_recent_backup.split()[0] + + action = await leader_unit.run_action(action_name="restore", **{"backup-id": backup_id}) + restore = await action.wait() + logger.info(f"Restore backup result {restore.results=}") + assert restore.results["restore-status"] == "restore started", "restore not successful" + + with ops_test.fast_forward("60s"): + ( + await ops_test.model.wait_for_idle( + apps=[CONFIG_SERVER_APP_NAME], status="active", idle_period=15 + ), + ) + + +@pytest.mark.abort_on_fail +async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): + """Checks that the LDAP user can write to the DB. + + This checks both authentication and authorisation. + """ + # We create a client which should be able to write + uri = await generate_mongodb_ldap_client( + ops_test, + substrate, + CONFIG_SERVER_APP_NAME, + database="superdb", + username="cn=johndoe,ou=superheroes,ou=users,dc=glauth,dc=com", + password="dogood", + ) + + result = await execute_on_mongod( + ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.insertOne({number: 1})" + ) + assert result.succeeded, "Failed to insert value with LDAP client" + + result = await execute_on_mongod( + ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.findOne({number: 1})" + ) + assert result.succeeded, "Failed to read value with LDAP client" + + result = await execute_on_mongod( + ops_test, + CONFIG_SERVER_APP_NAME, + substrate, + uri, + f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", + ) + assert result.succeeded, "Failed to read value with LDAP client" diff --git a/tests/integration/mongos/release/test_release.py b/tests/integration/mongos/release/test_release.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/spread/release/mongodb/lxd/test_release.py/task.yaml b/tests/spread/release/mongodb/lxd/test_release.py/task.yaml new file mode 100644 index 0000000000..535602590c --- /dev/null +++ b/tests/spread/release/mongodb/lxd/test_release.py/task.yaml @@ -0,0 +1,8 @@ +summary: test_release.py +environment: + TEST_MODULE: test_release.py +execute: | + tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" +systems: + - ubuntu-22.04 + - ubuntu-22.04-arm diff --git a/tests/spread/release/mongodb/microk8s/test_release.py/task.yaml b/tests/spread/release/mongodb/microk8s/test_release.py/task.yaml new file mode 100644 index 0000000000..73d9dedc27 --- /dev/null +++ b/tests/spread/release/mongodb/microk8s/test_release.py/task.yaml @@ -0,0 +1,8 @@ +summary: test_release.py +environment: + TEST_MODULE: test_release.py +execute: | + tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" +systems: + - ubuntu-22.04 + - ubuntu-22.04-arm diff --git a/tests/spread/release/mongos/lxd/test_release.py/task.yaml b/tests/spread/release/mongos/lxd/test_release.py/task.yaml new file mode 100644 index 0000000000..ab36d1bfc1 --- /dev/null +++ b/tests/spread/release/mongos/lxd/test_release.py/task.yaml @@ -0,0 +1,8 @@ +summary: test_release.py +environment: + TEST_MODULE: test_release.py +execute: | + tox run -e integration -- "tests/integration/mongos/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" +systems: + - ubuntu-22.04 + - ubuntu-22.04-arm diff --git a/tests/spread/release/mongos/microk8s/test_release.py/task.yaml b/tests/spread/release/mongos/microk8s/test_release.py/task.yaml new file mode 100644 index 0000000000..6da276a817 --- /dev/null +++ b/tests/spread/release/mongos/microk8s/test_release.py/task.yaml @@ -0,0 +1,8 @@ +summary: test_release.py +environment: + TEST_MODULE: test_release.py +execute: | + tox run -e integration -- "tests/integration/mongos/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" +systems: + - ubuntu-22.04 + - ubuntu-22.04-arm From d9e78064ba7acf3fa65792dac644e77e23133f07 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 11:18:33 +0200 Subject: [PATCH 02/35] fix: release testing basics --- spread.yaml | 24 +++++ tests/conftest.py | 12 +++ .../continuous_write_charm/actions.yaml | 24 +++++ .../continuous_write_charm/src/charm.py | 98 +++++++++++++++++++ .../src/continuous_reads.py | 2 +- tests/integration/conftest.py | 25 +++++ tests/integration/helpers/common.py | 40 +++++++- tests/integration/helpers/ldap.py | 9 +- 8 files changed, 228 insertions(+), 6 deletions(-) diff --git a/spread.yaml b/spread.yaml index 7ac776624a..22b4452bf1 100644 --- a/spread.yaml +++ b/spread.yaml @@ -134,6 +134,30 @@ suites: summary: Spread tests for Mongos VM tests/spread/mongos/microk8s/: summary: Spread tests for Mongos Kubernetes + tests/spread/release/mongodb/lxd: + summary: Spread mongodb release tests for VM + manual: True + environment: + MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" + MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" + tests/spread/release/mongos/lxd/: + summary: Spread mongos release tests for VM + manual: True + environment: + MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" + MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" + tests/spread/release/mongodb/microk8s: + summary: Spread mongodb release tests for Kubernetes + manual: True + environment: + MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" + MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" + tests/spread/release/mongos/microk8s/: + summary: Spread mongos release tests for Kubernetse + manual: True + environment: + MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" + MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" path: /root/spread_project diff --git a/tests/conftest.py b/tests/conftest.py index 720e5dbd75..ec1de87038 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,18 @@ def pytest_addoption(parser: Parser): choices=("lxd", "microk8s"), default="lxd", ) + parser.addoption( + "--mongodb-revision", + action="store", + help="MongoDB revisions for the said substrate in the shape of a json dictionary", + default="{}", + ) + parser.addoption( + "--mongos-revision", + action="store", + help="MongoDB revisions for the said substrate in the shape of a json dictionary", + default="{}", + ) def pytest_configure(config): diff --git a/tests/integration/applications/continuous_write_charm/actions.yaml b/tests/integration/applications/continuous_write_charm/actions.yaml index e7484c8692..692b79b715 100644 --- a/tests/integration/applications/continuous_write_charm/actions.yaml +++ b/tests/integration/applications/continuous_write_charm/actions.yaml @@ -36,3 +36,27 @@ stop-continuous-writes: type: string description: name of the collection to write to default: continuous_writes_collection + +start-continuous-reads: + description: Start continuous reads. + params: + db-name: + type: string + description: name of the database to read from + default: continuous_writes_database + collection-name: + type: string + description: name of the collection to read from + default: continuous_writes_collection + +stop-continuous-reads: + description: Stop continuous reads. + params: + db-name: + type: string + description: name of the database to read from + default: continuous_writes_database + collection-name: + type: string + description: name of the collection to read from + default: continuous_writes_collection diff --git a/tests/integration/applications/continuous_write_charm/src/charm.py b/tests/integration/applications/continuous_write_charm/src/charm.py index a1837a9c45..42c6c5a334 100755 --- a/tests/integration/applications/continuous_write_charm/src/charm.py +++ b/tests/integration/applications/continuous_write_charm/src/charm.py @@ -8,6 +8,7 @@ high availability of the MongoDB charm. """ +import json import logging import os import signal @@ -32,6 +33,7 @@ PEER = "application-peers" PROC_PID_KEY = "proc-pid" LAST_WRITTEN_FILE = "last_written_value" +N_READ_FILE = "n_read_value" CA_PATH = Path("/tmp/ca.crt") @@ -55,6 +57,13 @@ def __init__(self, *args): self.on.stop_continuous_writes_action, self._on_stop_continuous_writes_action ) + self.framework.observe( + self.on.start_continuous_reads_action, self._on_start_continuous_reads_action + ) + self.framework.observe( + self.on.stop_continuous_reads_action, self._on_stop_continuous_reads_action + ) + # Database related events self.database = DatabaseRequires(self, "mongodb", self.database_name) # Database related events @@ -218,14 +227,83 @@ def _stop_continuous_writes(self, db_name: str, collection_name: str) -> int | N logger.exception("Unable to query the database", exc_info=e) return -1 + def _start_continuous_reads( + self, db_name: str, collection_name: str + ) -> None: + """Start continuous writes to the MongoDB cluster.""" + if not self._database_config: + logger.warning("No database configured.") + return + + logger.info(f"Running start continuous reads with {db_name=} and {collection_name=}") + self._stop_continuous_reads(db_name, collection_name) + + uris: str = self._database_config.get("uris", "") + # Run continuous writes in the background + proc = subprocess.Popen( + [ + sys.executable, + "src/continuous_reads.py", + uris, + db_name, + collection_name, + ] + ) + + # Store the continuous writes process id in stored state to be able to stop it later + self.app_peer_data[self.read_proc_id_key(db_name, collection_name)] = str(proc.pid) + + def _stop_continuous_reads(self, db_name: str, collection_name: str) -> tuple[int | None, list[str]]: + """Stop continuous reads to the MongoDB cluster and return the number of successful reads.""" + if not self._database_config: + logger.warning("No database configured.") + return None, [] + + if not self.app_peer_data.get(self.read_proc_id_key(db_name, collection_name)): + return None, [] + + # Send a SIGTERM to the process and wait for the process to exit + try: + os.kill( + int(self.app_peer_data[self.read_proc_id_key(db_name, collection_name)]), signal.SIGTERM + ) + except ProcessLookupError: + logger.info( + f"Process {self.read_proc_id_key(db_name, collection_name)} was killed already (or never existed)" + ) + + del self.app_peer_data[self.read_proc_id_key(db_name, collection_name)] + + # read the last written_value + try: + for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(5)): + with attempt, open(self.n_read_filename(db_name, collection_name)) as fd: + data = json.load(fd) + number_of_reads = int(data.get("reads", -1)) + failed_reads = data.get("failed_reads", []) + os.remove(self.n_read_filename(db_name, collection_name)) + logger.info(f"Read {number_of_reads} times // Failed {len(failed_reads)}.") + return number_of_reads, failed_reads + except RetryError as e: + logger.exception("Unable to query the database", exc_info=e) + return -1, [] + def proc_id_key(self, db_name: str, collection_name: str) -> str: """Returns a process id key for the continuous writes process to a given db and coll.""" return f"{PROC_PID_KEY}-{db_name}-{collection_name}" + def read_proc_id_key(self, db_name: str, collection_name: str) -> str: + """Returns a process id key for the continuous reads process to a given db and coll.""" + return f"read-{PROC_PID_KEY}-{db_name}-{collection_name}" + def last_written_filename(self, db_name: str, collection_name: str) -> str: """Returns the filename for the written data for a given db and coll.""" return f"{LAST_WRITTEN_FILE}-{db_name}-{collection_name}" + def n_read_filename(self, db_name: str, collection_name: str) -> str: + """Returns the filename for the read data for a given db and coll.""" + return f"{N_READ_FILE}-{db_name}-{collection_name}.json" + # ============== # Handlers # ============== @@ -278,6 +356,26 @@ def _on_stop_continuous_writes_action(self, event: ActionEvent) -> None: event.set_results({"writes": writes or -1}) return None + def _on_start_continuous_reads_action(self, event) -> None: + """Handle the start continuous reads action event.""" + if not self._database_config: + return + + db_name = event.params.get("db-name") or self.database_name + collection_name = event.params.get("collection-name") or COLLECTION_NAME + self._start_continuous_reads(1, db_name, collection_name) + + def _on_stop_continuous_reads_action(self, event: ActionEvent) -> None: + """Handle the stop continuous reads action event.""" + if not self._database_config: + return event.set_results({"reads": -1}) + + db_name = event.params.get("db-name") or self.database_name + collection_name = event.params.get("collection-name") or COLLECTION_NAME + reads, failed_reads = self._stop_continuous_reads(db_name, collection_name) + event.set_results({"reads": reads or -1, "failed_reads": failed_reads}) + return None + def _on_database_created(self, event: DatabaseCreatedEvent) -> None: """Handle the database created event.""" if event.tls == "True": diff --git a/tests/integration/applications/continuous_write_charm/src/continuous_reads.py b/tests/integration/applications/continuous_write_charm/src/continuous_reads.py index 02bb23f273..ac3b5c8d25 100644 --- a/tests/integration/applications/continuous_write_charm/src/continuous_reads.py +++ b/tests/integration/applications/continuous_write_charm/src/continuous_reads.py @@ -24,7 +24,7 @@ def sigterm_handler(_signo, _stack_frame): run = False def n_read_filename(db_name: str, coll_name: str) -> str: - return f"n_read_value-{db_name}-{coll_name}" + return f"n_read_value-{db_name}-{coll_name}.json" def continous_reads( diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index cf8094c9a4..c404571f8c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -57,6 +57,20 @@ def architecture() -> str: return _architecture +@pytest.fixture(scope="session") +def mongodb_revision(request: pytest.FixtureRequest, arch: str): + """Revision for the correct arch.""" + data = json.loads(request.config.option.mongodb_revision) + return data[arch] + + +@pytest.fixture(scope="session") +def mongos_revision(request: pytest.FixtureRequest, arch: str): + """Revision for the correct arch.""" + data = json.loads(request.config.option.mongos_revision) + return data[arch] + + @pytest.fixture def application_path(architecture: str) -> str: """The test application path.""" @@ -121,6 +135,17 @@ def mongos_resource(mongos_metadata, substrate) -> dict[str, Any]: return {} +pytest.fixture + + +def mongodb_charm_name(substrate: Substrate) -> str: + return "mongodb" if substrate == "lxd" else "mongodb-k8s" + + +def mongos_charm_name(substrate: Substrate) -> str: + return "mongos" if substrate == "lxd" else "mongos-k8s" + + @pytest.fixture async def continuous_writes_to_db(ops_test: OpsTest, application_path: str): """Continuously writget_app_name the duration of the test.""" diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index a9dc77a389..275fb5e5c6 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -49,6 +49,8 @@ CONTINUOUS_WRITE_APPLICATION = "continuous-write" +CONTINUOUS_WRITE_APPLICATION_BIS = "continuous-write-bis" +READER_APPLICATION = "reader-application" # Keep in sync with tests/integration/applications/continuous_write_charm/src/charm.py DEFAULT_DATABASE_NAME = "continuous_writes_database" DEFAULT_COLLECTION_NAME = "continuous_writes_collection" @@ -100,10 +102,11 @@ async def deploy_charm( ops_test: OpsTest, charm: str, substrate: Substrate, - mongod_resource: dict[str, str], app_name: str, num_units: int = 3, + mongod_resource: dict[str, str] | None = None, channel: str | None = None, + revision: int | None = None, config: dict[str, str] | None = None, subordinate: bool = False, storage: dict[str, str] | None = None, @@ -115,8 +118,9 @@ async def deploy_charm( series = series or "noble" await ops_test.model.deploy( charm, - resources=(mongod_resource if not channel else None), application_name=app_name, + revision=revision, + resources=(mongod_resource if not channel else None), num_units=0 if subordinate else num_units, series=series, trust=True, @@ -127,8 +131,9 @@ async def deploy_charm( else: await ops_test.model.deploy( charm, - num_units=0 if subordinate else num_units, application_name=app_name, + num_units=0 if subordinate else num_units, + revision=revision, config=config, channel=channel, storage=storage, @@ -1033,6 +1038,20 @@ async def start_continous_writes( await start_writes_action.wait() +async def start_continuous_reads( + ops_test: OpsTest, + client_app_name: str, + db_name: str = DEFAULT_DATABASE_NAME, + coll_name: str = DEFAULT_COLLECTION_NAME, +): + """Helper function to run the `start-continuous-write` action on the continuous write app.""" + application_unit = ops_test.model.applications[client_app_name].units[0] + start_writes_action = await application_unit.run_action( + "start-continuous-reads", **{"db-name": db_name, "collection-name": coll_name} + ) + await start_writes_action.wait() + + async def stop_continous_writes( ops_test: OpsTest, client_app_name: str, @@ -1051,6 +1070,21 @@ async def stop_continous_writes( return int(stop_writes_action.results["writes"]) +async def stop_continuous_reads( + ops_test: OpsTest, + client_app_name: str, + db_name: str = DEFAULT_DATABASE_NAME, + coll_name: str = DEFAULT_COLLECTION_NAME, +) -> tuple[int, list[str]]: + """Helper function to run the `start-continuous-write` action on the continuous write app.""" + application_unit = ops_test.model.applications[client_app_name].units[0] + stop_writes_action = await application_unit.run_action( + "stop-continuous-reads", **{"db-name": db_name, "collection-name": coll_name} + ) + await stop_writes_action.wait() + return int(stop_writes_action.results["reads"]), stop_writes_action.results["failed_reads"] + + async def clear_continous_writes( ops_test: OpsTest, client_app_name: str, diff --git a/tests/integration/helpers/ldap.py b/tests/integration/helpers/ldap.py index 3216aedcd8..fd222c0e2a 100644 --- a/tests/integration/helpers/ldap.py +++ b/tests/integration/helpers/ldap.py @@ -153,7 +153,12 @@ async def teardown_offers(ops_test: OpsTest, kubernetes_model: Model) -> None: async def create_mongodb_user_roles( - ops_test: OpsTest, substrate: Substrate, app_name: str, role_name: str, mongos: bool = False + ops_test: OpsTest, + substrate: Substrate, + app_name: str, + role_name: str, + mongos: bool = False, + db: str = "superdb", ) -> None: """Creates the roles for mongodb with the provided role_name.""" uri = await generate_mongodb_client(ops_test, substrate, app_name, mongos=mongos) @@ -166,7 +171,7 @@ async def create_mongodb_user_roles( "db.createRole({" f" role: '{role_name}'," " privileges: []," - " roles: [{'db': 'superdb', 'role': 'readWrite'}, {'db': 'superdb', 'role': 'enableSharding'}]" + f" roles: [{'db': '{db}', 'role': 'readWrite'}, {'db': '{db}', 'role': 'enableSharding'}]" "})", ) assert result.succeeded, "Failed to create role" From ec640db70dcfc8173d9a26072833b883ef709683 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 11:24:16 +0200 Subject: [PATCH 03/35] chore: bump runners --- spread.yaml | 4 ++++ tests/spread/mongodb/lxd/test_ldap.py/task.yaml | 4 ++-- tests/spread/mongodb/lxd/test_sharding_ldap.py/task.yaml | 4 ++-- tests/spread/mongodb/microk8s/test_ldap.py/task.yaml | 4 ++-- .../mongodb/microk8s/test_sharding_ldap.py/task.yaml | 4 ++-- tests/spread/mongos/lxd/test_ldap.py/task.yaml | 4 ++-- tests/spread/mongos/microk8s/test_ldap.py/task.yaml | 4 ++-- .../release/{mongodb => }/lxd/test_release.py/task.yaml | 3 +-- .../spread/release/lxd/test_sharding_release.py/task.yaml | 7 +++++++ .../{mongodb => }/microk8s/test_release.py/task.yaml | 3 +-- .../release/microk8s/test_sharding_release.py/task.yaml | 7 +++++++ tests/spread/release/mongos/lxd/test_release.py/task.yaml | 8 -------- .../release/mongos/microk8s/test_release.py/task.yaml | 8 -------- 13 files changed, 32 insertions(+), 32 deletions(-) rename tests/spread/release/{mongodb => }/lxd/test_release.py/task.yaml (86%) create mode 100644 tests/spread/release/lxd/test_sharding_release.py/task.yaml rename tests/spread/release/{mongodb => }/microk8s/test_release.py/task.yaml (87%) create mode 100644 tests/spread/release/microk8s/test_sharding_release.py/task.yaml delete mode 100644 tests/spread/release/mongos/lxd/test_release.py/task.yaml delete mode 100644 tests/spread/release/mongos/microk8s/test_release.py/task.yaml diff --git a/spread.yaml b/spread.yaml index 22b4452bf1..0e5ec53ab9 100644 --- a/spread.yaml +++ b/spread.yaml @@ -124,6 +124,10 @@ backends: username: ubuntu - self-hosted-linux-arm64-noble-medium: username: ubuntu + - self-hosted-linux-amd64-noble-large: + username: ubuntu + - self-hosted-linux-arm64-noble-large: + username: ubuntu suites: tests/spread/mongodb/lxd/: diff --git a/tests/spread/mongodb/lxd/test_ldap.py/task.yaml b/tests/spread/mongodb/lxd/test_ldap.py/task.yaml index 93322172a0..8605851e5e 100644 --- a/tests/spread/mongodb/lxd/test_ldap.py/task.yaml +++ b/tests/spread/mongodb/lxd/test_ldap.py/task.yaml @@ -6,6 +6,6 @@ execute: | artifacts: - allure-results systems: - - ubuntu-22.04 + - self-hosted-linux-amd64-noble-large # TODO: Re-enable this when glauth charm supports arm64 - #- self-hosted-linux-arm64-noble-medium + #- self-hosted-linux-arm64-noble-large diff --git a/tests/spread/mongodb/lxd/test_sharding_ldap.py/task.yaml b/tests/spread/mongodb/lxd/test_sharding_ldap.py/task.yaml index cf7332f5dc..c331d295f4 100644 --- a/tests/spread/mongodb/lxd/test_sharding_ldap.py/task.yaml +++ b/tests/spread/mongodb/lxd/test_sharding_ldap.py/task.yaml @@ -6,6 +6,6 @@ execute: | artifacts: - allure-results systems: - - self-hosted-linux-amd64-noble-medium + - self-hosted-linux-amd64-noble-large # TODO: Re-enable this when glauth charm supports arm64 - #- self-hosted-linux-arm64-noble-medium + #- self-hosted-linux-arm64-noble-large diff --git a/tests/spread/mongodb/microk8s/test_ldap.py/task.yaml b/tests/spread/mongodb/microk8s/test_ldap.py/task.yaml index 917e218c8d..9f139741ad 100644 --- a/tests/spread/mongodb/microk8s/test_ldap.py/task.yaml +++ b/tests/spread/mongodb/microk8s/test_ldap.py/task.yaml @@ -6,6 +6,6 @@ execute: | artifacts: - allure-results systems: - - ubuntu-22.04 + - self-hosted-linux-arm64-noble-large # TODO: Re-enable this when glauth charm supports arm64 - #- self-hosted-linux-arm64-noble-medium + #- self-hosted-linux-arm64-noble-large diff --git a/tests/spread/mongodb/microk8s/test_sharding_ldap.py/task.yaml b/tests/spread/mongodb/microk8s/test_sharding_ldap.py/task.yaml index 127a5647a1..792c4950f1 100644 --- a/tests/spread/mongodb/microk8s/test_sharding_ldap.py/task.yaml +++ b/tests/spread/mongodb/microk8s/test_sharding_ldap.py/task.yaml @@ -6,6 +6,6 @@ execute: | artifacts: - allure-results systems: - - self-hosted-linux-amd64-noble-medium + - self-hosted-linux-amd64-noble-large # TODO: Re-enable this when glauth charm supports arm64 - #- self-hosted-linux-arm64-noble-medium + #- self-hosted-linux-arm64-noble-large diff --git a/tests/spread/mongos/lxd/test_ldap.py/task.yaml b/tests/spread/mongos/lxd/test_ldap.py/task.yaml index cbbbc0479f..c45cc8ae3d 100644 --- a/tests/spread/mongos/lxd/test_ldap.py/task.yaml +++ b/tests/spread/mongos/lxd/test_ldap.py/task.yaml @@ -6,6 +6,6 @@ execute: | artifacts: - allure-results systems: - - ubuntu-22.04 + - self-hosted-linux-arm64-noble-large # TODO: Re-enable this when glauth charm supports arm64 - #- self-hosted-linux-arm64-noble-medium + - self-hosted-linux-arm64-noble-large diff --git a/tests/spread/mongos/microk8s/test_ldap.py/task.yaml b/tests/spread/mongos/microk8s/test_ldap.py/task.yaml index c5c663ed23..120263acaf 100644 --- a/tests/spread/mongos/microk8s/test_ldap.py/task.yaml +++ b/tests/spread/mongos/microk8s/test_ldap.py/task.yaml @@ -6,6 +6,6 @@ execute: | artifacts: - allure-results systems: - - ubuntu-22.04 + - self-hosted-linux-arm64-noble-large # TODO: Re-enable this when glauth charm supports arm64 - #- self-hosted-linux-arm64-noble-medium + #- self-hosted-linux-arm64-noble-large diff --git a/tests/spread/release/mongodb/lxd/test_release.py/task.yaml b/tests/spread/release/lxd/test_release.py/task.yaml similarity index 86% rename from tests/spread/release/mongodb/lxd/test_release.py/task.yaml rename to tests/spread/release/lxd/test_release.py/task.yaml index 535602590c..9ea6f22f70 100644 --- a/tests/spread/release/mongodb/lxd/test_release.py/task.yaml +++ b/tests/spread/release/lxd/test_release.py/task.yaml @@ -4,5 +4,4 @@ environment: execute: | tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - - ubuntu-22.04 - - ubuntu-22.04-arm + - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/lxd/test_sharding_release.py/task.yaml b/tests/spread/release/lxd/test_sharding_release.py/task.yaml new file mode 100644 index 0000000000..201383dc2a --- /dev/null +++ b/tests/spread/release/lxd/test_sharding_release.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_sharding_release.py +environment: + TEST_MODULE: test_sharding_release.py +execute: | + tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" +systems: + - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/mongodb/microk8s/test_release.py/task.yaml b/tests/spread/release/microk8s/test_release.py/task.yaml similarity index 87% rename from tests/spread/release/mongodb/microk8s/test_release.py/task.yaml rename to tests/spread/release/microk8s/test_release.py/task.yaml index 73d9dedc27..52bf5195de 100644 --- a/tests/spread/release/mongodb/microk8s/test_release.py/task.yaml +++ b/tests/spread/release/microk8s/test_release.py/task.yaml @@ -4,5 +4,4 @@ environment: execute: | tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - - ubuntu-22.04 - - ubuntu-22.04-arm + - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/microk8s/test_sharding_release.py/task.yaml b/tests/spread/release/microk8s/test_sharding_release.py/task.yaml new file mode 100644 index 0000000000..aba55fd3a2 --- /dev/null +++ b/tests/spread/release/microk8s/test_sharding_release.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_sharding_release.py +environment: + TEST_MODULE: test_sharding_release.py +execute: | + tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" +systems: + - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/mongos/lxd/test_release.py/task.yaml b/tests/spread/release/mongos/lxd/test_release.py/task.yaml deleted file mode 100644 index ab36d1bfc1..0000000000 --- a/tests/spread/release/mongos/lxd/test_release.py/task.yaml +++ /dev/null @@ -1,8 +0,0 @@ -summary: test_release.py -environment: - TEST_MODULE: test_release.py -execute: | - tox run -e integration -- "tests/integration/mongos/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" -systems: - - ubuntu-22.04 - - ubuntu-22.04-arm diff --git a/tests/spread/release/mongos/microk8s/test_release.py/task.yaml b/tests/spread/release/mongos/microk8s/test_release.py/task.yaml deleted file mode 100644 index 6da276a817..0000000000 --- a/tests/spread/release/mongos/microk8s/test_release.py/task.yaml +++ /dev/null @@ -1,8 +0,0 @@ -summary: test_release.py -environment: - TEST_MODULE: test_release.py -execute: | - tox run -e integration -- "tests/integration/mongos/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" -systems: - - ubuntu-22.04 - - ubuntu-22.04-arm From 2bce7df3b8e683a14b59e81d629a4c5d509dc681 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 12:05:58 +0200 Subject: [PATCH 04/35] fix: release-testing --- .github/workflows/release_tests.yaml | 59 ++++----- spread.yaml | 16 +-- tests/integration/conftest.py | 57 ++++++++ tests/integration/mongodb/conftest.py | 57 -------- tests/integration/release/__init__.py | 0 .../{mongodb => }/release/conftest.py | 14 +- .../{mongodb => }/release/test_release.py | 63 ++++++++- .../release/test_sharding_release.py | 125 ++++++++++++++++-- .../release/lxd/test_release.py/task.yaml | 2 +- .../lxd/test_sharding_release.py/task.yaml | 2 +- .../microk8s/test_release.py/task.yaml | 2 +- .../test_sharding_release.py/task.yaml | 2 +- 12 files changed, 269 insertions(+), 130 deletions(-) create mode 100644 tests/integration/release/__init__.py rename tests/integration/{mongodb => }/release/conftest.py (70%) rename tests/integration/{mongodb => }/release/test_release.py (82%) rename tests/integration/{mongodb => }/release/test_sharding_release.py (78%) diff --git a/.github/workflows/release_tests.yaml b/.github/workflows/release_tests.yaml index 71895ce873..f13f89b0f4 100644 --- a/.github/workflows/release_tests.yaml +++ b/.github/workflows/release_tests.yaml @@ -1,60 +1,42 @@ on: workflow_call: inputs: - mongodb-revision: + mongodb-revisions: description: | MongoDB revisions to target (ex: '{"amd64": "161", "arm64": "162"}' required: true type: string - mongos-revision: + mongos-revisions: description: | Mongos revisions to target (ex: '{"amd64": "161", "arm64": "162"}' required: true type: string - charm: - description: | - The charm type we're targetting. - - One of mongodb or mongos or "..." (default, both charms). - type: string - required: false - default: "..." backend: description: | Backend to target. - Either lxd or microk8s, or "..." (default, all backends). - required: false + Either lxd or microk8s. + required: true type: string - default: "..." workflow_dispatch: inputs: - mongodb-revision: + mongodb-revisions: description: | - MongoDB revision to target + MongoDB revisions to target (ex: '{"amd64": "161", "arm64": "162"}' required: true type: number - charm: + mongos-revisions: description: | - The charm type we're targetting. - - One of mongodb or mongos or "..." (default, both charms). - type: string - required: false - default: "..." - mongos-revision: - description: | - MongoDB revision to target + Mongos revisions to target (ex: '{"amd64": "161", "arm64": "162"}' required: true type: number backend: description: | Backend to target. - Either lxd or microk8s, or "..." (default, all backends). - required: false + Either lxd or microk8s + required: true type: string - default: "..." jobs: collect-release-tests: @@ -82,8 +64,8 @@ jobs: import os import subprocess backend = "${{ inputs.backend }}" - charm_type = "${{ inputs.charm }} - pattern = f"github-ci:...:tests/spread/release/{charm_type}/{backend}/" + pattern = f"github-ci:...:tests/spread/release/{backend}/" + mongodb_revisions = json.loads("${{ inputs.mongodb-revisions }}") spread_jobs = ( subprocess.run( [pathlib.Path.home() / "go/bin/spread", "-list", pattern], @@ -107,13 +89,18 @@ jobs: architecture = "amd64" # Example: "test_charm.py | amd64" name = f"{task} | {architecture}" + + mongodb_revision = mongodb_revisions[architecture] + mongos_revision = mongos_revisions[architecture] # ":" character not valid in GitHub Actions artifact - name_in_artifact = f"{task.replace('/', '-')}-{architecture}" + name_in_artifact = f"{task.replace('/', '-')}-{architecture}-mongodb-{mongodb_revision}-mongos-{mongos_revision}" jobs.append({ "spread_job": job, "name": name, "name_in_artifact": name_in_artifact, "runner": runner, + "mongodb_revision": mongodb_revision, + "mongos_revision": mongos_revision, }) output = f"jobs={json.dumps(jobs)}" print(output) @@ -126,10 +113,10 @@ jobs: strategy: fail-fast: false matrix: - job: ${{ fromJSON(needs.collect-integration-tests.outputs.jobs) }} + job: ${{ fromJSON(needs.collect-release-tests.outputs.jobs) }} name: ${{ matrix.job.name }} needs: - - collect-integration-tests + - collect-release-tests runs-on: ${{ matrix.job.runner }} timeout-minutes: 230 # Sum of steps `timeout-minutes` + 5 steps: @@ -160,8 +147,8 @@ jobs: # https://github.com/canonical/charmcraft/issues/2130 fixed run: ~/go/bin/spread -vv -artifacts=artifacts '${{ matrix.job.spread_job }}' env: - MONGODB_REVISION: ${{ inputs.mongodb-revision }} - MONGOS_REVISION: ${{ inputs.mongos-revision }} + MONGODB_REVISION: ${{ matrix.job.mongodb_revision }} + MONGOS_REVISION: ${{ matrix.job.mongos_revision }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }} GCP_ACCESS_KEY: ${{ secrets.GCP_ACCESS_KEY }} @@ -200,7 +187,7 @@ jobs: if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }} uses: actions/upload-artifact@v4 with: - name: logs-integration-test-${{ matrix.job.name_in_artifact }} + name: logs-release-test-${{ matrix.job.name_in_artifact }} path: ~/logs/ if-no-files-found: error - name: Disk usage diff --git a/spread.yaml b/spread.yaml index 0e5ec53ab9..37502a7aec 100644 --- a/spread.yaml +++ b/spread.yaml @@ -138,30 +138,18 @@ suites: summary: Spread tests for Mongos VM tests/spread/mongos/microk8s/: summary: Spread tests for Mongos Kubernetes - tests/spread/release/mongodb/lxd: + tests/spread/release/lxd: summary: Spread mongodb release tests for VM manual: True environment: MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" - tests/spread/release/mongos/lxd/: - summary: Spread mongos release tests for VM - manual: True - environment: - MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" - MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" - tests/spread/release/mongodb/microk8s: + tests/spread/release/microk8s: summary: Spread mongodb release tests for Kubernetes manual: True environment: MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" - tests/spread/release/mongos/microk8s/: - summary: Spread mongos release tests for Kubernetse - manual: True - environment: - MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" - MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" path: /root/spread_project diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index c404571f8c..47aae6d2d6 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -9,7 +9,9 @@ import shutil import subprocess import time +import uuid import zipfile +from collections.abc import Generator from logging import getLogger from pathlib import Path from platform import machine @@ -24,6 +26,7 @@ from yaml import safe_load from .helpers.architecture import architecture as _architecture +from .helpers.backups import CloudConfigs, CloudConfiguration from .helpers.common import ( CONTINUOUS_WRITE_APPLICATION, MONGOS_PORT, @@ -499,3 +502,57 @@ def s3_bucket(storage_credentials, storage_config) -> None: s3 = session.resource("s3", endpoint_url=storage_config["endpoint"], verify="cert.pem") bucket = s3.Bucket(storage_config["bucket"]) yield bucket + + +@pytest.fixture(scope="session") +def cloud_configs_aws(substrate: Substrate) -> CloudConfiguration: + path = "mongodb-vm" if substrate == "lxd" else "mongodb-k8s" + configs: dict[str, str] = { + "endpoint": "https://s3.amazonaws.com", + "bucket": "data-charms-testing", + "path": f"{path}/{uuid.uuid4()}", + "region": "us-east-1", + } + credentials: dict[str, str] = { + "access-key": os.environ["AWS_ACCESS_KEY"], + "secret-key": os.environ["AWS_SECRET_KEY"], + } + return configs, credentials + + +@pytest.fixture(scope="session") +def cloud_configs_gcp(substrate: Substrate) -> CloudConfiguration: + path = "mongodb-vm" if substrate == "lxd" else "mongodb-k8s" + configs: dict[str, str] = { + "bucket": "data-charms-testing", + "endpoint": "https://storage.googleapis.com", + "region": "", + "path": f"{path}/{uuid.uuid4()}", + } + credentials: dict[str, str] = { + "access-key": os.environ["GCP_ACCESS_KEY"], + "secret-key": os.environ["GCP_SECRET_KEY"], + } + return configs, credentials + + +@pytest.fixture(scope="session") +def cloud_configs_gcs(substrate: Substrate) -> CloudConfiguration: + path = "mongodb-vm" if substrate == "lxd" else "mongodb-k8s" + configs: dict[str, str] = { + "bucket": "data-charms-testing", + "path": f"{path}/{uuid.uuid4()}", + } + credentials: dict[str, str] = { + "secret-key": os.environ["GCS_SERVICE_ACCOUNT"], + } + return configs, credentials + + +@pytest.fixture(scope="session") +def cloud_configs( + cloud_configs_gcp: CloudConfiguration, + cloud_configs_aws: CloudConfiguration, + cloud_configs_gcs: CloudConfiguration, +) -> Generator[CloudConfigs]: + yield {"AWS": cloud_configs_aws, "GCP": cloud_configs_gcp, "GCS": cloud_configs_gcs} diff --git a/tests/integration/mongodb/conftest.py b/tests/integration/mongodb/conftest.py index 902f642630..6260da32cb 100644 --- a/tests/integration/mongodb/conftest.py +++ b/tests/integration/mongodb/conftest.py @@ -1,7 +1,5 @@ # Copyright 2025 Canonical Ltd. # See LICENSE file for licensing details. -import os -import uuid from collections.abc import Generator from pathlib import Path from typing import Any @@ -9,7 +7,6 @@ import pytest from pytest_operator.plugin import OpsTest -from ..helpers.backups import CloudConfigs, CloudConfiguration from ..helpers.common import get_app_name from ..helpers.ha import deploy_chaos_mesh, destroy_chaos_mesh, update_restart_delay from ..helpers.types import Substrate @@ -33,60 +30,6 @@ def chaos_mesh(ops_test: OpsTest, substrate: Substrate) -> Generator[None, Any, yield -@pytest.fixture(scope="session") -def cloud_configs_aws(substrate: Substrate) -> CloudConfiguration: - path = "mongodb-vm" if substrate == "lxd" else "mongodb-k8s" - configs: dict[str, str] = { - "endpoint": "https://s3.amazonaws.com", - "bucket": "data-charms-testing", - "path": f"{path}/{uuid.uuid4()}", - "region": "us-east-1", - } - credentials: dict[str, str] = { - "access-key": os.environ["AWS_ACCESS_KEY"], - "secret-key": os.environ["AWS_SECRET_KEY"], - } - return configs, credentials - - -@pytest.fixture(scope="session") -def cloud_configs_gcp(substrate: Substrate) -> CloudConfiguration: - path = "mongodb-vm" if substrate == "lxd" else "mongodb-k8s" - configs: dict[str, str] = { - "bucket": "data-charms-testing", - "endpoint": "https://storage.googleapis.com", - "region": "", - "path": f"{path}/{uuid.uuid4()}", - } - credentials: dict[str, str] = { - "access-key": os.environ["GCP_ACCESS_KEY"], - "secret-key": os.environ["GCP_SECRET_KEY"], - } - return configs, credentials - - -@pytest.fixture(scope="session") -def cloud_configs_gcs(substrate: Substrate) -> CloudConfiguration: - path = "mongodb-vm" if substrate == "lxd" else "mongodb-k8s" - configs: dict[str, str] = { - "bucket": "data-charms-testing", - "path": f"{path}/{uuid.uuid4()}", - } - credentials: dict[str, str] = { - "secret-key": os.environ["GCS_SERVICE_ACCOUNT"], - } - return configs, credentials - - -@pytest.fixture(scope="session") -def cloud_configs( - cloud_configs_gcp: CloudConfiguration, - cloud_configs_aws: CloudConfiguration, - cloud_configs_gcs: CloudConfiguration, -) -> Generator[CloudConfigs]: - yield {"AWS": cloud_configs_aws, "GCP": cloud_configs_gcp, "GCS": cloud_configs_gcs} - - @pytest.fixture() async def reset_restart_delay(ops_test: OpsTest, substrate: Substrate, tmp_path: Path): """Resets service file delay on all units.""" diff --git a/tests/integration/release/__init__.py b/tests/integration/release/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/mongodb/release/conftest.py b/tests/integration/release/conftest.py similarity index 70% rename from tests/integration/mongodb/release/conftest.py rename to tests/integration/release/conftest.py index 945b683a1c..cd85c4e570 100644 --- a/tests/integration/mongodb/release/conftest.py +++ b/tests/integration/release/conftest.py @@ -15,8 +15,20 @@ logger = getLogger(__name__) +@pytest.fixture +def mongodb_base_app_name(mongod_metadata: dict[str, Any]) -> str: + """Default application name for testing.""" + return mongod_metadata["name"] + + +@pytest.fixture +def mongos_base_app_name(mongos_metadata: dict[str, Any]) -> str: + """Default application name for testing.""" + return mongos_metadata["name"] + + @pytest.fixture(scope="module") -async def kubernetes_model(ops_test: OpsTest, architecture: str) -> AsyncGenerator[Model, Any]: +async def kubernetes_model(ops_test: OpsTest) -> AsyncGenerator[Model]: try: k8s_cloud = await ops_test.add_k8s(skip_storage=False) logger.warning(f"created cloud {k8s_cloud}") diff --git a/tests/integration/mongodb/release/test_release.py b/tests/integration/release/test_release.py similarity index 82% rename from tests/integration/mongodb/release/test_release.py rename to tests/integration/release/test_release.py index ba2715f3a6..e78543a5b2 100644 --- a/tests/integration/mongodb/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -6,6 +6,7 @@ from logging import getLogger import pytest +from juju.model import Model from pytest_operator.plugin import OpsTest from tenacity import RetryError, Retrying from tenacity.stop import stop_after_delay @@ -58,17 +59,23 @@ @pytest.mark.abort_on_fail -async def test_build_and_deploy( +async def test_deploy_apps( ops_test: OpsTest, mongodb_charm_name: str, application_path: str, substrate: Substrate, mongodb_revision: int, - base_app_name: str, - kubernetes_model: str, + mongodb_base_app_name: str, + kubernetes_model: Model, ): - """Build and deploy one unit of MongoDB.""" + """Deploy MongoDB with the right revision. + + This also deploys a data integrator, alongside a continuous write application, + a self-signed-certificates application, and LDAP with all it needs. + """ tls_config = {"ca-common-name": "MongoDB release CA"} + + assert ops_test.model # it is possible for users to provide their own cluster for testing. Hence check if there # is a pre-existing cluster. asyncio.gather( @@ -77,7 +84,7 @@ async def test_build_and_deploy( revision=mongodb_revision, charm=mongodb_charm_name, substrate=substrate, - app_name=base_app_name, + app_name=mongodb_base_app_name, num_units=len(UNIT_IDS), ), deploy_application( @@ -105,7 +112,9 @@ async def test_build_and_deploy( await apply_ldif(ops_test, kubernetes_model, "ldap_entries.ldif") await ops_test.model.wait_for_idle( - apps=[base_app_name, TLS_CERTIFICATES_APP_NAME], timeout=DEPLOYMENT_TIMEOUT, status="active" + apps=[mongodb_base_app_name, TLS_CERTIFICATES_APP_NAME], + timeout=DEPLOYMENT_TIMEOUT, + status="active", ) @@ -113,7 +122,8 @@ async def test_build_and_deploy( async def test_integrate_with_tls( ops_test: OpsTest, ): - """Tests that we can integrate with TLS without losing data.""" + """Tests that we can integrate with TLS, and then add a writer and start writing.""" + assert ops_test.model app_name = await get_app_name(ops_test) await integrate_apps_with_tls(ops_test, applications=[app_name]) @@ -126,6 +136,8 @@ async def test_integrate_with_tls( async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): + """Tests that we can integrate with LDAP without losing data.""" + assert ops_test.model app_name = await get_app_name(ops_test) await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{app_name}:ldap") @@ -146,6 +158,10 @@ async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): @pytest.mark.abort_on_fail async def test_integrate_second_client(ops_test: OpsTest, application_path: str): + """Tests that we can integrate with a second client, and we also start writing on that client. + + The client is a continuous write application. + """ app_name = await get_app_name(ops_test) await deploy_application( @@ -165,6 +181,10 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) @pytest.mark.abort_on_fail async def test_integrate_third_client(ops_test: OpsTest, application_path: str): + """Tests that we can integrate with a third client, which will only read data. + + The client is a continuous write application. + """ app_name = await get_app_name(ops_test) await deploy_application( @@ -185,6 +205,11 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): @pytest.mark.abort_on_fail async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs): + """Tests that we can integrate with S3 and create a backup. + + This test ensures that the backup is created and finished. + """ + assert ops_test.model app_name = await get_app_name(ops_test) # deploy the s3 integrator charm @@ -220,6 +245,14 @@ async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs) @pytest.mark.abort_on_fail async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): + """Tests that we can restore a backup. + + This test starts by stopping the writes applications, and counting the number of writes + ensuring that we have never lost any write until now. + Then it restores the backup, counts the number of writes, + and checks that it is lower than what we had, proving that the backup was restored successfully. + """ + assert ops_test.model app_name = await get_app_name(ops_test) first_reported_writes = await stop_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) @@ -259,6 +292,21 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): with ops_test.fast_forward("60s"): (await ops_test.model.wait_for_idle(apps=[app_name], status="active", idle_period=15),) + first_number_writes_after_restore = await count_writes( + ops_test, substrate, app_name, leader_unit + ) + second_number_writes_after_restore = await count_writes( + ops_test, + substrate, + app_name, + leader_unit, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + + assert first_number_writes_after_restore < first_number_writes + assert second_number_writes_after_restore < second_number_writes + @pytest.mark.abort_on_fail async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): @@ -300,6 +348,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): @pytest.mark.abort_on_fail async def test_valid_reads(ops_test: OpsTest): + """Checks the reads at the end of the tests.""" reads, failed_reads = await stop_continuous_reads( ops_test, READER_APPLICATION, diff --git a/tests/integration/mongodb/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py similarity index 78% rename from tests/integration/mongodb/release/test_sharding_release.py rename to tests/integration/release/test_sharding_release.py index 56313f3f8f..c908ec3036 100644 --- a/tests/integration/mongodb/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -6,6 +6,7 @@ from logging import getLogger import pytest +from juju.model import Model from pytest_operator.plugin import OpsTest from tenacity import RetryError, Retrying from tenacity.stop import stop_after_delay @@ -19,6 +20,7 @@ DEFAULT_DATABASE_NAME, DEPLOYMENT_TIMEOUT, MONGOS_APP_NAME, + READER_APPLICATION, TIMEOUT, UNIT_IDS, count_writes, @@ -26,9 +28,10 @@ deploy_charm, execute_on_mongod, find_unit, - get_app_name, start_continous_writes, + start_continuous_reads, stop_continous_writes, + stop_continuous_reads, wait_for_mongodb_units_blocked, ) from tests.integration.helpers.ldap import ( @@ -62,10 +65,11 @@ SECOND_COLL_NAME = f"{DEFAULT_COLLECTION_NAME}_bis" MONGOS_BIS_APP_NAME = f"{MONGOS_APP_NAME}-bis" +MONGOS_TER_APP_NAME = f"{MONGOS_APP_NAME}-ter" @pytest.mark.abort_on_fail -async def test_build_and_deploy( +async def test_deploy_apps( ops_test: OpsTest, mongodb_charm_name: str, mongos_charm_name: str, @@ -73,9 +77,13 @@ async def test_build_and_deploy( substrate: Substrate, mongodb_revision: int, mongos_revision: int, - kubernetes_model: str, + kubernetes_model: Model, ): - """Build and deploy one unit of MongoDB.""" + """Deploys and integrate a cluster with the right revisions. + + This also deploys a data integrator, alongside a continuous write application, + a self-signed-certificates application, and LDAP with all it needs. + """ tls_config = {"ca-common-name": "MongoDB release CA"} # it is possible for users to provide their own cluster for testing. Hence check if there # is a pre-existing cluster. @@ -123,6 +131,14 @@ async def test_build_and_deploy( app_name=MONGOS_BIS_APP_NAME, num_units=(1 if substrate == "microk8s" else 0), ), + deploy_charm( + ops_test=ops_test, + revision=mongos_revision, + charm=mongos_charm_name, + substrate=substrate, + app_name=MONGOS_TER_APP_NAME, + num_units=(1 if substrate == "microk8s" else 0), + ), ops_test.model.deploy( TLS_CERTIFICATES_APP_NAME, channel=TLS_CERTIFICATES_CHANNEL, @@ -174,7 +190,8 @@ async def test_build_and_deploy( async def test_integrate_with_tls( ops_test: OpsTest, ): - """Tests that we can integrate with TLS without losing data.""" + """Tests that we can integrate with TLS, and then add a writer and start writing.""" + assert ops_test.model await ops_test.model.integrate( f"{MONGOS_APP_NAME}", f"{CONFIG_SERVER_APP_NAME}", @@ -250,6 +267,8 @@ async def test_integrate_with_tls( async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): + """Tests that we can integrate with LDAP without losing data.""" + assert ops_test.model await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{CONFIG_SERVER_APP_NAME}:ldap") await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_APP_NAME}:ldap") await ops_test.model.integrate( @@ -271,8 +290,11 @@ async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): @pytest.mark.abort_on_fail async def test_integrate_second_client(ops_test: OpsTest, application_path: str): - app_name = await get_app_name(ops_test) + """Tests that we can integrate with a second client, and we also start writing on that client. + The client is a continuous write application. + """ + assert ops_test.model await deploy_application( ops_test, application_path=application_path, @@ -305,7 +327,8 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) @pytest.mark.abort_on_fail async def test_integrate_third_shard( ops_test: OpsTest, substrate: Substrate, mongodb_charm_name: str, mongodb_revision: int | None -): +) -> None: + """Tests that we can integrate a new shard to the cluster.""" await deploy_charm( ops_test=ops_test, revision=mongodb_revision, @@ -325,10 +348,57 @@ async def test_integrate_third_shard( f"{SHARD_THREE_APP_NAME}:{SHARD_REL_NAME}", f"{CONFIG_SERVER_APP_NAME}:{CONFIG_SERVER_REL_NAME}", ) + await ops_test.model.wait_for_idle( + apps=[CONFIG_SERVER_APP_NAME, SHARD_ONE_APP_NAME, SHARD_TWO_APP_NAME, SHARD_THREE_APP_NAME], + status="active", + timeout=TIMEOUT, + ) + + +@pytest.mark.abort_on_fail +async def test_integrate_third_client(ops_test: OpsTest, application_path: str): + """Tests that we can integrate with a third client, which will only read data. + + The client is a continuous write application. + """ + assert ops_test.model + await deploy_application( + ops_test, + application_path=application_path, + app_name=READER_APPLICATION, + database_name=SECOND_DB_NAME, + ) + await ops_test.model.integrate( + f"{MONGOS_TER_APP_NAME}", + f"{READER_APPLICATION}", + ) + await integrate_apps_with_tls( + ops_test, + applications=[ + MONGOS_TER_APP_NAME, + ], + ) + await ops_test.model.integrate( + f"{TLS_CERTIFICATES_APP_NAME}", + f"{READER_APPLICATION}", + ) + + await start_continuous_reads( + ops_test, + READER_APPLICATION, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) @pytest.mark.abort_on_fail async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs): + """Tests that we can integrate with S3 and create a backup. + + This test ensures that the backup is created and finished. + """ + assert ops_test.model + # deploy the s3 integrator charm await ops_test.model.deploy(S3_APP_NAME, channel="1/edge") await ops_test.model.wait_for_idle(apps=[S3_APP_NAME], timeout=DEPLOYMENT_TIMEOUT) @@ -362,6 +432,13 @@ async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs) @pytest.mark.abort_on_fail async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): + """Tests that we can restore a backup. + + This test starts by stopping the writes applications, and counting the number of writes + ensuring that we have never lost any write until now. + Then it restores the backup, counts the number of writes, + and checks that it is lower than what we had, proving that the backup was restored successfully. + """ first_reported_writes = await stop_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) second_reported_writes = await stop_continous_writes( ops_test, @@ -399,12 +476,25 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): assert restore.results["restore-status"] == "restore started", "restore not successful" with ops_test.fast_forward("60s"): - ( - await ops_test.model.wait_for_idle( - apps=[CONFIG_SERVER_APP_NAME], status="active", idle_period=15 - ), + await ops_test.model.wait_for_idle( + apps=[CONFIG_SERVER_APP_NAME], status="active", idle_period=15 ) + first_number_writes_after_restore = await count_writes( + ops_test, substrate, CONFIG_SERVER_APP_NAME, leader_unit + ) + second_number_writes_after_restore = await count_writes( + ops_test, + substrate, + CONFIG_SERVER_APP_NAME, + leader_unit, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, + ) + + assert first_number_writes_after_restore < first_number_writes + assert second_number_writes_after_restore < second_number_writes + @pytest.mark.abort_on_fail async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): @@ -440,3 +530,16 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", ) assert result.succeeded, "Failed to read value with LDAP client" + + +@pytest.mark.abort_on_fail +async def test_valid_reads(ops_test: OpsTest): + """Checks the reads at the end of the tests.""" + reads, failed_reads = await stop_continuous_reads( + ops_test, + READER_APPLICATION, + db_name=DEFAULT_DATABASE_NAME, + coll_name=DEFAULT_COLLECTION_NAME, + ) + assert reads > 1000 + assert len(failed_reads) == 0 diff --git a/tests/spread/release/lxd/test_release.py/task.yaml b/tests/spread/release/lxd/test_release.py/task.yaml index 9ea6f22f70..406ef739ea 100644 --- a/tests/spread/release/lxd/test_release.py/task.yaml +++ b/tests/spread/release/lxd/test_release.py/task.yaml @@ -2,6 +2,6 @@ summary: test_release.py environment: TEST_MODULE: test_release.py execute: | - tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" + tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/lxd/test_sharding_release.py/task.yaml b/tests/spread/release/lxd/test_sharding_release.py/task.yaml index 201383dc2a..79e0c0848f 100644 --- a/tests/spread/release/lxd/test_sharding_release.py/task.yaml +++ b/tests/spread/release/lxd/test_sharding_release.py/task.yaml @@ -2,6 +2,6 @@ summary: test_sharding_release.py environment: TEST_MODULE: test_sharding_release.py execute: | - tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" + tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/microk8s/test_release.py/task.yaml b/tests/spread/release/microk8s/test_release.py/task.yaml index 52bf5195de..11353e07cb 100644 --- a/tests/spread/release/microk8s/test_release.py/task.yaml +++ b/tests/spread/release/microk8s/test_release.py/task.yaml @@ -2,6 +2,6 @@ summary: test_release.py environment: TEST_MODULE: test_release.py execute: | - tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" + tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - self-hosted-linux-amd64-noble-medium diff --git a/tests/spread/release/microk8s/test_sharding_release.py/task.yaml b/tests/spread/release/microk8s/test_sharding_release.py/task.yaml index aba55fd3a2..d3118e5d96 100644 --- a/tests/spread/release/microk8s/test_sharding_release.py/task.yaml +++ b/tests/spread/release/microk8s/test_sharding_release.py/task.yaml @@ -2,6 +2,6 @@ summary: test_sharding_release.py environment: TEST_MODULE: test_sharding_release.py execute: | - tox run -e integration -- "tests/integration/mongodb/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" + tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - self-hosted-linux-amd64-noble-medium From 7e66076b9c0e1555d15da1a267ccca689330f513 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 13:38:07 +0200 Subject: [PATCH 05/35] fix: release-testing --- .github/workflows/release_tests.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release_tests.yaml b/.github/workflows/release_tests.yaml index f13f89b0f4..c15df3102f 100644 --- a/.github/workflows/release_tests.yaml +++ b/.github/workflows/release_tests.yaml @@ -38,6 +38,7 @@ on: required: true type: string +name: Release testing jobs: collect-release-tests: name: Collect release test spread jobs From cd0963d407ece3fd239426ec480ae759f2a7ac97 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 13:43:48 +0200 Subject: [PATCH 06/35] fix: release-testing --- tests/integration/conftest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 47aae6d2d6..78dc362065 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -61,17 +61,15 @@ def architecture() -> str: @pytest.fixture(scope="session") -def mongodb_revision(request: pytest.FixtureRequest, arch: str): +def mongodb_revision(request: pytest.FixtureRequest): """Revision for the correct arch.""" - data = json.loads(request.config.option.mongodb_revision) - return data[arch] + return request.config.option.mongodb_revision @pytest.fixture(scope="session") -def mongos_revision(request: pytest.FixtureRequest, arch: str): +def mongos_revision(request: pytest.FixtureRequest): """Revision for the correct arch.""" - data = json.loads(request.config.option.mongos_revision) - return data[arch] + return request.config.option.mongos_revision @pytest.fixture From 9371558028775fb87dca84a11504ea3877412fce Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 13:45:52 +0200 Subject: [PATCH 07/35] fix: fixture --- tests/integration/conftest.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 78dc362065..18f70fe326 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -136,13 +136,12 @@ def mongos_resource(mongos_metadata, substrate) -> dict[str, Any]: return {} -pytest.fixture - - +@pytest.fixture def mongodb_charm_name(substrate: Substrate) -> str: return "mongodb" if substrate == "lxd" else "mongodb-k8s" +@pytest.fixture def mongos_charm_name(substrate: Substrate) -> str: return "mongos" if substrate == "lxd" else "mongos-k8s" From d11c8391e6a08d5a4829424404edfc77897f865e Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 13:49:59 +0200 Subject: [PATCH 08/35] fix: fixture --- tests/conftest.py | 10 ++++++---- tests/integration/conftest.py | 6 +++--- tests/integration/helpers/common.py | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ec1de87038..788c362d35 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,14 +20,16 @@ def pytest_addoption(parser: Parser): parser.addoption( "--mongodb-revision", action="store", - help="MongoDB revisions for the said substrate in the shape of a json dictionary", - default="{}", + help="MongoDB revision", + default=1, + type=int, ) parser.addoption( "--mongos-revision", action="store", - help="MongoDB revisions for the said substrate in the shape of a json dictionary", - default="{}", + help="MongoDB revision", + default=1, + type=int, ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 18f70fe326..41390f0c85 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -61,15 +61,15 @@ def architecture() -> str: @pytest.fixture(scope="session") -def mongodb_revision(request: pytest.FixtureRequest): +def mongodb_revision(request: pytest.FixtureRequest) -> int: """Revision for the correct arch.""" - return request.config.option.mongodb_revision + return int(request.config.option.mongodb_revision) @pytest.fixture(scope="session") def mongos_revision(request: pytest.FixtureRequest): """Revision for the correct arch.""" - return request.config.option.mongos_revision + return int(request.config.option.mongos_revision) @pytest.fixture diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 275fb5e5c6..3c0990f3ab 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -114,6 +114,8 @@ async def deploy_charm( constraints: dict[str, list[str]] | None = None, bind: dict[str, str] | None = None, ): + if revision is not None: + channel = "8/beta" if substrate == "microk8s": series = series or "noble" await ops_test.model.deploy( From ef93bf31ed22fc36479bda8a28808df2b5faf5cc Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 13:58:18 +0200 Subject: [PATCH 09/35] fix: testing --- tests/integration/release/test_release.py | 2 +- tests/integration/release/test_sharding_release.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index e78543a5b2..238f2985e5 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -78,7 +78,7 @@ async def test_deploy_apps( assert ops_test.model # it is possible for users to provide their own cluster for testing. Hence check if there # is a pre-existing cluster. - asyncio.gather( + await asyncio.gather( deploy_charm( ops_test=ops_test, revision=mongodb_revision, diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index c908ec3036..74d6381d89 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -87,7 +87,7 @@ async def test_deploy_apps( tls_config = {"ca-common-name": "MongoDB release CA"} # it is possible for users to provide their own cluster for testing. Hence check if there # is a pre-existing cluster. - asyncio.gather( + await asyncio.gather( deploy_charm( ops_test=ops_test, revision=mongodb_revision, From 22e9f8b30b889dceb9f8b86c812b291caf93162a Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 14:18:15 +0200 Subject: [PATCH 10/35] fix: spread config --- spread.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spread.yaml b/spread.yaml index 37502a7aec..f6f4b5fc3f 100644 --- a/spread.yaml +++ b/spread.yaml @@ -138,13 +138,13 @@ suites: summary: Spread tests for Mongos VM tests/spread/mongos/microk8s/: summary: Spread tests for Mongos Kubernetes - tests/spread/release/lxd: + tests/spread/release/lxd/: summary: Spread mongodb release tests for VM manual: True environment: MONGODB_REVISION: "$(HOST: echo $MONGODB_REVISION)" MONGOS_REVISION: "$(HOST: echo $MONGOS_REVISION)" - tests/spread/release/microk8s: + tests/spread/release/microk8s/: summary: Spread mongodb release tests for Kubernetes manual: True environment: From d8c93c54732e43f994869b6483bbcb6ec7f5a85e Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 14:58:48 +0200 Subject: [PATCH 11/35] fix: split deployment --- tests/integration/release/test_release.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 238f2985e5..81252efe90 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -102,9 +102,10 @@ async def test_deploy_apps( series="noble", config={"database-name": "test-database"}, ), - deploy_glauth(ops_test, kubernetes_model), ) + await deploy_glauth(ops_test, kubernetes_model) + # Consume the offers exposed by glauth await consume_glauth_offers(ops_test, kubernetes_model) From 070ab98d5e6c64c26aecc29e30cfaf2c4eee8379 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 15:21:09 +0200 Subject: [PATCH 12/35] fix: syntax on f-string --- tests/integration/helpers/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/helpers/ldap.py b/tests/integration/helpers/ldap.py index fd222c0e2a..d4514e3b51 100644 --- a/tests/integration/helpers/ldap.py +++ b/tests/integration/helpers/ldap.py @@ -171,7 +171,7 @@ async def create_mongodb_user_roles( "db.createRole({" f" role: '{role_name}'," " privileges: []," - f" roles: [{'db': '{db}', 'role': 'readWrite'}, {'db': '{db}', 'role': 'enableSharding'}]" + f" roles: [{{'db': '{db}', 'role': 'readWrite'}}, {{'db': '{db}', 'role': 'enableSharding'}}]" "})", ) assert result.succeeded, "Failed to create role" From fa7e9bac05066603f613b468c6624812d269e721 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 15:24:37 +0200 Subject: [PATCH 13/35] fix: remove offers --- tests/integration/release/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/release/conftest.py b/tests/integration/release/conftest.py index cd85c4e570..4f64429081 100644 --- a/tests/integration/release/conftest.py +++ b/tests/integration/release/conftest.py @@ -10,6 +10,8 @@ from kubernetes.config.config_exception import ConfigException from pytest_operator.plugin import OpsTest +from tests.integration.helpers.ldap import teardown_offers + TIMEOUT = 15 * 60 logger = getLogger(__name__) @@ -42,4 +44,6 @@ async def kubernetes_model(ops_test: OpsTest) -> AsyncGenerator[Model]: yield kubernetes_model + # Remove the offers and tear down deployment + await teardown_offers(ops_test, kubernetes_model) await ops_test.forget_model(alias="secondary", timeout=TIMEOUT, allow_failure=True) From 18ec4ac89626636d19347751271bcb020f4dc67f Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 15:36:01 +0200 Subject: [PATCH 14/35] fix: teardown workflow --- tests/integration/helpers/common.py | 2 +- tests/integration/release/conftest.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 3c0990f3ab..47b08f945c 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -674,7 +674,7 @@ async def remove_units( async def get_app_name( ops_test: OpsTest, charm_name: str = "mongodb", test_deployments: list[str] = [] -) -> str: +) -> str | None: """Returns the name of the cluster running MongoDB. This is important since not all deployments of the MongoDB charm have the application name diff --git a/tests/integration/release/conftest.py b/tests/integration/release/conftest.py index 4f64429081..50e6b52460 100644 --- a/tests/integration/release/conftest.py +++ b/tests/integration/release/conftest.py @@ -10,7 +10,11 @@ from kubernetes.config.config_exception import ConfigException from pytest_operator.plugin import OpsTest -from tests.integration.helpers.ldap import teardown_offers +from tests.integration.helpers.common import MONGOS_APP_NAME, get_app_name +from tests.integration.helpers.ldap import LDAP_CERT_OFFER, LDAP_OFFER, teardown_offers +from tests.integration.helpers.sharding import ( + CONFIG_SERVER_APP_NAME, +) TIMEOUT = 15 * 60 @@ -44,6 +48,23 @@ async def kubernetes_model(ops_test: OpsTest) -> AsyncGenerator[Model]: yield kubernetes_model + for app_name in ( + await get_app_name(ops_test), + await get_app_name(ops_test, CONFIG_SERVER_APP_NAME), + await get_app_name(ops_test, MONGOS_APP_NAME), + ): + if app_name is None: + continue + try: + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_OFFER}:ldap", f"{app_name}:ldap" + ) + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{app_name}:ldap-certificate-transfer" + ) + except Exception: + pass + # Remove the offers and tear down deployment await teardown_offers(ops_test, kubernetes_model) await ops_test.forget_model(alias="secondary", timeout=TIMEOUT, allow_failure=True) From 59fa69c78318f9ec3ec3309a055b1f5043b20f5b Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 13 Apr 2026 16:40:54 +0200 Subject: [PATCH 15/35] fix: release test --- tests/integration/release/conftest.py | 5 ++++- tests/integration/release/test_release.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/release/conftest.py b/tests/integration/release/conftest.py index 50e6b52460..3f42a19d40 100644 --- a/tests/integration/release/conftest.py +++ b/tests/integration/release/conftest.py @@ -66,5 +66,8 @@ async def kubernetes_model(ops_test: OpsTest) -> AsyncGenerator[Model]: pass # Remove the offers and tear down deployment - await teardown_offers(ops_test, kubernetes_model) + try: + await teardown_offers(ops_test, kubernetes_model) + except Exception: + pass await ops_test.forget_model(alias="secondary", timeout=TIMEOUT, allow_failure=True) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 81252efe90..7eb4c22521 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -136,6 +136,7 @@ async def test_integrate_with_tls( await start_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) +@pytest.mark.abort_on_fail async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): """Tests that we can integrate with LDAP without losing data.""" assert ops_test.model From dd401f986d72dd25538e5431c248fb3deb4c92c7 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Wed, 15 Apr 2026 13:43:15 +0200 Subject: [PATCH 16/35] fix: permissions --- tests/integration/helpers/common.py | 50 +++++++++++++++++-- tests/integration/helpers/ldap.py | 2 + tests/integration/helpers/tls.py | 23 ++------- tests/integration/mongodb/tls/test_tls.py | 4 +- tests/integration/release/test_release.py | 18 +++++-- .../release/test_sharding_release.py | 6 ++- 6 files changed, 71 insertions(+), 32 deletions(-) diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 47b08f945c..9c2472caab 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -7,6 +7,7 @@ import subprocess from dataclasses import dataclass from datetime import datetime +from pathlib import Path from random import choices from string import ascii_lowercase, digits from typing import Any @@ -32,8 +33,12 @@ wait_fixed, ) +from tests.integration.helpers.tls import scp_file_preserve_ctime from tests.integration.helpers.types import Substrate +MONGODB_SNAP_CONF_DIR = "/var/snap/charmed-mongodb/current/etc/mongod" +MONGODB_ROCK_CONF_DIR = "/etc/mongod" + MONGO_SHELL = "charmed-mongodb.mongosh" MONGOD_PORT = 27017 MONGOS_PORT = 27018 @@ -90,6 +95,24 @@ def mongosh(substrate: Substrate) -> str: return "mongosh" +def external_cert_path(substrate: Substrate): + if substrate == "lxd": + return f"{MONGODB_SNAP_CONF_DIR}/external-ca.crt" + return f"{MONGODB_ROCK_CONF_DIR}/external-ca.crt" + + +def external_pem_path(substrate: Substrate): + if substrate == "lxd": + return f"{MONGODB_SNAP_CONF_DIR}/external-cert.pem" + return f"{MONGODB_ROCK_CONF_DIR}/external-cert.pem" + + +def internal_cert_path(substrate: Substrate): + if substrate == "lxd": + return f"{MONGODB_SNAP_CONF_DIR}/internal-ca.crt" + return f"{MONGODB_ROCK_CONF_DIR}/internal-ca.crt" + + class ProcessError(Exception): """Raised when a process fails.""" @@ -987,17 +1010,24 @@ async def execute_on_mongod( uri: str, command: str, container_name: str = "mongod", + tls: bool = False, stringify: bool = True, expecting_output: bool = True, ) -> CommandResult: """Executes the command with mongosh.""" leader_id = await get_leader_id(ops_test, app_name) ssh_command = ["ssh", "--container", container_name] if substrate == "microk8s" else ["ssh"] + tls_string = "" + if tls: + tls_string = ( + f"--tls --tlsCAFile {external_cert_path(substrate)}" + f" --tlsCertificateKeyFile {external_pem_path(substrate)}" + ) if stringify: - formatted_string = f'"{uri}" --quiet --eval "EJSON.stringify({command})"' + formatted_string = f'"{uri}" --quiet --eval "EJSON.stringify({command})" {tls_string}' else: - formatted_string = f'"{uri}" --quiet --eval "{command}"' + formatted_string = f'"{uri}" --quiet --eval "{command}" {tls_string}' cmd = [f"{app_name}/{leader_id}", mongosh(substrate), formatted_string] @@ -1110,6 +1140,7 @@ async def count_writes( username: str = CHARMED_OPERATOR_USERNAME, db_name: str = DEFAULT_DATABASE_NAME, coll_name: str = DEFAULT_COLLECTION_NAME, + tls: bool = False, ) -> int: """New versions of pymongo no longer support the count operation, instead find is used.""" host = await get_address_of_unit(ops_test, substrate, get_unit_id(unit.name), app_name=app_name) @@ -1121,12 +1152,25 @@ async def count_writes( hosts=[host], username=username, ) + if mongos: + container = "mongos" + else: + container = "mongod" + if tls: + ca_file = await scp_file_preserve_ctime( + ops_test, substrate, unit.name, external_cert_path(substrate), container + ) + else: + ca_file = None - client = MongoClient(uri, directConnection=True) + client = MongoClient(uri, directConnection=True, tlsCaFile=ca_file, tls=tls) db = client[db_name] test_collection = db[coll_name] count = test_collection.count_documents({}) client.close() + + if ca_file: + Path(ca_file).unlink() return count diff --git a/tests/integration/helpers/ldap.py b/tests/integration/helpers/ldap.py index d4514e3b51..6ff9fecf3c 100644 --- a/tests/integration/helpers/ldap.py +++ b/tests/integration/helpers/ldap.py @@ -159,6 +159,7 @@ async def create_mongodb_user_roles( role_name: str, mongos: bool = False, db: str = "superdb", + tls: bool = False, ) -> None: """Creates the roles for mongodb with the provided role_name.""" uri = await generate_mongodb_client(ops_test, substrate, app_name, mongos=mongos) @@ -173,6 +174,7 @@ async def create_mongodb_user_roles( " privileges: []," f" roles: [{{'db': '{db}', 'role': 'readWrite'}}, {{'db': '{db}', 'role': 'enableSharding'}}]" "})", + tls=tls, ) assert result.succeeded, "Failed to create role" diff --git a/tests/integration/helpers/tls.py b/tests/integration/helpers/tls.py index 2c89ec1c64..61e888ff1a 100644 --- a/tests/integration/helpers/tls.py +++ b/tests/integration/helpers/tls.py @@ -19,11 +19,14 @@ MONGOS_APP_NAME, MONGOS_PORT, ProcessError, + external_cert_path, + external_pem_path, get_address_of_unit, get_application_relation_data, get_password, get_secret_content, get_secret_id, + internal_cert_path, mongosh, ) from tests.integration.helpers.types import Substrate @@ -38,31 +41,11 @@ DIFFERENT_CERTIFICATES_APP_NAME = "self-signed-certificates-separate" -MONGODB_SNAP_CONF_DIR = "/var/snap/charmed-mongodb/current/etc/mongod" -MONGODB_ROCK_CONF_DIR = "/etc/mongod" SNAP_MONGOD_SERVICE = "snap.charmed-mongodb.mongod.service" SNAP_MONGOS_SERVICE = "snap.charmed-mongodb.mongos.service" -def external_cert_path(substrate: Substrate): - if substrate == "lxd": - return f"{MONGODB_SNAP_CONF_DIR}/external-ca.crt" - return f"{MONGODB_ROCK_CONF_DIR}/external-ca.crt" - - -def external_pem_path(substrate: Substrate): - if substrate == "lxd": - return f"{MONGODB_SNAP_CONF_DIR}/external-cert.pem" - return f"{MONGODB_ROCK_CONF_DIR}/external-cert.pem" - - -def internal_cert_path(substrate: Substrate): - if substrate == "lxd": - return f"{MONGODB_SNAP_CONF_DIR}/internal-ca.crt" - return f"{MONGODB_ROCK_CONF_DIR}/internal-ca.crt" - - async def integrate_apps_with_tls( ops_test: OpsTest, applications: list[str], diff --git a/tests/integration/mongodb/tls/test_tls.py b/tests/integration/mongodb/tls/test_tls.py index 633d98dfd3..daf4e70529 100644 --- a/tests/integration/mongodb/tls/test_tls.py +++ b/tests/integration/mongodb/tls/test_tls.py @@ -17,8 +17,10 @@ clear_continous_writes, deploy_application, deploy_charm, + external_cert_path, get_app_name, get_secret_by_label, + internal_cert_path, start_continous_writes, stop_continous_writes, wait_for_mongodb_units_blocked, @@ -31,9 +33,7 @@ cannot_connect_without_tls, check_certs_correctly_distributed, check_tls, - external_cert_path, integrate_apps_with_tls, - internal_cert_path, remove_tls_integrations, set_invalid_private_key, set_private_key, diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 7eb4c22521..3dde1ef4ee 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -126,6 +126,7 @@ async def test_integrate_with_tls( """Tests that we can integrate with TLS, and then add a writer and start writing.""" assert ops_test.model app_name = await get_app_name(ops_test) + assert app_name await integrate_apps_with_tls(ops_test, applications=[app_name]) await ops_test.model.wait_for_idle( @@ -141,6 +142,7 @@ async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): """Tests that we can integrate with LDAP without losing data.""" assert ops_test.model app_name = await get_app_name(ops_test) + assert app_name await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{app_name}:ldap") await ops_test.model.integrate( @@ -150,9 +152,10 @@ async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): await create_mongodb_user_roles( ops_test, substrate, - app_name, + app_name=app_name, role_name="ou=superheroes,ou=users,dc=glauth,dc=com", db=DEFAULT_DATABASE_NAME, + tls=True, ) await ops_test.model.wait_for_idle(apps=[app_name], status="active", timeout=TIMEOUT) @@ -256,6 +259,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): """ assert ops_test.model app_name = await get_app_name(ops_test) + assert app_name first_reported_writes = await stop_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) second_reported_writes = await stop_continous_writes( @@ -266,7 +270,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): ) leader_unit = await find_unit(ops_test, leader=True, app_name=app_name) # count total writes - first_number_writes = await count_writes(ops_test, substrate, app_name, leader_unit) + first_number_writes = await count_writes(ops_test, substrate, app_name, leader_unit, tls=True) second_number_writes = await count_writes( ops_test, substrate, @@ -274,6 +278,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): leader_unit, db_name=SECOND_DB_NAME, coll_name=SECOND_COLL_NAME, + tls=True, ) assert first_number_writes == first_reported_writes assert second_number_writes == second_reported_writes @@ -295,7 +300,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): (await ops_test.model.wait_for_idle(apps=[app_name], status="active", idle_period=15),) first_number_writes_after_restore = await count_writes( - ops_test, substrate, app_name, leader_unit + ops_test, substrate, app_name, leader_unit, tls=True ) second_number_writes_after_restore = await count_writes( ops_test, @@ -304,6 +309,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): leader_unit, db_name=SECOND_DB_NAME, coll_name=SECOND_COLL_NAME, + tls=True, ) assert first_number_writes_after_restore < first_number_writes @@ -317,6 +323,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): This checks both authentication and authorisation. """ app_name = await get_app_name(ops_test) + assert app_name # We create a client which should be able to write uri = await generate_mongodb_ldap_client( @@ -329,12 +336,12 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): ) result = await execute_on_mongod( - ops_test, app_name, substrate, uri, "db.test.insertOne({number: 1})" + ops_test, app_name, substrate, uri, "db.test.insertOne({number: 1})", tls=True ) assert result.succeeded, "Failed to insert value with LDAP client" result = await execute_on_mongod( - ops_test, app_name, substrate, uri, "db.test.findOne({number: 1})" + ops_test, app_name, substrate, uri, "db.test.findOne({number: 1})", tls=True ) assert result.succeeded, "Failed to read value with LDAP client" @@ -344,6 +351,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): substrate, uri, f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", + tls=True, ) assert result.succeeded, "Failed to read value with LDAP client" diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 74d6381d89..f61f2c6615 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -281,6 +281,7 @@ async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): CONFIG_SERVER_APP_NAME, role_name="ou=superheroes,ou=users,dc=glauth,dc=com", db=DEFAULT_DATABASE_NAME, + tls=True, ) await ops_test.model.wait_for_idle( @@ -513,12 +514,12 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): ) result = await execute_on_mongod( - ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.insertOne({number: 1})" + ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.insertOne({number: 1})", tls=True ) assert result.succeeded, "Failed to insert value with LDAP client" result = await execute_on_mongod( - ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.findOne({number: 1})" + ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.findOne({number: 1})", tls=True ) assert result.succeeded, "Failed to read value with LDAP client" @@ -528,6 +529,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): substrate, uri, f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", + tls=True, ) assert result.succeeded, "Failed to read value with LDAP client" From 35036c106d00e1e97322c19fdfe78d24500e1e4d Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Wed, 15 Apr 2026 13:57:49 +0200 Subject: [PATCH 17/35] feat: Use microceph --- tests/integration/release/test_release.py | 16 +++++++++++----- .../integration/release/test_sharding_release.py | 15 ++++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 3dde1ef4ee..0d3253c1ab 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -12,7 +12,7 @@ from tenacity.stop import stop_after_delay from tenacity.wait import wait_fixed -from tests.integration.helpers.backups import S3_APP_NAME, CloudConfigs, count_logical_backups +from tests.integration.helpers.backups import S3_APP_NAME, count_logical_backups from tests.integration.helpers.common import ( CONTINUOUS_WRITE_APPLICATION, CONTINUOUS_WRITE_APPLICATION_BIS, @@ -209,24 +209,30 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): @pytest.mark.abort_on_fail -async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs): +async def test_integrate_with_s3( + ops_test: OpsTest, + storage_credentials: dict[str, str], + storage_config: dict[str, str], +): """Tests that we can integrate with S3 and create a backup. This test ensures that the backup is created and finished. """ assert ops_test.model app_name = await get_app_name(ops_test) + assert app_name # deploy the s3 integrator charm await ops_test.model.deploy(S3_APP_NAME, channel="1/edge") await ops_test.model.wait_for_idle(apps=[S3_APP_NAME], timeout=DEPLOYMENT_TIMEOUT) - configuration_parameters, credentials = cloud_configs["AWS"] s3_integrator_unit = ops_test.model.applications[S3_APP_NAME].units[0] # apply new configuration options - await ops_test.model.applications[S3_APP_NAME].set_config(configuration_parameters) - action = await s3_integrator_unit.run_action(action_name="sync-s3-credentials", **credentials) + await ops_test.model.applications[S3_APP_NAME].set_config(storage_config) + action = await s3_integrator_unit.run_action( + action_name="sync-s3-credentials", **storage_credentials + ) await action.wait() await ops_test.model.wait_for_idle( diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index f61f2c6615..fc36a33157 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -12,7 +12,7 @@ from tenacity.stop import stop_after_delay from tenacity.wait import wait_fixed -from tests.integration.helpers.backups import S3_APP_NAME, CloudConfigs, count_logical_backups +from tests.integration.helpers.backups import S3_APP_NAME, count_logical_backups from tests.integration.helpers.common import ( CONTINUOUS_WRITE_APPLICATION, CONTINUOUS_WRITE_APPLICATION_BIS, @@ -393,7 +393,11 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): @pytest.mark.abort_on_fail -async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs): +async def test_integrate_with_s3( + ops_test: OpsTest, + storage_credentials: dict[str, str], + storage_config: dict[str, str], +): """Tests that we can integrate with S3 and create a backup. This test ensures that the backup is created and finished. @@ -404,12 +408,13 @@ async def test_integrate_with_s3(ops_test: OpsTest, cloud_configs: CloudConfigs) await ops_test.model.deploy(S3_APP_NAME, channel="1/edge") await ops_test.model.wait_for_idle(apps=[S3_APP_NAME], timeout=DEPLOYMENT_TIMEOUT) - configuration_parameters, credentials = cloud_configs["AWS"] s3_integrator_unit = ops_test.model.applications[S3_APP_NAME].units[0] # apply new configuration options - await ops_test.model.applications[S3_APP_NAME].set_config(configuration_parameters) - action = await s3_integrator_unit.run_action(action_name="sync-s3-credentials", **credentials) + await ops_test.model.applications[S3_APP_NAME].set_config(storage_config) + action = await s3_integrator_unit.run_action( + action_name="sync-s3-credentials", **storage_credentials + ) await action.wait() await ops_test.model.wait_for_idle( From b5ec632f54c44755d582508a970eb45ad59eefa0 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Wed, 15 Apr 2026 14:08:23 +0200 Subject: [PATCH 18/35] feat: imports --- tests/integration/helpers/common.py | 30 +++++++++++++++++++++++++-- tests/integration/helpers/mongos.py | 4 ++-- tests/integration/helpers/sharding.py | 4 ++-- tests/integration/helpers/tls.py | 28 +------------------------ 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 9c2472caab..b5cfd314e9 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -33,7 +33,6 @@ wait_fixed, ) -from tests.integration.helpers.tls import scp_file_preserve_ctime from tests.integration.helpers.types import Substrate MONGODB_SNAP_CONF_DIR = "/var/snap/charmed-mongodb/current/etc/mongod" @@ -491,7 +490,7 @@ async def find_unit(ops_test: OpsTest, leader: bool, app_name: str | None = None return ret_unit -async def get_leader_id(ops_test: OpsTest, app_name=None) -> int: +async def get_leader_id(ops_test: OpsTest, app_name: str | None = None) -> int: """Returns the unit number of the juju leader unit.""" app_name = app_name or await get_app_name(ops_test) for unit in ops_test.model.applications[app_name].units: @@ -1420,3 +1419,30 @@ async def execute_on_server( shell=True, universal_newlines=True, ) + + +async def scp_file_preserve_ctime( + ops_test: OpsTest, substrate: Substrate, unit_name: str, path: str, container: str = "mongod" +) -> str: + """Returns the unix timestamp of when a file was created on a specified unit.""" + # Retrieving the file + filename = path.split("/")[-1] + if substrate == "lxd": + complete_command = f"exec --unit {unit_name} -- sudo cat {path}" + return_code, stdout, stderr = await ops_test.juju(*complete_command.split(), check=True) + with open(filename, mode="w") as fd: + fd.write(stdout.strip()) + else: + complete_command = f"scp --container {container} {unit_name}:{path} {filename}" + return_code, _, stderr = await ops_test.juju(*complete_command.split()) + + if return_code != 0: + logger.error(stderr) + raise ProcessError( + "Expected command %s to succeed instead it failed: %s; %s", + complete_command, + return_code, + stderr, + ) + + return f"{filename}" diff --git a/tests/integration/helpers/mongos.py b/tests/integration/helpers/mongos.py index f926754c42..b3526a00df 100644 --- a/tests/integration/helpers/mongos.py +++ b/tests/integration/helpers/mongos.py @@ -20,6 +20,7 @@ TIMEOUT, clear_continous_writes, deploy_charm, + external_cert_path, find_unit, get_address_of_unit, get_application_relation_data, @@ -28,6 +29,7 @@ get_secret_data, get_unit_hostnames, get_unit_id, + internal_cert_path, mongosh, start_continous_writes, stop_continous_writes, @@ -39,10 +41,8 @@ cannot_connect_without_tls, check_certs_correctly_distributed, check_tls, - external_cert_path, get_file_content, integrate_apps_with_tls, - internal_cert_path, remove_tls_integrations, set_private_keys, time_file_created, diff --git a/tests/integration/helpers/sharding.py b/tests/integration/helpers/sharding.py index f1482b214e..113fe6b163 100644 --- a/tests/integration/helpers/sharding.py +++ b/tests/integration/helpers/sharding.py @@ -14,8 +14,10 @@ DEPLOYMENT_TIMEOUT, MONGOS_PORT, deploy_charm, + external_cert_path, get_direct_mongo_client, get_leader_id, + internal_cert_path, mongodb_uri, ) from tests.integration.helpers.tls import ( @@ -24,9 +26,7 @@ cannot_connect_without_tls, check_certs_correctly_distributed, check_tls, - external_cert_path, get_file_content, - internal_cert_path, set_private_keys, time_file_created, time_process_started, diff --git a/tests/integration/helpers/tls.py b/tests/integration/helpers/tls.py index 61e888ff1a..77c12f8770 100644 --- a/tests/integration/helpers/tls.py +++ b/tests/integration/helpers/tls.py @@ -28,6 +28,7 @@ get_secret_id, internal_cert_path, mongosh, + scp_file_preserve_ctime, ) from tests.integration.helpers.types import Substrate @@ -341,33 +342,6 @@ def process_systemctl_time(systemctl_output) -> datetime: return datetime.strptime(time_as_str, "%Y-%m-%dT%H:%M:%S") -async def scp_file_preserve_ctime( - ops_test: OpsTest, substrate: Substrate, unit_name: str, path: str, container: str = "mongod" -) -> str: - """Returns the unix timestamp of when a file was created on a specified unit.""" - # Retrieving the file - filename = path.split("/")[-1] - if substrate == "lxd": - complete_command = f"exec --unit {unit_name} -- sudo cat {path}" - return_code, stdout, stderr = await ops_test.juju(*complete_command.split(), check=True) - with open(filename, mode="w") as fd: - fd.write(stdout.strip()) - else: - complete_command = f"scp --container {container} {unit_name}:{path} {filename}" - return_code, _, stderr = await ops_test.juju(*complete_command.split()) - - if return_code != 0: - logger.error(stderr) - raise ProcessError( - "Expected command %s to succeed instead it failed: %s; %s", - complete_command, - return_code, - stderr, - ) - - return f"{filename}" - - async def check_certs_correctly_distributed( ops_test: OpsTest, substrate: Substrate, From 083b0bbbba40a6d579617591660b6b91f447288a Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 16 Apr 2026 18:11:00 +0200 Subject: [PATCH 19/35] fix: tests --- .../applications/continuous_write_charm/src/charm.py | 2 +- tests/integration/release/test_release.py | 2 ++ tests/integration/release/test_sharding_release.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/applications/continuous_write_charm/src/charm.py b/tests/integration/applications/continuous_write_charm/src/charm.py index 42c6c5a334..0a890d4f09 100755 --- a/tests/integration/applications/continuous_write_charm/src/charm.py +++ b/tests/integration/applications/continuous_write_charm/src/charm.py @@ -363,7 +363,7 @@ def _on_start_continuous_reads_action(self, event) -> None: db_name = event.params.get("db-name") or self.database_name collection_name = event.params.get("collection-name") or COLLECTION_NAME - self._start_continuous_reads(1, db_name, collection_name) + self._start_continuous_reads(db_name, collection_name) def _on_stop_continuous_reads_action(self, event: ActionEvent) -> None: """Handle the stop continuous reads action event.""" diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 0d3253c1ab..1df31d1e87 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -235,6 +235,8 @@ async def test_integrate_with_s3( ) await action.wait() + await ops_test.model.integrate(S3_APP_NAME, app_name) + await ops_test.model.wait_for_idle( apps=[S3_APP_NAME, app_name], status="active", timeout=TIMEOUT ) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index fc36a33157..0b54e793c3 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -417,6 +417,8 @@ async def test_integrate_with_s3( ) await action.wait() + await ops_test.model.integrate(S3_APP_NAME, CONFIG_SERVER_APP_NAME) + await ops_test.model.wait_for_idle( apps=[S3_APP_NAME, CONFIG_SERVER_APP_NAME], status="active", timeout=TIMEOUT ) From a03914b600ab39c54aef3b51a8fb01719528e1d8 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Fri, 17 Apr 2026 13:27:16 +0200 Subject: [PATCH 20/35] fix: smaller issues --- .../continuous_write_charm/src/charm.py | 4 +- tests/integration/helpers/common.py | 41 ++++++++++++++++--- tests/integration/release/test_release.py | 3 ++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/tests/integration/applications/continuous_write_charm/src/charm.py b/tests/integration/applications/continuous_write_charm/src/charm.py index 0a890d4f09..d9ddc42189 100755 --- a/tests/integration/applications/continuous_write_charm/src/charm.py +++ b/tests/integration/applications/continuous_write_charm/src/charm.py @@ -271,8 +271,10 @@ def _stop_continuous_reads(self, db_name: str, collection_name: str) -> tuple[in logger.info( f"Process {self.read_proc_id_key(db_name, collection_name)} was killed already (or never existed)" ) + return (None, []) + finally: + del self.app_peer_data[self.read_proc_id_key(db_name, collection_name)] - del self.app_peer_data[self.read_proc_id_key(db_name, collection_name)] # read the last written_value try: diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index b5cfd314e9..5e61517c50 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -206,13 +206,23 @@ async def relate_mongodb_and_application( mongodb_application_name: The mongodb charm application name application_name: The continuous writes test charm application name """ - if is_relation_joined(ops_test, "mongodb", "database"): + if is_relation_joined( + ops_test, "mongodb", "database", app_one=application_name, app_two=mongodb_application_name + ): return await ops_test.model.integrate( f"{application_name}:mongodb", f"{mongodb_application_name}:database" ) - await ops_test.model.block_until(lambda: is_relation_joined(ops_test, "mongodb", "database")) + await ops_test.model.block_until( + lambda: is_relation_joined( + ops_test, + "mongodb", + "database", + app_one=application_name, + app_two=mongodb_application_name, + ) + ) await ops_test.model.wait_for_idle( apps=[mongodb_application_name, application_name], @@ -971,17 +981,38 @@ async def check_app_status( assert app.status_message == message -def is_relation_joined(ops_test: OpsTest, endpoint_one: str, endpoint_two: str) -> bool: +def is_relation_joined( + ops_test: OpsTest, + endpoint_one: str, + endpoint_two: str, + app_one: str | None = None, + app_two: str | None = None, +) -> bool: """Check if a relation is joined. Args: ops_test: The ops test object passed into every test case endpoint_one: The first endpoint of the relation endpoint_two: The second endpoint of the relation + app_one: Application name for the first endpoint of the relation + app_two: Application name for the second endpoint of the relation """ for rel in ops_test.model.relations: - endpoints = [endpoint.name for endpoint in rel.endpoints] - if endpoint_one in endpoints and endpoint_two in endpoints: + endpoints = rel.endpoints + endpoint_names = [endpoint.name for endpoint in endpoints] + invalid = False + if endpoint_one not in endpoint_names or endpoint_two not in endpoint_names: + continue + if not app_one and not app_two: + return True + for endpoint in endpoints: + if endpoint.name == endpoint_one: + if app_one and endpoint.application.name != app_one: + invalid = True + if endpoint.name == endpoint_two: + if app_two and endpoint.application.name != app_two: + invalid = True + if not invalid: return True return False diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 1df31d1e87..3be3497fb0 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -255,6 +255,9 @@ async def test_integrate_with_s3( except RetryError: assert backups == 1, "Backup not created." + # Wait for status to go back to idle. + await ops_test.model.wait_for_idle(apps=[app_name], status="active", timeout=TIMEOUT) + @pytest.mark.abort_on_fail async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): From 635ccc05f26f2a55eed52f9af04c4732fa90afd5 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 20 Apr 2026 13:40:52 +0200 Subject: [PATCH 21/35] fix: async --- tests/integration/release/test_release.py | 4 ++-- tests/integration/release/test_sharding_release.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 3be3497fb0..2ec41d5373 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -307,8 +307,8 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): logger.info(f"Restore backup result {restore.results=}") assert restore.results["restore-status"] == "restore started", "restore not successful" - with ops_test.fast_forward("60s"): - (await ops_test.model.wait_for_idle(apps=[app_name], status="active", idle_period=15),) + async with ops_test.fast_forward("60s"): + await ops_test.model.wait_for_idle(apps=[app_name], status="active", idle_period=15) first_number_writes_after_restore = await count_writes( ops_test, substrate, app_name, leader_unit, tls=True diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 0b54e793c3..bac0359444 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -483,7 +483,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): logger.info(f"Restore backup result {restore.results=}") assert restore.results["restore-status"] == "restore started", "restore not successful" - with ops_test.fast_forward("60s"): + async with ops_test.fast_forward("60s"): await ops_test.model.wait_for_idle( apps=[CONFIG_SERVER_APP_NAME], status="active", idle_period=15 ) From 0a9b43c1389a56361cafd388644ed71d6a3185a1 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Mon, 20 Apr 2026 16:11:25 +0200 Subject: [PATCH 22/35] fix: correct testing --- .../continuous_write_charm/src/charm.py | 7 ++++--- .../src/continuous_reads.py | 8 +++++--- tests/integration/helpers/common.py | 11 ++++++----- tests/integration/release/test_release.py | 15 +++------------ .../integration/release/test_sharding_release.py | 15 +++------------ 5 files changed, 21 insertions(+), 35 deletions(-) diff --git a/tests/integration/applications/continuous_write_charm/src/charm.py b/tests/integration/applications/continuous_write_charm/src/charm.py index d9ddc42189..a54b98b276 100755 --- a/tests/integration/applications/continuous_write_charm/src/charm.py +++ b/tests/integration/applications/continuous_write_charm/src/charm.py @@ -260,6 +260,7 @@ def _stop_continuous_reads(self, db_name: str, collection_name: str) -> tuple[in return None, [] if not self.app_peer_data.get(self.read_proc_id_key(db_name, collection_name)): + logger.warning("Missing read proc id.") return None, [] # Send a SIGTERM to the process and wait for the process to exit @@ -269,7 +270,7 @@ def _stop_continuous_reads(self, db_name: str, collection_name: str) -> tuple[in ) except ProcessLookupError: logger.info( - f"Process {self.read_proc_id_key(db_name, collection_name)} was killed already (or never existed)" + f"Process {self.app_peer_data[self.read_proc_id_key(db_name, collection_name)]} was killed already (or never existed)" ) return (None, []) finally: @@ -370,12 +371,12 @@ def _on_start_continuous_reads_action(self, event) -> None: def _on_stop_continuous_reads_action(self, event: ActionEvent) -> None: """Handle the stop continuous reads action event.""" if not self._database_config: - return event.set_results({"reads": -1}) + return event.set_results({"reads": -1, "failed-reads": []}) db_name = event.params.get("db-name") or self.database_name collection_name = event.params.get("collection-name") or COLLECTION_NAME reads, failed_reads = self._stop_continuous_reads(db_name, collection_name) - event.set_results({"reads": reads or -1, "failed_reads": failed_reads}) + event.set_results({"reads": reads or -1, "failed-reads": failed_reads}) return None def _on_database_created(self, event: DatabaseCreatedEvent) -> None: diff --git a/tests/integration/applications/continuous_write_charm/src/continuous_reads.py b/tests/integration/applications/continuous_write_charm/src/continuous_reads.py index ac3b5c8d25..7a859b4700 100644 --- a/tests/integration/applications/continuous_write_charm/src/continuous_reads.py +++ b/tests/integration/applications/continuous_write_charm/src/continuous_reads.py @@ -46,14 +46,16 @@ def continous_reads( # run some basic sampling test_collection.aggregate([{"$sample": {"size": 10}}, {"$sort": {"number": 1}}]) elif rand < 0.6: - n_docs = test_collection.count_documents() + n_docs = test_collection.count_documents({}) # get one single sample test_collection.aggregate([{"$skip": math.floor(n_docs * random.random())}, {"$limit": 1}]) else: - n_docs = test_collection.count_documents() + n_docs = test_collection.count_documents({}) test_collection.find({"number": {"$lte": math.floor(n_docs /2)}}) - except PyMongoError as err: + except Exception as err: failed_reads.append(str(err)) + with open("error.log", mode="a") as fd: + fd.write(f"{err}\n") continue finally: client.close() diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 5e61517c50..1459f8e284 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -1106,12 +1106,12 @@ async def start_continuous_reads( db_name: str = DEFAULT_DATABASE_NAME, coll_name: str = DEFAULT_COLLECTION_NAME, ): - """Helper function to run the `start-continuous-write` action on the continuous write app.""" + """Helper function to run the `start-continuous-reads` action on the continuous write app.""" application_unit = ops_test.model.applications[client_app_name].units[0] - start_writes_action = await application_unit.run_action( + start_reads_action = await application_unit.run_action( "start-continuous-reads", **{"db-name": db_name, "collection-name": coll_name} ) - await start_writes_action.wait() + await start_reads_action.wait() async def stop_continous_writes( @@ -1138,13 +1138,14 @@ async def stop_continuous_reads( db_name: str = DEFAULT_DATABASE_NAME, coll_name: str = DEFAULT_COLLECTION_NAME, ) -> tuple[int, list[str]]: - """Helper function to run the `start-continuous-write` action on the continuous write app.""" + """Helper function to run the `stop-continuous-reads` action on the continuous write app.""" application_unit = ops_test.model.applications[client_app_name].units[0] stop_writes_action = await application_unit.run_action( "stop-continuous-reads", **{"db-name": db_name, "collection-name": coll_name} ) await stop_writes_action.wait() - return int(stop_writes_action.results["reads"]), stop_writes_action.results["failed_reads"] + logger.warning(f"Failed reads: {stop_writes_action.results['failed-reads']}") + return int(stop_writes_action.results["reads"]), stop_writes_action.results["failed-reads"] async def clear_continous_writes( diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 2ec41d5373..ffc7895824 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -341,7 +341,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): ops_test, substrate, app_name, - database="superdb", + database=DEFAULT_DATABASE_NAME, username="cn=johndoe,ou=superheroes,ou=users,dc=glauth,dc=com", password="dogood", ) @@ -356,16 +356,6 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): ) assert result.succeeded, "Failed to read value with LDAP client" - result = await execute_on_mongod( - ops_test, - app_name, - substrate, - uri, - f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", - tls=True, - ) - assert result.succeeded, "Failed to read value with LDAP client" - @pytest.mark.abort_on_fail async def test_valid_reads(ops_test: OpsTest): @@ -377,4 +367,5 @@ async def test_valid_reads(ops_test: OpsTest): coll_name=DEFAULT_COLLECTION_NAME, ) assert reads > 1000 - assert len(failed_reads) == 0 + # We can allow for a few errors during restore for example + assert len(failed_reads) < 50 diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index bac0359444..fabe89ba36 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -515,7 +515,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): ops_test, substrate, CONFIG_SERVER_APP_NAME, - database="superdb", + database=DEFAULT_DATABASE_NAME, username="cn=johndoe,ou=superheroes,ou=users,dc=glauth,dc=com", password="dogood", ) @@ -530,16 +530,6 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): ) assert result.succeeded, "Failed to read value with LDAP client" - result = await execute_on_mongod( - ops_test, - CONFIG_SERVER_APP_NAME, - substrate, - uri, - f"db.{DEFAULT_COLLECTION_NAME}.find().limit(10)", - tls=True, - ) - assert result.succeeded, "Failed to read value with LDAP client" - @pytest.mark.abort_on_fail async def test_valid_reads(ops_test: OpsTest): @@ -551,4 +541,5 @@ async def test_valid_reads(ops_test: OpsTest): coll_name=DEFAULT_COLLECTION_NAME, ) assert reads > 1000 - assert len(failed_reads) == 0 + # We can allow for a few errors during restore. + assert len(failed_reads) < 10 From 2a8f57576523e9733e08e4313d5f6d0d74b79d83 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Tue, 21 Apr 2026 17:28:13 +0200 Subject: [PATCH 23/35] fix: remove unsued code --- tests/integration/release/conftest.py | 28 --------------------------- 1 file changed, 28 deletions(-) diff --git a/tests/integration/release/conftest.py b/tests/integration/release/conftest.py index 3f42a19d40..cd85c4e570 100644 --- a/tests/integration/release/conftest.py +++ b/tests/integration/release/conftest.py @@ -10,12 +10,6 @@ from kubernetes.config.config_exception import ConfigException from pytest_operator.plugin import OpsTest -from tests.integration.helpers.common import MONGOS_APP_NAME, get_app_name -from tests.integration.helpers.ldap import LDAP_CERT_OFFER, LDAP_OFFER, teardown_offers -from tests.integration.helpers.sharding import ( - CONFIG_SERVER_APP_NAME, -) - TIMEOUT = 15 * 60 logger = getLogger(__name__) @@ -48,26 +42,4 @@ async def kubernetes_model(ops_test: OpsTest) -> AsyncGenerator[Model]: yield kubernetes_model - for app_name in ( - await get_app_name(ops_test), - await get_app_name(ops_test, CONFIG_SERVER_APP_NAME), - await get_app_name(ops_test, MONGOS_APP_NAME), - ): - if app_name is None: - continue - try: - await ops_test.model.applications[app_name].remove_relation( - f"{LDAP_OFFER}:ldap", f"{app_name}:ldap" - ) - await ops_test.model.applications[app_name].remove_relation( - f"{LDAP_CERT_OFFER}:send-ca-cert", f"{app_name}:ldap-certificate-transfer" - ) - except Exception: - pass - - # Remove the offers and tear down deployment - try: - await teardown_offers(ops_test, kubernetes_model) - except Exception: - pass await ops_test.forget_model(alias="secondary", timeout=TIMEOUT, allow_failure=True) From 615526c1135b78e9279d1ae18a37c39e3360d792 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Wed, 22 Apr 2026 13:50:25 +0200 Subject: [PATCH 24/35] fix: don't asyncio all --- tests/integration/release/test_sharding_release.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index fabe89ba36..4b22156643 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -148,9 +148,10 @@ async def test_deploy_apps( deploy_application( ops_test, application_path=application_path, app_name=CONTINUOUS_WRITE_APPLICATION ), - deploy_glauth(ops_test, kubernetes_model), ) + (await deploy_glauth(ops_test, kubernetes_model),) + # Consume the offers exposed by glauth await consume_glauth_offers(ops_test, kubernetes_model) From ab9608c8d5b312668abd023838f4c5acc4adf5a8 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Wed, 22 Apr 2026 15:41:27 +0200 Subject: [PATCH 25/35] fix: integrations --- .../release/test_sharding_release.py | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 4b22156643..ae6d4770d6 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -150,7 +150,7 @@ async def test_deploy_apps( ), ) - (await deploy_glauth(ops_test, kubernetes_model),) + await deploy_glauth(ops_test, kubernetes_model) # Consume the offers exposed by glauth await consume_glauth_offers(ops_test, kubernetes_model) @@ -186,6 +186,20 @@ async def test_deploy_apps( raise_on_error=False, ) + await ops_test.model.integrate( + f"{MONGOS_APP_NAME}", + f"{CONTINUOUS_WRITE_APPLICATION}", + ) + await ops_test.model.wait_for_idle( + apps=[ + MONGOS_APP_NAME, + CONTINUOUS_WRITE_APPLICATION, + ], + idle_period=15, + timeout=TIMEOUT, + raise_on_blocked=False, + ) + @pytest.mark.abort_on_fail async def test_integrate_with_tls( @@ -229,41 +243,6 @@ async def test_integrate_with_tls( idle_period=60, ) - await ops_test.model.integrate( - f"{MONGOS_APP_NAME}", - f"{CONTINUOUS_WRITE_APPLICATION}", - ) - - await ops_test.model.wait_for_idle( - apps=[ - CONTINUOUS_WRITE_APPLICATION, - MONGOS_APP_NAME, - SHARD_ONE_APP_NAME, - SHARD_TWO_APP_NAME, - CONFIG_SERVER_APP_NAME, - ], - idle_period=20, - raise_on_blocked=False, - timeout=TIMEOUT, - raise_on_error=False, - ) - - await ops_test.model.integrate(CONTINUOUS_WRITE_APPLICATION, TLS_CERTIFICATES_APP_NAME) - - await ops_test.model.wait_for_idle( - apps=[ - CONTINUOUS_WRITE_APPLICATION, - MONGOS_APP_NAME, - SHARD_ONE_APP_NAME, - SHARD_TWO_APP_NAME, - CONFIG_SERVER_APP_NAME, - ], - idle_period=20, - raise_on_blocked=False, - timeout=TIMEOUT, - raise_on_error=False, - ) - await start_continous_writes(ops_test, CONTINUOUS_WRITE_APPLICATION) @@ -307,15 +286,26 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) f"{MONGOS_BIS_APP_NAME}", f"{CONTINUOUS_WRITE_APPLICATION_BIS}", ) + await ops_test.model.wait_for_idle( + apps=[ + MONGOS_BIS_APP_NAME, + CONTINUOUS_WRITE_APPLICATION_BIS, + ], + idle_period=15, + timeout=TIMEOUT, + raise_on_blocked=False, + ) await integrate_apps_with_tls( ops_test, applications=[ MONGOS_BIS_APP_NAME, ], ) - await ops_test.model.integrate( - f"{TLS_CERTIFICATES_APP_NAME}", - f"{CONTINUOUS_WRITE_APPLICATION_BIS}", + + await ops_test.model.wait_for_idle( + apps=[MONGOS_BIS_APP_NAME, CONTINUOUS_WRITE_APPLICATION_BIS, CONFIG_SERVER_APP_NAME], + timeout=DEPLOYMENT_TIMEOUT, + status="active", ) await start_continous_writes( @@ -374,15 +364,26 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): f"{MONGOS_TER_APP_NAME}", f"{READER_APPLICATION}", ) + await ops_test.model.wait_for_idle( + apps=[ + MONGOS_TER_APP_NAME, + READER_APPLICATION, + ], + idle_period=15, + timeout=TIMEOUT, + raise_on_blocked=False, + ) await integrate_apps_with_tls( ops_test, applications=[ MONGOS_TER_APP_NAME, ], ) - await ops_test.model.integrate( - f"{TLS_CERTIFICATES_APP_NAME}", - f"{READER_APPLICATION}", + + await ops_test.model.wait_for_idle( + apps=[MONGOS_TER_APP_NAME, READER_APPLICATION, CONFIG_SERVER_APP_NAME], + timeout=DEPLOYMENT_TIMEOUT, + status="active", ) await start_continuous_reads( From 3cf853ccdda2081c6c37a554aaec748201d04bb5 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Wed, 22 Apr 2026 16:12:38 +0200 Subject: [PATCH 26/35] fix: xlarge runners --- spread.yaml | 4 ++-- tests/spread/release/lxd/test_release.py/task.yaml | 2 +- tests/spread/release/lxd/test_sharding_release.py/task.yaml | 2 +- tests/spread/release/microk8s/test_release.py/task.yaml | 2 +- .../release/microk8s/test_sharding_release.py/task.yaml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spread.yaml b/spread.yaml index f6f4b5fc3f..86e92bbfe5 100644 --- a/spread.yaml +++ b/spread.yaml @@ -124,9 +124,9 @@ backends: username: ubuntu - self-hosted-linux-arm64-noble-medium: username: ubuntu - - self-hosted-linux-amd64-noble-large: + - self-hosted-linux-amd64-noble-xlarge: username: ubuntu - - self-hosted-linux-arm64-noble-large: + - self-hosted-linux-arm64-noble-xlarge: username: ubuntu suites: diff --git a/tests/spread/release/lxd/test_release.py/task.yaml b/tests/spread/release/lxd/test_release.py/task.yaml index 406ef739ea..dbfec19be4 100644 --- a/tests/spread/release/lxd/test_release.py/task.yaml +++ b/tests/spread/release/lxd/test_release.py/task.yaml @@ -4,4 +4,4 @@ environment: execute: | tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - - self-hosted-linux-amd64-noble-medium + - self-hosted-linux-amd64-noble-xlarge diff --git a/tests/spread/release/lxd/test_sharding_release.py/task.yaml b/tests/spread/release/lxd/test_sharding_release.py/task.yaml index 79e0c0848f..b5aeea6fa4 100644 --- a/tests/spread/release/lxd/test_sharding_release.py/task.yaml +++ b/tests/spread/release/lxd/test_sharding_release.py/task.yaml @@ -4,4 +4,4 @@ environment: execute: | tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate lxd --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - - self-hosted-linux-amd64-noble-medium + - self-hosted-linux-amd64-noble-xlarge diff --git a/tests/spread/release/microk8s/test_release.py/task.yaml b/tests/spread/release/microk8s/test_release.py/task.yaml index 11353e07cb..50ad924309 100644 --- a/tests/spread/release/microk8s/test_release.py/task.yaml +++ b/tests/spread/release/microk8s/test_release.py/task.yaml @@ -4,4 +4,4 @@ environment: execute: | tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - - self-hosted-linux-amd64-noble-medium + - self-hosted-linux-amd64-noble-xlarge diff --git a/tests/spread/release/microk8s/test_sharding_release.py/task.yaml b/tests/spread/release/microk8s/test_sharding_release.py/task.yaml index d3118e5d96..989b56594c 100644 --- a/tests/spread/release/microk8s/test_sharding_release.py/task.yaml +++ b/tests/spread/release/microk8s/test_sharding_release.py/task.yaml @@ -4,4 +4,4 @@ environment: execute: | tox run -e integration -- "tests/integration/release/$TEST_MODULE" --substrate microk8s --model testing --mongodb-revision $MONGODB_REVISION --mongos-revision $MONGOS_REVISION" systems: - - self-hosted-linux-amd64-noble-medium + - self-hosted-linux-amd64-noble-xlarge From 844c3968fb942d88c7b06a4fd60c8051d9b296f8 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 23 Apr 2026 09:56:34 +0200 Subject: [PATCH 27/35] fix: Valid integrations --- .../release/test_sharding_release.py | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index ae6d4770d6..8ffe2e9223 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -250,10 +250,13 @@ async def test_integrate_with_ldap(ops_test: OpsTest, substrate: Substrate): """Tests that we can integrate with LDAP without losing data.""" assert ops_test.model await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{CONFIG_SERVER_APP_NAME}:ldap") - await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_APP_NAME}:ldap") await ops_test.model.integrate( f"{LDAP_CERT_OFFER}:send-ca-cert", f"{CONFIG_SERVER_APP_NAME}:ldap-certificate-transfer" ) + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_APP_NAME}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_APP_NAME}:ldap-certificate-transfer" + ) # Create the roles on MongoDB await create_mongodb_user_roles( ops_test, @@ -301,6 +304,10 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) MONGOS_BIS_APP_NAME, ], ) + await ops_test.model.integrate( + f"{MONGOS_BIS_APP_NAME}", + f"{CONFIG_SERVER_APP_NAME}", + ) await ops_test.model.wait_for_idle( apps=[MONGOS_BIS_APP_NAME, CONTINUOUS_WRITE_APPLICATION_BIS, CONFIG_SERVER_APP_NAME], @@ -308,6 +315,16 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) status="active", ) + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" + ) + await ops_test.model.wait_for_idle( + apps=[MONGOS_BIS_APP_NAME, CONTINUOUS_WRITE_APPLICATION_BIS, CONFIG_SERVER_APP_NAME], + timeout=DEPLOYMENT_TIMEOUT, + status="active", + ) + await start_continous_writes( ops_test, CONTINUOUS_WRITE_APPLICATION_BIS, @@ -379,6 +396,21 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): MONGOS_TER_APP_NAME, ], ) + await ops_test.model.integrate( + f"{MONGOS_BIS_APP_NAME}", + f"{CONFIG_SERVER_APP_NAME}", + ) + + await ops_test.model.wait_for_idle( + apps=[MONGOS_TER_APP_NAME, READER_APPLICATION, CONFIG_SERVER_APP_NAME], + timeout=DEPLOYMENT_TIMEOUT, + status="active", + ) + + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" + ) await ops_test.model.wait_for_idle( apps=[MONGOS_TER_APP_NAME, READER_APPLICATION, CONFIG_SERVER_APP_NAME], From 58b5f6eb9d66a0bdee3a723a58ee02e5c94f1667 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 23 Apr 2026 10:13:36 +0200 Subject: [PATCH 28/35] fix: valid integrations --- tests/integration/release/test_sharding_release.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 8ffe2e9223..22c05a83f6 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -309,16 +309,11 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) f"{CONFIG_SERVER_APP_NAME}", ) - await ops_test.model.wait_for_idle( - apps=[MONGOS_BIS_APP_NAME, CONTINUOUS_WRITE_APPLICATION_BIS, CONFIG_SERVER_APP_NAME], - timeout=DEPLOYMENT_TIMEOUT, - status="active", - ) - await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") await ops_test.model.integrate( f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" ) + await ops_test.model.wait_for_idle( apps=[MONGOS_BIS_APP_NAME, CONTINUOUS_WRITE_APPLICATION_BIS, CONFIG_SERVER_APP_NAME], timeout=DEPLOYMENT_TIMEOUT, From ec178d53bcac6d85bfd620ab163910879e9cdbdb Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 23 Apr 2026 11:03:59 +0200 Subject: [PATCH 29/35] fix: valid integrations --- tests/integration/helpers/common.py | 5 +- .../release/test_sharding_release.py | 59 +++++++++---------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/tests/integration/helpers/common.py b/tests/integration/helpers/common.py index 2d036cbe7a..c14feb3965 100644 --- a/tests/integration/helpers/common.py +++ b/tests/integration/helpers/common.py @@ -1184,10 +1184,7 @@ async def count_writes( hosts=[host], username=username, ) - if mongos: - container = "mongos" - else: - container = "mongod" + container = "mongod" if tls: ca_file = await scp_file_preserve_ctime( ops_test, substrate, unit.name, external_cert_path(substrate), container diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 22c05a83f6..bd108aad21 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -289,6 +289,17 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) f"{MONGOS_BIS_APP_NAME}", f"{CONTINUOUS_WRITE_APPLICATION_BIS}", ) + await integrate_apps_with_tls( + ops_test, + applications=[ + MONGOS_BIS_APP_NAME, + ], + ) + + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" + ) await ops_test.model.wait_for_idle( apps=[ MONGOS_BIS_APP_NAME, @@ -298,22 +309,12 @@ async def test_integrate_second_client(ops_test: OpsTest, application_path: str) timeout=TIMEOUT, raise_on_blocked=False, ) - await integrate_apps_with_tls( - ops_test, - applications=[ - MONGOS_BIS_APP_NAME, - ], - ) + await ops_test.model.integrate( f"{MONGOS_BIS_APP_NAME}", f"{CONFIG_SERVER_APP_NAME}", ) - await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") - await ops_test.model.integrate( - f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" - ) - await ops_test.model.wait_for_idle( apps=[MONGOS_BIS_APP_NAME, CONTINUOUS_WRITE_APPLICATION_BIS, CONFIG_SERVER_APP_NAME], timeout=DEPLOYMENT_TIMEOUT, @@ -376,6 +377,17 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): f"{MONGOS_TER_APP_NAME}", f"{READER_APPLICATION}", ) + await integrate_apps_with_tls( + ops_test, + applications=[ + MONGOS_TER_APP_NAME, + ], + ) + + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") + await ops_test.model.integrate( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" + ) await ops_test.model.wait_for_idle( apps=[ MONGOS_TER_APP_NAME, @@ -385,12 +397,6 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): timeout=TIMEOUT, raise_on_blocked=False, ) - await integrate_apps_with_tls( - ops_test, - applications=[ - MONGOS_TER_APP_NAME, - ], - ) await ops_test.model.integrate( f"{MONGOS_BIS_APP_NAME}", f"{CONFIG_SERVER_APP_NAME}", @@ -402,17 +408,6 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): status="active", ) - await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") - await ops_test.model.integrate( - f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" - ) - - await ops_test.model.wait_for_idle( - apps=[MONGOS_TER_APP_NAME, READER_APPLICATION, CONFIG_SERVER_APP_NAME], - timeout=DEPLOYMENT_TIMEOUT, - status="active", - ) - await start_continuous_reads( ops_test, READER_APPLICATION, @@ -486,7 +481,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): leader_unit = await find_unit(ops_test, leader=True, app_name=CONFIG_SERVER_APP_NAME) # count total writes first_number_writes = await count_writes( - ops_test, substrate, CONFIG_SERVER_APP_NAME, leader_unit + ops_test, substrate, CONFIG_SERVER_APP_NAME, leader_unit, tls=True, mongos=True ) second_number_writes = await count_writes( ops_test, @@ -495,6 +490,8 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): leader_unit, db_name=SECOND_DB_NAME, coll_name=SECOND_COLL_NAME, + mongos=True, + tls=True, ) assert first_number_writes == first_reported_writes assert second_number_writes == second_reported_writes @@ -518,7 +515,7 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): ) first_number_writes_after_restore = await count_writes( - ops_test, substrate, CONFIG_SERVER_APP_NAME, leader_unit + ops_test, substrate, CONFIG_SERVER_APP_NAME, leader_unit, tls=True, mongos=True ) second_number_writes_after_restore = await count_writes( ops_test, @@ -527,6 +524,8 @@ async def tests_restore_backup(ops_test: OpsTest, substrate: Substrate): leader_unit, db_name=SECOND_DB_NAME, coll_name=SECOND_COLL_NAME, + tls=True, + mongos=True, ) assert first_number_writes_after_restore < first_number_writes From 763db59ceb70b52e42eb7475002fc7edb654f8f4 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 23 Apr 2026 11:48:41 +0200 Subject: [PATCH 30/35] fix: take your time --- .../release/test_sharding_release.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index bd108aad21..b15375a224 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -199,14 +199,6 @@ async def test_deploy_apps( timeout=TIMEOUT, raise_on_blocked=False, ) - - -@pytest.mark.abort_on_fail -async def test_integrate_with_tls( - ops_test: OpsTest, -): - """Tests that we can integrate with TLS, and then add a writer and start writing.""" - assert ops_test.model await ops_test.model.integrate( f"{MONGOS_APP_NAME}", f"{CONFIG_SERVER_APP_NAME}", @@ -215,13 +207,21 @@ async def test_integrate_with_tls( apps=[ CONTINUOUS_WRITE_APPLICATION, MONGOS_APP_NAME, - SHARD_ONE_APP_NAME, CONFIG_SERVER_APP_NAME, ], + status="active", idle_period=20, timeout=TIMEOUT, ) + +@pytest.mark.abort_on_fail +async def test_integrate_with_tls( + ops_test: OpsTest, +): + """Tests that we can integrate with TLS, and then add a writer and start writing.""" + assert ops_test.model + await integrate_apps_with_tls( ops_test, applications=[ From e23577950a05537bb86a5feaeab319140f020fde Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Thu, 23 Apr 2026 13:29:00 +0200 Subject: [PATCH 31/35] fix: integrate correct app --- tests/integration/release/test_sharding_release.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index b15375a224..8f54382929 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -384,9 +384,9 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): ], ) - await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_BIS_APP_NAME}:ldap") + await ops_test.model.integrate(f"{LDAP_OFFER}:ldap", f"{MONGOS_TER_APP_NAME}:ldap") await ops_test.model.integrate( - f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_BIS_APP_NAME}:ldap-certificate-transfer" + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{MONGOS_TER_APP_NAME}:ldap-certificate-transfer" ) await ops_test.model.wait_for_idle( apps=[ @@ -398,7 +398,7 @@ async def test_integrate_third_client(ops_test: OpsTest, application_path: str): raise_on_blocked=False, ) await ops_test.model.integrate( - f"{MONGOS_BIS_APP_NAME}", + f"{MONGOS_TER_APP_NAME}", f"{CONFIG_SERVER_APP_NAME}", ) From 2778f8d0d37f5c1a33c5f59f68e1d612890ac01d Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Fri, 24 Apr 2026 11:59:53 +0200 Subject: [PATCH 32/35] fix: final fixes --- .../release/test_sharding_release.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 8f54382929..e37f930686 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -542,19 +542,31 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): uri = await generate_mongodb_ldap_client( ops_test, substrate, - CONFIG_SERVER_APP_NAME, + MONGOS_APP_NAME, database=DEFAULT_DATABASE_NAME, username="cn=johndoe,ou=superheroes,ou=users,dc=glauth,dc=com", password="dogood", ) result = await execute_on_mongod( - ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.insertOne({number: 1})", tls=True + ops_test, + MONGOS_APP_NAME, + substrate, + uri, + "db.test.insertOne({number: 1})", + tls=True, + container_name="mongos", ) assert result.succeeded, "Failed to insert value with LDAP client" result = await execute_on_mongod( - ops_test, CONFIG_SERVER_APP_NAME, substrate, uri, "db.test.findOne({number: 1})", tls=True + ops_test, + MONGOS_APP_NAME, + substrate, + uri, + "db.test.findOne({number: 1})", + tls=True, + container_name="mongos", ) assert result.succeeded, "Failed to read value with LDAP client" @@ -565,8 +577,8 @@ async def test_valid_reads(ops_test: OpsTest): reads, failed_reads = await stop_continuous_reads( ops_test, READER_APPLICATION, - db_name=DEFAULT_DATABASE_NAME, - coll_name=DEFAULT_COLLECTION_NAME, + db_name=SECOND_DB_NAME, + coll_name=SECOND_COLL_NAME, ) assert reads > 1000 # We can allow for a few errors during restore. From 410b19054267de788f3141e14f7aaa945c8a3eb6 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Fri, 24 Apr 2026 13:26:16 +0200 Subject: [PATCH 33/35] fix: final fixes --- tests/integration/release/test_sharding_release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index e37f930686..6fa274174d 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -546,6 +546,7 @@ async def test_ldap_user_can_write(ops_test: OpsTest, substrate: Substrate): database=DEFAULT_DATABASE_NAME, username="cn=johndoe,ou=superheroes,ou=users,dc=glauth,dc=com", password="dogood", + mongos=True, ) result = await execute_on_mongod( From cd0d381080cc7608bc3fe6c816563bd97c6b45e3 Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Fri, 24 Apr 2026 15:26:38 +0200 Subject: [PATCH 34/35] fix: teardown --- tests/integration/release/test_release.py | 17 ++++++++++ .../release/test_sharding_release.py | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index ffc7895824..42fb49c3e1 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -43,6 +43,7 @@ create_mongodb_user_roles, deploy_glauth, generate_mongodb_ldap_client, + teardown_offers, ) from tests.integration.helpers.tls import ( TLS_CERTIFICATES_APP_NAME, @@ -369,3 +370,19 @@ async def test_valid_reads(ops_test: OpsTest): assert reads > 1000 # We can allow for a few errors during restore for example assert len(failed_reads) < 50 + + +@pytest.mark.abort_on_fail +async def test_teardown(ops_test: OpsTest, kubernetes_model: Model): + """Teardown of the whole offers and relations.""" + app_name = await get_app_name(ops_test) + assert app_name + + # Removing the second relation should go into active + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_OFFER}:ldap", f"{app_name}:ldap" + ) + await ops_test.model.wait_for_idle(apps=[app_name], status="active", timeout=TIMEOUT) + + # Remove the offers and tear down deployment + await teardown_offers(ops_test, kubernetes_model) diff --git a/tests/integration/release/test_sharding_release.py b/tests/integration/release/test_sharding_release.py index 6fa274174d..c5a0cea670 100644 --- a/tests/integration/release/test_sharding_release.py +++ b/tests/integration/release/test_sharding_release.py @@ -42,6 +42,7 @@ create_mongodb_user_roles, deploy_glauth, generate_mongodb_ldap_client, + teardown_offers, ) from tests.integration.helpers.sharding import ( CONFIG_SERVER_APP_NAME, @@ -584,3 +585,34 @@ async def test_valid_reads(ops_test: OpsTest): assert reads > 1000 # We can allow for a few errors during restore. assert len(failed_reads) < 10 + + +@pytest.mark.abort_on_fail +async def test_teardown(ops_test: OpsTest, kubernetes_model: Model): + for app_name in (MONGOS_APP_NAME, MONGOS_BIS_APP_NAME, MONGOS_TER_APP_NAME): + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_OFFER}:ldap", f"{app_name}:ldap" + ) + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{app_name}:ldap-certificate-transfer" + ) + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_OFFER}:ldap", f"{CONFIG_SERVER_APP_NAME}:ldap" + ) + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{CONFIG_SERVER_APP_NAME}:ldap-certificate-transfer" + ) + + await ops_test.model.wait_for_idle( + apps=[ + CONFIG_SERVER_APP_NAME, + SHARD_ONE_APP_NAME, + SHARD_TWO_APP_NAME, + MONGOS_APP_NAME, + MONGOS_BIS_APP_NAME, + MONGOS_TER_APP_NAME, + ], + status="active", + timeout=TIMEOUT, + ) + await teardown_offers(ops_test, kubernetes_model) From 2878cfaeed706063a08e0b484d72c3514cbf595a Mon Sep 17 00:00:00 2001 From: Neha Oudin Date: Fri, 24 Apr 2026 16:19:01 +0200 Subject: [PATCH 35/35] fix: remove both relations --- tests/integration/release/test_release.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/release/test_release.py b/tests/integration/release/test_release.py index 42fb49c3e1..69e6b6d55e 100644 --- a/tests/integration/release/test_release.py +++ b/tests/integration/release/test_release.py @@ -378,10 +378,12 @@ async def test_teardown(ops_test: OpsTest, kubernetes_model: Model): app_name = await get_app_name(ops_test) assert app_name - # Removing the second relation should go into active await ops_test.model.applications[app_name].remove_relation( f"{LDAP_OFFER}:ldap", f"{app_name}:ldap" ) + await ops_test.model.applications[app_name].remove_relation( + f"{LDAP_CERT_OFFER}:send-ca-cert", f"{app_name}:ldap-certificate-transfer" + ) await ops_test.model.wait_for_idle(apps=[app_name], status="active", timeout=TIMEOUT) # Remove the offers and tear down deployment