How to add a configuration option to a charm

See first: Juju | Application configuration, Juju | How to configure an application

Contents:

  1. Define a configuration option
  2. Observe the config-changed event and define the event handler
  3. (If applicable) Update and restart the Pebble layer
  4. Test observing the event
  5. Verify the configuration option

Define a configuration option

In the charmcraft.yaml file of the charm, under config.options, add a configuration definition, including a name, a description, the type, and the default value. The example below shows how to define two configuration options, one called name of type string and default value Wiki, and one called skin with type string and default value vector:

config:
  options:
    name:
      default: Wiki
      description: The name, or Title of the Wiki
      type: string
    skin:
      default: vector
      description: skin for the Wiki
      type: string

See more: File charmcraft.yaml > config

Observe the config-changed event and define the event handler

In the src/charm.py file of the charm project, in the __init__ function of the charm, set up an observer for the config changed event and pair that with an event handler:

self.framework.observe(self.on.config_changed, self._on_config_changed)

See more: Event config-changed

Then, in the body of the charm definition, define the event handler. Here you may want to read the current configuration value, validate it (Juju only checks that the type is valid), and log it, among other things. Sample code for an option called server-port, with type int, and default value 8000:

def _on_config_changed(self, event):
   port = self.config["server-port"] 

   if port == 22:
       self.unit.status = ops.BlockedStatus("invalid port number, 22 is reserved for SSH")
       return
   
   logger.debug("New application port is requested: %s", port)
   self._update_layer_and_restart(None)

See more: ops.CharmBase.config

  • Multiple configuration values can be changed at one time through Juju, resulting in only one config_changed event. Thus, your charm code must be able to process more than one config value changing at a time.
  • If juju config is run with values the same as the current configuration, the config_changed event will not run. Therefore, if you have a single config value, there is no point in tracking its previous value – the event will only be triggered if the value changes.
  • Configuration cannot be changed from within the charm code. Charms, by design, aren’t able to mutate their own configuration by themselves (e.g., in order to ignore an admin-provided configuration), or to configure other applications. In Ops, one typically interacts with config via a read-only facade.

(If applicable) Update and restart the Pebble layer

If your charm is a Kubernetes charm and the config affects the workload: Update the Pebble layer to fetch the current configuration value and then restart the Pebble layer.

Test observing the event

See first: Get started with charm testing

You’ll want to add two levels of tests: unit and Scenario.

Write unit tests

See first: How to write unit tests for a charm

To use a unit test to verify that the configuration change is handled correct, the test needs to trigger the config-changed event and then check that the update method was called. In your tests/unit/test_charm.py file, add the following test functions to the file:

def test_invalid_port_configuration():
    harness = ops.testing.Harness()
    harness.begin()

    harness.update_config({"server-port": 22})
    assert isinstance(harness.model.unit.status, ops.BlockedStatus)

def test_port_configuration(monkeypatch):
    update_called = False
    def mock_update(*args):
        update_called = True
    monkeypatch.setattr(MyCharm, "_update_layer_and_restart", mock_update)

    harness = ops.testing.Harness()
    harness.begin()

    harness.update_config({"server-port": 8080})

    assert update_called

Write scenario tests

See first: How to write scenario tests for a charm

To use a Scenario test to verify that the config-changed event validates the port, pass the new config to the State, and, after running the event, check the unit status. For example, in your tests/scenario/test_charm.py file, add the following test function:

def test_open_port():
    ctx = scenario.Context(MyCharm)

    state_out = ctx.run("config_changed", scenario.State(config={"server-port": 22}))

    assert isinstance(state_out.unit_status, ops.BlockedStatus)

Verify the configuration option

To verify that the configuration option works as intended, pack your charm, update it in the Juju model, and run juju config followed by the name of the application deployed by your charm and then your newly defined configuration option key set to some value. For example, given the server-port key defined above, you could try:

juju config <name of application deployed by your charm> server-port=4000

See more: Juju | How to manage applications > Configure an application

Contributors: @ibraaoad @mmkay

How do I know which of the config options changed?

Hi Kos,

As far as I know you can’t do this at the moment. A common pattern to for people to compare against values in StoredState or similar, but clearly you’ll want to minimise how much of config you’re replicating into StoredState.

Happy to take a look at the particular case and assist.

Jon :slight_smile:

1 Like

I am a bit skeptical about using the StoredState in the config-change hook. Let’s say two config options change causing the respective callbacks to be called. The first callback goes through and updates the the StoredState, the second callback throws an exception. This results in the StoredState not storing the value of the first config option. I guess this is why the docs [1] say:

Invocations of associated callbacks should be idempotent and should not make changes to the environment, or restart services, unless there is a material change to the charm’s configuration.

This was a problem with the old reactive framework too. From the docs [2] the semantics of the StoreState do not seem to have changed:

The Charmed Operator Framework will only persist changes in stored state to the Juju storage backend when the lifecycle event from which it is being manipulated returns successfully. If an exception is thrown that causes the event invocation to exit early, the state will not be saved.

[1] Juju | Lifecycle Events
[2] Juju | Framework Constructs

1 Like

I added some notes at the bottom to clarify how the config-changed event works

1 Like

Thanks Nick!

Perhaps we should change self.model.config to self.config in the example code?