Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions charmcraft/templates/init-kubernetes/charmcraft.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ config:
default: "info"
type: string

# (Optional) Actions that an operator can run against a deployed unit.
# More information on this section at:
# https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/#actions
# General actions documentation:
# https://documentation.ubuntu.com/juju/3.6/reference/action/
actions:
# An example action to restart the workload in the container.
restart:
description: |
Restart the workload service in the container.

Use this to apply a change that the workload only picks up on startup,
or to recover a workload that has stopped responding.

# Your workload's containers.
# https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/#containers
containers:
Expand Down
16 changes: 16 additions & 0 deletions charmcraft/templates/init-kubernetes/src/charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class {{ class_name }}(ops.CharmBase):
def __init__(self, framework: ops.Framework):
super().__init__(framework)
framework.observe(self.on["some_container"].pebble_ready, self._on_pebble_ready)
framework.observe(self.on["restart"].action, self._on_restart_action)
self.container = self.unit.get_container("some-container")

def _on_pebble_ready(self, event: ops.PebbleReadyEvent):
Expand Down Expand Up @@ -51,6 +52,21 @@ class {{ class_name }}(ops.CharmBase):
self.unit.set_workload_version(version)
self.unit.status = ops.ActiveStatus()

def _on_restart_action(self, event: ops.ActionEvent):
"""Handle the restart action."""
self.unit.status = ops.MaintenanceStatus("restarting workload")
try:
self.container.restart(SERVICE_NAME)
self.wait_for_ready()
except ops.pebble.ConnectionError:
# Pebble isn't running yet, or the container is going away.
# Checking with 'can_connect' before restarting wouldn't help: the
# container can stop being reachable between the check and the restart.
self.unit.status = ops.WaitingStatus("waiting for the workload container")
event.fail("cannot connect to the workload container")
return
self.unit.status = ops.ActiveStatus()

def is_ready(self) -> bool:
"""Check whether the workload is ready to use."""
# We'll first check whether all Pebble services are running.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def test_deploy(charm: pathlib.Path, juju: jubilant.Juju):
juju.wait(jubilant.all_active)


def test_restart(charm: pathlib.Path, juju: jubilant.Juju):
"""Restart the workload with the restart action."""
juju.run("{{ name }}/0", "restart")
juju.wait(jubilant.all_active)


# If you implement {{ workload_module }}.get_version in the charm source,
# remove the @pytest.mark.skip line to enable this test.
# Alternatively, remove this test if you don't need it.
Expand Down
44 changes: 44 additions & 0 deletions charmcraft/templates/init-kubernetes/tests/unit/test_charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,47 @@ def test_pebble_ready_service_not_ready():
# Act & assert:
with pytest.raises(testing.errors.UncaughtCharmError):
ctx.run(ctx.on.pebble_ready(container_in), state_in)


def test_restart_action():
"""Test that the restart action starts the service and leaves the unit active."""
# Arrange:
ctx = testing.Context({{ class_name }})
check_in = testing.CheckInfo(
CHECK_NAME,
level=ops.pebble.CheckLevel.READY,
status=ops.pebble.CheckStatus.UP, # Simulate the Pebble check passing.
)
container_in = testing.Container(
"some-container",
can_connect=True,
layers={"base": MOCK_LAYER},
service_statuses={SERVICE_NAME: ops.pebble.ServiceStatus.INACTIVE},
check_infos={check_in},
)
state_in = testing.State(containers={container_in})

# Act:
state_out = ctx.run(ctx.on.action("restart"), state_in)

# Assert:
container_out = state_out.get_container(container_in.name)
assert container_out.service_statuses[SERVICE_NAME] == ops.pebble.ServiceStatus.ACTIVE
assert state_out.unit_status == testing.ActiveStatus()


def test_restart_action_container_not_ready():
"""Test that the restart action fails when the container can't be reached."""
# Arrange:
ctx = testing.Context({{ class_name }})
container_in = testing.Container("some-container", can_connect=False)
state_in = testing.State(containers={container_in})

# Act & assert:
with pytest.raises(testing.ActionFailed) as exc_info:
ctx.run(ctx.on.action("restart"), state_in)
assert exc_info.value.message == "cannot connect to the workload container"
assert exc_info.value.state is not None
assert exc_info.value.state.unit_status == testing.WaitingStatus(
"waiting for the workload container"
)
18 changes: 18 additions & 0 deletions charmcraft/templates/init-machine/charmcraft.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,21 @@ config:
Acceptable values are: "info", "debug", "warning", "error" and "critical"
default: "info"
type: string

