What makes a bare charm (without operator framework)

Charming isn’t easy without the operator framework: hook tools abstractions, pebble bindings, charm libraries, custom events, stored state, test harness, … all gone. But it’s interesting to see where juju “ends” and operator framework “begins”.

Charm’s execution environment

Charms are run with the juju hook tools in the PATH and with a bunch of environment variables that provide the juju context (try printing os.environ from within a charm; see Charm environment variables).

To quickly experiment with some juju hook tools, try the following on a deployed charm:

juju run --unit my-app/0  -- ls /var/lib/juju/tools/unit-my-app-0
juju run --unit my-app/0  -- config-get
juju run --unit my-app/0  -- status-get
juju run --unit my-app/0  -- relation-get --help

The *.charm file

If you take a bare charm,

import os
import sys
from subprocess import call

if __name__ == "__main__":
    # /var/lib/juju/tools/unit-bare-0/ is already in the PATH,
    # so can call hooks without full path.
    # (os.environ is inherited to the callee.)
    if hook_name := os.environ.get("JUJU_HOOK_NAME"):
        call(["juju-log", "-l", "INFO", f"Hook: {hook_name}"])
    elif action_name := os.environ.get("JUJU_ACTION_NAME"):
        call(["juju-log", "-l", "INFO", f"Action: {action_name}"])
    else:
        call([
            "juju-log",
            "-l",
            "ERROR",
            "This is odd: JUJU_HOOK_NAME nor JUJU_ACTION_NAME are set!"
        ])
    call(["status-set", "active", "Woohoo!"])

and pack it, you’d get a *.charm file:

$ charmcraft pack
Packing the charm
Created 'bare_ubuntu-20.04-amd64.charm'.
Charms packed:
    bare_ubuntu-20.04-amd64.charm

which is actually a zip file:

$ unzip -l bare_ubuntu-20.04-amd64.charm | grep -Ev 'venv/|__pycache__/'  
Archive:  bare_ubuntu-20.04-amd64.charm  
 Length      Date    Time    Name  
---------  ---------- -----   ----  
     102  2022-05-14 23:56   dispatch  
     140  2022-05-14 23:39   actions.yaml  
     250  2022-05-14 23:57   manifest.yaml  
     616  2022-05-14 23:45   metadata.yaml  
     697  2022-05-14 23:45   README.md  
   11358  2022-05-14 23:39   LICENSE  
     151  2022-05-14 23:39   config.yaml  
     102  2022-05-14 23:56   hooks/upgrade-charm  
     102  2022-05-14 23:56   hooks/start  
     102  2022-05-14 23:56   hooks/install  
     707  2022-05-14 23:55   src/charm.py  
---------                     -------  
 5547259                     560 files

Packing introduces several new files (which are there for historical reasons),

  • dispatch
  • hooks/install
  • hooks/start
  • hooks/upgrade-charm

all of which have the exact same contents:

#!/bin/sh

JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv \
  exec ./src/charm.py

which is the charm-entrypoint (ref) of the charm.

Empirically testing events emission sequence

If you deploy two instances of the bare charm:

juju deploy ./bare_ubuntu-20.04-amd64.charm bare1 --num-units 2
juju deploy ./bare_ubuntu-20.04-amd64.charm bare2 --num-units 2

and relate them

juju relate bare1:some-regular-provider bare2:some-regular-requirer

followed by cleanup:

juju remove-application bare1 bare2

then your log should say something along the lines of:

$ juju debug-log --ms --include unit-bare1-0 --replay
01:00:52.331  Hook: install
01:00:52.751  replicas:32: Hook: replicas-relation-created
01:00:53.121  Hook: leader-elected
01:00:53.507  Hook: config-changed
01:00:53.863  Hook: start
01:00:54.235  replicas:32: Hook: replicas-relation-joined
01:00:54.596  replicas:32: Hook: replicas-relation-changed

01:02:21.910  some-regular-provider:34: Hook: some-regular-provider-relation-created
01:02:22.273  some-regular-provider:34: Hook: some-regular-provider-relation-joined
01:02:22.617  some-regular-provider:34: Hook: some-regular-provider-relation-changed
01:02:22.970  some-regular-provider:34: Hook: some-regular-provider-relation-joined
01:02:23.357  some-regular-provider:34: Hook: some-regular-provider-relation-changed

01:09:08.241  replicas:32: Hook: replicas-relation-departed
01:09:08.632  some-regular-provider:34: Hook: some-regular-provider-relation-departed
01:09:08.965  some-regular-provider:34: Hook: some-regular-provider-relation-departed
01:09:09.310  some-regular-provider:34: Hook: some-regular-provider-relation-broken
01:09:09.731  Hook: stop
01:09:10.092  Hook: remove

where you can empirically convince yourself that, for example:

  • “peer relation created” may fire as early as next after install.
  • “peer relation depated” never fires
  • “regular relation departed” fires per related unit
  • “regular relation broken” fires per app

Not necessarily python operators

Since the juju context is provided via environment variables, operators don’t have to be written in python:

but modern charms are all written in python using the operator framework.

5 Likes

This is such an excellent writeup with fantastic code examples!

I will try to use this in workshops and teachings of juju further on!

It could be used in conjuction with the state-diagram aswell. @pedroleaoc @tmihoc @ppasotti

1 Like

nicely done Leon,

as someone who writes exclusively in bash-only … I like to hear how others see this feature… I hope to stay away from Python as long as possible… as well as k8s… but the world seems to be requiring a lot more from orchestration these days so… we’ll see