Scriptlets and Unit-Less Charms

Unit-less-ness

At the Madrid engineering sprint this year, our AI hackathon yielded the first proof-of-concept for what we call a unit-less charm.

Just like a regular charm, a unit-less charm responds to changes and schedules actions in order to converge on its desired state, but it has no provisioned cloud resource or workload – it runs directly on the controller.

But how do we get an application with behaviour, yet without any units?

Scriptlets

The basis for such charms are scriptlets written in Starlark, a lightweight Python-like scripting language designed for embedded use.

Where regular charms rely on an execution environment in their provisioned machine/container – Python, Bash etc, the controller provides the execution environment for unit-less charms. This can include the enforcement of bounds for memory and compute in order to protect the controller from rogue scriptlets, but that detail is not the subject of this post; see the Canonical Starlark repository linked above for more information.

Not That Different

To anchor in the familiar, let’s take the common charm in its Juju context.

  • The Juju agent watches for changes and compares local/remote state to determine what hooks to fire.
  • The charm defines logic to execute for each hook it wants to handle.
  • Juju dispatches hooks with a context, which includes data about the hook that the charm might need to know.
  • The charm upon receiving a hook can operate its workload, query Juju state and take actions via hook tools.

How does a scriptlet-based charm compare?

Strikingly similar, right? Events are synonymous with hooks, complete with associated data and per-event handlers. Scriptlets can’t have side-effects, but they can return intents, which Juju will action on behalf of the scriptlet, much like the committing of a regular hook at completion.

Even the syntax should be familiar to established charmers. This is from the charm that we wrote during the Madrid hackathon, which admittedly doesn’t do a lot, but shows the congruence with regular charm syntax.

def init():
    juju.observe("config_changed", on_config_changed)
    juju.observe("relation_created", on_relation_created)
    juju.observe("relation_joined", on_relation_joined)
    juju.observe("relation_changed", on_relation_changed)
    juju.observe("relation_departed", on_relation_departed)
    juju.observe("relation_broken", on_relation_broken)

def on_config_changed(event):
    config = event.config
    parts = []
    for key in sorted(config.keys()):
        parts.append("%s=%s" % (key, config[key]))
    msg = ", ".join(parts) if parts else "no config"
    juju.set_status("active", message = "config changed :" + msg)

def on_relation_created(event):
    juju.set_status("active", message = "relation created")

def on_relation_joined(event):
    juju.set_status("active", message = "related")
    juju.set_state("controller-uuid", event.controller_uuid)
    juju.set_state("model-name", event.model_name)

def on_relation_changed(event):
    juju.set_status("active", message = "relation changed")

def on_relation_departed(event):
    juju.set_status("waiting", message = "relation departed")

def on_relation_broken(event):
    juju.set_status("waiting", message = "relation broken")

How Might This Be of Use?

Canonical folks whose work intersects with the Juju ecosystem may over the years have come upon terms like stacks and governors. Stacks in particular are exactly the kind of long-desired capabilities that are within our reach now.

Stacks are the grouping of of charms so that their aggregation can be represented as a single charm for the purposes of integrations. Being able to integrate a model with the Canonical observability stack, rather than each of the individual charms is just one potential scenario for stacks.

The Future

The Juju team is adding this capability right now. But we need input from charm authors.

Today, the author of a charm has the following touch points with the unit agent:

  • The list of hooks that the charm can be issued and what changes they represent in state.
  • The data supplied with a hook context and the tools available to further query the current state, such as network-get.
  • The side effects that charms have as a result of handling hooks - open-ports, relation-set etc.

Now let your imagination run wild. What would be amazing? Think of the magic things that you would do if you could write a charm that endows the controller/model with your behaviour. What do you need to achieve this magic?

  • What events do you want to be notified about?
  • What data do you need about these events, and what do you want to query about the wider state of the world?
  • What actions do you want the controller to perform for you as a result of handing these events?

Reach out to the Juju team with your ideas and questions.

Acknowledgements

It must be remembered that although we anticipate exciting features in Juju, the work on scriptlets was the labour of years from the OCTO team, particularly @marco6 and @kcza.

13 Likes

Very neat!

Where I’d really like to use scriptlets is to get rid of integrator/proxy charms. It feels relatively wasteful of finite resources when you have to siphon off a certain amount of compute to use integrator charms that just forward information to another charm. It’d be great if the controller/model could respond to config-changed, action, and secrets-related events.

For example, it’d be great if we could convert ldap-integrator into a scriptlet so that I don’t have to explain why an entire machine needs to be dedicated to a charm that just forwards LDAP endpoint information to another charm.

