View Source The Operator

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

About this Livebook

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

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

the-operator

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
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

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:

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.

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

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) 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.

{: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

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.

defmodule MyOperator.Application do
  use Application

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

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

pluggable-pipeline-and-steps

Pluggable Pipeline and Steps

The operator implements a 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.