Functional charms: fast iterations for simple tester charms

Sometimes you want to have a simple charm to test some behaviour or juju interaction; but going through the whole charmcraft init --> edit --> charmcraft pack --> deploy is just a little bit too much trouble.

Suppose you could, instead, write this:

my-charm.py

import functional

@functional.charm
def foo(self: CharmBase, logger: logging.Logger = None):
    from ops.model import ActiveStatus
    return ActiveStatus('welcome to functional charms')

And then run:

jhack charm func /path/to/my-charm.py -d foo

The script will inject the code you decorated with @functional.charm into a templated charm’s __init__ method and deploy it under application name foo.

The unit takes the usual time to come up, but deploying the charm (including packing) are reduced to less than a second!

Limitations

This hack is made with a specific use case in mind, namely testing very simple logic and relation data, and generating once a reusable template. As a consequence there are several limitations.

Static template

At the moment the charm template is static and so is the venv attached to it, meaning that you can’t use in the functional charm any dependencies that don’t come with the template.

Of course you can charmcraft pack a charm that you wish to promote to template, and then do:

jhack charm func /path/to/my-charm.py -t /path/to/template.charm -d foo

Which again is a one-off investment.

Secondly, this design implies that any modules you’ll require in the charm will need to be imported inside the function.

import bar
@functional.charm
def foo(self: CharmBase, logger: logging.Logger = None):
    from ops.model import ActiveStatus
    bar.do_things()  # will raise NameError('bar')
    return ActiveStatus('welcome to functional charms')

@functional.charm
def foo(self: CharmBase, logger: logging.Logger = None):
    import bar
    from ops.model import ActiveStatus
    bar.do_things()  # will work!
    return ActiveStatus('welcome to functional charms')

We could work on automating this part, or facilitating injecting a whole charm.py file in the template instead of a single function.

Static metadata

The metadata is also static: so if you wish to access any event except the core lifecycle ones (e.g. relation events, actions, pebble-ready) you’ll have to get the necessary yaml specs manually in place in the running unit or the packed charm, or tweak the template.

I think adding support for this should not be too hard.

No observers

In order to mark a function as an event handler like so: self.framework.observe(self.on.foo, _on_foo), _on_foo needs to point to a method of ‘self’. Not a function, not a lambda. So at the moment that is also not supported. Unless of course you go and tweak the template.

I plan on adding some support for this in the near future.