# (Optional) Actions that an operator can run against a deployed unit.
# More information on this section at:
# https://documentation.ubuntu.com/charmcraft/stable/reference/files/charmcraft-yaml-file/#actions
# General actions documentation:
# https://documentation.ubuntu.com/juju/3.6/reference/action/
actions:
# Example actions to take a unit out of service and put it back again.
pause:
description: |
Stop the workload, leaving the unit deployed.

Use this to take a unit out of service, for example before performing
maintenance on the machine. Run the resume action to start the workload
again.
resume:
description: |
Start the workload again after running the pause action.
13 changes: 13 additions & 0 deletions charmcraft/templates/init-machine/src/charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class {{ class_name }}(ops.CharmBase):
super().__init__(framework)
framework.observe(self.on.install, self._on_install)
framework.observe(self.on.start, self._on_start)
framework.observe(self.on["pause"].action, self._on_pause_action)
framework.observe(self.on["resume"].action, self._on_resume_action)

def _on_install(self, event: ops.InstallEvent):
"""Install the workload on the machine."""
Expand All @@ -35,6 +37,17 @@ class {{ class_name }}(ops.CharmBase):
self.unit.set_workload_version(version)
self.unit.status = ops.ActiveStatus()

def _on_pause_action(self, event: ops.ActionEvent):
"""Handle the pause action: stop the workload, but leave the unit deployed."""
{{ workload_module }}.stop()
self.unit.status = ops.MaintenanceStatus("paused: run the resume action to start again")

def _on_resume_action(self, event: ops.ActionEvent):
"""Handle the resume action: start the workload again."""
self.unit.status = ops.MaintenanceStatus("starting workload")
{{ workload_module }}.start()
self.unit.status = ops.ActiveStatus()


if __name__ == "__main__": # pragma: nocover
ops.main({{ class_name }})
6 changes: 6 additions & 0 deletions charmcraft/templates/init-machine/src/workload.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ def start() -> None:
# Ideally, this function should only return once the workload is ready to use.


def stop() -> None:
"""Stop the workload (by running a command, for example)."""
# You'll need to implement this function.
# Ideally, this function should only return once the workload has stopped.


# Functions for interacting with the workload, for example over HTTP:


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def test_deploy(charm: pathlib.Path, juju: jubilant.Juju):
juju.wait(jubilant.all_active)


def test_pause_and_resume(charm: pathlib.Path, juju: jubilant.Juju):
"""Take the unit out of service with the pause action, then put it back."""
juju.run("{{ name }}/0", "pause")
juju.wait(jubilant.all_maintenance)
juju.run("{{ name }}/0", "resume")
juju.wait(jubilant.all_active)


# If you implement {{ workload_module }}.get_version in the charm source,
# remove the @pytest.mark.skip line to enable this test.
# Alternatively, remove this test if you don't need it.
Expand Down
21 changes: 21 additions & 0 deletions charmcraft/templates/init-machine/tests/unit/test_charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,24 @@ def test_start(monkeypatch: pytest.MonkeyPatch):
# Assert:
assert state_out.workload_version is not None
assert state_out.unit_status == testing.ActiveStatus()


def test_pause_action():
"""Test that the unit reports being paused after the pause action runs."""
# Arrange:
ctx = testing.Context({{ class_name }})
# Act:
state_out = ctx.run(ctx.on.action("pause"), testing.State())
# Assert:
assert isinstance(state_out.unit_status, testing.MaintenanceStatus)


def test_resume_action():
"""Test that the unit is active again after the resume action runs."""
# Arrange:
ctx = testing.Context({{ class_name }})
state_in = testing.State(unit_status=testing.MaintenanceStatus("paused"))
# Act:
state_out = ctx.run(ctx.on.action("resume"), state_in)
# Assert:
assert state_out.unit_status == testing.ActiveStatus()
11 changes: 11 additions & 0 deletions docs/release-notes/charmcraft-4.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ Minor features
Charmcraft 4.5 brings the following minor changes.


Example actions in the machine and Kubernetes profiles
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Charms created with the ``machine`` profile now declare ``pause`` and ``resume``
actions, which stop and start the workload without removing the unit. Charms created
with the ``kubernetes`` profile now declare a ``restart`` action, which restarts the
workload's Pebble service.

Both profiles scaffold unit and integration tests for their actions.


<Feature A>
~~~~~~~~~~~

Expand Down
Loading