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
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 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
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
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 typeK8s.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 otherPluggable
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'sinit/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
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 = [
{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:
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 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 theapp.kubernetes.io/managed-by
label to all descendants registered within the pipeline.Bonny.Pluggable.AddMissingGVK
- Add fieldsapiVersion
andkind
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 guide.