How to manage resources

See also:

Implement the feature

A charm can require file or oci-image resources, defined in charmcraft.yaml. You’ll upload the resources to Charmhub as one of the charm publishing steps. Then when a user deploys your charm from Charmhub, the resources will be available to your charm code.

For example, suppose your charmcraft.yaml file contains this simple resource definition:

resources:
  my-resource:
    type: file
    filename: somefile.txt
    description: test resource

In your charm’s src/charm.py you can now use Model.resources.fetch(<resource_name>) to get the path to the resource, then manipulate it as needed. For example:

import logging

import ops

logger = logging.getLogger(__name__)


class MyCharm(ops.CharmBase):
    def _on_config_changed(self, event: ops.ConfigChangedEvent):
        # Get the path to the file resource named 'my-resource'.
        try:
            resource_path = self.model.resources.fetch('my-resource')
        except NameError:
            logger.exception('Resource my-resource is not declared.')
            self.unit.status = ops.BlockedStatus(
                "Resource 'my-resource' not found; did you forget to "
                'declare it in charmcraft.yaml?'
            )
            return
        except ops.ModelError:
            logger.exception('Could not claim resource my-resource.')
            self.unit.status = ops.BlockedStatus(
                "Could not claim resource 'my-resource'; run "
                '`juju debug-log` for more information'
            )
            return

        with resource_path.open() as f:
            content = f.read()
        # Do something with the content.

fetch() raises NameError if the resource isn’t declared in charmcraft.yaml, and ops.ModelError if it is declared but Juju can’t provide it. Otherwise it returns a pathlib.Path pointing at the resource.

During development it’s often useful to specify the resource at deploy time, so that you can test a change without publishing a new charm or resource for every minor fix. In the snippet below, we create a file with some text content and pass it to the Juju controller to use in place of any published my-resource resource:

echo "TEST" > /tmp/somefile.txt
charmcraft pack
juju deploy ./my-charm.charm --resource my-resource=/tmp/somefile.txt

Test the feature

Write unit tests

See first: How to write unit tests for a charm

If your charm needs access to a resource, make it available with ops.testing.State.resources, passing an ops.testing.Resource for each one. For example, to make the my-resource file resource available:

import pathlib

from ops import testing

ctx = testing.Context(
    MyCharm,
    meta={
        'name': 'julie',
        'resources': {'my-resource': {'type': 'file'}},
    },
)
resource = testing.Resource(name='my-resource', path='/path/to/somefile.txt')
with ctx(ctx.on.start(), testing.State(resources={resource})) as mgr:
    path = mgr.charm.model.resources.fetch('my-resource')
    assert path == pathlib.Path('/path/to/somefile.txt')

Write integration tests

See first: How to write integration tests for a charm

During development and testing, it’s useful to specify resource locations when deploying the charm.

The conventional place to specify resource locations for testing is the upstream-source field in charmcraft.yaml’s resources section:

import pathlib

import jubilant
import pytest
import yaml


METADATA = yaml.safe_load(pathlib.Path('./charmcraft.yaml').read_text())


@pytest.mark.juju_setup
def test_deploy(charm: pathlib.Path, juju: jubilant.Juju):
    resources = {
        name: res['upstream-source']
        for name, res in METADATA['resources'].items()
    }
    juju.deploy(charm, resources=resources)
    juju.wait(jubilant.all_active)

See also: jubilant.Juju.deploy

Examples: valkey-operator, kafka-k8s-operator

We recommend including the charm fixture (even though it’s not used) so that the test fails immediately if a .charm file isn’t available.