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
6 changes: 6 additions & 0 deletions charms/garm-configurator/charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ config:
Juju secret containing the GitHub App private key (PEM format).
The secret content must have a "value" key with the private key string.

requires:
image:
interface: github_runner_image_v0
limit: 1
Comment thread
cbartz marked this conversation as resolved.
optional: false

parts:
charm:
source: .
Expand Down
50 changes: 35 additions & 15 deletions charms/garm-configurator/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import ops

from charm_state import CharmConfigInvalidError, CharmState
from charm_state import IMAGE_RELATION_NAME, CharmConfigInvalidError, CharmState


class GarmConfiguratorCharm(ops.CharmBase):
Expand All @@ -21,29 +21,49 @@ def __init__(self, *args: typing.Any) -> None:
args: passthrough to CharmBase.
"""
super().__init__(*args)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.collect_unit_status, self._on_collect_unit_status)
for event in [
self.on.config_changed,
self.on.secret_changed,
self.on[IMAGE_RELATION_NAME].relation_joined,
self.on[IMAGE_RELATION_NAME].relation_changed,
self.on[IMAGE_RELATION_NAME].relation_broken,
]:
Comment thread
cbartz marked this conversation as resolved.
self.framework.observe(event, self._reconcile)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

holistic pattern


def _on_config_changed(self, _event: ops.ConfigChangedEvent) -> None:
"""Validate configuration on config-changed."""
try:
CharmState.from_charm(self)
except CharmConfigInvalidError as e:
self.unit.status = ops.BlockedStatus(e.msg)
return
def _reconcile(self, event: ops.EventBase) -> None:
"""Reconcile all charm state for every event.

def _on_collect_unit_status(self, event: ops.CollectStatusEvent) -> None:
"""Handle collect-unit-status event.
Reads full current state and acts idempotently: forwards OpenStack
credentials to the image builder relation, and reports unit status.

Args:
event: The collect status event.
event: The triggering event.
"""
try:
CharmState.from_charm(self)
state = CharmState.from_charm(self)
except CharmConfigInvalidError as e:
self.unit.status = ops.BlockedStatus(e.msg)
return
event.add_status(ops.ActiveStatus("Ready"))

relation = self.model.get_relation(IMAGE_RELATION_NAME)
if relation is not None:
relation.data[self.unit].update(
{
"auth_url": state.provider_config.auth_url,
"password": state.provider_config.password,
"project_domain_name": state.provider_config.project_domain_name,
Comment thread
cbartz marked this conversation as resolved.
"project_name": state.provider_config.project_name,
"user_domain_name": state.provider_config.user_domain_name,
"username": state.provider_config.username,
}
)

if relation is None:
self.unit.status = ops.WaitingStatus("Waiting for image builder relation")
elif state.image_id is None:
self.unit.status = ops.WaitingStatus("Waiting for image UUID from image builder")
else:
Comment thread
cbartz marked this conversation as resolved.
self.unit.status = ops.ActiveStatus("Ready")
Comment thread
cbartz marked this conversation as resolved.


if __name__ == "__main__":
Expand Down
31 changes: 29 additions & 2 deletions charms/garm-configurator/src/charm_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
GITHUB_APP_INSTALLATION_ID_CONFIG_NAME = "github-app-installation-id"
GITHUB_APP_PRIVATE_KEY_CONFIG_NAME = "github-app-private-key" # nosec

IMAGE_RELATION_NAME = "image"


class CharmConfigInvalidError(Exception):
"""Raised when charm configuration is invalid.
Expand Down Expand Up @@ -100,7 +102,7 @@ def from_charm(cls, charm: ops.CharmBase) -> "ProviderConfig":
)
try:
secret = charm.model.get_secret(id=str(password_secret_id))
password = secret.get_content()["value"]
password = secret.get_content(refresh=True)["value"]
except (ops.SecretNotFoundError, KeyError) as e:
raise CharmConfigInvalidError(
f"{OPENSTACK_PASSWORD_CONFIG_NAME} secret is invalid or missing 'value' key"
Expand Down Expand Up @@ -163,7 +165,7 @@ def from_charm(cls, charm: ops.CharmBase) -> "GithubAppConfig":
)
try:
secret = charm.model.get_secret(id=str(private_key_secret_id))
private_key = secret.get_content()["value"]
private_key = secret.get_content(refresh=True)["value"]
except (ops.SecretNotFoundError, KeyError) as e:
raise CharmConfigInvalidError(
f"{GITHUB_APP_PRIVATE_KEY_CONFIG_NAME} secret is invalid or missing 'value' key"
Expand All @@ -182,22 +184,26 @@ class CharmState:
Attributes:
provider_config: OpenStack provider configuration.
github_app_config: GitHub App configuration.
image_id: OpenStack image UUID received from the image builder relation, or None.
"""

def __init__(
self,
*,
provider_config: ProviderConfig,
github_app_config: GithubAppConfig,
image_id: str | None,
) -> None:
"""Initialize the charm state.

Args:
provider_config: The OpenStack provider configuration.
github_app_config: The GitHub App configuration.
image_id: The OpenStack image UUID from the image builder relation.
"""
self.provider_config = provider_config
self.github_app_config = github_app_config
self.image_id = image_id

@classmethod
def from_charm(cls, charm: ops.CharmBase) -> "CharmState":
Expand All @@ -214,7 +220,28 @@ def from_charm(cls, charm: ops.CharmBase) -> "CharmState":
"""
provider_config = ProviderConfig.from_charm(charm)
github_app_config = GithubAppConfig.from_charm(charm)
image_id = _get_image_id_from_relation(charm)
return cls(
provider_config=provider_config,
github_app_config=github_app_config,
image_id=image_id,
)


def _get_image_id_from_relation(charm: ops.CharmBase) -> str | None:
"""Return the OpenStack image UUID from the image builder relation, if available.

Args:
charm: The charm instance.

Returns:
The image UUID string, or None if the relation is absent or no UUID has been set yet.
"""
relation = charm.model.get_relation(IMAGE_RELATION_NAME)
if relation is None:
return None
for unit in relation.units:
image_id = relation.data[unit].get("id")
if image_id:
return image_id
return None
Loading
Loading