Mix.install([:bonny, :inflex])
Application.put_env(:bonny, :operator_name, "livebook-operator")
ExUnit.start(autorun: false)
In this guide we're going to create a simple controller and write a test for it. The controller's custom resource is called ConfigMapToPluralize
and we expect the controller to map the resource to a ConfigMap
on the cluster. The ConfigMap
should contain the same fields as the ConfigMapToPluralize
but the fielt's values are pluralized.
For the following ConfigMapToPluralize
resource:
apiVersion: example.com/v1
kind: ConfigMapToPluralize
metadata:
name: foo
namespace: default
data:
first: House
second: Hero
the controller will create the following ConfigMap
:
apiVersion: v1
kind: ConfigMap
metadata:
name: foo
namespace: default
data:
first: Houses
second: Heroes
Let's define the API version V1
for the ConfigMapToPluralize
CRD. It defines a schema with a property .data
which is an object allowing for arbitrary fields with string values.
defmodule V1.ConfigMapToPluralize do
use Bonny.API.Version,
hub: true
def manifest() do
struct!(
defaults(),
schema: %{
openAPIV3Schema: %{
type: :object,
properties: %{
data: %{
type: :object,
additionalProperties: %{
type: :string
},
"x-kubernetes-preserve-unknown-fields": true
}
}
}
}
)
end
end
The ConfigMapToPluralizeController
handles :add
and :modify
and :reconcile
events through the same function where the ConfigMap
with pluralized field values is created and registered as descendant.
defmodule ConfigMapToPluralizeController do
use Bonny.ControllerV2
step :handle_event
def handle_event(%Bonny.Axn{action: action} = axn, _opts)
when action in [:add, :modify, :reconcile] do
%Bonny.Axn{resource: resource} = axn
name = K8s.Resource.FieldAccessors.name(resource)
namespace = K8s.Resource.FieldAccessors.namespace(resource)
new_data =
resource
|> Map.get("data")
|> Enum.map(fn {key, value} ->
{key, Inflex.pluralize(value)}
end)
|> Enum.into(%{})
cm = %{
"apiVersion" => "v1",
"kind" => "ConfigMap",
"metadata" => %{
"name" => name,
"namespace" => namespace
},
"data" => new_data
}
axn
|> register_descendant(cm)
|> success_event()
end
def handle_event(axn, _opts) do
# since we added the owner reference above, there's nothing to do here.
# Kubernetes will delete the referencing objects i.e. the ConfigMap for us.
success_event(axn)
end
end
Testing a Bonny controller is similar to testing a Phoenix controller. Controllers use the %Bonny.Axn{}
token to register descending resources, update resource status and registering Kubernetes events. In our test, we need to assert the state of the returned Bonny.Axn{}
struct is as expected.
Bonny.Axn.Test
is a helper module for tests. It provides a function axn/2
to create a %Bonny.Axn{}
token and other helper functions which are imported to your test when using the helper module.
defmodule ConfigMapToPluralizeControllerTest do
use ExUnit.Case, async: true
use Bonny.Axn.Test
# Module Under Test
alias ConfigMapToPluralizeController, as: MUT
setup do
cm_to_pluralize = %{
"apiVersion" => "example.com/v1",
"kind" => "ConfigMapToPluralize",
"metadata" => %{
"namespace" => "default",
"name" => "test-1"
},
"data" => %{
"first" => "House",
"second" => "Hero"
}
}
[
axn: axn(:add, resource: cm_to_pluralize, conn: %K8s.Conn{})
]
end
test "registers descending ConfigMap with pluralized fields", %{axn: axn} do
assert [cm] = axn |> MUT.call([]) |> descendants()
assert "Houses" == cm["data"]["first"]
assert "Heroes" == cm["data"]["second"]
end
test "registers a success event", %{axn: axn} do
assert [event] = axn |> MUT.call([]) |> events()
assert :Normal == event.event_type
end
end
ExUnit.run()
You can also test your code against a real Kubernetes cluster setup locally (e.g. with k3d
) and in your CI. You will need to setup a %K8s.Conn{}
defines the connection your cluster but is oonly used for the integration tests. Have a look at the follwing files in the Bonny repo:
test/support/integration_helper.ex
- defines aconn/0
functiontest/bonny/controller_v2_integration_test.exs
- implements integration tests using the helper above.