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.

6 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

1 Like

Generally, as soon as you start getting into developing a more complex software - the code also becomes more complex, requiring more complex constructs where the abstractions needs to be easier. This is where Python comes in to play and its rich ecosystem of libraries etc. which are all possible to include in your charms.

Going into “ops” is a natural step for you @emcp =)

1 Like

definitely… I think for me this is a hobby so far… and I have plenty of Ops work at the day job as you know :wink:

1 Like