|
| 1 | +# Testing Ansible Operators with Molecule |
| 2 | + |
| 3 | +## Getting started |
| 4 | + |
| 5 | +### Requirements |
| 6 | +To begin, you sould have: |
| 7 | +- The latest version of the [operator-sdk](https://github.com/operator-framework/operator-sdk) installed. |
| 8 | +- Docker installed and running |
| 9 | +- [Molecule](https://github.com/ansible/molecule) >= v2.20 (currently that will require installation from source, `pip install git+https://github.com/ansible/molecule.git`) |
| 10 | +- [Ansible](https://github.com/ansible/ansible) >= v2.7 |
| 11 | +- [jmespath](https://pypi.org/project/jmespath/) |
| 12 | +- [The OpenShift Python client](https://github.com/openshift/openshift-restclient-python) >= v0.8 |
| 13 | +- An initialized Ansible Operator project, with the molecule directory present. If you initialized a project with a previous |
| 14 | + version of operator-sdk, you can generate a new dummy project and copy in the `molecule` directory. Just be sure |
| 15 | + to generate the dummy project with the same `api-version` and `kind`, or some of the generated files will not work |
| 16 | + without modification. Your top-level project structure should look like this: |
| 17 | + ``` |
| 18 | + . |
| 19 | + ├── build |
| 20 | + ├── deploy |
| 21 | + ├── molecule |
| 22 | + ├── roles |
| 23 | + ├── playbook.yml (optional) |
| 24 | + └── watches.yaml |
| 25 | + ``` |
| 26 | +
|
| 27 | +### Molecule scenarios |
| 28 | +If you look into the `molecule` directory, you will see three directories (`default`, `test-local`, `test-cluster`). |
| 29 | +Each of those directories contains a set of files that together make up what is known as a molecule *scenario*. |
| 30 | +
|
| 31 | +Our molecule scenarios have the following basic structure: |
| 32 | +
|
| 33 | +``` |
| 34 | +. |
| 35 | +├── molecule.yml |
| 36 | +├── prepare.yml |
| 37 | +└── playbook.yml |
| 38 | +``` |
| 39 | +
|
| 40 | +`molecule.yml` is a configuration file for molecule. It defines what driver to use to stand up an environment and the associated configuration, linting rules, and a variety of other configuration options. For full documentation on the options available here, see the [molecule configuration documentation](https://molecule.readthedocs.io/en/latest/configuration.html) |
| 41 | +
|
| 42 | +`prepare.yml` is an Ansible playbook that is run once during the set up of a scenario. You |
| 43 | +can put any arbitrary Ansible in this playbook. It is used for one-time configuration |
| 44 | +of your test environment, for example, creating the cluster-wide `CustomResourceDefinition` |
| 45 | +that your Operator will watch. |
| 46 | +
|
| 47 | +`playbook.yml` is an Ansible playbook that contains your core logic for the scenario. In a |
| 48 | +normal molecule scenario, this would import and run the associated role. For Ansible |
| 49 | +Operator, we mostly use this to create the Kubernetes resources and then execute a |
| 50 | +series of asserts that verify your cluster state. |
| 51 | +
|
| 52 | +Below we will walk through the structure and function of each file for each scenario. |
| 53 | +
|
| 54 | +#### default |
| 55 | +The default scenario is intended for use during the development of your Ansible role or playbook, and will run it |
| 56 | +outside of the context of an operator. |
| 57 | +You can run this scenario with |
| 58 | +`molecule test` |
| 59 | +or |
| 60 | +`molecule converge`. There is no corresponding `operator-sdk` command for this scenario. |
| 61 | +
|
| 62 | +The scenario has the following structure: |
| 63 | +
|
| 64 | +``` |
| 65 | +molecule/default |
| 66 | +├── asserts.yml |
| 67 | +├── molecule.yml |
| 68 | +├── playbook.yml |
| 69 | +└── prepare.yml |
| 70 | +``` |
| 71 | +
|
| 72 | +`asserts.yml` is an Ansible playbook contains Ansible assert tasks that will be run by all three scenarios. |
| 73 | +If you would like to write specific asserts for individual scenarios, you can instead remove the `asserts.yml` |
| 74 | +playbook import from that scenario's `playbook.yml`, or if you only want to add additional asserts, you can |
| 75 | +create a new playbook in that scenario and import it at the bottom of that scenario's `playbook.yml`. |
| 76 | +
|
| 77 | +`molecule.yml` for this scenario tells molecule to use the docker driver to bring up a Kubernetes-in-Docker container, |
| 78 | +and exposes the API on the host's port 9443. It also specifies a few inventory and environment |
| 79 | +variables which are used in `prepare.yml` and `playbook.yml`. |
| 80 | +
|
| 81 | +`prepare.yml` ensures that a kubeconfig properly configured to connect to the Kubernetes-in-Docker cluster exists and is mapped to the proper port, and also waits for the Kubernetes API to become |
| 82 | +available before allowing testing to begin. |
| 83 | +
|
| 84 | +`playbook.yml` only imports your role or playbook and then imports the `asserts.yml` playbook. |
| 85 | +
|
| 86 | +#### test-local |
| 87 | +The test-local scenario is a more full integration test of your operator. It brings up a Kubernetes-in-docker cluster, builds your Operator, deploys it |
| 88 | +into the cluster, and then creates an instance of your CustomResource and runs your assertions to make sure the Operator responded properly. You can run |
| 89 | +this scenario with |
| 90 | +`molecule test -s local`, which is equivalent to `operator-sdk test local`, or with `molecule converge -s test-local`, which will leave the environment up |
| 91 | +afterward. |
| 92 | +
|
| 93 | +The scenario has the following structure: |
| 94 | +
|
| 95 | +``` |
| 96 | +molecule/test-local |
| 97 | +├── molecule.yml |
| 98 | +├── playbook.yml |
| 99 | +└── prepare.yml |
| 100 | +``` |
| 101 | +
|
| 102 | +`molecule.yml` for this scenario tells molecule to use the docker driver to bring up a Kubernetes-in-Docker container with the project root mounted, |
| 103 | +and exposes the API on the host's port 10443. It also specifies a few inventory and environment |
| 104 | +variables which are used in `prepare.yml` and `playbook.yml`. It is very similar to the default scenario's configuration. |
| 105 | +
|
| 106 | +`prepare.yml` first runs the `prepare.yml` from the default scenario to ensure the kubeconfig is present and the API is up. It then creates the CustomResourceDefinition, namespace, and RBAC |
| 107 | +resources specified in the `deploy/` directory. |
| 108 | +
|
| 109 | +`playbook.yml` is the most complicated file in this project. First, it connects to your |
| 110 | +Kubernetes-in-Docker container, and uses your mounted project root to build your Operator. |
| 111 | +This makes your Operator available to the cluster without needing to push it to an external |
| 112 | +registry. Next, it will ensure that a fresh deployment of your Operator is present in the |
| 113 | +cluster, and once there is it will create an instance of your Custom Resource |
| 114 | +(specified in `deploy/crds/`). It will then wait for the CustomResource to report a successful |
| 115 | +run, and once it has, will import the `asserts.yml` from the default scenario. |
| 116 | +
|
| 117 | +#### test-cluster |
| 118 | +The test-cluster scenario is intended as a full integration test against |
| 119 | +an existing Kubernetes cluster, and assumes that the cluster is already available, the dependent resources from the `deploy/` directory |
| 120 | +are created, the operator image is built with `--enable-tests`, and that the image is available in a container registry. It connects |
| 121 | +to the existing Kubernetes cluster and deploys the test Operator, creates a Custom Resource, and runs your asserts. You shouldn't |
| 122 | +call this scenario directly, rather you should build your operator with the `--enable-tests` flag, in which case a new entrypoint will |
| 123 | +be added that runs this scenario when the container starts up. It is recommended that you only interact with this scenario through |
| 124 | +`operator-sdk test cluster`. |
| 125 | +
|
| 126 | +The scenario has the following structure: |
| 127 | +
|
| 128 | +``` |
| 129 | +molecule/test-cluster |
| 130 | +├── molecule.yml |
| 131 | +└── playbook.yml |
| 132 | +``` |
| 133 | +`molecule.yml` for this scenario is very simple, as it assumes an environment is already |
| 134 | +present. It essentially is just specifying the metadata of the scenario, and telling molecule |
| 135 | +not to try and create or destroy anything when run. |
| 136 | +
|
| 137 | +`playbook.yml` is also pretty simple, compared to the previous scenarios. All it does is create |
| 138 | +an instance of your Custom Resource (specified in `deploy/crds`), and then import the `asserts.yml` from the `default` scenario. |
| 139 | +
|
| 140 | +#### converge vs test |
| 141 | +The two most common molecule commands for testing during development are `molecule test` and `molecule converge`. |
| 142 | +`molecule test` performs a full loop, bringing a cluster up, preparing it, running your tasks, and tearing it down. |
| 143 | +`molecule converge` is more useful for iterative development, as it leaves your environment up between runs. This |
| 144 | +can cause unexpected problems if you end up corrupting your environment during testing, but running `molecule destroy` |
| 145 | +will reset it. |
| 146 | +
|
| 147 | +
|
| 148 | +
|
| 149 | +## operator-sdk test commands |
| 150 | +
|
| 151 | +### test local |
| 152 | +
|
| 153 | +The `operator-sdk test local` command kicks off an end-to-end test of your Operator. It will bring up a [Kubernetes-in-Docker (kind)](https://github.com/bsycorp/kind) cluster, builds your Operator |
| 154 | +image and make it available to that cluster, create all the required resources from the `deploy/` directory, create an instance of your |
| 155 | +Custom Resource (specified in the `deploy/crds` directory), and then verify that the Operator has responded appropriately by running |
| 156 | +the asserts from `molecule/default/asserts.yml`. |
| 157 | +
|
| 158 | +
|
| 159 | +### test cluster |
| 160 | +
|
| 161 | +The `operator-sdk test cluster` command does much less than the `test local` command. It is intended as a full integration test against |
| 162 | +an existing Kubernetes cluster, and assumes that the cluster is already available, the dependent resources from the `deploy/` directory |
| 163 | +are created, the operator image is built with `--enable-tests`, and that the image is available in a container registry. When you run the command, it will connect |
| 164 | +to the existing Kubernetes cluster and deploy the test Operator, create a Custom Resource, and run the asserts in `molecule/default/asserts.yml`. |
| 165 | +
|
| 166 | +## Writing tests |
| 167 | +
|
| 168 | +### Adding a task |
| 169 | +The default operator that is generated by `operator-sdk new` doesn't do anything, so first we will need to add an |
| 170 | +Ansible task so that the Operator does something we can verify. For this example, we will create a simple ConfigMap |
| 171 | +with a single key. |
| 172 | +We'll be adding the task to `roles/example/tasks/main.yml`, which should now look like this: |
| 173 | +
|
| 174 | +``` |
| 175 | +--- |
| 176 | +# tasks file for exampleapp |
| 177 | +- name: create Example configmap |
| 178 | + k8s: |
| 179 | + definition: |
| 180 | + apiVersion: v1 |
| 181 | + kind: ConfigMap |
| 182 | + metadata: |
| 183 | + name: 'test-data' |
| 184 | + namespace: '{{ meta.namespace }}' |
| 185 | + data: |
| 186 | + hello: world |
| 187 | +``` |
| 188 | +
|
| 189 | +
|
| 190 | +
|
| 191 | +### Adding a test |
| 192 | +
|
| 193 | +Now that our Operator actually does some work, we can add a corresponding assert to `molecule/default/asserts.yml`. |
| 194 | +We'll also add a debug message so that we can see what the ConfigMap looks like. |
| 195 | +The file should now look like this: |
| 196 | +
|
| 197 | +``` |
| 198 | +--- |
| 199 | + |
| 200 | +- name: Verify |
| 201 | + hosts: localhost |
| 202 | + connection: local |
| 203 | + vars: |
| 204 | + ansible_python_interpreter: '{{ ansible_playbook_python }}' |
| 205 | + tasks: |
| 206 | + - debug: var=cm |
| 207 | + vars: |
| 208 | + cm: '{{ lookup("k8s", api_version="v1", kind="ConfigMap", namespace=namespace, resource_name="test-data") }}' |
| 209 | + - assert: |
| 210 | + that: cm.data.hello == 'world' |
| 211 | + vars: |
| 212 | + cm: '{{ lookup("k8s", api_version="v1", kind="ConfigMap", namespace=namespace, resource_name="test-data") }}' |
| 213 | +``` |
| 214 | +
|
| 215 | +Now that we have a functional Operator, and an assertion of its behavior, we can verify that everything is working |
| 216 | +by running `operator-sdk test local`. |
| 217 | +
|
| 218 | +#### The Ansible `assert` and `fail` modules |
| 219 | +These modules are handy for adding assertions and failure conditions to your Ansible Operator tests: |
| 220 | +
|
| 221 | +- [assert](https://docs.ansible.com/ansible/latest/modules/assert_module.html) |
| 222 | +- [fail](https://docs.ansible.com/ansible/latest/modules/fail_module.html) |
0 commit comments