The thing to keep in mind about juju relations is that we dont bind to any official spec. We're not forcing you to implement an RFC to exchange information between the services.
With that said, lets dissect your particular question here.
In terms of Server and Application, lets use gitlab-ci and gitlab-ci-runner as our server/application model. With gitlab-ci being the server, and gitlab-ci-runner as the application. our deployment model is a one to many, with 1 server, and many applications. (we can have many servers, but i want to keep the example concise)
Gitlab-CI metadata
We first define the relationship in the gitlab-ci metadata.
provides:
ci-job:
interface: ci-worker
This single statement tells juju that we have a ci-job relation, using the ci-worker interface. With this relationship definition, we have 4 possible states that we can consume to make changes.
ci-job-joined, ci-job-changed, ci-job-broken, ci-job-departed
The convention for this, is relation-name-event - so moving forward, we have those 4 possible event hooks attached to a relation name. They execute in the following sequence:
- relation-joined
- relation-changed
- relation-broken
- relation-departed
Joined is for "pre-work" and is a place for you to do any work necessary to prep the service to receive the relationship (backing up an existing local SQLite database, for example).
Changed is executed any time a change is detected in the relationship. This sometimes gets triggered when the remote charm has a config update, and may not necessarily reflect a required change on your service - so be 100% sure its idempotent.
Broken is executed when you first break the relationship. This is when you would want to do any data-backups, and reconfiguration to remove the remote configuration.
Departed is any final wrap-up work for removing the relationship. Such as re-instating the SQLite backup for operation and writing the proper configuration.
There are several charms that exercise these hooks in the charm store. I would suggest looking at the hook contents of a few of the charms. MongoDB, MediaWiki, Ghost
Gitlab-CI-Worker Metadata
The opposite end service is going to have a very similar story to the service listed above, but instead of provides, we will be using a requires statement to make the relationship. This is very much a Tab A, Slot B configuration - you define what can talk to what with these definitions.
requires:
ci-job:
interface: ci-worker
And our hooks will look basically the same: ci-job-joined, ci-job-changed, ci-job-broken, ci-job-departed
The contents of the hook code are entirely up to you.
Sending the IP of the unit
There are conventions shipped with juju - depending on the language that you are using to write the charm. Be sure that you read the docs about relation-get, relation-set, and unit-get
unit-get
is intended to read system level variables. eg: unit-get public-address
relation-set
is what you would want to use on the host sending the information. In the instance of the gitlab-ci setup, we would relation-set publicip=$(unit-get public-address)
relation-get
is how we consume data sent across the wire on the receiving service. relation-get publicip
I hope this helps!
Best Answer
How do I define a relationship?
So, in order to define a relationship between two charms you must first, as you have alluded to, define the relation in each charms
metadata.yaml
file. Since you've defined a server/client role I'm going to stick to that in my examples below withfoo-server
andfoo-client
charms. Since it's likely the server providing the majority of the data to the client their metadata.yaml files would look as such:foo-server
foo-client
Juju has two primary relation types. Provides and Requires. In this case the server charm is providing "foo" as an interface. The client charm requires the "foo" interface to operate. This provides/requires lets juju know which charms can talk to which other charms.
The interface is an arbitrary name, in this case foo, but could be anything. There's a large list of already defined interfaces, such as: mysql, http, mongodb, etc. If your service provides one of these existing interfaces you'd want to consider implementing it. If not feel free to create a new one.
How do I get/send data?
Once you've defined your metadata, you'll need to create a few new hooks the hook names are defined in the linked documentation, but since you're just sending the address information we'll keep with a simple bash example of the implementation of each hook.
So, we have two charms,
foo-server
andfoo-client
.foo-server
provides a "server" relation with the foo interface.foo-client
requires a "backend" relation with the foo interface. Relation hooks are named based on the relation-name (not the interface name). These could both be called server, but to illustrate that juju matches on interface and not relation, I've made thefoo-client
relation name "backend".foo-server/hooks/server-relation-joined
This is a very basic example, where we're creating a relation key called
hostname
and setting the value, usingunit-get
command, to the private-address of the unit the charm is deployed to. This address will vary from provider to provider, but it will always be reachable within a juju environment. You can set multiple keys by adding a space between keys, for example:This will send two keys,
hostname
andpublic-address
to whatever service it's connected to.foo-client/hooks/backend-relation-changed
Notice the difference in file name, this is invoking the
relation-changed
hook instead ofrelation-joined
. Presumably the server is just giving the details of where it lives, so the client charm needs to know where that address is. By putting this in the relation-changed hook every time data on the relation is updated the hook is called again.Now, there's a little more involved in this hook. Taking it line by line, the first three are just standard stuff. It's a bash charm and
set -eux
is there to make sure the hook behaves as it should. The next line usesrelation-get
which will read relation data from the connection. Now, everything in a juju environment is orchestrated asynchronously. So you're never 100% certain you'll have data when you callrelation-get
. This is where theif
block helps resolve that. If there's nothing in "$server_address", ie we didn't get a return value, the hook will simple exit. However, it's exiting with a zero status so it won't crop up as an error in juju.I know this seems counter intuitive, we technically have a problem because we don't have data. Yes, but, it's more along the lines of "We don't have data, yet". By exiting zero, once the corresponding service actually sets the value, it'll trigger the
relation-changed
hook again and we'll be able to read the value. This is considered an example of an idempotency guard which are crucial as you write hooks.