diff --git a/.gitignore b/.gitignore index 5ec90e3e..6a9256bb 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ build/ # Editor/IDE .idea/ -.vscode/ \ No newline at end of file +.vscode/ + +# Git worktrees +.worktrees/ diff --git a/charms/garm/charmcraft.yaml b/charms/garm/charmcraft.yaml index d6dca0b6..8da138f9 100644 --- a/charms/garm/charmcraft.yaml +++ b/charms/garm/charmcraft.yaml @@ -32,3 +32,8 @@ requires: limit: 1 garm-configurator: interface: garm_configurator_v0 + +actions: + get-credentials: + description: Show the GARM admin credentials. + diff --git a/charms/garm/src/charm.py b/charms/garm/src/charm.py index 50f0b787..7ca84c6f 100755 --- a/charms/garm/src/charm.py +++ b/charms/garm/src/charm.py @@ -292,6 +292,9 @@ def __init__(self, *args: typing.Any) -> None: self.on[GARM_CONFIGURATOR_RELATION_NAME].relation_broken, self._reconcile, ) + self.framework.observe( + self.on.get_credentials_action, self._on_get_credentials_action + ) self.framework.observe(self.on.update_status, self._reconcile) @block_if_invalid_data @@ -299,6 +302,18 @@ def _reconcile(self, _: ops.EventBase) -> None: """Reconcile charm state.""" self.restart() + def _on_get_credentials_action(self, event: ops.ActionEvent) -> None: + """Return the GARM admin credentials to the operator. + + Args: + event: The action event. + """ + credentials = self._get_admin_credentials() + if credentials is None: + event.fail("GARM admin credentials are not yet available") + return + event.set_results(credentials) + @property def _workload_config(self) -> WorkloadConfig: """Pin GARM to a fixed port and disable the default metrics scrape job. diff --git a/charms/garm/tests/unit/test_charm.py b/charms/garm/tests/unit/test_charm.py index de41a791..3a12a330 100644 --- a/charms/garm/tests/unit/test_charm.py +++ b/charms/garm/tests/unit/test_charm.py @@ -520,6 +520,44 @@ def test_maybe_first_run_skips_on_missing_credential_key(): mock_client_cls.assert_not_called() +def test_get_credentials_action_returns_credentials_when_available(): + """ + arrange: Admin credentials secret exists and contains valid content. + act: Call _on_get_credentials_action(). + """ + charm = MagicMock() + event = MagicMock() + credentials = { + "username": "admin", + "password": "s3cr3t", + "email": "admin@garm.local", + "full-name": "GARM Admin", + } + charm._get_admin_credentials.return_value = credentials + + GarmCharm._on_get_credentials_action(charm, event) + + event.set_results.assert_called_once_with(credentials) + event.fail.assert_not_called() + + +def test_get_credentials_action_fails_when_credentials_unavailable(): + """ + arrange: Admin credentials secret does not exist yet. + act: Call _on_get_credentials_action(). + assert: event.fail is called with a message containing "not yet available" and + event.set_results is not called. + """ + charm = MagicMock() + event = MagicMock() + charm._get_admin_credentials.return_value = None + + GarmCharm._on_get_credentials_action(charm, event) + + event.fail.assert_called_once() + fail_message = event.fail.call_args[0][0] + assert "not yet available" in fail_message + event.set_results.assert_not_called() def test_reconcile_scalesets_skips_when_no_admin_credentials(): """ arrange: Admin credentials secret is unavailable. diff --git a/charms/tests/integration/test_garm.py b/charms/tests/integration/test_garm.py index 6386401d..44493650 100644 --- a/charms/tests/integration/test_garm.py +++ b/charms/tests/integration/test_garm.py @@ -770,3 +770,31 @@ def _wait_for_scaleset( ) return scaleset + +def test_garm_get_credentials_action( + juju: jubilant.Juju, + garm_app: str, +): + """ + arrange: The GARM charm is deployed and active (leader has initialized secrets). + act: Run the get-credentials action on the first unit. + assert: The action succeeds and returns all four credential fields (username, + password, email, full-name), with username "admin" and email "admin@garm.local". + """ + unit = f"{garm_app}/0" + logger.info("Running get-credentials action on unit %s", unit) + + # juju.run() raises TaskError if the action fails, so a clean return means success. + task = juju.run(unit, "get-credentials") + + for key in ("username", "password", "email", "full-name"): + assert key in task.results, ( + f"Expected '{key}' in action results, got: {list(task.results)}" + ) + assert task.results["username"] == "admin", ( + f"Expected username 'admin', got: {task.results['username']!r}" + ) + assert task.results["email"] == "admin@garm.local", ( + f"Expected email 'admin@garm.local', got: {task.results['email']!r}" + ) + assert task.results["password"], "Expected non-empty password in action results" diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 27cca196..18f9172b 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -8,3 +8,4 @@ The following guides cover key processes and common tasks for managing and using Contribute Enable log forwarding + Retrieve GARM admin credentials diff --git a/docs/how-to/retrieve-garm-credentials.md b/docs/how-to/retrieve-garm-credentials.md new file mode 100644 index 00000000..94e59339 --- /dev/null +++ b/docs/how-to/retrieve-garm-credentials.md @@ -0,0 +1,39 @@ +# How to retrieve GARM admin credentials + +The `get-credentials` action retrieves the GARM admin credentials generated by the charm during start-up. Use it when you need to authenticate via the GARM API or when registering new profiles using `garm-cli`. + +## Prerequisites + +The GARM charm must be fully initialized. The unit status must be `active` before running this action. If the charm has not completed its first-run setup, the credentials will not yet be available and the action will fail. + +## Run the action + +```bash +juju run garm/0 get-credentials +``` + +## Action output + +The action returns four fields: + +| Field | Description | +|-------|-------------| +| `username` | The GARM admin username. | +| `password` | The GARM admin password. | +| `email` | The email address associated with the admin account. | +| `full-name` | The full name associated with the admin account. | + +Example output: + +```{terminal} +:output-only: + +Running operation 1 with 1 task + - task 2 on unit-garm-0 + +Waiting for task 2... +email: admin@garm.local +full-name: GARM Admin +password: +username: admin +```