5 Likes

It’s not entirely clear to me if scriptlets are all supposed to be stateless, or if it’s just because of the simplicity of the PoC. If the former, as a not-so-established charmer, I’m curious: is this somehow a return to the ideas behind charms.reactive Python functions, or even Ensemble formulas?

Great stuff!

We have some examples of charms that are “without workload”, only to be able to act on relation data. For example, a role distribution charm converting input config into role placement over multiple apps, and a relation translation charm. These are kind of painful to place.

What is the concurrency model for these scriptlets? What is the actual limit within a model? Since the juju agents executes hooks in a serialized manner, this might bring the same limitation and overall slowness back into play.

1 Like

Charms like these are perfect candidates for scriptlets. We will provide an event model familiar to charmers who want to write application configuration, act when it changes and participate in relations.

Yes; no local state, and no persistent thread/process. Scriptlet execution is deterministic.

We’ll have to play with the configurable aspects of scriptlet safety – allocations, steps etc in order to give guidance on what a given controller spec can reasonably accommodate.

The dispatch of events across different applications will be semantically concurrent, but initially we’ll go with the same semantics per application as machines have today. That is, one event at a time per application.

Distribution across controllers in HA is something @simonrichardson and I are actively thinking about.

Regarding performance, we already have advantages over today’s units that run on provisioned compute:

  • There are no local sockets, and no new process spin-up.
  • No file-based locking – the execution semantics serialise per application already.
  • No communication via the Juju API – we are already on the controller, using services directly.

I can see a use case for event filters or batching. If the scriptlet would be able to intercept and mutate the event queue for other charms… Imagine the possibilities. We could write scriptlets that ‘buffer and compress’ long queues of events during event storms, or even synthesise custom events.

juju → relation-changed:1 & relation-changed:2 & config-changed & relation-changed:3 → charm

could become:

juju → foo-relation-changed:1 & foo-relation-changed:2 & config-changed & foo-relation-changed:3 → scriptlet → relations-changed → charm

I’m sure with this I have the attention of the whole DP team :stuck_out_tongue:

1 Like

Note that something not addressed so far is a normal charm that can include scriptlets as hook handlers. I avoided it deliberately for this post so as not to present too many rabbit holes, but here goes…

What I have been mulling is the notion of a dedicated unit agent for what we’ve come to know as holistic charms. If this was regarded as a viable idea it is also where I would put the ability to run scriptlet hooks.

In such a setup, a particular list of hook types becomes a single “something changed” trigger. What this tends towards is the firing of fewer hooks. Consider the sequence of:

  • hook A → handle,
  • hook B → handle,
  • hook C → handle,

where handle is the generic resolver. These would be collapsed and sent to the charm as a single change hook.

Filtering would be feasible in a scritplet, but less so batching up hooks. The reason is that without any persistent state, an executing scriptlet can’t reason about one hook in the context of others firing proximate to it. However, this could be pushed up into Juju’s logic for dispatch. For example, if we simply decided that events were dispatched in plural, whatever accumulated while one set was being handled, would be de-duplicated and dispatched subsequently.

We’ll develop these ideas in coming pulses.

3 Likes

Another idea struck me, if we want to create these type of unit less charm able to manage k8s resources, how do we handle access to k8s api?

Currently, with a charm based in k8s, it’s very easy, as we can just have --trust (right token laid out in the container), and have the operator talk to k8s api resolved. But in the context of a controller managing multi clouds, and the controller not being deployed on the current k8s, it’s making this scenario harder.

There’s a pattern that multiple k8s charm use to get a loadbalancer IP assigned to them. While I do think that ultimately this should be a Juju responsibility, getting into a middle ground with a unit-less charm responsible for assigning would be better than encapsuled libraries into the charms, as this would make the behaviour much easier to manage on day-2 operations.

edit: this might actually be desirable, if you fall in this category of needs, then the scriptlets is not for you, and you should keep relying on the safety mechanisms? or give the unitless charms the right credentials?

What about code to validate configuration options (or even relations) in a kind of pre-hook validation, so if the scriplet does not accept the config option (or any other check), then it is not changed and no hooks are fired?

For example: juju config app class=“something”

  • Invalid config class, something is not in (“valid1”, “valid2”).

This would essentially allow scriptlets to enforce a schema on config options or relation constraints, saving the workload units from dealing with bad data.


Another nice addition could be the inclusion of timer capabilities for the “scriptlet charms”, so they can act as very lightweight schedulers to other charms.

1 Like