Repository: canonical/mongo-single-kernel-library
Pull request #19: [DPE-6591] get statuses charm and backup (context free!)
Author: login: MiaAltieri
Labels: name: Libraries: Out of sync
description: The charm libs used are out-of-sync
Created: 2025-02-28T15:49:19Z | Merged: 2025-03-14T15:25:13Z

<!-- Provide a general summary of your changes in the Title above -->
<!-- markdownlint-disable MD041 -->

## 🏷 Type of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation Update
- [ ] Tooling and CI changes
- [ ] Dependencies upgrade or change

## 📝 Description
Update backup + charm (MongoDB VM) to have function that can compute the state/status of the charm regardless of the context

## 📑 Motivation and Context
We want to begin preparing the charm for Advanced Status Handling. Before migrating all managers to have a status compute function we first try with backup + mongodb charm manager

## Future work
1. add status computers for all managers
2. move all statuses to config file
3. implement advanced status handling 

## 🧪 How Has This Been Tested?
Charm was tested by injecting the code into the MongoDB operator and manually verifying behavior

## ✅ Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have read the [**CONTRIBUTING**](../blob/6/edge/CONTRIBUTING.md) document.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.



Diff:
diff --git a/single_kernel_mongo/config/statuses.py b/single_kernel_mongo/config/statuses.py
new file mode 100644
index 0000000000..a892572f18
--- /dev/null
+++ b/single_kernel_mongo/config/statuses.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from enum import Enum
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+class MongoDBStatuses(Enum):
+    """MongoDB related statuses.
+
+    TODO: add the remaining statuses for mongodb charm.
+    """
+
+    MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+    EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+    SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+    UNSUPPORTED_MONGOS_REL = BlockedStatus(
+        "Relation to mongos not supported, config role must be config-server"
+    )
+    INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+        "Relation to s3-integrator is not supported, config role must be config-server."
+    )
+
+
+class MongosStatuses(Enum):
+    """Mongos related statuses.
+
+    TODO: add the remaining statuses for mongos charm.
+    """
+
+    ...
+
+
+class CharmStatuses(Enum):
+    """Charm Statuses.
+
+    TODO: add remaining statuses related to the two charms.
+    """
+
+    MONGODB_NOT_INSTALLED = BlockedStatus("MongoDB not installed")
+    mongodb = MongoDBStatuses
+    mongos = MongosStatuses
+
+
+class BackupStatuses(Enum):
+    """Backup manager related statuses.
+
+    TODO: add the remaining statuses for backup manager.
+    """
+
+    # note unlike other daemons (exporter and mongod) this status belongs to the backup manager
+    # since certain configurations are required for pbm to be active and running.
+    PBM_NOT_STARTED = WaitingStatus("waiting for pbm to start")
+    PBM_MISSING_CONFIGS = BlockedStatus("s3 configurations missing.")
+    PBM_INCORRECT_CREDS = BlockedStatus("s3 credentials are incorrect.")
+    PBM_INCOMPATIBLE_CONF = BlockedStatus("s3 configurations are incompatible.")
+    UNKNOWN_PBM_ERROR = BlockedStatus("Unknown PBM error, check logs")
diff --git a/single_kernel_mongo/events/lifecycle.py b/single_kernel_mongo/events/lifecycle.py
index 2de97fa11b..fee2bd78ef 100644
--- a/single_kernel_mongo/events/lifecycle.py
+++ b/single_kernel_mongo/events/lifecycle.py
@@ -72,7 +72,8 @@ def __init__(self, dependent: OperatorProtocol, rel_name: PeerRelationNames):
 
         if self.charm.substrate == Substrates.K8S:
             self.framework.observe(
-                getattr(self.charm.on, f"{dependent.name.value}_pebble_ready"), self.on_start
+                getattr(self.charm.on, f"{dependent.name.value}_pebble_ready"),
+                self.on_start,
             )
 
         self.framework.observe(getattr(self.charm.on, "config_changed"), self.on_config_changed)
@@ -91,10 +92,12 @@ def __init__(self, dependent: OperatorProtocol, rel_name: PeerRelationNames):
 
         if self.dependent.name == CharmKind.MONGOD:
             self.framework.observe(
-                getattr(self.charm.on, "mongodb_storage_attached"), self.on_storage_attached
+                getattr(self.charm.on, "mongodb_storage_attached"),
+                self.on_storage_attached,
             )
             self.framework.observe(
-                getattr(self.charm.on, "mongodb_storage_detaching"), self.on_storage_detaching
+                getattr(self.charm.on, "mongodb_storage_detaching"),
+                self.on_storage_detaching,
             )
 
         if self.charm.substrate == Substrates.VM and self.dependent.name == CharmKind.MONGOD:
