# Controllers

```elixir
Mix.install([:kino, :bonny])

Application.put_env(:bonny, :operator_name, "livebook")
```

## Creating your first Controller

When running `mix bonny.init`, the task asks you to define CRDs and controllers already. It is optional but I advise to do so as it initializes your operator then. To create additional controllers, run `mix bonny.gen.controller`.

`mix bonny.gen.controller` does not register the controller with your operator. (Note however, that `mix bonny.init`does!) In order for your controller to do something, you need to add an entry to the `controllers/2` callback in your operator. See the operators guide for more information about operators.

## Action Event Handlers

A controller is a [`Pluggable`](https://hex.pm/packages/pluggable) step and uses `Pluggable.StepBuilder` underneath. When an action event is dispatched, the controller is called and all its steps are executed.Your task is to add steps to the controller which process the action event.

If you used `mix bonny.gen.controller` to create the controller, a `handle_event/2` step and its implementation are added to your controller by the script. Besides that, `mix bonny.gen.controller` also adds steps for skipping observed generations. See the section about [skipping observed generations](#skipping-observed-generations) for more information on those.

```elixir
defmodule AppleController do
  use Bonny.ControllerV2

  step :handle_event

  # apply the resource
  def handle_event(%Bonny.Axn{action: action} = axn, _opts)
      when action in [:add, :modify, :reconcile] do
    IO.inspect(axn.resource)
    success_event(axn)
  end

  # delete the resource
  def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do
    IO.inspect(axn.resource)
    axn
  end
end
```

The step s you implement are called with a `%Bonny.Axn{}` token. It contains the `action` which triggered this event, the `resource` the event regards and other fields. Use pattern matching or a adispatch mechanism to handle the four different action types:

- `add/1` - resource was created in the cluster.
- `delete/1` - resource was deleted from the cluster.
- `modify/1` - resource was modified in the cluster.
- `reconcile/1` - Called on a regular basis in case we missed an action or to fix diverged state.

Your event handlers should return the struct it received as first parameter. However, your controller can use helper functions from the `Bonny.Axn` module to modify it before returning it.

## Descendant Resources

Your controller might create descendant resources for its custom resource. For example, a `MyAppController` would create deployments and services for a `MyApp` resource.

Use `Bonny.Axn.register_descendant/3` to add such descendants. Descendants are not directly applied to the cluster. They are only registered within the `%Bonny.Axn{}` token. **Note that you need to add step `Bonny.Pluggable.ApplyDescendants` to either your controller or operator in order to apply descendants to the cluster.**

### Owner Reference

If your controller creates descendant resources for your custom resource, it is good practice to reference the owner(s). In kubernetes, you do this by adding an entry to `.metadata.ownerReferences`. `Bonny.Axn.register_descendant/3` does that for you unless you pass `ommit_owner_ref: true` as option.

<!-- livebook:{"reevaluate_automatically":true} -->

```elixir
defmodule MyAppController do
  use Bonny.ControllerV2

  step :handle_event

  # apply the resource
  def handle_event(%Bonny.Axn{action: action} = axn, _opts)
      when action in [:add, :modify, :reconcile] do
    depl = %{
      "apiVersion" => "apps/v1",
      "kind" => "Deployment",
      "metadata" => %{"namespace" => "default", "name" => axn.resource["metadata"]["name"]}
      # spec
    }

    svc = %{
      "apiVersion" => "v1",
      "kind" => "Service",
      "metadata" => %{"namespace" => "default", "name" => axn.resource["metadata"]["name"]}
      # spec
    }

    axn
    |> Bonny.Axn.register_descendant(depl)
    |> Bonny.Axn.register_descendant(svc)
    |> success_event()
  end

  # delete the resource
  def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do
    # nothing to do because of owner reference
    success_event(axn)
  end
end
```

Let's see the result of the above event handler:

```elixir
Bonny.Axn.new!(
  action: :add,
  conn: nil,
  resource: %{
    "apiVersion" => "example.com/v1",
    "kind" => "MyApp",
    "metadata" => %{
      "name" => "foo",
      "namespace" => "default",
      "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
    }
  }
)
|> MyAppController.call([])
|> Map.get(:descendants)
```

## The Status Subresource

Controllers should use a resource's status subresource to communicate back data to the client. This can be results from underlaying APIs or stats. In your handler, use `Bonny.Axn.update_status/2` to update the status. Make sure the status subresource is represented in your CRD's schema.

```elixir
defmodule StatusController do
  use Bonny.ControllerV2

  step :handle_event

  def handle_event(axn, _) do
    axn
    |> Bonny.Axn.update_status(fn status ->
      put_in(status, [Access.key(:some, %{}), :field], "foo")
    end)
    |> Bonny.Axn.success_event()
  end
end
```

```elixir
resource = %{
  "apiVersion" => "example.com/v1",
  "kind" => "MyApp",
  "metadata" => %{
    "name" => "foo",
    "namespace" => "default",
    "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
  }
}

Bonny.Axn.new!(action: :add, conn: nil, resource: resource)
|> StatusController.call([])
|> Map.get(:status)
```

## Skipping Observed Generations

One of the kubernetes operator best practices is observing generations. [This blog post explains it really well](https://alenkacz.medium.com/kubernetes-operator-best-practices-implementing-observedgeneration-250728868792). It is extremly useful especially when you work with status subresources to not get another `modify` event for updating the status.

Bonny skips observed generations if you add the `Bonny.Pluggable.SkipObservedGenrations` step to your controller.

### Preparations

In order to use the `Bonny.Pluggable.SkipObservedGenrations` step on a custom resource, you have to enable the status subresource and define the correct openAPIV3 schema in your [CRD version file](./crd_versions.livemd#skipped-observed-generations):

```elixir
defmodule MyOperator.API.V1.CronTab do
  use Bonny.API.Version

  @impl Bonny.API.Version
  def manifest() do
    defaults()
    |> add_observed_generation_status()
  end
end

MyOperator.API.V1.CronTab.manifest()
```

### How it works

`Bonny.Pluggable.SkipObservedGenrations` compares the current resource's fields `.metadata.generation` with the field defined by the `:observed_generation_key` option (`.status.observedGeneration` by default). It halts the pipeline if the two values match, i.e. if the resource generation had already been observed.

You can define for which actions this rule applies by adding the option `:actions` when placing the step. By default this rule applies to `[:add, :modify]` actions.

Finally, before the resource status is applied, the module copies the value in `.metadata.generation` to the field defined by the `:observed_generation_key` option (`.status.observedGeneration` by default).

### Example

This example shows how only `:reconcile` and `:delete` events are handled by the controller:

```elixir
defmodule MyThirdResourceController do
  use Bonny.ControllerV2

  step Bonny.Pluggable.SkipObservedGenerations,
    #  default
    actions: [:add, :modify],
    #  default
    observed_generation_key: ["status", "observedGeneration"]


  step :handle_event

  def handle_event(axn, _) do
    IO.puts("handling #{axn.action} event.")
    axn
  end
end

action =
  Kino.Input.select("Test Events",
    add: "add",
    modify: "modify",
    reconcile: "reconcile",
    delete: "delete"
  )
```

```elixir
resource = %{
  "apiVersion" => "example.com/v1",
  "kind" => "MyApp",
  "metadata" => %{
    "name" => "foo",
    "namespace" => "default",
    "uid" => "e19b6f40-3293-11ed-a261-0242ac120002",
    "generation" => 1
  },
  "status" => %{"observedGeneration" => 1}
}

axn = Bonny.Axn.new!(action: Kino.Input.read(action), conn: nil, resource: resource)
MyThirdResourceController.call(axn, [])
:ok
```

## Conditions

[Conditions](https://maelvls.dev/kubernetes-conditions/) provide a way to communicate the resource's status in a machine readable state. One use case for conditions are [custom health checks in ArgoCD](https://argo-cd.readthedocs.io/en/stable/operator-manual/health/#custom-health-checks). If you define conditions on your resources, users of your operator can - when using ArgoCD - define a custom health check which changes the status of resources to. `degraded` if specified conditions fail.

### Preparations

In order to use conditions on custom resources, they have to be [initialized in your CRD manifest](./crd_versions.livemd#skipped-observed-generations):

```elixir
defmodule MyOperator.API.V1.CronTab2 do
  use Bonny.API.Version

  @impl Bonny.API.Version
  def manifest() do
    defaults()
    |> add_conditions()
  end
end

MyOperator.API.V1.CronTab2.manifest()
```

### Usage

I find the most elegant usage of conditions is in combination with a `with` statement. In the following code example, let's assume the controller needs to lookup a secret and a configmap in order to finally create a descendant resource:

```elixir
defmodule MyFourthResourceController do
  use Bonny.ControllerV2

  step :handle_event

  def handle_event(axn, _) when axn.action in [:add, :modify, :reconcile] do
    with {:secret, {:ok, secret}} <- {:secret, get_secret(axn.resource)},
         axn <- set_condition(axn, "Secret", true, "the secret was loaded successfully."),
         {:configmap, axn, {:ok, cm}} <- {:configmap, axn, get_configmap(axn.resource)},
         axn <- set_condition(axn, "Configmap", true, "the configmap was loaded successfully."),
         {:descendant, axn, :ok} <-
           {:descendant, axn, create_descendant(axn.resource, secret, cm)} do
      axn
      |> success_event()
      |> set_condition("Descendant", true, "Descendant was created successfully")
    else
      {:secret, {:error, error}} ->
        axn
        |> failure_event(message: error)
        |> set_condition("Secret", false, error)

      {:configmap, axn, {:error, error}} ->
        axn
        |> failure_event(message: error)
        |> set_condition("Configmap", false, error)

      {:descendant, axn, {:error, error}} ->
        axn
        |> failure_event(message: error)
        |> set_condition("Descendant", false, error)
    end
  end

  # dummy functions:
  def get_secret(_), do: {:ok, :secret}
  def get_configmap(_), do: {:ok, :configmap}
  def create_descendant(_, :secret, :configmap), do: {:error, "Something went wrong"}
end

Bonny.Axn.new!(action: :add, conn: nil, resource: %{})
|> MyFourthResourceController.call([])
|> Map.get(:status)
```

## Kubernetes Events

Kubernetes events provide a way to report back to the client. A Kubernetes event always references the object to which the event relates. For a controller the regarding object would be the handled resource. The user can then use `kubectl describe` on the custom resource to see the events.

Use `Bonny.Axn.success_event/2`, `Bonny.Axn.failure_event/2` or `Bonny.Axn.register_event/6` do register events in the `%Bonny.Axn{}` struct. Events are going to be applied to the cluster at the end of the Operator pipeline. There is no need to register a `step` for that.

```elixir
defmodule MyResourceController do
  use Bonny.ControllerV2

  step :handle_event

  def handle_event(axn, _) when axn.action == :add do
    Bonny.Axn.success_event(axn)
  end

  def handle_event(axn, _) when axn.action == :modify do
    Bonny.Axn.failure_event(axn)
  end
end

action = Kino.Input.select("Test Events", add: "Success", modify: "Failure")
```

```elixir
resource = %{
  "apiVersion" => "example.com/v1",
  "kind" => "MyApp",
  "metadata" => %{
    "name" => "foo",
    "namespace" => "default",
    "uid" => "e19b6f40-3293-11ed-a261-0242ac120002"
  }
}

action = Kino.Input.read(action)
axn = Bonny.Axn.new!(action: action, conn: nil, resource: resource)
MyResourceController.call(axn, []) |> Map.get(:events)
```

## RBAC Rules

Your controller might need special permissions on the kubernetes cluster. Maybe it needs to be able to read secrets. Or it has to be able to create pods. These permissions need to be reflected in the final manifest generated by `mix bonny.gen.manifest` through RBAC rules.

You can define such rules one by defining the `rbac_rules/0` callback. This callback should return a list of rbac rules of the following spec:

<!-- livebook:{"force_markdown":true} -->

```elixir
@type rbac_rule :: %{
  apiGroups: list(binary()),
  resources: list(binary()),
  binary()s: list(binary())}
```

You can use the helper function `to_rbac_rule/1` to convert a tuple to an rbac rule. Its spec is:

<!-- livebook:{"force_markdown":true} -->

```elixir
  @spec to_rbac_rule({
    binary() | list(binary()),
    binary() | list(binary()),
    binary() | list(binary())
  }) :: rbac_rule
```

```elixir
defmodule MySecondResourceController do
  use Bonny.ControllerV2

  @impl Bonny.ControllerV2
  def rbac_rules() do
    [
      to_rbac_rule(
        {"apps/v1", "Deployment", ["get", "list", "create", "update", "patch", "delete"]}
      )
    ]
  end
end

MySecondResourceController.rbac_rules()
```
