Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a demo of how to write a functional charm, starting from charmcraft's Kubernetes profile #1522

Open
dwilding opened this issue Jan 8, 2025 · 8 comments
Assignees
Labels
docs Improvements or additions to documentation

Comments

@dwilding
Copy link
Contributor

dwilding commented Jan 8, 2025

Our tutorial Create a minimal Kubernetes charm says:

Once you’ve mastered the basics, you can speed things up by navigating to your empty charm project directory and running charmcraft init --profile kubernetes.

I'd like to try doing this. Specifically, I'd like to create a minimal demo that:

  • Starts with charmcraft init --profile kubernetes
  • Produces a functional Kubernetes charm for the demo FastAPI server
  • Has the observers in src/charm.py and the logic for driving the FastAPI container in a separate module, in the style of jnsgruk/zinc-k8s-operator

This is partly for my own learning 🙂 - but I expect it to be a helpful reference as we work on making the tutorial shorter.

As a follow-on task, I'd like to give new templates to the charmcraft team so that charmcraft init makes charms with observer and workload logic in separate files. Trying it for FastAPI first should help to iron out any kinks.


TBD: This work might also be helpful if we want to update the Give it a try example in the Ops README.

@dwilding dwilding added docs Improvements or additions to documentation 25.04 labels Jan 8, 2025
@dwilding dwilding self-assigned this Jan 8, 2025
@tmihoc
Copy link
Member

tmihoc commented Jan 8, 2025

Hi David,

However, my understanding is that using charmcraft init without specifying a profile isn't the recommended way to start.

Using charmcraft init without specifying a profile just means the default profile kicks in, which is the simple profile -- a Kubernetes profile with lots of scaffolding, suitable for beginners.

Distill the tutorial charm into a minimal demo of how to start with charmcraft init --profile kubernetes and produce a functional Kubernetes charm. The observer logic should be in src/charm.py and the workload container logic should be in a separate module.

While a demo sounds nice, demos in general are expensive to maintain and I'm also not sure it would be the most effective means in this case in particular either. I would personally prioritize updating the existing tutorial + how-to content. As in, I'd revisit all the places where we say "in your src/charm.py, add..." and see if it's appropriate to keep that or rather change it to "in your src/workload.py". It should be quick enough for the how-to guides (in most cases where we say "in your src/charm.py file" we talk about registering event observers, and -- if I understand correctly -- that is in fact content we want to keep in src/charm.py. So it's likely only the tutorials that we'll need to update.

Open PRs to charmcraft with adjustments to the Kubernetes profile as needed, especially to separate the charm logic. Also propose charmcraft doc updates to accompany the adjustments (for example, to How to set up a charm project and charmcraft init).

I'm not sure this will be necessary. I mean, if there's a change in the behavior of charmcraft init, the Charmcraft team is probably aware of it and should update the docs accordingly on their own. (They could use Ops help if their docs included any code, but they don't -- they just need to update any content that shows the files creates by chamcraft init.)

tl;dr I think the TODO here is in fact smaller than we originally thought.

@dwilding dwilding removed the 25.04 label Jan 8, 2025
@tonyandrewmeyer
Copy link
Contributor

I'd like to try doing this. Specifically, I'd like to create a minimal demo that:

  • Starts with charmcraft init --profile kubernetes
  • Produces a functional Kubernetes charm for the demo FastAPI server

For what it's worth, this is basically --profile=simple but with that server instead of httpbin. I've always felt like "simple" was a bad name, and it's really "demo" (for one thing, it's the least simple of the three core profiles). Although if you add in a bunch of actions that use the workload API (and therefore the workload module) it would be more and still a demo.

As a follow-on task, I'd like to give new templates to the charmcraft team so that charmcraft init makes charms with observer and workload logic in separate files. Trying it for FastAPI first should help to iron out any kinks.

I still feel that it's going to be difficult to really promote this approach using the profiles. machine and kubernetes are very minimal and don't really have anything that would be moved to a workload module, so you'd either have a module with just docs and maybe a placeholder function (even the name is tricky, since it should probably be the name of the workload, not workload.py) or we need to extend what the profiles do (maybe setting the workload version?). More could be added to simple I guess but I don't think anyone should start off with simple unless they're just learning.

On the other hand, I don't really know how else we'd encourage this behaviour, other than adjusting the docs.

@dwilding
Copy link
Contributor Author

dwilding commented Jan 9, 2025

Thanks Tony! I'll see how far I get with experimenting today, and then maybe we can review tomorrow to see if anything looks promising?