@@ -136,10 +139,7 @@ def on_config_changed(self, event: ConfigChangedEvent):
 
     def on_update_status(self, event: UpdateStatusEvent):
         """Update Status Event."""
-        try:
-            self.dependent.on_update_status()
-        except Exception:
-            return
+        self.dependent.on_update_status()
 
     def on_secret_changed(self, event: SecretChangedEvent):
         """Secret changed event."""
diff --git a/single_kernel_mongo/managers/backups.py b/single_kernel_mongo/managers/backups.py
index 19f73f4b1a..8f172bd30c 100644
--- a/single_kernel_mongo/managers/backups.py
+++ b/single_kernel_mongo/managers/backups.py
@@ -47,6 +47,7 @@
     Substrates,
 )
 from single_kernel_mongo.config.models import CharmSpec
+from single_kernel_mongo.config.statuses import BackupStatuses
 from single_kernel_mongo.core.status_provider import StatusProvider
 from single_kernel_mongo.core.structured_config import MongoDBRoles
 from single_kernel_mongo.exceptions import (
@@ -66,7 +67,9 @@
 from single_kernel_mongo.workload.backup_workload import PBMWorkload
 
 if TYPE_CHECKING:
-    from single_kernel_mongo.managers.mongodb_operator import MongoDBOperator  # pragma: nocover
+    from single_kernel_mongo.managers.mongodb_operator import (
+        MongoDBOperator,
+    )  # pragma: nocover
 
 BackupListType = NewType("BackupListType", list[tuple[str, str, str]])
 
@@ -281,9 +284,10 @@ def restore_backup(self, backup_id: str, remapping_pattern: str | None = None) -
             raise RestoreError(fail_message)
 
     def get_status(self) -> StatusBase | None:
-        """Gets the PBM status."""
-        if not self.workload.active():
-            return WaitingStatus("waiting for pbm to start")
+        """Gets the PBM status.
+
+        TODO: once implementation spec is approved update this function to return multiple statuses
+        """
         if not self.state.s3_relation:
             logger.info("No configuration for backups, not relation to s3-charm")
             return None
@@ -291,13 +295,18 @@ def get_status(self) -> StatusBase | None:
             logger.info(
                 "Relation to S3 charm exists but not all necessary configurations have been set."
             )
-            return BlockedStatus("s3 configurations missing.")
+            return BackupStatuses.PBM_MISSING_CONFIGS.value
+
+        # PBM requires all configuration to be set in order to run.
+        if not self.workload.active():
+            return BackupStatuses.PBM_NOT_STARTED.value
+
         try:
             previous_status = self.charm.unit.status
             pbm_status = self.pbm_status
-            pbm_error = self.process_pbm_error(pbm_status)
-            if pbm_error:
-                return BlockedStatus(pbm_error)
+
+            if pbm_error := self.process_pbm_error(pbm_status):
+                return pbm_error
 
             processed_status = self.process_pbm_status(pbm_status)
             operation_result = self._get_backup_restore_operation_result(
@@ -307,7 +316,7 @@ def get_status(self) -> StatusBase | None:
             return processed_status
         except Exception as e:
             logger.error(f"Failed to get pbm status: {e}")
-            return BlockedStatus("PBM error")
+            return BackupStatuses.UNKNOWN_PBM_ERROR.value
 
     def resync_config_options(self):  # pragma: nocover
         """Attempts to resync config options and sets status in case of failure."""
@@ -387,7 +396,9 @@ def set_config_options(self, credentials: dict[str, str]) -> None:
         for pbm_key, pbm_value in config.items():
             try:
                 self.workload.run_bin_command(
-                    "config", ["--set", f"{pbm_key}={pbm_value}"], environment=self.environment
+                    "config",
+                    ["--set", f"{pbm_key}={pbm_value}"],
+                    environment=self.environment,
                 )
             except WorkloadExecError:
                 logger.error(f"Failed to configure PBM option: {pbm_key}")
@@ -401,7 +412,9 @@ def clear_pbm_config_file(self) -> None:
                 "# this file is to be left empty. Changes in this file will be ignored.\n",
             )
         self.workload.run_bin_command(
-            "config", ["--file", str(self.workload.paths.pbm_config)], environment=self.environment
+            "config",
+            ["--file", str(self.workload.paths.pbm_config)],
+            environment=self.environment,
         )
 
     def retrieve_error_message(self, pbm_status: dict) -> str:
@@ -439,10 +452,10 @@ def get_backup_error_status(self, backup_id: str) -> str:
 
         return ""
 
-    def process_pbm_error(self, pbm_status: str) -> str:
+    def process_pbm_error(self, pbm_status: str) -> StatusBase | None:
         """Look up PBM status for errors."""
         error_message: str
-        message = ""
+
         try:
             pbm_as_dict = json.loads(pbm_status)
             error_message = self.retrieve_error_message(pbm_as_dict)
@@ -450,12 +463,17 @@ def process_pbm_error(self, pbm_status: str) -> str:
             error_message = pbm_status
 
         if StatusCodeError.FORBIDDEN in error_message:
-            message = "s3 credentials are incorrect."
-        elif StatusCodeError.NOTFOUND in error_message:
-            message = "s3 configurations are incompatible."
-        elif StatusCodeError.MOVED_PERMANENTLY in error_message:
-            message = "s3 configurations are incompatible."
-        return message
+            return BackupStatuses.PBM_INCORRECT_CREDS.value
+        if StatusCodeError.NOTFOUND in error_message:
+            return BackupStatuses.PBM_INCOMPATIBLE_CONF.value
+        if StatusCodeError.MOVED_PERMANENTLY in error_message:
+            return BackupStatuses.PBM_INCOMPATIBLE_CONF.value
+
+        if error_message:
+            logger.info("PBM error: %s", error_message)
+            return BackupStatuses.UNKNOWN_PBM_ERROR.value
+
+        return None
 
     def process_pbm_status(self, pbm_status: str) -> StatusBase:
         """Processes the pbm status if there's no error."""
@@ -560,9 +578,7 @@ def _wait_pbm_status(self) -> None:
                 )
                 raise ResyncError
         except WorkloadExecError as e:
