Custom relations API¶
The paas_charm.relations module is the public, stable extension API for
adding custom Juju relations to a 12-factor charm. It is designed so that
charm authors never need to import or subclass paas-charm internals.
paas_charm.relations.Context¶
Context injected by the framework before
setup() runs. It exposes app_name, framework_name, port,
container_name, and a merged config dict (Juju-secret values resolved).
It is the stable, versioned contract — paas-charm can refactor its internals
without breaking relations that use self.context exclusively.
paas_charm.relations.OnChange¶
Callback provided by the framework for relation to request reconcile/restart optionally asking to re-run migrations.
paas_charm.relations.CustomRelation¶
Author-implemented extension point for a custom Juju relation. Provides a few helper methods. The main API consists of the following methods:
setup— Called once during charm initialisation with appropriateOnChangehandler.ensure_ready— The framework uses it to determine whether or not the relation is ready. May raiseRelationDataErrororInvalidRelationDataErrorif the relation is not ready because of missing or invalid data.gen_environment— Called unconditionally. Provides a way for the relation to add workload environment variables. Must not raise an exception.
API methods can use self.context (a read-only
Context injected by the framework) to access
charm’s configuration context.
self.charm is also available as an escape hatch for upstream requirer
charm libraries that require a ops.CharmBase in their constructor,
and for live model state (self.charm.model.get_relation(...),
self.charm.unit.is_leader(), self.charm.unit.get_container(...)).
Authors who do not need a charm lib should use self.context exclusively.
Registration¶
A custom relation is registered by setting the custom_relations class attribute
on the charm to a list of CustomRelation
subclasses (the class, not an instance):
class MyCharm(paas_charm.flask.Charm):
custom_relations = [MyRelation]
The framework instantiates each class as cls(charm), injects
Context and the required flag (read from the
metadata optional field), calls setup(on_change=self._reconcile), and
stores the instances in the charm state consulted at readiness, environment
generation, and reconcile time.
During initialization the framework validates if custom relation is actually
referenced in the requires mapping of the charmcraft.yaml and aborts
execution if relation is not listed.
Required vs. optional¶
Whether a custom relation is required depends on its definition within the
requires mapping or the optional flag status of the matching endpoint
in charmcraft.yaml.
If custom relation is explicitly listed in the requires mapping and its
optional flag is set to false — it is considered to be required and
a missing relation results in BlockedStatus("missing integrations: <name>").
Note
By default optional is set to false, making each custom relation
required. Make sure to explicitly set optional: true when necessary.
Caveat: ordering during __init__¶
custom_relations is processed during PaasCharm.__init__, which runs
before a subclass’ __init__ body. Each relation must therefore be
self-contained: it uses self.context and self.charm (both injected
before setup()), not attributes the charm subclass sets after
super().__init__(). Push author-specific state into the relation itself
rather than into the charm subclass constructor.
Error semantics¶
ensure_ready() may raise RelationDataError, or
InvalidRelationDataError (carrying a relation attribute) when the
relation is not ready or relation data is missing or malformed. The framework
calls ensure_ready() for every established relation and converts a raised
RelationDataError (or InvalidRelationDataError) into BlockedStatus("RelationDataError: <exception message>").
If another required integration is also missing, the
missing integrations: <name> status is reported first.
The same information is logged with the ERROR priority.
Raising RelationDataError from gen_environment() blocks the charm with the
exception message. Any other exception fails the hook.
Situation |
|
Outcome |
|---|---|---|
Data present but malformed |
raises |
|
No relation |
not called |
required → |
Integrated but data not usable |
raises |
|
Data valid and usable → |
called always, even when relation is not ready |
must return without raising |
Event observation helper¶
CustomRelation provides a helper method _framework_observe() to wire
Juju events to the framework’s on_change callback:
self._framework_observe(
self.charm.on[self.relation_name].relation_changed,
on_change,
rerun_migrations=True,
)
This binds on_change as an observer method on the relation instance,
satisfying the Ops framework’s requirement that observers be bound methods of
an ops.Object.
Public exception types¶
The following exception types are part of the public API surface for custom
relations and are re-exported from paas_charm.relations:
paas_charm.exceptions.InvalidRelationDataErrorpaas_charm.exceptions.RelationDataError
Single definitive env-var mapping¶
Custom relations produce a single definitive env-var mapping, authored directly
in gen_environment, used as-is across the framework. A 12-factor charm
targets exactly one framework, so the author always knows which environment
variable names that framework’s workload expects and can emit them directly.
Per-framework remapping for custom relations is out of scope.
Environment-variable collisions¶
Custom environment variables are merged after the built-in and framework environment variables. If a custom variable’s name collides with an existing environment variable (whether built-in, framework-provided, or from another custom relation), the custom variable overwrites it and the charm logs a warning naming the relation and the colliding variable.
Explicit config.options mappings in paas-config.yaml are applied after custom
relation variables and take precedence when their source configuration option has a value.
Optional charm libraries¶
A custom relation’s requirer typically imports an author-supplied charm
library. Providing that library is the author’s responsibility. The framework
does not wrap setup() and gen_environment() in
try/except ImportError: a missing author-supplied library surfaces as an
ordinary charm error. Authors who want a softer failure mode guard or defer
their own imports. Since gen_environment() call is not wrapped in
try/except, relations should not intentionally raise an exception when
creating environment variables.