Skip to content

Commit a531e4b

Browse files
authored
Merge pull request #849 from juju/application-status-from-api
#849 #### Description Determining the status of an application is a convoluted and logically a bit tricky process in Juju. Some applications are reported as in the `error` state not because of its units' `workload_status` or the `agent_status`, but for instance in caas models because of the `operator_status`, and through the API this can only be observed in the `DetailedStatus` of an `Application` coming from the `FullStatus` (and not through our conventional way of getting a delta about it via the AllWatcher). Therefore pylibjuju is unable to see that the application is in the error state, while in the `juju status` we clearly see that it is, in fact, in the error state. [This LP bug](https://bugs.launchpad.net/juju/+bug/1981833) is an example of this scenario (see the QA steps below to reproduce it). This change remedies this by introducing `application.get_status()` that calls the `FullStatus` to get the most up to date status info from the API and include that in our `derive_status` process. #### QA Steps This requires a caas model, as it's not observed in machine models. (this is because the `admission-webhook` charm transitions into the error state for different reasons in machine models than on the caas models). Additionally, for the same reason, it's not possible to make an integration test as our tests are set up to run on the lxd. Though maybe devising an elaborate setup to simulate that behavior through the API might be possible. Bootstrap a k8s model on microk8s. ``` $ juju bootstrap microk8s removeme ``` Deploy the `admission-webhook` charm. ``` $ juju deploy admission-webhook --channel 1.4/stable ``` It will go into the `error` state. Confirm this with `juju status`. It may take a couple seconds to get there. Now run `wait_for_idle` as follows. I modified one of the examples in the `examples` folder. ```python await model.wait_for_idle( apps=["admission-webhook"], status="active", raise_on_blocked=True, raise_on_error=True, timeout=1500, ) ``` As also said in the reported [LP bug](https://bugs.launchpad.net/juju/+bug/1981833), without this PR this `wait_for_idle` call returns, because the application status is set as `active` (because all the unit workload_statuses are active and the agent_status is idle). With this change, it will correctly raise the `JujuAppError` because the application's status is correctly set to `error`, coming through the API. #### Notes & Discussion This should probably be cherry-picked onto the main branch later on.
2 parents d1c9cb2 + 4efa266 commit a531e4b

3 files changed

Lines changed: 40 additions & 7 deletions

File tree

juju/application.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,15 @@ def status(self):
9191
"""
9292
status = self.safe_data['status']['current']
9393
if status == "unset":
94-
unit_status = []
94+
known_statuses = []
9595
for unit in self.units:
96-
unit_status.append(unit.workload_status)
97-
return derive_status(unit_status)
96+
known_statuses.append(unit.workload_status)
97+
# If the self.get_status() is called (i.e. the status
98+
# is received by FullStatus from the API) then add
99+
# that into this computation as it might be more up
100+
# to date (and more severe).
101+
known_statuses.append(self._status)
102+
return derive_status(known_statuses)
98103
return status
99104

100105
@property
@@ -445,6 +450,23 @@ async def get_actions(self, schema=False):
445450
actions = {k: v.description for k, v in actions.items()}
446451
return actions
447452

453+
async def get_status(self):
454+
"""Get the application status using info from the FullStatus
455+
as well, because it might be more up to date than our model
456+
457+
:return: str status
458+
"""
459+
460+
client_facade = client.ClientFacade.from_connection(self.connection)
461+
462+
full_status = await client_facade.FullStatus(patterns=None)
463+
_app = full_status.applications.get(self.name, None)
464+
if not _app:
465+
raise JujuError(f"application is not in FullStatus : {self.name}")
466+
467+
self._status = derive_status([self.status, _app.status.status])
468+
return self._status
469+
448470
def attach_resource(self, resource_name, file_name, file_obj):
449471
"""Updates the resource for an application by uploading file from
450472
local disk to the Juju controller.

juju/model.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ def __init__(self, entity_id, model, history_index=-1, connected=True):
264264
self._history_index = history_index
265265
self.connected = connected
266266
self.connection = model.connection()
267+
self._status = 'unknown'
267268

268269
def __repr__(self):
269270
return '<{} entity_id="{}">'.format(type(self).__name__,
@@ -2516,9 +2517,10 @@ def _raise_for_status(entities, status):
25162517
busy.append(app_name + " (missing)")
25172518
continue
25182519
app = self.applications[app_name]
2519-
if raise_on_error and app.status == "error":
2520+
app_status = await app.get_status()
2521+
if raise_on_error and app_status == "error":
25202522
errors.setdefault("App", []).append(app.name)
2521-
if raise_on_blocked and app.status == "blocked":
2523+
if raise_on_blocked and app_status == "blocked":
25222524
blocks.setdefault("App", []).append(app.name)
25232525
# Check if wait_for_exact_units flag is used
25242526
if wait_for_exact_units > 0:

tests/unit/test_model.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from juju import jasyncio
1414
from juju.errors import JujuConnectionError
1515

16+
from .. import base
17+
1618

1719
def _make_delta(entity, type_, data=None):
1820
from juju.client.client import Delta
@@ -297,7 +299,7 @@ async def test_timeout(self):
297299
async def test_wait_for_active_status(self):
298300
# create a custom apps mock
299301
from types import SimpleNamespace
300-
apps = {"dummy_app": SimpleNamespace(
302+
app = SimpleNamespace(
301303
status="active",
302304
units=[SimpleNamespace(
303305
name="mockunit/0",
@@ -306,7 +308,14 @@ async def test_wait_for_active_status(self):
306308
machine=None,
307309
agent_status="idle",
308310
)],
309-
)}
311+
)
312+
# This is a small hack to act like we're getting 'unknown'
313+
# from the api (the get_status() call), which shouldn't
314+
# change the semantics of this test, as the 'unknown'
315+
# has the lowest severity (so the app's 'active' status
316+
# will overrule it)
317+
app.get_status = base.AsyncMock(return_value='unknown')
318+
apps = {"dummy_app": app}
310319

311320
with patch.object(Model, 'applications', new_callable=PropertyMock) as mock_apps:
312321
mock_apps.return_value = apps

0 commit comments

Comments
 (0)