How to add a custom relation to a 12-factor charm¶
The 12-factor tooling ships with a set of built-in Juju relations (see
Observability and relations). When you need a relation that is not
built in, use the public paas_charm.relations.CustomRelation
extension API to add one without importing or subclassing paas-charm
internals.
A custom relation can:
Contribute environment variables to the workload.
Declare the workload not-ready (a missing required relation, or a related relation with missing or invalid data, blocks the workload with a
BlockedStatus).Trigger the reconcile/
restartpath on relation events, optionally re-running database migrations.
Use a custom relation when you need integration data that does not belong in the framework, or when you want to drive a relation that the framework does not support yet. The most relevant relations will be added to the framework, while others never will — this extension point gives you a stable, supported path for the latter.
Prerequisites¶
A 12-factor charm generated by a Charmcraft framework extension.
The endpoint you want to integrate with declared in
charmcraft.yamlunderrequires.Any charm library the relation needs fetched into the charm. The framework does not guard missing author libraries; a missing library surfaces as an ordinary charm error.
Register a custom relation¶
A custom relation is a subclass of CustomRelation
that you register on the charm via the custom_relations class attribute:
import ops
from charms.temporal_k8s.v0.temporal_host_info import TemporalHostInfoRequirer
import paas_charm.flask
from paas_charm.relations import (
CustomRelation,
InvalidRelationDataError,
OnChange,
RelationDataError,
)
class TemporalRelation(CustomRelation):
relation_name = "temporal-host-info"
def setup(self, on_change: OnChange) -> None:
self._requirer = TemporalHostInfoRequirer(self.charm)
self._framework_observe(
self._requirer.on.temporal_host_info_changed,
on_change,
)
self._framework_observe(
self._requirer.on.temporal_host_info_unavailable,
on_change,
)
def ensure_ready(self) -> None:
if self._requirer is None or self._requirer.relation is None:
raise RelationDataError(
"Temporal relation not ready", relation=self.relation_name
)
self._connection_info()
def gen_environment(self) -> dict[str, str]:
connection_info = self._connection_info()
if connection_info is None:
return {}
host, port = connection_info
return {"TEMPORAL_HOST": host, "TEMPORAL_PORT": str(port)}
def _connection_info(self) -> tuple[str, int] | None:
if self._requirer is None or self._requirer.relation is None:
return None
try:
host = self._requirer.host
port = self._requirer.port
except (TypeError, ValueError) as exc:
raise InvalidRelationDataError(
"invalid Temporal port", relation=self.relation_name
) from exc
if not host or port is None:
raise InvalidRelationDataError(
"missing Temporal host or port", relation=self.relation_name
)
if not 1 <= port <= 65535:
raise InvalidRelationDataError(
"Temporal port must be between 1 and 65535",
relation=self.relation_name,
)
return host, port
class FlaskCharm(paas_charm.flask.Charm):
custom_relations = [TemporalRelation]
if __name__ == "__main__":
ops.main.main(FlaskCharm)
Declare the endpoint in charmcraft.yaml. The optional flag is the
single source of truth for whether the relation is required:
requires:
temporal-host-info:
interface: temporal-host-info
optional: true
limit: 1
Workload integration behavior:
Relation Status |
Workload Impact |
|---|---|
No relation and |
the workload runs without Temporal configuration |
A related Temporal server provides valid connection data |
|
A related app provides incomplete or invalid connection data |
|
Relation removed |
removes environment variables and restarts the workload. |
Add the Temporal charm library to the charm’s charm-libs before building:
charm-libs:
- lib: temporal-k8s.temporal_host_info
version: "0"
Set optional: false instead when the application cannot operate without
Temporal. A missing relation will then produce
BlockedStatus("missing integrations: temporal-host-info").
Implement custom relation API¶
For lightweight implementations, custom relation logic may be placed
directly in src/charm.py.
Method |
Env-var relation |
Side-effect relation |
Comments |
|---|---|---|---|
|
Wire requirer events → |
Wire relation events; instantiate a requirer or call a |
|
|
Override → raise |
Leave at default (no-op) — never blocks the workload. |
|
|
Override → read the databag, return an env-var mapping. |
Leave at default (returns |
Ensure it never raises an exception intentionally |
setup(on_change) is the only required method. The framework injects an
on_change callback (typed by the OnChange
protocol). Use the helper method
self._framework_observe(bound_event, on_change, rerun_migrations=...) to
observe relation events: pass rerun_migrations=True when a relation event
should also re-run database migrations.
Read more¶
See Custom relations API to learn more about Custom Relations API, and Observability and relations for the information about built-in relations.
A complete, runnable Flask charm that wires a custom temporal-host-info relation
ships under examples/flask/ (declared with optional: true in
charmcraft.yaml).