Skip to content

Commit 2fca48c

Browse files
Initial best practices work.
1 parent d54a26a commit 2fca48c

File tree

5 files changed

+170
-41
lines changed

5 files changed

+170
-41
lines changed

docs/howto/index.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
```{toctree}
55
:maxdepth: 2
66
7+
Manage charms <manage-charms>
78
Manage logs <manage-logs>
89
Run workloads with a charm machines <run-workloads-with-a-charm-machines>
910
Run workloads with a charm Kubernetes <run-workloads-with-a-charm-kubernetes>
@@ -25,6 +26,4 @@ Write unit tests for a charm <write-scenario-tests-for-a-charm>
2526
Write integration tests for a charm <write-integration-tests-for-a-charm>
2627
Write legacy unit tests for a charm <write-unit-tests-for-a-charm>
2728
Turn a hooks-based charm into an ops charm <turn-a-hooks-based-charm-into-an-ops-charm>
28-
2929
```
30-

docs/howto/manage-charms.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
(how-to-manage-charms)=
2+
# How to manage charms
3+
4+
> See first: {external+juju:ref}`Juju | How to manage charms or bundles <manage-charms>`
5+
6+
The primary programming language charms are written in is Python, and the
7+
primary framework for developing charms is the Python Operator Framework, or
8+
`ops`.
9+
10+
`charmcraft init` provides you with a `src/charm.py` file that demonstrates the
11+
basis of using the library.
12+
13+
```python
14+
# Import ops and use names from there.
15+
import ops
16+
17+
# Charms always have a class that inherits from ops.CharmBase that define the
18+
# charm's functionality.
19+
class MyCharm(ops.CharmBase):
20+
# There's always an __init__ method that sets up observers.
21+
def __init__(self, framework: ops.Framework):
22+
super().__init__(framework)
23+
framework.observe(...)
24+
25+
# There will always be one or more event handlers that will be called in
26+
# response to a Juju, ops, or custom event.
27+
# Note that this has a leading underscore. These methods should only be
28+
# called by the ops framework, so should not be public.
29+
def _my_event_handler(self, event: ops.EventBase): # or a more specific type
30+
...
31+
self.unit.status = ops.ActiveStatus()
32+
33+
# Finally, the charm.py file always ends with a call to ops.main, passing in the
34+
# charm class.
35+
if __name__ == "__main__":
36+
ops.main(MyCharm)
37+
```
38+
39+
## Interacting with the workload
40+
41+
For simple interactions with an application or service or when a high quality
42+
Python binding is not available, subprocess or API calls should be used to
43+
perform the required operations on the application or service.
44+
45+
TODO: Create a `src/{workload}.py` file for each workload or service that your charm
46+
is managing, and ... For example:
47+
48+
```python
49+
import subprocess
50+
51+
try:
52+
# Comment to explain why subprocess is used.
53+
result = subprocess.run(
54+
# Array based execution.
55+
["/usr/bin/echo", "hello world"],
56+
capture_output=True,
57+
check=True,
58+
)
59+
logger.debug("Command output: %s", result.stdout)
60+
except subprocess.CalledProcessError as err:
61+
logger.error("Command failed with code %i: %s", err.returncode, err.stderr)
62+
raise
63+
```
64+
65+
```{admonition} Best Practice
66+
:class: hint
67+
Limit the use of shell scripts and commands as much as possible in favour of
68+
writing Python for charm code. Examples where it could be reasonable to use a
69+
script include: extracting data from a machine or container which can't be
70+
obtained through Python; or issuing commands to applications that do not have
71+
Python bindings (such as starting a process on a machine).
72+
```
73+
74+
## Base / Python
75+
76+
Charms have access to the default Python version in the Ubuntu release defined
77+
as the base.
78+
79+
Your code should be compatible with the operating system and Juju versions it
80+
will be executed on. For example, if your charm is to be deployed with Juju 3.6,
81+
its Python code should be compatible with Python 3.8.
82+
83+
TODO: put in the relevant bits in pyproject.toml to have tools recognise this.
84+
85+
TODO: can we be more detailed, or link to something here that says which Ubuntu
86+
has which Python, and which base you should support?
87+
88+
## Dependencies
89+
90+
TODO: put in instructions for dependencies from the spec (pyproject, generate requirements.txt)
91+
92+
TODO: instructions on putting libs in charmcraft.yaml and running fetch-libs and then keeping that updated.
93+
94+
> See more: charmcraft.yaml, charmcraft fetch-libs
95+
96+
```{admonition} Best Practice
97+
:class: hint
98+
Including an external dependency in a charm is a significant choice. It can help
99+
with reducing the complexity and development cost. However, every additional
100+
dependency increases the risk for supply chain security issues, the maintenance
101+
burden of having to manage the dependencies, and makes it more difficult to
102+
completely understand all of the code involved in the charm.
103+
```
104+
105+
> See more: [The software dependency issue](https://research.swtch.com/deps)
106+
107+
TODO: is that still the article we want to link to? There are a lot on this problem.
108+
109+
## Events
110+
111+
TODO: There are Juju events, ops events, and custom events. A bit more about how to
112+
respond to each of these, probably mostly linking out.
113+
114+
> See also: Juju events, something about the ops events.
115+
116+
```{admonition} Best Practice
117+
:class: hint
118+
Custom events are defined and emitted by charm libraries. Charms should never
119+
define or emit custom events themselves.
120+
```
121+
122+
> See more: manage libraries
123+

docs/howto/manage-libraries.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
(manage-libraries)=
22
# Manage libraries
3-
> See first: {external+charmcraft:ref}`Charmcraft | Manage libraries <manage-libraries>`
43

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

@@ -30,6 +30,12 @@ class DatabaseRequirer(ops.framework.Object):
3030
self.on.ready.emit()
3131
```
3232

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

3440
## Write tests for a library
3541

docs/howto/manage-logs.md

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33

44
> See first: {external+juju:ref}`Juju | Log <log>`, {external+juju:ref}`Juju | Manage logs <manage-logs>`
55
6-
<!--
7-
>
8-
> - **tl;dr:** <br>
9-
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"`.
10-
-->
6+
```{tip}
7+
The default logging level for a Juju model is `INFO`. To see, for example,
8+
`DEBUG` level messages, change the model configuration:
9+
`juju model-config logging-config="<root>=DEBUG"`.
10+
```
1111

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

1415
```python
1516
import logging
@@ -27,38 +28,32 @@ class HelloOperatorCharm(ops.CharmBase):
2728
self._stored.things.append(current)
2829
```
2930

30-
> See more:
31-
> - [`logging`](https://docs.python.org/3/library/logging.html), [`logging.getLogger()`](https://docs.python.org/3/library/logging.html#logging.getLogger)
32-
> - [`logging.getLogger().critical()`](https://docs.python.org/3/library/logging.html#logging.Logger.critical)
33-
> - [`logging.getLogger().error()`](https://docs.python.org/3/library/logging.html#logging.Logger.error)
34-
> - [`logging.getLogger().warning()`](https://docs.python.org/3/library/logging.html#logging.Logger.warning)
35-
> - [`logging.getLogger().info()`](https://docs.python.org/3/library/logging.html#logging.Logger.info)
36-
> - [`logging.getLogger().debug()`](https://docs.python.org/3/library/logging.html#logging.Logger.debug)
37-
38-
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.
39-
40-
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.
31+
```{admonition} Best Practice
32+
:class: hint
33+
Avoid spurious logging, ensure that log messages are clear and meaningful,
34+
and provide the information a user would require to rectify any issues.
35+
```
4136

42-
**Tips for good practice:**
37+
```{admonition} Best Practice
38+
:class: hint
39+
Never log credentials or other sensitive information.
40+
```
4341

44-
- 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.
42+
> See more:
43+
> - [`logging`](https://docs.python.org/3/library/logging.html), [`logging.getLogger()`](https://docs.python.org/3/library/logging.html#logging.getLogger)
4544
46-
- 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.
45+
Juju automatically picks up logs from charm code that uses the Python logging
46+
package and any warnings generated by the warnings module, so we can use the
47+
Juju `debug-log` command to display logs for a model.
48+
Note that it shows logs from the charm code (charm container), but not the
49+
workload container.
4750

48-
- 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:
51+
> See more: {external+juju:ref}`juju CLI commands > juju debug-log <command-juju-debug-log>`, [`warnings`](https://docs.python.org/3/library/warnings.html)
4952
50-
<!--
51-
| DON'T &#10060; | DO :white_check_mark: |
52-
|-|-|
53-
| `logger.info("Got some information {}".format(info))`| `logger.info("Got some information %s", info)` |
54-
|`logger.info(f"Got some more information {more_info}")`| |
55-
-->
53+
Besides logs, `stderr` is also captured by Juju. This behaviour is consistent
54+
between K8s charms and machine charms.
5655

57-
```python
58-
# Do this!
59-
logger.info("Got some information %s", info)
60-
# Don't do this
61-
logger.info("Got some information {}".format(info))
62-
# Or this ...
63-
logger.info(f"Got some more information {more_info}")
56+
```{note}
57+
Some logging is performed automatically by the Juju controller, for example when
58+
an event handler is called. Try not to replicate this behaviour in your own code.
6459
```

docs/howto/manage-stored-state.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,31 @@ the charm machine or (for Kubernetes charms) container - for state that should
1111
have the same lifetime as the machine or container, and storing state in a Juju
1212
peer relation - for state that should have the same lifetime as the application.
1313

14+
```{admonition} Best Practice
15+
:class: hint
16+
Use state sparingly -- where possible, write your charm to be stateless. For
17+
sharing state between units of the same application, use peer relation data bags.
18+
```
19+
1420
## Storing state for the lifetime of the charm container or machine
1521

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

21-
[caution]
27+
```{caution}
2228
Note that for Kubernetes charms, container recreation is expected: even if there
2329
are no errors that require the container to be recreated, the container will be
2430
recreated with every charm update.
25-
[/caution]
31+
```
2632

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

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

0 commit comments

Comments
 (0)