From 380cdd009eed0baa5eff198e7cf5296941d907e4 Mon Sep 17 00:00:00 2001 From: tphan025 Date: Wed, 24 Jun 2026 18:25:09 +0200 Subject: [PATCH 1/8] chore: add .worktrees/ to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5ec90e3e..a1afc1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,7 @@ build/ # Editor/IDE .idea/ -.vscode/ \ No newline at end of file +.vscode/ + +# Git worktrees +.worktrees/ \ No newline at end of file From 37f436000f9b33d5f04f18ce11ad34392cede409 Mon Sep 17 00:00:00 2001 From: tphan025 Date: Wed, 24 Jun 2026 19:13:56 +0200 Subject: [PATCH 2/8] feat(garm): add get-credentials action --- charms/garm/charmcraft.yaml | 4 +++ charms/garm/src/charm.py | 15 +++++++++ charms/garm/tests/unit/test_charm.py | 40 ++++++++++++++++++++++++ charms/tests/integration/test_garm.py | 30 ++++++++++++++++++ docs/how-to/index.rst | 1 + docs/how-to/retrieve-garm-credentials.md | 37 ++++++++++++++++++++++ 6 files changed, 127 insertions(+) create mode 100644 docs/how-to/retrieve-garm-credentials.md diff --git a/charms/garm/charmcraft.yaml b/charms/garm/charmcraft.yaml index 62fccbbf..11a5284b 100644 --- a/charms/garm/charmcraft.yaml +++ b/charms/garm/charmcraft.yaml @@ -34,3 +34,7 @@ requires: interface: garm_configurator_v0 limit: 1 +actions: + get-credentials: + description: Show the GARM admin credentials. + diff --git a/charms/garm/src/charm.py b/charms/garm/src/charm.py index 07c5328a..58ae7b86 100755 --- a/charms/garm/src/charm.py +++ b/charms/garm/src/charm.py @@ -273,6 +273,9 @@ def __init__(self, *args: typing.Any) -> None: self.on[GARM_CONFIGURATOR_RELATION_NAME].relation_broken, self._on_configurator_relation_changed, ) + self.framework.observe( + self.on.get_credentials_action, self._on_get_credentials_action + ) def _on_install(self, _: ops.InstallEvent) -> None: """Ensure secrets exist on first install.""" @@ -282,6 +285,18 @@ def _on_leader_elected(self, _: ops.LeaderElectedEvent) -> None: """Ensure secrets exist when the leader is elected.""" self._ensure_secrets() + 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 f13bf774..609329e5 100644 --- a/charms/garm/tests/unit/test_charm.py +++ b/charms/garm/tests/unit/test_charm.py @@ -540,3 +540,43 @@ def test_maybe_first_run_skips_on_missing_credential_key(): GarmCharm._maybe_first_run(charm) 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() diff --git a/charms/tests/integration/test_garm.py b/charms/tests/integration/test_garm.py index 835f0750..52950fd6 100644 --- a/charms/tests/integration/test_garm.py +++ b/charms/tests/integration/test_garm.py @@ -501,3 +501,33 @@ def test_garm_api_has_configurator_provider( f"Expected a 'garm-configurator-*' provider in GARM API, " f"got: {api_provider_names}" ) + + +def test_garm_get_credentials_action( + juju: jubilant.Juju, + garm_app: str, +): + """ + arrange: The GARM charm is deployed and active (leader has initialised 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") + + logger.info("get-credentials results: %s", dict(task.results)) + 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..b099c695 --- /dev/null +++ b/docs/how-to/retrieve-garm-credentials.md @@ -0,0 +1,37 @@ +# 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 initialised. 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: + +``` +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 +``` From c650a3d284d6ede51c19071787968d35a664e52f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 05:09:54 +0000 Subject: [PATCH 3/8] fix(garm): remove logger line that could expose credentials in CI logs --- charms/tests/integration/test_garm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/charms/tests/integration/test_garm.py b/charms/tests/integration/test_garm.py index 52950fd6..1477a8af 100644 --- a/charms/tests/integration/test_garm.py +++ b/charms/tests/integration/test_garm.py @@ -519,7 +519,6 @@ def test_garm_get_credentials_action( # juju.run() raises TaskError if the action fails, so a clean return means success. task = juju.run(unit, "get-credentials") - logger.info("get-credentials results: %s", dict(task.results)) for key in ("username", "password", "email", "full-name"): assert key in task.results, ( f"Expected '{key}' in action results, got: {list(task.results)}" From bee811cbe0436df8ab9e1e2f55e1a818a0ac867e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:19:02 +0000 Subject: [PATCH 4/8] fix: add trailing newline to .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a1afc1bd..6a9256bb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,4 @@ build/ .vscode/ # Git worktrees -.worktrees/ \ No newline at end of file +.worktrees/ From 41cfabb1f16ed234e7010aae8f59f1e12c280825 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:23:29 +0000 Subject: [PATCH 5/8] fix(garm): move get-credentials integration test to end of file --- charms/tests/integration/test_garm.py | 57 +++++++++++++-------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/charms/tests/integration/test_garm.py b/charms/tests/integration/test_garm.py index 31ecff95..44493650 100644 --- a/charms/tests/integration/test_garm.py +++ b/charms/tests/integration/test_garm.py @@ -520,35 +520,6 @@ def garm_login_from_secret(juju: jubilant.Juju, garm_app_name: str, garm_url: st ), None, ) - - -def test_garm_get_credentials_action( - juju: jubilant.Juju, - garm_app: str, -): - """ - arrange: The GARM charm is deployed and active (leader has initialised 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" assert garm_secret_uri, f"{GARM_ADMIN_CREDENTIALS_LABEL} not found for {garm_app_name}" secret_json = juju.cli("show-secret", "--reveal", "--format=json", garm_secret_uri) @@ -799,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" From d7c09d6999f336963080fb96b32beb12dc76e6e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:43:05 +0000 Subject: [PATCH 6/8] fix(garm): remove unrelated limit:1 from garm-configurator relation in charmcraft.yaml --- charms/garm/charmcraft.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/charms/garm/charmcraft.yaml b/charms/garm/charmcraft.yaml index 11a5284b..8da138f9 100644 --- a/charms/garm/charmcraft.yaml +++ b/charms/garm/charmcraft.yaml @@ -32,7 +32,6 @@ requires: limit: 1 garm-configurator: interface: garm_configurator_v0 - limit: 1 actions: get-credentials: From f62e0503c05e93b8e4b8b14a06403e791a391ecd Mon Sep 17 00:00:00 2001 From: Phan Trung Thanh Date: Sun, 28 Jun 2026 13:02:24 +0200 Subject: [PATCH 7/8] Update docs/how-to/retrieve-garm-credentials.md Co-authored-by: Erin Conley --- docs/how-to/retrieve-garm-credentials.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/retrieve-garm-credentials.md b/docs/how-to/retrieve-garm-credentials.md index b099c695..d7cd775d 100644 --- a/docs/how-to/retrieve-garm-credentials.md +++ b/docs/how-to/retrieve-garm-credentials.md @@ -4,7 +4,7 @@ The `get-credentials` action retrieves the GARM admin credentials generated by t ## Prerequisites -- The GARM charm must be fully initialised. 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. +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 From 6803af9ea3815c0e140de22790766e2cd627076a Mon Sep 17 00:00:00 2001 From: Phan Trung Thanh Date: Thu, 2 Jul 2026 13:12:49 +0200 Subject: [PATCH 8/8] Update docs/how-to/retrieve-garm-credentials.md Co-authored-by: Erin Conley --- docs/how-to/retrieve-garm-credentials.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/how-to/retrieve-garm-credentials.md b/docs/how-to/retrieve-garm-credentials.md index d7cd775d..94e59339 100644 --- a/docs/how-to/retrieve-garm-credentials.md +++ b/docs/how-to/retrieve-garm-credentials.md @@ -25,7 +25,9 @@ The action returns four fields: Example output: -``` +```{terminal} +:output-only: + Running operation 1 with 1 task - task 2 on unit-garm-0