diff --git a/05-quickstarts.md b/05-quickstarts.md index 85702168cc95deb8669a535f55b50a883905ac4a..61aeada0cdd36ee6884becddad2e2159f05bea5e 100644 --- a/05-quickstarts.md +++ b/05-quickstarts.md @@ -778,3 +778,293 @@ helm show values stable/openldap ``` When instantiating with OSM, all you need to do is place those params under `additionalParamsForVnf:[VNF_INDEX]:additionalParamsForKdu:[KDU_INDEX]:additionalParams`, with the right indentation. + + +## Starting with Juju Bundles + +This section covers the basics to start with Juju bundles. + +First of all, let's download the `charmcraft` snap that will help us creating and building the charm. + +```bash +sudo snap install charmcraft +``` + +### Folder structure (VNF package) + +This is the folder structure that we will use to include Juju bundles to our package. + +``` +└── juju-bundles + ├── bundle.yaml + ├── charm + │ └── example-operator + └── ops + └── example-operator +``` + +Inside the `juju-bundles` folder: + +- `bundle.yaml`: File with the Juju bundle. We will show it in detail in the following sections. +- `charms/`: Folder with the built operators (charms). +- `ops/`: Folder with the operators' (charms) source code. + +### Create charm + +To create a charm, we will first change the directory to the folder of the operators' source code. + +```bash +cd juju-bundles/ops/ +``` + +Now we will create a folder for our charm operator, and initialize it: + +```bash +mkdir example-operator +charmcraft init --project-dir example-operator --name example +``` + +> Good practise: +> - The folder name should contain the application name, followed by `-operator`. +> - The charm name should just be the name of the application. + +Now we have the charm initialized! + +### Deployment type and service + +The deployment type and service of the Kubernetes pod can be defined in the `metadata.yaml` adding the following content: + +```yaml +deployment: + type: stateful | stateless + service: cluster | loadbalancer +``` + +### Add storage + +If the workload needs some persistent storage, it can be defined in the `metadata.yaml` adding the following content: + +```yaml +storage: + storage-name: + type: filesystem + location: /path/to/storage +``` + +The location of the storage name will be mounted in a persistent volume in Kubernetes, so the data available there will persist even if the pod restarts. Charms with storage must be `stateful`. + +### Operator code + +This section shows the base code for all the Kubernetes charms. If you follow copy-paste the following content, you will just ned to update the pod_spec dictionary to match want you want. + +In the code, there are comments explaining what is each `key` for. + +#### src/charm.py + +```python +#!/usr/bin/env python3 + + +import logging + +from ops.charm import CharmBase +from ops.main import main +from ops.model import ActiveStatus + +logger = logging.getLogger(__name__) + +file_mount_path = "/tmp/files" +file_name = "my-file" +file_content = """ +This is the content of a file +that will be mounted as a configmap +to my container +""" + + +class ExampleCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.framework.observe(self.on.config_changed, self.configure_pod) + self.framework.observe(self.on.leader_elected, self.configure_pod) + + def configure_pod(self, _): + self.model.pod.set_spec( + { + "version": 3, + "containers": [ # list of containers for the pod + { + "name": "example", # container name + "image": "httpd:2.4", # image for the container + "ports": [ # ports exposed by the container + { + "name": "http", + "containerPort": 80, + "protocol": "TCP", + } + ], + "kubernetes": { # k8s specific container attributes + "livenessProbe": {"httpGet": {"path": "/", "port": 80}}, + "readinessProbe": {"httpGet": {"path": "/", "port": 80}}, + "startupProbe": {"httpGet": {"path": "/", "port": 80}}, + }, + "envConfig": { # Environment variables that wil be passed to the container + "ENVIRONMENT_VAR_1": "value", + "ENVIRONMENT_VAR_2": "value", + }, + "volumeConfig": [ # files to mount as configmap + { + "name": "example-file", + "mountPath": file_mount_path, + "files": [{"path": file_name, "content": file_content}], + } + ], + } + ], + } + ) + self.unit.status = ActiveStatus() + + +if __name__ == "__main__": + main(ExampleCharm) +``` + +### actions.yaml + +Remove the `fortune` action included when initializing the charm. Replace the file with the following content: + +```yaml +{} +``` + +### config.yaml + +Remove the `thing` option included when initializing the charm. Replace the file with the following content: + +```yaml +options: {} +``` + +### Config + +Sometimes the workload we are deploying can have several configuration options. In the charm, we can expose those options as config, and then change the configuration as desired in each deployment. + +To add the config, we just need to update the `config.yaml` file. + +```yaml +options: + port: + description: Port for service + default: 80 + type: int + debug: + description: Indicate if debugging mode should be enabled or not. + default: false + type: boolean + username: + description: Default username for authenticating to the service + default: admin + type: string +``` + +To get the config value in the code it is pretty simple: + +```python +... + +class ExampleCharm(CharmBase): + def __init__(self, *args): + ... + + def configure_pod(self, _): + port = self.config["port"] + username = self.config["username"] + debug = self.config["debug"] +``` + +### Actions + +To add an action to the charm, we need to edit the `actions.yaml` file with a content similar to this: + +```yaml +actions: + touch: + params: + filename: + description: Filename to the file that will be created - Full path + type: string + required: + - filename +``` + +The content above defines the high-level information about the action and the parameters accepted by it. + +Here is how we can implement the code for the function above in `src/charm.py`: + +```python +... +import subprocess + + +class ExampleCharm(CharmBase): + def __init__(self, *args): + ... + self.framework.observe(self.on.touch_action, self.touch) + + def touch(self, event): + filename = event["filename"] + try: + subprocess.run(["touch", filename]) + event.set_results({ + "output": f"File {filename} created successfully" + }) + except Exception as e: + event.fail(f"Touch action failed with the following exception: {e}") +``` + +> IMPORTANT: The action is executed in the workload pod, not in the operator one. This means that since the action code is in python, the container must have python installed. This limitation will go away soon with Juju 2.9. + +### Requirements + +Add any extra python dependencies needed by the charm in the `requirements.txt` file. Those will be downloaded when building the charm. + + +### Build charm + +Build the charm with the following command: + +```bash +charmcraft build +``` + +Now we need to move the `build` folder to `juju-bundles/charms/`, and then reference the built charm from the Juju bundle. + +```bash +mv build/ ../../charms/example-operator +``` + +### Create bundle + +Go to the `juju-bundles` folder and fill the bundle.yaml with the following content: + +```yaml +description: Example Bundle +bundle: kubernetes +applications: + example: + charm: './charms/example-operator' + scale: 1 + options: + port: 80 + debug: true + username: osm +``` +### More + +- Lifecycle events: You can find documentation about the Lifecycle events in the charms [here](https://juju.is/docs/sdk/events). +- Pod spec references: + - https://discourse.charmhub.io/t/k8s-spec-v3-changes/2698 + - https://discourse.charmhub.io/t/k8s-spec-reference/3495 +- Juju docs: https://juju.is/docs/sdk +- Operator framework docs: https://ops.readthedocs.io/en/latest/index.html