diff --git a/kepler/charmcraft.yaml b/kepler/charmcraft.yaml index 6b14eae..fa58923 100644 --- a/kepler/charmcraft.yaml +++ b/kepler/charmcraft.yaml @@ -47,6 +47,17 @@ containers: demo-server: resource: demo-server-image +actions: + snapshot: + description: Take a snapshot of the database. + params: + filename: + type: string + description: The name of the snapshot file. + required: + - filename + additionalProperties: false + resources: # An OCI image resource for the container listed above. demo-server-image: diff --git a/kepler/src/charm.py b/kepler/src/charm.py index f31b740..16f3b2d 100755 --- a/kepler/src/charm.py +++ b/kepler/src/charm.py @@ -31,6 +31,23 @@ def __init__(self, framework: ops.Framework) -> None: super().__init__(framework) self.pebble_service_name = "fastapi-service" framework.observe(self.on["demo-server"].pebble_ready, self._on_demo_server_pebble_ready) + framework.observe(self.on["snapshot"].action, self._on_snapshot_action) + + def _on_snapshot_action(self, event: ops.ActionEvent) -> None: + """Handle the snapshot action, mirroring the ops docs example.""" + filename = event.params["filename"] + event.log(f"Generating snapshot into {filename}") + success = self.do_snapshot(filename=filename) + if not success: + event.fail("Failed to generate snapshot.") + return + msg = f"Stored snapshot in {filename}." + event.set_results({"result": msg}) + + def do_snapshot(self, *, filename: str) -> bool: + """Pretend to take a snapshot of the database.""" + del filename # Unused in this demo stub. + return True def _on_demo_server_pebble_ready(self, event: ops.PebbleReadyEvent) -> None: """Define and start a workload using the Pebble API.""" diff --git a/kepler/tests/unit/test_snapshot_action.py b/kepler/tests/unit/test_snapshot_action.py new file mode 100644 index 0000000..f830534 --- /dev/null +++ b/kepler/tests/unit/test_snapshot_action.py @@ -0,0 +1,44 @@ +# Copyright 2026 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ + +"""Tests validating the ops 'manage actions' unit-test example. + +The docs (https://canonical.com/juju/docs/ops/latest/howto/manage-actions/#write-unit-tests) +show a snapshot action handler that calls ``event.set_results({'result': msg})`` +and then a unit test that asserts ``'snapshot-size' in ctx.action_results``. + +The handler only ever sets a ``result`` key, so ``snapshot-size`` cannot be +present in the results. This test asserts that understanding: if it passes, the +documented assertion would fail and the doc example is incorrect. +""" + +from ops import testing + +from charm import KosmosCharm + + +def test_snapshot_action_does_not_set_snapshot_size(): + """The snapshot action sets 'result', not 'snapshot-size'.""" + ctx = testing.Context(KosmosCharm) + ctx.run( + ctx.on.action("snapshot", params={"filename": "db-snapshot.tar.gz"}), + testing.State(), + ) + # The doc's handler sets results to {'result': msg}, so 'snapshot-size' + # is absent. The documented assertion ``'snapshot-size' in + # ctx.action_results`` would therefore fail. + assert "snapshot-size" not in ctx.action_results + assert ctx.action_results == {"result": "Stored snapshot in db-snapshot.tar.gz."}