# The Operator

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

defmodule MyApp.API.V1.TestResource do
  use Bonny.API.Version
  def manifest(), do: defaults()
end

defmodule MyApp.Controller.GenericController do
  use Bonny.ControllerV2

  step :handle_event
  def handle_event(axn, _), do: axn
end

Logger.configure(level: :info)
```

## About this Livebook

This livebook connects to a kubernetes cluster. Please define here the connection to a cluster you have access to:

```elixir
{:ok, conn} = K8s.Conn.from_file("~/.kube/config", context: "k3d-bonny-ex")
```

## The Operator

The operator defines custom resources, watch queries and their controllers and serves as the entry point to the watching and handling processes.

Overall, an operator has the following responsibilities:

- to provide a wrapper for starting and stopping the
  operator as part of a supervision tree
- To define the resources to be watched together with the
  controllers which handle action events on those resources.
- to define an initial pluggable pipeline containing the step `:delegate_to_controller` for all action events
  to pass through
- To define any custom resources ending up in the manifest
  generated by `mix bonny.gen.manifest`

```elixir
defmodule MyApp.Operator do
  use Bonny.Operator, default_watch_namespace: "default"

  @impl Bonny.Operator
  def crds() do
    [
      %Bonny.API.CRD{
        group: "example.com",
        scope: :Namespaced,
        names: Bonny.API.CRD.kind_to_names("MyCustomResource"),
        versions: [MyApp.API.V1.TestResource]
      }
    ]
  end

  step :delegate_to_controller
  step Bonny.Pluggable.ApplyStatus
  step Bonny.Pluggable.ApplyDescendants

  @impl Bonny.Operator
  def controllers(watching_namespace, _opts) do
    [
      %{
        query:
          K8s.Client.watch("example.com/v1", "MyCustomResource", namespace: watching_namespace),
        controller: MyApp.Controller.GenericController
      },
      %{
        query: K8s.Client.watch("apps/v1", "Deployment", namespace: watching_namespace),
        controller: MyApp.Controller.GenericController
      }
    ]
  end
end
```

## The `crds/0` Callback

By implementing the `crds/0` callback, you tell Bonny what custom resources your operator defines. It is read only when running `mix bonny.gen.manifest` in order to generate the operator manifest:

```elixir
crds =
  [MyApp.Operator]
  |> Bonny.Mix.Operator.crds()
  |> Ymlr.documents!()

IO.puts(crds)
```

In order to run the operator in this livebook, we have to apply the CRD to the cluster. This step has nothing to do with the operator directly. We just do it in order to run the operator.

```elixir
crds
|> YamlElixir.read_all_from_string!()
|> Bonny.Resource.apply_async(conn, field_manager: "livebook")
|> Enum.each(fn {_, {:ok, applied_crd}} -> dbg(applied_crd) end)
```

## The controllers/2 Callback

In `controllers/2` we define the queries and their event handlers, i.e. controllers. function should return a list where each element of the list is a map with these 2 keys:

- `:query` - A list operation of type `K8s.Operation.t()`. Bonny will watch the cluster with this operation and forward all events to the `:controller`.
- `:controller` - A controller (See the [controller guide](controllers.livemd)) or any other `Pluggable` step. Accepts a module or a `{controller :: module(), init_opts :: keyword()}` tuple. If a tuple is given, the init_opts are passed to the controller's `init/1` function.

If you managed to define a valid `conn` above, you can now run the operator defined above in this livebook. The code below starts the operator and shows the supervision tree. Note how the operator starts an `EventRecorder` and two proceses for each controller defined in `controllers/2`. These two processes are the `Watcher` and the `Reconciler`. The `Watcher` watches for `ADD`, `MODIFY` and `DELETE` events in the cluster. The `Reconciler` regularly creates `:reconcile` events for each resource found in the cluster.

```elixir
{:ok, supervisor} = Supervisor.start_link([{MyApp.Operator, conn: conn}], strategy: :one_for_one)
Kino.Process.render_sup_tree(supervisor)
```

## Adding the Operator to your Supervisor

Once your operator is implemented, you need to add it to your application supervision tree. You can pass `:conn` and `:watch_namespace` as init arguments if you like. If you don't pass them, `:conn` will be retrieved from the callback in your `config.exs` and `:watch_namespace` will fall back to the `:default_watch_namespace` you configured your operator with.

```elixir
defmodule MyOperator.Application do
  use Application

  def start(_type, env: env) do
    children = [
      {MyOperator.Operator, conn: MyOperator.K8sConn.get!(env), watch_namespace: :all}
    ]

    opts = [strategy: :one_for_one, name: MyOperator.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

## Leader Election - Running Multiple Replicas

From https://docs.okd.io/latest/operators/operator_sdk/osdk-leader-election.html:

> During the lifecycle of an Operator, it is possible that there may be more than one instance running at any given time, for example when rolling out an upgrade for the Operator. In such a scenario, it is necessary to avoid contention between multiple Operator instances using leader election. This ensures only one leader instance handles the reconciliation while the other instances are inactive but ready to take over when the leader steps down.

In other words, if you want to run multiple replicas of your oprator, you need to turn on leader election.

Leader election is enabled per default. To disable it, you have to pass the `enable_leader_election: false` option when [adding the operator to your Supervisor](#adding-the-operator-to-your-supervisor):

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

```elixir
defmodule MyOperator.Application do
  use Application

  def start(_type, env: env) do
    children = [
      {MyOperator.Operator,
       conn: MyOperator.K8sConn.get!(env),
       watch_namespace: :all,
       enable_leader_election: false} # <-- disables the leader elector
    ]

    opts = [strategy: :one_for_one, name: MyOperator.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

## Pluggable Pipeline and Steps

The operator implements a [Pluggable](https://hex.pm/packages/pluggable) pipeline, the controller represents one step in this pipeline but can contain sub-steps as well.

Bonny comes with a few steps to your convenience. In most caes it makes sense to add at least `Bonny.Pluggable.ApplyDescendants` and `Bonny.Pluggable.ApplyStatus` to the end of your operator pipeline.

- `Bonny.Pluggable.AddManagedByLabelToDescendants` - Adds the `app.kubernetes.io/managed-by` label to all descendants registered within the pipeline.
- `Bonny.Pluggable.AddMissingGVK` - Add fields `apiVersion` and `kind` to the resource if they are missing.
- `Bonny.Pluggable.ApplyDescendants` - applies all the descendants added to the `%Bonny.Axn{}` struct.
- `Bonny.Pluggable.ApplyStatus` - applies the status of the given `%Bonny.Axn{}` struct to the status subresource.
- `Bonny.Pluggable.Finalizer` - declares a finalizer and its implementation.
- `Bonny.Pluggable.Logger`- logs an action event and when status, descendants and events are applied to the cluster. If desired, it makes sense to be placed as first step in your operator pipeline but can also be added to a controller pipeline.
- `Bonny.Pluggable.SkipObservedGenerations` - halts the pipelines for a defined list of actions if the observed generation equals the resource's generation. You'll find further documentation on this module in the [Controller](controllers.livemd) guide.
