As described elsewhere, picking an interface is a strong statement; whether you're providing or requiring a given interface, you need to be compatible with all providers and requirers of that interface.
In short, this means that you need to set the same settings as do all the other charms with the same role for the interface; and you should only expect to be able to read those settings set by the other charms with the counterpart role.
Note: Some popular interfaces are documented and we have reference implementations for them. If you are just starting out with interfaces, you may find it easier to start with these.
This is in some respects an uncomfortable situation to be in. If two charms declare compatibility, but fail when run together due to disagreement about the actual definition of that interface; it's not immediately clear which is at fault, because the "standard" is defined only by consensus among the existing charms that provide or require that interface.
On the upside, this means that reading any charm that already implements the interface is theoretically good enough to figure it out; in practice, it's sometimes hard to understand the code in isolation. The incontrovertible way to determine the protocol defined by an interface is to deploy a pair of charms that already use that interface, and intercept the communications between them.
juju deploy mongodb juju deploy node-app
In a separate window, once node-app/0 reports
juju debug-hooks node-app/0 *
This opens a tmux session; when the unit wants to run a hook, it'll create a new window in which you can interactively inspect the environment. In a relation hook, you'll want to run the following commands:
# discover what's been set by the unit on the other end of the relation: $ relation-get $JUJU_REMOTE_UNIT address: example.com:37070 username: bob password: seekrit # get the local unit's relation settings, for comparison afterwards: $ relation-get $JUJU_UNIT_NAME # (no results) # run the actual hook: $ ./hooks/$JUJU_HOOK_NAME # get the local settings again, to see if any changes were made by the hook: $ relation-get $JUJU_UNIT_NAME # (no results)
...and then close that tmux window and wait to see if another one opens for the next hook. Assuming you started debugging before creating the relation, you'll see exactly one -joined hook, and at least one -changed hook, and you're only done when new -changed hooks stop popping up.
The relation-get tool accepts a
parameter that accepts
yaml values, in case you want to consume
In practice, what you'll most likely find is that one side sets a couple of keys, the other side gets them, and that's that... in no more than two changed hooks. Be aware, though, that it is possible in principle for an interface to imply a more demanding interface, involving multiple rounds of setting and getting.
In light of the above, there is a clear need for some means of documenting the
above. The optional
sets fields in a charm's relation metadata
should be used for this purpose.
Please be especially careful to note that this format is not checked by juju today. But it does encode the information that's most helpful to your fellow charmers, and by doing so in a consistent and machine-readable format we maximise our chances of one day making use of this information automatically.
The simple form, which will be the most common form, looks like this:
name: python-django ... provides: website: interface: http sets: [host, port]
name: haproxy ... requires: reverseproxy: interface: http gets: [host, port]
...indicating that a relation can surely be made between python-django and
haproxy, because the
website relation unconditionally sets the keys that the
reverseproxy relation requires in order to function.
The more complex form, which is only required for complex protocols, is defined as follows:
getsholds a list of settings keys that must all be set by each unit on the remote end of a relation in order for the local unit to function.
setsholds a list of settings that will be set by each unit of the local charm. These settings can either be a plain string, indicating that no remote settings need exist for this key to be written; or a single-element map, with the setting to be written mapped to the remote settings that must exist for this to be done.
For example, consider charms
b, which do the following handshake dance
before they can complete their configuration:
b/0starts running -joined, which takes a while.
a/0runs -joined and -changed before
b/0has finished -joined. In the -changed hook, it fails to find the remote settings it expected, and exits without error.
b/0finishes -joined, setting
Y, thus triggering -changed on
Qin response, causing a -changed on
b/0completes local configuration using
Q, and sets
Z, triggering a final -changed on
a/0completes local configuration using
Z, and sets nothing.
For example, in the hypothetical complex protocol described in the previous
section, the metadata for charm
a above would contain:
requires: ab: interface: ab sets: - P: [X, Y] - Q: [X, Y] gets: [X, Y, Z]
Q will only be written when
Y have; and
that configuration will not complete without
Z. Meanwhile charm
b would contain:
provides: ab: interface: ab sets: - X - Y - Z: [P, Q] gets: [P, Q]
...indicating that it will always write
Y, and that
Z will be
Q have; and that configuration will not be complete
Peer relations are a different matter, in that peer relations are the mechanism by which service units share information internally. It's not unusual for peer service units to maintain convenient caches of distributed information in their own peer settings, and this inevitably involves generating settings keys at runtime.
Although we have described above that interfaces arrive by convention, there are several well-used interfaces which have enough implementations to define a defacto standard.
Below is a list of the interfaces for which we have compiled documentation and reference implementations.