How to set the workload version

Contents:

Set the workload version

Applications modelled by charms have their own version; each application will have its own versioning scheme, and its own way of accessing that information. To make things easier for Juju admins, the charm should expose the workload version through Juju - it will be visible in juju status (in the default tabular view, in the application table, in the “Version” column; in the JSON or YAML format, under applications.<app name>.version).

If the charm has not set the workload version, then the field will not be present in JSON or YAML format, and if the version string is too long or contains particular characters then it will not be displayed in the tabular format.

For Kubernetes charms, the workload is typically started in the <container>-pebble-ready event, and the version can be retrieved and passed to Juju at that point. If the workload cannot immediately provide a version string, then your charm will need to do this in a later event instead.

For machine charms, the workload should be available in the start event, so you can retrieve the version from it and pass it to Juju in a start event handler. In this case, if you don’t already have a start handler, in the src/charm.py file, in the __init__ function of your charm, set up an observer for the start event and pair that with an event handler. For example:

self.framework.observe(self.on.start, self._on_start)

See more: ops.StartEvent

Now, in the body of the charm definition, define the event handler. Typically, the workload version is retrieved from the workload itself, with a subprocess (machine charms) or Pebble exec (Kubernetes charms) call or HTTP request. For example:

def _on_start(self, event: ops.StartEvent):
    # The workload exposes the version via HTTP at /version
    version = requests.get("http://localhost:8000/version").text
    self.unit.set_workload_version(version)

See more: ops.Unit.set_workload_version

Examples: jenkins-k8s sets the workload version after getting it from the Jenkins package, discourse-k8s sets the workload version after getting it via an exec call, synapse sets the workload version after getting it via an API call

Test setting the workload version

See first: Get started with charm testing

You’ll want to add three levels of tests, unit, scenario, and integration.

Write unit tests

See first: How to write unit tests for a charm

To verify the workload version is set in a unit test, use the ops.testing.Harness.get_workload_version() method to get the version that the charm set. In your tests/unit/test_charm.py file, add a new test to verify the workload version is set; for example:

# You may already have this fixture to use in other tests.
@pytest.fixture()
def harness():
    yield ops.testing.Harness(MyCharm)
    harness.cleanup()

def test_start(harness):
    # Suppose that the charm gets the workload version by running the command
    # `/bin/server --version` in the container. Firstly, we mock that out:
    harness.handle_exec("webserver", ["/bin/server", "--version"], result="1.2\n")
    # begin_with_initial_hooks will trigger the 'start' event, and we expect
    # the charm's 'start' handler to set the workload version.
    harness.begin_with_initial_hooks()
    assert harness.get_workload_version() == "1.2"

See more: ops.testing.Harness.get_workload_version

Examples: grafana-k8s checking the workload version (and the earlier mocking), sdcore-webui checks both that the version is set when it is available, and not set when not

Write scenario tests

See first: How to write scenario tests for a charm

To verify the workload version is set using Scenario, retrieve the workload version from the State. In your tests/scenario/test_charm.py file, add a new test that verifies the workload version is set. For example:

def test_workload_version_is_set():
    ctx = scenario.Context(MyCharm, meta={"name": "foo"})
    # Suppose that the charm gets the workload version by running the command
    # `/bin/server --version` in the container. Firstly, we mock that out:
    container = scenario.Container(
        "webserver",
        exec_mock={("/bin/server", "--version"): scenario.ExecOutput(stdout="1.2\n")},
    )
    out = ctx.run('start', scenario.State(containers=[container]))
    assert out.workload_version == "1.2"

Write integration tests

See first: How to write integration tests for a charm

To verify that setting the workload version works correctly in an integration test, get the status of the model, and check the workload_version attribute of the unit. In your tests/integration/test_charm.py file, after the test_build_and_deploy test that charmcraft init provides, add a new test that verifies the workload version is set. For example:

# `charmcraft init` will provide you with this test.
async def test_build_and_deploy(ops_test: OpsTest):
    # Build and deploy charm from local source folder
    charm = await ops_test.build_charm(".")

    # Deploy the charm and wait for active/idle status
    await asyncio.gather(
        ops_test.model.deploy(charm, application_name=APP_NAME),
        ops_test.model.wait_for_idle(
            apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000
        ),
    )

async def test_workload_version_is_set(ops_test: OpsTest):
    # Verify that the workload version has been set.
    status = await ops_test.model.get_status()
    version = status.applications[APP_NAME].units[f"{APP_NAME}/0"].workload_version
    # We'll need to update this version every time we upgrade to a new workload
    # version. If the workload has an API or some other way of getting the
    # version, the test should get it from there and use that to compare to the
    # unit setting.
    assert version == "3.14"

Examples: synapse checking that the unit’s workload version matches the one reported by the server


Contributors:@tmihoc, @tony-meyer