@dwilding
Copy link
Contributor Author

dwilding commented Jan 9, 2025

I'm experimenting in this repo: https://github.com/dwilding/demo-kubernetes-charm

@dwilding
Copy link
Contributor Author

dwilding commented Jan 9, 2025

Here's an initial attempt at a template, with the logic split into charm.py and services.py:
https://github.com/dwilding/demo-kubernetes-charm/tree/979661aaa1326e9c1f54a93c518bd6e82bf78a41/demo/src

I went with the term "services" rather than "workload" because the level of abstraction I had in mind was "the stuff managed by Pebble". Somehow I like "services" more than "workload", which kind of refers to the container itself... but "services" wouldn't really make sense for machine charms - unless Pebble establishes itself there too.

Anyway, right now I mainly want to see if this separation of concerns is reasonable as a starting point for charmers.

@tmihoc
Copy link
Member

tmihoc commented Jan 9, 2025

Just a small FYI that "services" used to be Juju jargon. It was probably long enough ago that it wouldn't matter anymore. At the same time, the jargon mandated at present is "workload".

@tonyandrewmeyer
Copy link
Contributor

Some initial notes:

The Pebble Layer

I understand that the Zinc charm does this, but I feel like having a Pebble layer defined in the workload module is wrong. I think it would be ideal if the workload module doesn't import anything from ops at all, so is entirely standalone from the charming world.

This supports the goal of having modules that are potentially useful outside of charming, and could be published separately, and the inverse of that where there's already an existing Python package for managing a service and that's used by the charm.

In a layer, there are some things that are quite specific to the workload and some that are more about Pebble itself. I think we could establish some sort of standard for defining the workload pieces inside the workload module (or class, if we're going to have a class).

For example (this is off the cuff and not thought through in depth):

  • layer summary and description: these can probably be defined in the charm module
  • summary: maybe something like the module name?
  • description: maybe the module or class docstring?
  • command: maybe an attribute of the module/class with a specific name?
  • override: this feels more charm.py than workload module
  • startup: also feels more charm.py than workload module
  • user, working dir, etc: maybe some sort of well defined typeddict that's an attribute of the module/class?

We'd need to also think about how checks and log targets would get added to this in a more complete charm.

Module name

Rather than workload.py or services.py, I think we ideally want it to be something like demofastapi.py so that when you have a charm that has more than one workload, each of them has a separate management module. This is probably hard (maybe not possible?) to do when rendering a charmcraft profile, though.

TODOs

In requirements.txt, does ops ~= 2.8 need changing?

I think it is good to set this to the latest (minor) release. What you actually need is to have the latest release containing all the functionality you're using (and none of the bugs that would hit you). However, we want people to rapidly move to the latest version, and only really support that version, and one way we push people to that is to not specify which version features are added. So the safest thing is to just keep that version set to the latest.

I would actually have the ops version in a pyproject.toml file and have something (like uv) generate the requirements.txt file as a lock file, but that's somewhat separate from this.

What else might this class typically provide?
We might contact the service (via localhost) to get its version/status

I think this would be the best choice, yes. If you wanted more than that, then you could have the charm have an action that added a name, or listed all the names, and then the workload module/class would need to support that too.

BUT, suppose we want to push a new config file for a service...
Can we use pebble.push() from within this module? Would we even want to do that?

You could, but in my opinion (as above) I think we do not want to.

Would we be better to have this module build the service config,
then charm.py uses self._pebble.push() - similarly to how we handle the layer spec

Yes, I'd do this - a function that returns the config. Note that the config might be a string that needs to be written to a file somewhere or it might be a set of environment variables that need to go into the Pebble layer.

@dwilding
Copy link
Contributor Author

Thank you very much for the comments so far!

Capturing some internal discussion within Charm Tech: If we recommend a pattern for how to split logic between charm.py and a workload module, it would be helpful to have a small & clear example (as planned with this issue) plus a real-sized reference charm that uses the same pattern.

There's definitely a preference that the workload module shouldn't import anything from ops. Perhaps it would help if we very tightly define the expected scope of the workload module - at least as a starting point.

For instance, should the workload module even know how to start the workload? (Meaning, per Tony's comment, does the workload module provide the starting command to charm.py somehow?) Maybe not. What if we took the view that the workload module is supposed to be a "client" module that knows how to communicate with the workload's running process (via an HTTP API for example), then we could reasonably say that it's out of scope for the workload module to care about how the process gets started.

Anyway, just one of many thoughts...! I'll let this sit over the weekend then work on adjusting the approach next week

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants