Skip to content

Commit

Permalink
Initial best practices work.
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyandrewmeyer committed Feb 19, 2025
1 parent d54a26a commit 2fca48c
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 41 deletions.
3 changes: 1 addition & 2 deletions docs/howto/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
```{toctree}
:maxdepth: 2
Manage charms <manage-charms>
Manage logs <manage-logs>
Run workloads with a charm machines <run-workloads-with-a-charm-machines>
Run workloads with a charm Kubernetes <run-workloads-with-a-charm-kubernetes>
Expand All @@ -25,6 +26,4 @@ Write unit tests for a charm <write-scenario-tests-for-a-charm>
Write integration tests for a charm <write-integration-tests-for-a-charm>
Write legacy unit tests for a charm <write-unit-tests-for-a-charm>
Turn a hooks-based charm into an ops charm <turn-a-hooks-based-charm-into-an-ops-charm>
```

123 changes: 123 additions & 0 deletions docs/howto/manage-charms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
(how-to-manage-charms)=
# How to manage charms

> See first: {external+juju:ref}`Juju | How to manage charms or bundles <manage-charms>`
The primary programming language charms are written in is Python, and the
primary framework for developing charms is the Python Operator Framework, or
`ops`.

`charmcraft init` provides you with a `src/charm.py` file that demonstrates the
basis of using the library.

```python
# Import ops and use names from there.
import ops

# Charms always have a class that inherits from ops.CharmBase that define the
# charm's functionality.
class MyCharm(ops.CharmBase):
# There's always an __init__ method that sets up observers.
def __init__(self, framework: ops.Framework):
super().__init__(framework)
framework.observe(...)

# There will always be one or more event handlers that will be called in
# response to a Juju, ops, or custom event.
# Note that this has a leading underscore. These methods should only be
# called by the ops framework, so should not be public.
def _my_event_handler(self, event: ops.EventBase): # or a more specific type
...
self.unit.status = ops.ActiveStatus()

# Finally, the charm.py file always ends with a call to ops.main, passing in the
# charm class.
if __name__ == "__main__":
ops.main(MyCharm)
```

## Interacting with the workload

For simple interactions with an application or service or when a high quality
Python binding is not available, subprocess or API calls should be used to
perform the required operations on the application or service.

TODO: Create a `src/{workload}.py` file for each workload or service that your charm
is managing, and ... For example:

```python
import subprocess

try:
# Comment to explain why subprocess is used.
result = subprocess.run(
# Array based execution.
["/usr/bin/echo", "hello world"],
capture_output=True,
check=True,
)
logger.debug("Command output: %s", result.stdout)
except subprocess.CalledProcessError as err:
logger.error("Command failed with code %i: %s", err.returncode, err.stderr)
raise
```

```{admonition} Best Practice
:class: hint
Limit the use of shell scripts and commands as much as possible in favour of
writing Python for charm code. Examples where it could be reasonable to use a
script include: extracting data from a machine or container which can't be
obtained through Python; or issuing commands to applications that do not have
Python bindings (such as starting a process on a machine).
```

## Base / Python

Charms have access to the default Python version in the Ubuntu release defined
as the base.

Your code should be compatible with the operating system and Juju versions it
will be executed on. For example, if your charm is to be deployed with Juju 3.6,
its Python code should be compatible with Python 3.8.

TODO: put in the relevant bits in pyproject.toml to have tools recognise this.

TODO: can we be more detailed, or link to something here that says which Ubuntu
has which Python, and which base you should support?

## Dependencies

TODO: put in instructions for dependencies from the spec (pyproject, generate requirements.txt)

TODO: instructions on putting libs in charmcraft.yaml and running fetch-libs and then keeping that updated.

> See more: charmcraft.yaml, charmcraft fetch-libs
```{admonition} Best Practice
:class: hint
Including an external dependency in a charm is a significant choice. It can help
with reducing the complexity and development cost. However, every additional
dependency increases the risk for supply chain security issues, the maintenance
burden of having to manage the dependencies, and makes it more difficult to
completely understand all of the code involved in the charm.
```

