Skip to content

docs: grab workload version in k8s tutorial#2559

Open
tromai wants to merge 13 commits into
canonical:mainfrom
tromai:grab-workload-version-in-k8s-tutorial
Open

docs: grab workload version in k8s tutorial#2559
tromai wants to merge 13 commits into
canonical:mainfrom
tromai:grab-workload-version-in-k8s-tutorial

Conversation

@tromai

@tromai tromai commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Closes #2282.

This PR modify the From zero to hero: Write your first Kubernetes charm tutorial:

  • Added a section in Chapter 1: creating a helper module for the charm to interact with the workload application. This is inspired by what we have in the machine charm tutorial. The helper module contains the function to get the version of the FastAPI web server.
  • Added another section in Chapter 1: exposing this version as the workload version to Juju (following this guide).
  • Added unit tests and integration tests to cover the above logic.
  • Propagate the changes to the other chapters.

The PR reviewer can focus on the Markdown docs. We can leave the code changes in examples/ at the end, when we have finalized the tutorial, as the changes in examples/ are plenty and mostly reflect the tutorial.

@tromai tromai self-assigned this Jun 10, 2026
tromai added 3 commits June 11, 2026 17:02
port the grab workfload version code to tutorial 2->5 and use auto fixture for patching get_version in unit tests

and also change localhost -> 0.0.0.0 in the fastapi_demo.py modules in examples
Comment thread examples/k8s-1-minimal/tests/unit/test_charm.py Outdated
@tromai

tromai commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

While working on this task, I noticed that in Chapter 5, we introduced requests for the integration tests. However, we didn't mention adding this dependency using uv add or modifying pyproject.toml. Please let me know if this is a real gap in the doc, and I can fix it in this PR too.

@tromai tromai added the rainy day Small items done in ~10% of each week's time label Jun 15, 2026
@tromai tromai marked this pull request as ready for review June 15, 2026 06:49
@dwilding

Copy link
Copy Markdown
Contributor

While working on this task, I noticed that in Chapter 5, we introduced requests for the integration tests. However, we didn't mention adding this dependency using uv add or modifying pyproject.toml. Please let me know if this is a real gap in the doc, and I can fix it in this PR too.

Oops, you're quite right. Well spotted! In the tutorial we say "Then import pytest, pytest_jubilant, and requests" - but only the first two are predefined dependencies in the Charmcraft profile.

@tromai

tromai commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

While working on this task, I noticed that in Chapter 5, we introduced requests for the integration tests. However, we didn't mention adding this dependency using uv add or modifying pyproject.toml. Please let me know if this is a real gap in the doc, and I can fix it in this PR too.

Oops, you're quite right. Well spotted! In the tutorial we say "Then import pytest, pytest_jubilant, and requests" - but only the first two are predefined dependencies in the Charmcraft profile.

Thanks for the confirmation David. I added the step to run uv add requests in the tutorial 80bbc83

@dwilding dwilding left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a serious chunk of work - thanks for working through it so thoroughly! It's looking great. I'm sending a first batch of comments and will hold off reviewing further until you've had a chance to think about them.

1. The same Juju controller injects Pebble -- a lightweight, API-driven process supervisor -- into each workload container and overrides the container entrypoint so that Pebble starts when the container is ready.
1. When the Kubernetes API reports that a workload container is ready, the Juju controller informs the charm that the instance of Pebble in that container is ready. At that point, the charm knows that it can start communicating with Pebble.
1. Typically, at this point the charm will make calls to Pebble so that Pebble can configure and start the workload and begin operations.
1. Besides Pebble exec, the charm can communicate to the workload using HTTP requests. This is possible because they share the same pod (Kubernetes charms are deployed following sidecar pattern).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's good to make this feel more sequential, following on from "begin operations" in the previous item. Also I don't think we need to mention the sidecar pattern, as item 1 essentially explains it.

Suggested change
1. Besides Pebble exec, the charm can communicate to the workload using HTTP requests. This is possible because they share the same pod (Kubernetes charms are deployed following sidecar pattern).
1. During operations, the charm may need to directly communicate with the workload using HTTP requests. This is possible because the charm container and workload container share the same pod.


### Write a helper module

Your charm will interact with our workload application `api_demo_server`. It’s a good idea to write a helper module that wraps `api_demo_server`. Charmcraft created `src/fastapi_demo.py` as a placeholder helper module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Your charm will interact with our workload application `api_demo_server`. Its a good idea to write a helper module that wraps `api_demo_server`. Charmcraft created `src/fastapi_demo.py` as a placeholder helper module.
Your charm will interact with our workload application `api_demo_server`. It's a good idea to write a helper module that wraps `api_demo_server`. Charmcraft created `src/fastapi_demo.py` as a placeholder helper module.

Comment on lines +92 to +94
The helper module will be independent of the main logic of your charm. This will make it easier to test your charm. In this tutorial, the helper module only contains the logic to get the version of `api_demo_server`. The server has an endpoint at `/version` that returns a JSON payload containing the version number (see {ref}`study-your-application`).