-            self.charm.status_manager.set_and_share_status(
-                BlockedStatus(self.process_pbm_error(e.stdout))
-            )
+            self.charm.status_manager.set_and_share_status(self.process_pbm_error(e.stdout))
 
     def _get_backup_restore_operation_result(
         self, current_pbm_status: StatusBase, previous_pbm_status: StatusBase
diff --git a/single_kernel_mongo/managers/mongodb_operator.py b/single_kernel_mongo/managers/mongodb_operator.py
index 182a5719b9..199530fcfa 100644
--- a/single_kernel_mongo/managers/mongodb_operator.py
+++ b/single_kernel_mongo/managers/mongodb_operator.py
@@ -14,7 +14,7 @@
     get_charm_revision,
 )
 from ops.framework import Object
-from ops.model import Container, MaintenanceStatus, Unit
+from ops.model import Container, MaintenanceStatus, StatusBase, Unit
 from pymongo.errors import PyMongoError, ServerSelectionTimeoutError
 from tenacity import Retrying, stop_after_attempt, wait_fixed
 from typing_extensions import override
@@ -30,18 +30,24 @@
 )
 from single_kernel_mongo.config.models import ROLES
 from single_kernel_mongo.config.relations import RelationNames
+from single_kernel_mongo.config.statuses import CharmStatuses
 from single_kernel_mongo.core.kubernetes_upgrades import KubernetesUpgrade
 from single_kernel_mongo.core.machine_upgrades import MachineUpgrade
 from single_kernel_mongo.core.operator import OperatorProtocol
 from single_kernel_mongo.core.secrets import generate_secret_label
 from single_kernel_mongo.core.structured_config import MongoDBRoles
 from single_kernel_mongo.core.version_checker import VersionChecker
-from single_kernel_mongo.events.backups import INVALID_S3_INTEGRATION_STATUS, BackupEventsHandler
+from single_kernel_mongo.events.backups import (
+    BackupEventsHandler,
+)
 from single_kernel_mongo.events.cluster import ClusterConfigServerEventHandler
 from single_kernel_mongo.events.database import DatabaseEventsHandler
 from single_kernel_mongo.events.password_actions import PasswordActionEvents
 from single_kernel_mongo.events.primary_action import PrimaryActionHandler
-from single_kernel_mongo.events.sharding import ConfigServerEventHandler, ShardEventHandler
+from single_kernel_mongo.events.sharding import (
+    ConfigServerEventHandler,
+    ShardEventHandler,
+)
 from single_kernel_mongo.events.tls import TLSEventsHandler
 from single_kernel_mongo.events.upgrades import UpgradeEventHandler
 from single_kernel_mongo.exceptions import (
@@ -541,7 +547,10 @@ def on_storage_detaching(self) -> None:
         try:
             # retries over a period of 10 minutes in an attempt to resolve race conditions it is
             # not possible to defer in storage detached.
-            logger.debug("Removing %s from replica set", self.state.unit_peer_data.internal_address)
+            logger.debug(
+                "Removing %s from replica set",
+                self.state.unit_peer_data.internal_address,
+            )
             for attempt in Retrying(
                 stop=stop_after_attempt(600),
                 wait=wait_fixed(1),
@@ -556,12 +565,19 @@ def on_storage_detaching(self) -> None:
                 self.charm.unit.name,
             )
         except PyMongoError as e:
-            logger.error("Failed to remove %s from replica set, error=%r", self.charm.unit.name, e)
+            logger.error(
+                "Failed to remove %s from replica set, error=%r",
+                self.charm.unit.name,
+                e,
+            )
 
     @override
     def on_update_status(self) -> None:
         """Status update Handler."""
-        if not self.pass_status_basic_checks():
+        # TODO update the usage of this once the spec is approved and we have a consistent way of
+        # handling statuses
+        if charm_statuses := self.get_statuses():
+            self.charm.status_manager.set_and_share_status(charm_statuses[0])
             return
 
         if self.state.is_role(MongoDBRoles.SHARD):
@@ -787,30 +803,6 @@ def _restart_related_services(self) -> None:
             self.charm.status_manager.to_blocked("couldn't start pbm-agent")
             raise
 
-    def pass_status_basic_checks(self) -> bool:
-        """Integration and initial checks for update-status events."""
-        if not self.state.is_sharding_component and self.state.has_sharding_integration:
-            self.charm.status_manager.to_blocked("sharding interface cannot be used by replicas")
-            return False
-        if not self.backup_manager.is_valid_s3_integration():
-            self.charm.status_manager.to_blocked(INVALID_S3_INTEGRATION_STATUS)
-            return False
-        if (
-            revision_mismatch_status
-            := self.cluster_version_checker.get_cluster_mismatched_revision_status()
-        ):
-            self.charm.status_manager.set_and_share_status(revision_mismatch_status)
-            return False
-        if not self.cluster_manager.is_valid_mongos_integration():
-            self.charm.status_manager.to_blocked(
-                "Relation to mongos not supported, config role must be config-server"
-            )
-            return False
-        if not self.state.db_initialised:
-            return False
-
-        return True
-
     @override
     def is_relation_feasible(self, rel_name: str) -> bool:
         """Checks if the relation is feasible in the current context.
@@ -901,3 +893,32 @@ def _initialise_replica_set(self):
     def is_removing_last_replica(self) -> bool:
         """Returns True if the last replica (juju unit) is getting removed."""
         return self.state.planned_units == 0 and len(self.state.peers_units) == 0
+
+    def get_statuses(self) -> list[StatusBase]:
+        """Returns the statuses of the charm manager.."""
+        charm_statuses: list[StatusBase] = []
+
+        if not self.workload.workload_present:
+            charm_statuses.append(CharmStatuses.MONGODB_NOT_INSTALLED.value)
+        else:  # don't bother checking if started if not installed
+            if not self.state.db_initialised:
+                charm_statuses.append(CharmStatuses.mongodb.value.MONGODB_NOT_STARTED.value)
+
+            if not self.mongodb_exporter_config_manager.workload.active():
+                charm_statuses.append(CharmStatuses.mongodb.value.EXPORTER_NOT_STARTED.value)
+
+        if not self.state.is_sharding_component and self.state.has_sharding_integration:
+            charm_statuses.append(CharmStatuses.mongodb.value.SHARDING_ON_REPLICA.value)
+        elif (  # don't bother checking revision mismatch on sharding interface if replica
+            revision_mismatch_status
+            := self.cluster_version_checker.get_cluster_mismatched_revision_status()
+        ):
+            charm_statuses.append(revision_mismatch_status)
+
+        if not self.cluster_manager.is_valid_mongos_integration():
+            charm_statuses.append(CharmStatuses.mongodb.value.UNSUPPORTED_MONGOS_REL.value)
+
+        if not self.backup_manager.is_valid_s3_integration():
+            charm_statuses.append(CharmStatuses.mongodb.value.INVALID_S3_INTEGRATION_STATUS.value)
+
+        return charm_statuses
diff --git a/tests/unit/test_backup_manager.py b/tests/unit/test_backup_manager.py
index 4744f31f8b..0abcc0f51c 100644
--- a/tests/unit/test_backup_manager.py
+++ b/tests/unit/test_backup_manager.py
@@ -67,15 +67,20 @@ def test_environment_is_valid(harness: Harness[MongoTestCharm]):
 
 def test_get_status_fail(harness: Harness[MongoTestCharm], mocker):
     backup_manager = harness.charm.operator.backup_manager
+    status = backup_manager.get_status()
+    assert status is None
+
+    mocker.patch(
+        "single_kernel_mongo.managers.backups.BackupManager.validate_s3_config",
+        return_value=True,
+    )
+
+    harness.add_relation(ExternalRequirerRelations.S3_CREDENTIALS.value, "s3-integrator")
 
     mocker.patch("single_kernel_mongo.core.vm_workload.VMWorkload.active", return_value=False)
     status = backup_manager.get_status()
     assert status == WaitingStatus("waiting for pbm to start")
 
-    mocker.patch("single_kernel_mongo.core.vm_workload.VMWorkload.active", return_value=True)
-    status = backup_manager.get_status()
-    assert status is None
-
 
 @pytest.mark.parametrize(
     ("pbm_status", "expected"),
@@ -83,7 +88,7 @@ def test_get_status_fail(harness: Harness[MongoTestCharm], mocker):
         ("status code: 403", "s3 credentials are incorrect."),
         ("status code: 404", "s3 configurations are incompatible."),
         ("status code: 301", "s3 configurations are incompatible."),
-        ("Unknown message", "PBM error"),
+        ("Unknown message", "Unknown PBM error, check logs"),
         (
             '{"cluster": [{"nodes":[{"host": "mongodb/1.1.1.1:27018", "errors": "status code: 403"}], "rs": "test-mongodb"}]}',
             "s3 credentials are incorrect.",
@@ -97,7 +102,8 @@ def test_get_status_pbm_error(harness: Harness[MongoTestCharm], mocker, pbm_stat
     harness.charm.operator.state.app_peer_data.role = MongoDBRoles.REPLICATION.value
     mocker.patch("single_kernel_mongo.core.vm_workload.VMWorkload.active", return_value=True)
     mocker.patch(
-        "single_kernel_mongo.managers.backups.BackupManager.validate_s3_config", return_value=True
+        "single_kernel_mongo.managers.backups.BackupManager.validate_s3_config",
+        return_value=True,
     )
     relation_id = harness.add_relation(
         ExternalRequirerRelations.S3_CREDENTIALS.value, "s3-integrator"
@@ -362,7 +368,8 @@ def test_can_restore_fail_params(
     )
     harness.add_relation_unit(relation_id, "s3-integrator/0")
     mocker.patch(
-        "single_kernel_mongo.managers.backups.BackupManager.get_status", return_value=ActiveStatus()
+        "single_kernel_mongo.managers.backups.BackupManager.get_status",
+        return_value=ActiveStatus(),
     )
     mocker.patch(
         "single_kernel_mongo.managers.backups.BackupManager._needs_provided_remap_arguments",


Reviews:
user: login: Gu1nness
body: That is great work and I love the interface!
I have left a few questions on the statuses config but overall I love what I'm seeing :heart_eyes: 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T13:10:06Z
user: login: MiaAltieri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:24:33Z
user: login: MiaAltieri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:24:51Z
user: login: Gu1nness
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T10:35:40Z
user: login: MiaAltieri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:45:54Z
user: login: MiaAltieri
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T14:46:06Z
user: login: Gu1nness
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:47:31Z
user: login: Mehdi-Bendriss
body: Thanks Mia! 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:12:09Z

Review comments:
diff_hunk: @@ -3,7 +3,7 @@
 
 [project]
 name = "mongo-charms-single-kernel"
-version = "0.0.3"
+version = "0.0.4"
path: pyproject.toml
user: login: Gu1nness
body: This will have to be updated, I published 0.0.4 a few minutes ago.

created_at: 2025-03-04T13:00:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from dataclasses import dataclass
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+@dataclass(frozen=True)
+class CharmStatuses:
+    """Charm Statuses.
+
+    TODO: add remaining statuses related to the two charms.
+    """
+
+    MONGODB_NOT_INSTALLED = BlockedStatus("MongoDB not installed")
+
+    class MongoDB:
+        """MongoDB related statuses.
+
+        TODO: add the remaining statuses for mongodb charm.
+        """
+
+        MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+        EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+        SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+        UNSUPPORTED_MONGOS_REL = BlockedStatus(
+            "Relation to mongos not supported, config role must be config-server"
+        )
+        INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+            "Relation to s3-integrator is not supported, config role must be config-server."
+        )
+
+    class Mongos:
+        """Mongos related statuses.
+
+        TODO: add the remaining statuses for mongos charm.
+        """
+
+        ...
path: single_kernel_mongo/config/statuses.py
user: login: Gu1nness
body: I'm not sure I'm comfortable with nested classes like that unless absolutely necessary.
I'm also not sure why you set CharmStatuses as a dataclass if there is no field attribute in the class?

Can you please explain why we need this structure compared to:
 * Inheritance with a `CharmStatuses` and then `MongoDBCharmStatuses(CharmStatuses)` and `MongosCharmStatuses(CharmStatuses)`?
 * Two enums (one for MongoDB and one for Mongos) + those two enums as field attributes in this dataclass?

created_at: 2025-03-04T13:09:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from dataclasses import dataclass
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+@dataclass(frozen=True)
+class CharmStatuses:
+    """Charm Statuses.
+
+    TODO: add remaining statuses related to the two charms.
+    """
+
+    MONGODB_NOT_INSTALLED = BlockedStatus("MongoDB not installed")
+
+    class MongoDB:
+        """MongoDB related statuses.
+
+        TODO: add the remaining statuses for mongodb charm.
+        """
+
+        MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+        EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+        SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+        UNSUPPORTED_MONGOS_REL = BlockedStatus(
+            "Relation to mongos not supported, config role must be config-server"
+        )
+        INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+            "Relation to s3-integrator is not supported, config role must be config-server."
+        )
+
+    class Mongos:
+        """Mongos related statuses.
+
+        TODO: add the remaining statuses for mongos charm.
+        """
+
+        ...
+
+
+@dataclass(frozen=True)
+class BackupStatuses:
+    """Backup manager related statuses.
+
+    TODO: add the remaining statuses for backup manager.
+    """
+
+    # note unlike other daemons (exporter and mongod) this status belongs to the backup manager
+    # since certain configurations are required for pbm to be active and running.
+    PBM_NOT_STARTED = WaitingStatus("waiting for pbm to start")
+    PBM_MISSING_CONFIGS = BlockedStatus("s3 configurations missing.")
+    PBM_INCORRECT_CREDS = BlockedStatus("s3 credentials are incorrect.")
+    PBM_INCOMPATIBLE_CONF = BlockedStatus("s3 configurations are incompatible.")
+    UNKNOWN_PBM_ERROR = BlockedStatus("Unknown PBM error, check logs")
path: single_kernel_mongo/config/statuses.py
user: login: Gu1nness
body: Why is this a dataclass and not an enum?
created_at: 2025-03-04T13:09:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from dataclasses import dataclass
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+@dataclass(frozen=True)
+class CharmStatuses:
+    """Charm Statuses.
+
+    TODO: add remaining statuses related to the two charms.
+    """
+
+    MONGODB_NOT_INSTALLED = BlockedStatus("MongoDB not installed")
+
+    class MongoDB:
+        """MongoDB related statuses.
+
+        TODO: add the remaining statuses for mongodb charm.
+        """
+
+        MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+        EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+        SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+        UNSUPPORTED_MONGOS_REL = BlockedStatus(
+            "Relation to mongos not supported, config role must be config-server"
+        )
+        INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+            "Relation to s3-integrator is not supported, config role must be config-server."
+        )
+
+    class Mongos:
+        """Mongos related statuses.
+
+        TODO: add the remaining statuses for mongos charm.
+        """
+
+        ...
path: single_kernel_mongo/config/statuses.py
user: login: MiaAltieri
body: Thanks for using this as a knowledge transfer opportunity! I made the change we discussed and am so grateful for your help! 
created_at: 2025-03-12T10:24:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from dataclasses import dataclass
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+@dataclass(frozen=True)
+class CharmStatuses:
+    """Charm Statuses.
+
+    TODO: add remaining statuses related to the two charms.
+    """
+
+    MONGODB_NOT_INSTALLED = BlockedStatus("MongoDB not installed")
+
+    class MongoDB:
+        """MongoDB related statuses.
+
+        TODO: add the remaining statuses for mongodb charm.
+        """
+
+        MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+        EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+        SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+        UNSUPPORTED_MONGOS_REL = BlockedStatus(
+            "Relation to mongos not supported, config role must be config-server"
+        )
+        INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+            "Relation to s3-integrator is not supported, config role must be config-server."
+        )
+
+    class Mongos:
+        """Mongos related statuses.
+
+        TODO: add the remaining statuses for mongos charm.
+        """
+
+        ...
+
+
+@dataclass(frozen=True)
+class BackupStatuses:
+    """Backup manager related statuses.
+
+    TODO: add the remaining statuses for backup manager.
+    """
+
+    # note unlike other daemons (exporter and mongod) this status belongs to the backup manager
+    # since certain configurations are required for pbm to be active and running.
+    PBM_NOT_STARTED = WaitingStatus("waiting for pbm to start")
+    PBM_MISSING_CONFIGS = BlockedStatus("s3 configurations missing.")
+    PBM_INCORRECT_CREDS = BlockedStatus("s3 credentials are incorrect.")
+    PBM_INCOMPATIBLE_CONF = BlockedStatus("s3 configurations are incompatible.")
+    UNKNOWN_PBM_ERROR = BlockedStatus("Unknown PBM error, check logs")
path: single_kernel_mongo/config/statuses.py
user: login: MiaAltieri
body: this was a miss as discussed in our 1:1 - again thank you for the help! 
created_at: 2025-03-12T10:24:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from enum import Enum
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+class MongoDB(Enum):
+    """MongoDB related statuses.
+
+    TODO: add the remaining statuses for mongodb charm.
+    """
+
+    MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+    EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+    SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+    UNSUPPORTED_MONGOS_REL = BlockedStatus(
+        "Relation to mongos not supported, config role must be config-server"
+    )
+    INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+        "Relation to s3-integrator is not supported, config role must be config-server."
+    )
+
+
+class Mongos(Enum):
+    """Mongos related statuses.
+
+    TODO: add the remaining statuses for mongos charm.
+    """
+
+    ...
+
+
+class CharmStatuses(Enum):
+    """Charm Statuses.
+
+    TODO: add remaining statuses related to the two charms.
+    """
+
+    MONGODB_NOT_INSTALLED = BlockedStatus("MongoDB not installed")
+    mongodb = MongoDB
+    mongos = Mongos
path: single_kernel_mongo/config/statuses.py
user: login: Gu1nness
body: Along with the two other suggestions on this file
```suggestion
    mongodb = MongoDBStatuses()
    mongos = MongosStatuses()
```
created_at: 2025-03-12T10:29:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from enum import Enum
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+class MongoDB(Enum):
+    """MongoDB related statuses.
+
+    TODO: add the remaining statuses for mongodb charm.
+    """
+
+    MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+    EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+    SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+    UNSUPPORTED_MONGOS_REL = BlockedStatus(
+        "Relation to mongos not supported, config role must be config-server"
+    )
+    INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+        "Relation to s3-integrator is not supported, config role must be config-server."
+    )
+
+
+class Mongos(Enum):
path: single_kernel_mongo/config/statuses.py
user: login: Gu1nness
body: Can we change the name to a more specific one?
```suggestion
class MongosStatuses(Enum):
```
created_at: 2025-03-12T10:30:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from enum import Enum
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+class MongoDB(Enum):
path: single_kernel_mongo/config/statuses.py
user: login: Gu1nness
body: Can we change the name to a more specific one?
```suggestion
class MongoDBStatuses(Enum):
```
created_at: 2025-03-12T10:30:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -901,3 +893,32 @@ def _initialise_replica_set(self):
     def is_removing_last_replica(self) -> bool:
         """Returns True if the last replica (juju unit) is getting removed."""
         return self.state.planned_units == 0 and len(self.state.peers_units) == 0
+
+    def get_statuses(self) -> list[StatusBase]:
+        """Returns the statuses of the charm manager.."""
+        charm_statuses: list[StatusBase] = []
+
+        if not self.workload.workload_present:
+            charm_statuses.append(CharmStatuses.MONGODB_NOT_INSTALLED)
+        else:  # don't bother checking if started if not installed
+            if not self.state.db_initialised:
+                charm_statuses.append(CharmStatuses.mongodb.MONGODB_NOT_STARTED)
+
+            if not self.mongodb_exporter_config_manager.workload.active():
+                charm_statuses.append(CharmStatuses.mongodb.EXPORTER_NOT_STARTED)
+
+        if not self.state.is_sharding_component and self.state.has_sharding_integration:
+            charm_statuses.append(CharmStatuses.mongodb.SHARDING_ON_REPLICA)
+        elif (  # don't bother checking revision mismatch on sharding interface if replica
+            revision_mismatch_status
+            := self.cluster_version_checker.get_cluster_mismatched_revision_status()
+        ):
+            charm_statuses.append(revision_mismatch_status)
+
+        if not self.cluster_manager.is_valid_mongos_integration():
+            charm_statuses.append(CharmStatuses.mongodb.UNSUPPORTED_MONGOS_REL)
+
+        if not self.backup_manager.is_valid_s3_integration():
+            charm_statuses.append(CharmStatuses.mongodb.INVALID_S3_INTEGRATION_STATUS)
path: single_kernel_mongo/managers/mongodb_operator.py
user: login: Gu1nness
body: For all this, you have to add the value of the enum not the enum entry itself.
While for many cases it's the same thing, it doesn't hold the same type (and mypy complains about it, see [here](https://github.com/canonical/mongo-single-kernel-library/actions/runs/13809027912/job/38626126234?pr=19#step:6:51)

If mypy is too painful about this, let me know :)
created_at: 2025-03-12T10:35:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from enum import Enum
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+class MongoDB(Enum):
+    """MongoDB related statuses.
+
+    TODO: add the remaining statuses for mongodb charm.
+    """
+
+    MONGODB_NOT_STARTED = WaitingStatus("Waiting to start mongod...")
+    EXPORTER_NOT_STARTED = WaitingStatus("Waiting to start mongodb-exporter...")
+    SHARDING_ON_REPLICA = BlockedStatus("sharding interface cannot be used by replicas")
+    UNSUPPORTED_MONGOS_REL = BlockedStatus(
+        "Relation to mongos not supported, config role must be config-server"
+    )
+    INVALID_S3_INTEGRATION_STATUS = BlockedStatus(
+        "Relation to s3-integrator is not supported, config role must be config-server."
+    )
+
+
+class Mongos(Enum):
path: single_kernel_mongo/config/statuses.py
user: login: MiaAltieri
body: yes! good idea!
created_at: 2025-03-12T14:45:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Copyright 2025 Canonical Ltd.
+# See LICENSE file for licensing details.
+
+"""File containing all possible statuses for Mongo* charms.
+
+TODO (Future PR(s)):
+- Add all statuses here
+- Update to be consistent with the spec
+
+Note: The structure of this file is subject to change, as the implementation spec is still in
+progress. However the idea that all statuses belong in one file holds true regardless of the spec.
+"""
+
+from enum import Enum
+
+from ops.model import BlockedStatus, WaitingStatus
+
+
+class MongoDB(Enum):
path: single_kernel_mongo/config/statuses.py
user: login: MiaAltieri
body: yes good idea!
created_at: 2025-03-12T14:46:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: MiaAltieri
created_at: 2025-02-28T15:53:32Z
body: /test/mongodb/k8s


author_association: CONTRIBUTOR
user: login: MiaAltieri
created_at: 2025-02-28T15:53:40Z
body: /test/mongodb/vm


author_association: CONTRIBUTOR
user: login: MiaAltieri
created_at: 2025-03-04T13:04:39Z
body: 
/test/mongodb/k8s


author_association: CONTRIBUTOR
user: login: MiaAltieri
created_at: 2025-03-04T13:04:45Z
body: 
/test/mongodb/vm


author_association: CONTRIBUTOR
user: login: MiaAltieri
created_at: 2025-03-04T13:21:50Z
body: 
/test/mongodb/k8s



author_association: CONTRIBUTOR
user: login: MiaAltieri
created_at: 2025-03-04T13:21:57Z
body: /test/mongodb/vm
author_association: CONTRIBUTOR
user: login: Gu1nness
created_at: 2025-03-04T13:28:20Z
body: /test/mongodb/k8s
author_association: CONTRIBUTOR
user: login: Gu1nness
created_at: 2025-03-04T13:40:44Z
body: /test/mongodb/k8s
author_association: CONTRIBUTOR
user: login: Gu1nness
created_at: 2025-03-04T14:50:34Z
body: /test/mongodb/k8s 
author_association: CONTRIBUTOR
user: login: Gu1nness
created_at: 2025-03-04T14:57:48Z
body: /test/mongodb/k8s
author_association: CONTRIBUTOR
user: login: Gu1nness
created_at: 2025-03-04T14:58:06Z
body: Sorry to use your PR as test for the action Mia :sob: 

author_association: CONTRIBUTOR
user: login: MiaAltieri
created_at: 2025-03-04T16:06:47Z
body: @Gu1nness please do use it as a test! I am excited to see it working! 
author_association: CONTRIBUTOR