-
Notifications
You must be signed in to change notification settings - Fork 2
feat(garm): drain resources before application removal #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |
| CredentialSpec, | ||
| GithubReconciler, | ||
| ) | ||
| from resource_cleanup import GarmResourceCleanup | ||
| from scaleset_reconciler import ScalesetReconciler, ScalesetSpec | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
@@ -162,12 +163,25 @@ def __init__(self, *args: typing.Any) -> None: | |
| self._reconcile, | ||
| ) | ||
| self.framework.observe(self.on.update_status, self._reconcile) | ||
| self.framework.observe(self.on.remove, self._on_remove) | ||
|
|
||
| @block_if_invalid_data | ||
| def _reconcile(self, _: ops.EventBase) -> None: | ||
| """Reconcile charm state.""" | ||
| self.restart() | ||
|
|
||
| def _on_remove(self, _: ops.RemoveEvent) -> None: | ||
| """Drain GARM resources before Juju removes the application.""" | ||
| admin_creds = self._get_admin_credentials() | ||
| if not admin_creds: | ||
| raise RuntimeError("GARM admin credentials are unavailable; refusing removal") | ||
|
|
||
| base_url = f"http://127.0.0.1:{GARM_PORT}/api/v1" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the base_url be a constant? |
||
| auth_client = GarmAuthenticatedClient.from_login( | ||
| base_url, admin_creds["username"], admin_creds["password"] | ||
| ) | ||
| GarmResourceCleanup(auth_client).run() | ||
|
|
||
| def _on_get_credentials_action(self, event: ops.ActionEvent) -> None: | ||
| """Return the GARM admin credentials to the operator. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -499,6 +499,32 @@ def test_missing_configurator_relation_prunes_orphaned_scalesets( | |
| assert out.unit_status == ops.WaitingStatus("Waiting for garm-configurator relation") | ||
|
|
||
|
|
||
| def test_remove_runs_garm_cleanup_before_charm_termination(ctx: Context, garm_api: _GarmApiMocks): | ||
| """ | ||
| arrange: A ready charm with GARM admin credentials. | ||
| act: Emit the application remove event. | ||
| assert: Cleanup runs with the authenticated client before removal completes. | ||
| """ | ||
| with patch("charm.GarmResourceCleanup") as cleanup_cls: | ||
| out = ctx.run(ctx.on.remove(), _state(secrets=_owned_secrets())) | ||
|
|
||
| cleanup_cls.assert_called_once_with(garm_api.auth_client) | ||
| cleanup_cls.return_value.run.assert_called_once_with() | ||
| assert out is not None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just a question, is there a way to assert the state, ie that the actual resource is removed, rather than just asserting that a method is called?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, that would be:
However, I think this isn't quite worth the effort here because we may end up testing the fake client's implementation. |
||
|
|
||
|
|
||
| def test_remove_refuses_without_admin_credentials(ctx: Context, garm_api: _GarmApiMocks): | ||
| """ | ||
| arrange: A charm without GARM admin credentials. | ||
| act: Emit the application remove event. | ||
| assert: Removal fails and no authenticated client is created. | ||
| """ | ||
| with pytest.raises(UncaughtCharmError, match="credentials are unavailable"): | ||
| ctx.run(ctx.on.remove(), _state()) | ||
|
|
||
| garm_api.auth.from_login.assert_not_called() | ||
|
|
||
|
|
||
| def test_unpopulated_configurator_relation_does_not_prune( | ||
| ctx: Context, garm_api: _GarmApiMocks, caplog: pytest.LogCaptureFixture | ||
| ): | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we add this removal flow to an existing integration test (if any)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, although I think we may have skipped that to reduce the cost of heavy integration test. If it doesn't exist, i'll test it via staging.