This is called the workload version. To make things easier for Juju admins, our charm should expose the workload version to Juju. It will be visible in `juju status`. For more information, see {ref}`how-to-set-the-workload-version`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK to drop the reference link because people typically follow in sequence and there's not much be gained by jumping back at this point.

A couple of other suggestions: It's best to use the term "Juju users" instead of admins (although we still say "admins" in a bunch of places for historical reasons). I recommend changing juju status because we haven't introduced this command yet.

Suggested change
The helper module will be independent of the main logic of your charm. This will make it easier to test your charm. In this tutorial, the helper module only contains the logic to get the version of `api_demo_server`. The server has an endpoint at `/version` that returns a JSON payload containing the version number (see {ref}`study-your-application`).
This is called the workload version. To make things easier for Juju admins, our charm should expose the workload version to Juju. It will be visible in `juju status`. For more information, see {ref}`how-to-set-the-workload-version`.
The helper module will be independent of the main logic of your charm. This will make it easier to test your charm. In this tutorial, the helper module only contains the logic to get the version of `api_demo_server`. The server has an endpoint at `/version` that returns a JSON payload containing the version number. This is called the workload version.
To make things easier for Juju users, your charm should expose the workload version to Juju. It will be visible in Juju's status output. For more information, see {ref}`how-to-set-the-workload-version`.


This is called the workload version. To make things easier for Juju admins, our charm should expose the workload version to Juju. It will be visible in `juju status`. For more information, see {ref}`how-to-set-the-workload-version`.

We first define a function to get the workload version. Replace the content of `src/fastapi_demo.py` with:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the first sentence can be dropped because we've already said what what we're going to do (and the code is short and clear enough)

Suggested change
We first define a function to get the workload version. Replace the content of `src/fastapi_demo.py` with:
Replace the content of `src/fastapi_demo.py` with:



def get_version(port: int) -> str:
"""Get the version of fastapi_demo installed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this instead?

Suggested change
"""Get the version of fastapi_demo installed.
"""Get the version of fastapi_demo that is running.

"Failed to get version from the server: %s. Please double check your port config",
version_e,
)
self.unit.status = ops.BlockedStatus(str(version_e))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's best to use a hardcoded status string here. Blocked status implies that the Juju user should be able to unblock the charm by themself, and ideally the message should give some indication of how.

As I write this, now I'm wondering whether blocked is really right. I'd like to hear other opinions - but here's what I'm thinking:

  • If getting the version fails because we can't connect, it might be that the workload container is temporarily unavailable. The charm should go into Maintenance status with an appropriate message. The user needs to wait.
  • If we got the version, but we couldn't decode it, there's nothing the user can do. We shouldn't catch the error; we should let the charm go into Error state. (The user needs to bug the charm developer about it.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed this a bit with Nhan, and a couple of things:

  • Agreed it's better to use a hard-coded message, more like the log message, for example: "Failed to get version from server: check port config"
  • I don't think Maintenance status would be good here. That means we the charm is doing some maintenance and we know it's going to come right in a minute. But here it might never come right until they fix the port. So I'd recommend keeping BlockedStatus
  • Agreed re decoding being a hard error -- I think we should just omit the JSONDecodeError and let that one bubble up


When the config is loaded as part of creating the Pebble layer, if the config is invalid (in our case, if the port is set to 22), then a `ValueError` will be raised. The `_replan_workload` method handles that by logging the error and setting the status of the unit to blocked, letting the Juju user know that they need to take action.

We also add error handling for getting the workload version. If the charm fails to obtain a valid version number, we set the unit status to blocked. This lets the Juju user know that either the charm code or the workload application behaved unexpectedly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need updating if the error handling strategy changes (which I think it should)


```python
def test_config_changed():
def test_config_changed(mock_version):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Continuing discussion from another comment]

I agree that we need mock_version here. Because _on_config_changed calls _replan_workload, which gets the workload version.

I think it would be worth adding a brief sentence or two immediately after the code block, to explain why mock_version is needed in this instance. Then don't mention it again for the rest of the tutorial.

And define a method that does the various checks, adding appropriate statuses. The library will take care of selecting the 'most significant' status for you.

```python
def _on_collect_status(self, event: ops.CollectStatusEvent) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably we should get the workload version at the end of this method, as a way of confirming that the workload has started correctly. I'd class that as icing on the cake, not a must-have for this PR.


```python
def test_get_db_info_action():
def test_get_db_info_action(mock_version):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test shouldn't need mock_version, because _on_get_db_info_action doesn't end up getting the workload version. But if we change _on_collect_status so that it gets the workload version, then we will need mock_version here.

@james-garner-canonical james-garner-canonical left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good. I agree with David's suggestions.

@tromai

tromai commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks everyone for the reviews. I really appreciate it !

I will address the feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rainy day Small items done in ~10% of each week's time

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grab workload version in K8s tutorial

4 participants