From 72eedc10b4a197d634af96446590bebeba3e98ba Mon Sep 17 00:00:00 2001 From: Daniel Arndt Date: Wed, 18 Oct 2023 13:25:33 -0300 Subject: [PATCH] Set units to blocked status when scaling beyond 1 Instead of throwing an error, block the units when scaled beyond 1. Throwing an error causes the charm to go into a bad state, where it can't even be removed without manual intervention. This even causes an issue with a single unit, as sometimes leader status is removed before the charm has finished processing all events. --- src/charm.py | 8 +++++++- tests/integration/test_integration.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/charm.py b/src/charm.py index 74b1783..08543f2 100755 --- a/src/charm.py +++ b/src/charm.py @@ -51,7 +51,13 @@ class PCFOperatorCharm(CharmBase): def __init__(self, *args): super().__init__(*args) if not self.unit.is_leader(): - raise NotImplementedError("Scaling is not implemented for this charm") + # NOTE: In cases where leader status is lost before the charm is + # finished processing all teardown events, this prevents teardown + # event code from running. Luckily, for this charm, none of the + # teardown code is necessary to preform if we're removing the + # charm. + self.unit.status = BlockedStatus("Scaling is not implemented for this charm") + return self._container_name = self._service_name = "pcf" self._container = self.unit.get_container(self._container_name) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 7d528be..7b9bf59 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -4,10 +4,12 @@ import logging +from collections import Counter from pathlib import Path import pytest import yaml +from juju.application import Application from pytest_operator.plugin import OpsTest logger = logging.getLogger(__name__) @@ -134,3 +136,23 @@ async def test_restore_tls_and_wait_for_active_status(ops_test: OpsTest, build_a relation1=APPLICATION_NAME, relation2=TLS_PROVIDER_NAME ) await ops_test.model.wait_for_idle(apps=[APPLICATION_NAME], status="active", timeout=1000) # type: ignore[union-attr] # noqa: E501 + + +@pytest.mark.abort_on_fail +async def test_when_scale_app_beyond_1_then_only_one_unit_is_active( + ops_test: OpsTest, build_and_deploy +): + assert ops_test.model + assert isinstance(app := ops_test.model.applications[APPLICATION_NAME], Application) + await app.scale(3) + await ops_test.model.wait_for_idle( + apps=[APPLICATION_NAME], timeout=1000, wait_for_at_least_units=3 + ) + unit_statuses = Counter(unit.workload_status for unit in app.units) + assert unit_statuses.get("active") == 1 + assert unit_statuses.get("blocked") == 2 + + +async def test_remove_app(ops_test: OpsTest, build_and_deploy): + assert ops_test.model + await ops_test.model.remove_application(APPLICATION_NAME, block_until_done=True)