> See more: [The software dependency issue](https://research.swtch.com/deps)
TODO: is that still the article we want to link to? There are a lot on this problem.

## Events

TODO: There are Juju events, ops events, and custom events. A bit more about how to
respond to each of these, probably mostly linking out.

> See also: Juju events, something about the ops events.
```{admonition} Best Practice
:class: hint
Custom events are defined and emitted by charm libraries. Charms should never
define or emit custom events themselves.
```

> See more: manage libraries
8 changes: 7 additions & 1 deletion docs/howto/manage-libraries.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(manage-libraries)=
# Manage libraries
> See first: {external+charmcraft:ref}`Charmcraft | Manage libraries <manage-libraries>`

> See first: {external+charmcraft:ref}`Charmcraft | Manage libraries <manage-libraries>`
## Write a library

Expand Down Expand Up @@ -30,6 +30,12 @@ class DatabaseRequirer(ops.framework.Object):
self.on.ready.emit()
```

```{admonition} Best Practice
:class: hint
Libraries should never change the status of a unit or application.
Use return values, or raise exceptions and let them bubble back up to the charm
for the charm author to handle as they see fit.
```

## Write tests for a library

Expand Down
63 changes: 29 additions & 34 deletions docs/howto/manage-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@

> See first: {external+juju:ref}`Juju | Log <log>`, {external+juju:ref}`Juju | Manage logs <manage-logs>`
<!--
>
> - **tl;dr:** <br>
The default logging level for a Juju model is `INFO`. To see, e.g., `DEBUG` level messages, you should change the model configuration: `juju model-config logging-config="<root>=DEBUG"`.
-->
```{tip}
The default logging level for a Juju model is `INFO`. To see, for example,
`DEBUG` level messages, change the model configuration:
`juju model-config logging-config="<root>=DEBUG"`.
```

To log a message in a charm, import Python's `logging` module, then use the `getLogger()` function with the desired level. For example:
To log a message in a charm, import Python's `logging` module, then create a
module-level logger object using `getLogger()`. For example:

```python
import logging
Expand All @@ -27,38 +28,32 @@ class HelloOperatorCharm(ops.CharmBase):
self._stored.things.append(current)
```

> See more:
> - [`logging`](https://docs.python.org/3/library/logging.html), [`logging.getLogger()`](https://docs.python.org/3/library/logging.html#logging.getLogger)
> - [`logging.getLogger().critical()`](https://docs.python.org/3/library/logging.html#logging.Logger.critical)
> - [`logging.getLogger().error()`](https://docs.python.org/3/library/logging.html#logging.Logger.error)
> - [`logging.getLogger().warning()`](https://docs.python.org/3/library/logging.html#logging.Logger.warning)
> - [`logging.getLogger().info()`](https://docs.python.org/3/library/logging.html#logging.Logger.info)
> - [`logging.getLogger().debug()`](https://docs.python.org/3/library/logging.html#logging.Logger.debug)
Juju automatically picks up logs from charm code that uses the Python [logging facility](https://docs.python.org/3/library/logging.html), so we can use the Juju [`debug-log` command](https://juju.is/docs/juju/juju-debug-log) to display logs for a model. Note that it shows logs from the charm code (charm container), but not the workload container. Read ["Use `juju debug-log`"](https://juju.is/docs/sdk/get-logs-from-a-kubernetes-charm#heading--use-juju-debug-log) for more information.

Besides logs, `stderr` is also captured by Juju. So, if a charm generates a warning, it will also end up in Juju's debug log. This behaviour is consistent between K8s charms and machine charms.
```{admonition} Best Practice
:class: hint
Avoid spurious logging, ensure that log messages are clear and meaningful,
and provide the information a user would require to rectify any issues.
```

**Tips for good practice:**
```{admonition} Best Practice
:class: hint
Never log credentials or other sensitive information.
```

- Note that some logging is performed automatically by the Juju controller, for example when an event handler is called. Try not to replicate this behaviour in your own code.
> See more:
> - [`logging`](https://docs.python.org/3/library/logging.html), [`logging.getLogger()`](https://docs.python.org/3/library/logging.html#logging.getLogger)
- Keep developer specific logging to a minimum, and use `logger.debug()` for such output. If you are debugging a problem, ensure you comment out or remove large information dumps (such as config files, etc.) from the logging once you are finished.
Juju automatically picks up logs from charm code that uses the Python logging
package and any warnings generated by the warnings module, so we can use the
Juju `debug-log` command to display logs for a model.
Note that it shows logs from the charm code (charm container), but not the
workload container.

- When passing messages to the logger, do not build the strings yourself. Allow the logger to do this for you as required by the specified log level. That is:
> See more: {external+juju:ref}`juju CLI commands > juju debug-log <command-juju-debug-log>`, [`warnings`](https://docs.python.org/3/library/warnings.html)
<!--
| DON'T &#10060; | DO :white_check_mark: |
|-|-|
| `logger.info("Got some information {}".format(info))`| `logger.info("Got some information %s", info)` |
|`logger.info(f"Got some more information {more_info}")`| |
-->
Besides logs, `stderr` is also captured by Juju. This behaviour is consistent
between K8s charms and machine charms.

```python
# Do this!
logger.info("Got some information %s", info)
# Don't do this
logger.info("Got some information {}".format(info))
# Or this ...
logger.info(f"Got some more information {more_info}")
```{note}
Some logging is performed automatically by the Juju controller, for example when
an event handler is called. Try not to replicate this behaviour in your own code.
```
14 changes: 10 additions & 4 deletions docs/howto/manage-stored-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,31 @@ the charm machine or (for Kubernetes charms) container - for state that should
have the same lifetime as the machine or container, and storing state in a Juju
peer relation - for state that should have the same lifetime as the application.

```{admonition} Best Practice
:class: hint
Use state sparingly -- where possible, write your charm to be stateless. For
sharing state between units of the same application, use peer relation data bags.
```

## Storing state for the lifetime of the charm container or machine

Where some state is required, and the state should share the same lifetime as
the machine or (for Kubernetes charms) container, `ops` provides
[](ops.StoredState) where data is persisted to the `ops` unit database in the
charm machine or container.

[caution]
```{caution}
Note that for Kubernetes charms, container recreation is expected: even if there
are no errors that require the container to be recreated, the container will be
recreated with every charm update.
[/caution]
```

[note]
```{note}
In Kubernetes charms that use the older 'podspec' model, rather than the sidecar
pattern, or when the `use_juju_for_storage` option is set, this data will be
stored in Juju instead, and will persist for the life of the application.
Avoid using `StoredState` objects in these situations.
[/note]
```

A `StoredState` object is capable of persisting simple data types, such as
integers, strings, or floats, and lists, sets, and dictionaries containing those
Expand Down

0 comments on commit 2fca48c

Please sign in to comment.