Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
14953bf
Add support for integration test with juju secrets
yhaliaw Apr 17, 2026
c8dbc15
Remove incorrect argument for juju.deploy
yhaliaw Apr 17, 2026
97345c0
Merge remote-tracking branch 'origin/main' into feat/juju-secret-config
cbartz Apr 17, 2026
725e81c
fix(test): remove invalid log kwarg from juju.deploy
cbartz Apr 17, 2026
1715b66
docs: document juju secret config options in changelog
cbartz Apr 17, 2026
ef2e801
test(integration): skip test_charm_upgrade until latest/edge has secr…
cbartz Apr 17, 2026
e70587b
fix(charm_state): mention token-secret-id in missing-auth error
cbartz Apr 17, 2026
cdf2060
Apply suggestions from code review
cbartz Apr 17, 2026
028b739
docs(test): explain why plaintext fallback is not used in upgrade tes…
cbartz Apr 17, 2026
77067cb
fix(charm_state): tailor openstack clouds yaml parse error to source
cbartz Apr 17, 2026
8de25ca
refactor(charm): collapse config-changed flush detection into a loop
cbartz Apr 17, 2026
2bd7e7f
refactor(test): thread openstack clouds yaml as helper parameter
cbartz Apr 17, 2026
0b97597
fix(charm_state): silence bandit on openstack secret-id constant
cbartz Apr 17, 2026
dbd8a6a
fix(charm): track plaintext clouds yaml + sharpen secret error paths
cbartz Apr 17, 2026
d959a99
refactor(charm): don't track plaintext openstack-clouds-yaml in _stored
cbartz Apr 17, 2026
262747a
fix(charm_state): don't log decrypted clouds.yaml on parse error
cbartz Apr 17, 2026
5e80b6a
Enable the upgrade charm test
yhaliaw Apr 20, 2026
5075455
Revert "Enable the upgrade charm test"
yhaliaw Apr 20, 2026
a517f96
Remove extra code
yhaliaw Apr 20, 2026
b6ea872
test: skip fork path-change integration test
cbartz Apr 20, 2026
b3ca376
Add back from None for avoid lint
yhaliaw Apr 21, 2026
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
23 changes: 19 additions & 4 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ config:
type: string
default: ""
description: >-
The clouds.yaml yaml necessary for OpenStack integration.
The format for the clouds.yaml is described in the docs:
(Compatibility fallback) The clouds.yaml yaml necessary for OpenStack integration.
Prefer setting openstack-clouds-yaml-secret-id.
The format for clouds.yaml is described in the docs:
(https://docs.openstack.org/python-openstackclient/pike/configuration/index.html#clouds-yaml).
openstack-clouds-yaml-secret-id:
type: string
default: ""
description: >-
Juju secret ID containing OpenStack clouds.yaml content under the `clouds-yaml` field.
When set, this takes precedence over `openstack-clouds-yaml`.
openstack-flavor:
type: string
default: ""
Expand Down Expand Up @@ -137,10 +144,18 @@ config:
type: string
default: ""
description: >-
The GitHub Personal Access Token for registering the self-hosted runners. The token requires
'repo' scope for repository runners and 'repo' + 'admin:org' scope for organization runners.
(Compatibility fallback) The GitHub Personal Access Token for registering self-hosted
runners. Prefer setting token-secret-id.
The token requires 'repo' scope for repository runners and 'repo' + 'admin:org' scope for
organization runners.
For fine grained token scopes, see
Comment thread
cbartz marked this conversation as resolved.
Outdated
https://charmhub.io/github-runner/docs/how-to-change-token.
token-secret-id:
type: string
default: ""
description: >-
Juju secret ID containing the GitHub token under the `github-token` field.
When set, this takes precedence over `token`.
github-app-client-id:
type: string
description: >-
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This changelog documents user-relevant changes to the GitHub runner charm.

## 2026-04-17

- Added `token-secret-id` and `openstack-clouds-yaml-secret-id` configuration options, allowing the GitHub token and OpenStack `clouds.yaml` to be supplied through Juju user secrets. When set, these take precedence over the plaintext `token` and `openstack-clouds-yaml` options, which remain supported as a compatibility fallback.

## 2026-04-13

- Fixed Juju secrets not picking up new revisions. The charm now uses `refresh=True` when reading secret contents, ensuring it always retrieves the latest revision instead of a cached one.
Expand Down
17 changes: 17 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
LABELS_CONFIG_NAME,
PATH_CONFIG_NAME,
PLANNER_INTEGRATION_NAME,
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME,
TOKEN_CONFIG_NAME,
TOKEN_SECRET_ID_CONFIG_NAME,
CharmConfigInvalidError,
CharmState,
OpenstackImage,
Expand Down Expand Up @@ -205,6 +207,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self._stored.set_default(
path=self.config[PATH_CONFIG_NAME], # for detecting changes
token=self.config[TOKEN_CONFIG_NAME], # for detecting changes
token_secret_id=self.config.get(TOKEN_SECRET_ID_CONFIG_NAME), # for detecting changes
github_app_client_id=self.config.get(
GITHUB_APP_CLIENT_ID_CONFIG_NAME
), # for detecting changes
Expand All @@ -214,6 +217,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
github_app_private_key_secret_id=self.config.get(
GITHUB_APP_PRIVATE_KEY_SECRET_ID_CONFIG_NAME
), # for detecting changes
openstack_clouds_yaml_secret_id=self.config.get(
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME
), # for detecting changes
labels=self.config[LABELS_CONFIG_NAME], # for detecting changes
allow_external_contributor=self.config[
ALLOW_EXTERNAL_CONTRIBUTOR_CONFIG_NAME
Expand Down Expand Up @@ -353,6 +359,9 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
if self.config[TOKEN_CONFIG_NAME] != self._stored.token:
self._stored.token = self.config[TOKEN_CONFIG_NAME]
flush_runners = True
if self.config.get(TOKEN_SECRET_ID_CONFIG_NAME) != self._stored.token_secret_id:
self._stored.token_secret_id = self.config.get(TOKEN_SECRET_ID_CONFIG_NAME)
flush_runners = True
if self.config.get(GITHUB_APP_CLIENT_ID_CONFIG_NAME) != self._stored.github_app_client_id:
self._stored.github_app_client_id = self.config.get(GITHUB_APP_CLIENT_ID_CONFIG_NAME)
flush_runners = True
Expand All @@ -378,6 +387,14 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
if self.config[LABELS_CONFIG_NAME] != self._stored.labels:
self._stored.labels = self.config[LABELS_CONFIG_NAME]
flush_runners = True
if (
self.config.get(OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME)
!= self._stored.openstack_clouds_yaml_secret_id
):
self._stored.openstack_clouds_yaml_secret_id = self.config.get(
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME
)
flush_runners = True
if (
self.config[ALLOW_EXTERNAL_CONTRIBUTOR_CONFIG_NAME]
!= self._stored.allow_external_contributor
Expand Down
32 changes: 32 additions & 0 deletions src/charm_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
MAX_TOTAL_VIRTUAL_MACHINES_CONFIG_NAME = "max-total-virtual-machines"
MANAGER_SSH_PROXY_COMMAND_CONFIG_NAME = "manager-ssh-proxy-command"
OPENSTACK_CLOUDS_YAML_CONFIG_NAME = "openstack-clouds-yaml"
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME = "openstack-clouds-yaml-secret-id"
OPENSTACK_NETWORK_CONFIG_NAME = "openstack-network"
OPENSTACK_FLAVOR_CONFIG_NAME = "openstack-flavor"
PATH_CONFIG_NAME = "path"
Expand All @@ -60,6 +61,7 @@
TEST_MODE_CONFIG_NAME = "test-mode"
# bandit thinks this is a hardcoded password.
TOKEN_CONFIG_NAME = "token" # nosec
TOKEN_SECRET_ID_CONFIG_NAME = "token-secret-id" # nosec
USE_APROXY_CONFIG_NAME = "experimental-use-aproxy"
APROXY_EXCLUDE_ADDRESSES_CONFIG_NAME = "aproxy-exclude-addresses"
APROXY_REDIRECT_PORTS_CONFIG_NAME = "aproxy-redirect-ports"
Expand Down Expand Up @@ -168,6 +170,7 @@ def from_charm(cls, charm: CharmBase) -> "GithubConfig": # noqa: C901

path_str = cast(str, charm.config.get(PATH_CONFIG_NAME, ""))
token = cast(str, charm.config.get(TOKEN_CONFIG_NAME)) or None
token_secret_id = cast(str, charm.config.get(TOKEN_SECRET_ID_CONFIG_NAME)) or None
app_client_id = cast(str, charm.config.get(GITHUB_APP_CLIENT_ID_CONFIG_NAME)) or None
installation_id = (
cast(int, charm.config.get(GITHUB_APP_INSTALLATION_ID_CONFIG_NAME)) or None
Expand All @@ -187,6 +190,17 @@ def from_charm(cls, charm: CharmBase) -> "GithubConfig": # noqa: C901
app_fields = (app_client_id, installation_id, private_key_secret_id)
app_fields_set = sum(field is not None for field in app_fields)

if token_secret_id:
try:
token_secret = charm.model.get_secret(id=token_secret_id)
token = token_secret.get_content(refresh=True).get("github-token")
except SecretNotFoundError as exc:
raise CharmConfigInvalidError(f"GitHub token secret {token_secret_id} not found") from exc
if not token:
raise CharmConfigInvalidError(
f"GitHub token secret {token_secret_id} is missing github-token"
)

if token and app_fields_set:
raise CharmConfigInvalidError(
"Configure either token or GitHub App authentication, not both"
Comment thread
cbartz marked this conversation as resolved.
Expand Down Expand Up @@ -385,9 +399,27 @@ def _parse_openstack_clouds_config(cls, charm: CharmBase) -> OpenStackCloudsYAML
Returns:
The openstack clouds yaml.
"""
openstack_clouds_yaml_secret_id = cast(
str, charm.config.get(OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME)
) or None
openstack_clouds_yaml_str: str | None = cast(
str, charm.config.get(OPENSTACK_CLOUDS_YAML_CONFIG_NAME)
)

if openstack_clouds_yaml_secret_id:
try:
cloud_secret = charm.model.get_secret(id=openstack_clouds_yaml_secret_id)
openstack_clouds_yaml_str = cloud_secret.get_content(refresh=True).get("clouds-yaml")
Comment thread
cbartz marked this conversation as resolved.
Outdated
except SecretNotFoundError as exc:
Comment thread
cbartz marked this conversation as resolved.
raise CharmConfigInvalidError(
f"OpenStack clouds.yaml secret {openstack_clouds_yaml_secret_id} not found"
) from exc
if not openstack_clouds_yaml_str:
raise CharmConfigInvalidError(
"OpenStack clouds.yaml secret "
f"{openstack_clouds_yaml_secret_id} is missing clouds-yaml"
)

if not openstack_clouds_yaml_str:
raise CharmConfigInvalidError("No openstack_clouds_yaml")
Comment thread
cbartz marked this conversation as resolved.
Outdated

Expand Down
1 change: 0 additions & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,6 @@ def image_builder_fixture(
"virt-type": "virtual-machine",
"cores": "2",
},
log=False,
)

yield image_builder_app_name
Expand Down
35 changes: 31 additions & 4 deletions tests/integration/helpers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@
GITHUB_APP_CLIENT_ID_CONFIG_NAME,
GITHUB_APP_INSTALLATION_ID_CONFIG_NAME,
GITHUB_APP_PRIVATE_KEY_SECRET_ID_CONFIG_NAME,
OPENSTACK_CLOUDS_YAML_CONFIG_NAME,
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME,
PATH_CONFIG_NAME,
RECONCILE_INTERVAL_CONFIG_NAME,
TEST_MODE_CONFIG_NAME,
TOKEN_CONFIG_NAME,
TOKEN_SECRET_ID_CONFIG_NAME,
)
from manager_service import _get_log_file_path

Expand Down Expand Up @@ -160,7 +163,7 @@ def deploy_github_runner_charm(
RECONCILE_INTERVAL_CONFIG_NAME: reconcile_interval,
}

secret_name = None
secret_names: list[str] = []
if github_config.has_app_auth:
assert github_config.app_client_id is not None
assert github_config.installation_id is not None
Expand All @@ -170,26 +173,50 @@ def deploy_github_runner_charm(
name=secret_name,
content={"private-key": github_config.private_key},
)
secret_names.append(secret_name)
default_config[GITHUB_APP_CLIENT_ID_CONFIG_NAME] = github_config.app_client_id
default_config[GITHUB_APP_INSTALLATION_ID_CONFIG_NAME] = github_config.installation_id
default_config[GITHUB_APP_PRIVATE_KEY_SECRET_ID_CONFIG_NAME] = str(secret_id)
else:
default_config[TOKEN_CONFIG_NAME] = github_config.token
token_secret_name = f"{app_name}-github-token"
token_secret_id = juju.add_secret(
name=token_secret_name,
content={"github-token": github_config.token},
)
secret_names.append(token_secret_name)
default_config[TOKEN_SECRET_ID_CONFIG_NAME] = str(token_secret_id)

Comment thread
cbartz marked this conversation as resolved.
clouds_yaml = None
if config:
clouds_yaml = cast(str | None, config.get(OPENSTACK_CLOUDS_YAML_CONFIG_NAME))

if clouds_yaml and not (config and config.get(OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME)):
openstack_secret_name = f"{app_name}-openstack-clouds"
openstack_secret_id = juju.add_secret(
name=openstack_secret_name,
content={"clouds-yaml": clouds_yaml},
)
secret_names.append(openstack_secret_name)
default_config[OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME] = str(openstack_secret_id)

default_config[TOKEN_CONFIG_NAME] = ""
default_config[OPENSTACK_CLOUDS_YAML_CONFIG_NAME] = ""

if config:
default_config.update(config)
Comment thread
cbartz marked this conversation as resolved.
if OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME in default_config:
default_config[OPENSTACK_CLOUDS_YAML_CONFIG_NAME] = ""

juju.deploy(
charm_file,
app=app_name,
base=base,
config=default_config,
constraints=constraints or DEFAULT_RUNNER_CONSTRAINTS,
log=False,
**(deploy_kwargs or {}),
)

if secret_name:
for secret_name in secret_names:
juju.grant_secret(secret_name, app_name)

if wait_idle:
Expand Down
7 changes: 7 additions & 0 deletions tests/integration/test_charm_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
pytestmark = pytest.mark.openstack


@pytest.mark.skip(
reason=(
"latest/edge charm predates token-secret-id and openstack-clouds-yaml-secret-id "
"config options. Re-enable once a release containing these options has been "
"promoted to latest/edge."
)
)
Comment thread
cbartz marked this conversation as resolved.
def test_charm_upgrade(
juju: jubilant.Juju,
deployment_context: DeploymentContext,
Expand Down
24 changes: 24 additions & 0 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
IMAGE_INTEGRATION_NAME,
LABELS_CONFIG_NAME,
OPENSTACK_CLOUDS_YAML_CONFIG_NAME,
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME,
OPENSTACK_FLAVOR_CONFIG_NAME,
PATH_CONFIG_NAME,
PLANNER_DEFAULT_PLATFORM,
Expand All @@ -45,6 +46,7 @@
PLANNER_PLATFORM_RELATION_KEY,
PLANNER_PRIORITY_RELATION_KEY,
TOKEN_CONFIG_NAME,
TOKEN_SECRET_ID_CONFIG_NAME,
USE_APROXY_CONFIG_NAME,
OpenStackCloudsYAML,
OpenstackImage,
Expand Down Expand Up @@ -258,6 +260,12 @@ def test_on_config_changed_failure(harness: Harness):
[
pytest.param(PATH_CONFIG_NAME, "initial-path", "updated-path", id="Path"),
pytest.param(TOKEN_CONFIG_NAME, "initial-token", "updated-token", id="Token"),
pytest.param(
TOKEN_SECRET_ID_CONFIG_NAME,
"secret:token-old",
"secret:token-new",
id="Token secret ID",
),
pytest.param(
GITHUB_APP_CLIENT_ID_CONFIG_NAME, "Iv23liOld", "Iv23liNew", id="GitHub App Client ID"
),
Expand All @@ -274,6 +282,12 @@ def test_on_config_changed_failure(harness: Harness):
id="GitHub App Private Key Secret ID",
),
pytest.param(LABELS_CONFIG_NAME, "label-a", "label-b", id="Labels"),
pytest.param(
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME,
"secret:clouds-old",
"secret:clouds-new",
id="OpenStack clouds secret ID",
),
],
)
def test__on_config_changed_flush(
Expand Down Expand Up @@ -306,6 +320,11 @@ def test__on_config_changed_flush(
[
pytest.param(PATH_CONFIG_NAME, secrets.token_hex(16), id="Path"),
pytest.param(TOKEN_CONFIG_NAME, secrets.token_hex(16), id="Token"),
pytest.param(
TOKEN_SECRET_ID_CONFIG_NAME,
f"secret:{secrets.token_hex(8)}",
id="Token secret ID",
),
pytest.param(GITHUB_APP_CLIENT_ID_CONFIG_NAME, "Iv23liExample", id="GitHub App Client ID"),
pytest.param(
GITHUB_APP_INSTALLATION_ID_CONFIG_NAME,
Expand All @@ -318,6 +337,11 @@ def test__on_config_changed_flush(
id="GitHub App Private Key Secret ID",
),
pytest.param(LABELS_CONFIG_NAME, secrets.token_hex(16), id="Labels"),
pytest.param(
OPENSTACK_CLOUDS_YAML_SECRET_ID_CONFIG_NAME,
f"secret:{secrets.token_hex(8)}",
id="OpenStack clouds secret ID",
),
],
)
def test__on_config_changed_no_flush(
Expand Down
Loading
Loading