View Source CRD Versions

Mix.install([:bonny])

Application.put_env(:bonny, :operator_name, "livebook-operator")

CRD Definition and Versions

When defining your operator (use Bonny.Operator), you have to implement the callback crds/0 where you define your custom resources. A custom resource definition (CRD) is represented by a %Bonny.API.CRD{} struct which defines 4 fields:

  • :scope - either :Namespaced or :Cluster

  • :group - The API group of your controller / this resource

  • :names - A map with 3-4 keys defining the names of this resource.

    • plural: name to be used in the URL: /apis/<group>/<version>/<plural> - e.g. crontabs
    • singular: singular name to be used as an alias on the CLI and for display - e.g. crontab
    • kind: is normally the CamelCased singular type. Your resource manifests use this. - e.g. CronTab
    • shortnames: allow shorter string to match your resource on the CLI - e.g. [ct]
  • versions: list of API Version modules for this Resource, defaults to the versions in config.exs

defmodule MyOperator.Operator do
  use Bonny.Operator, default_watch_namespace: "default"

  step(:delegate_to_controller)

  def controllers(_watching_namespace, _opts), do: []

  def crds() do
    [
      %Bonny.API.CRD{
        names: %{kind: "CronTab", plural: "crontabs", shortNames: ["ct"], singular: "crontab"},
        group: "example.com",
        versions: [MyOperator.API.V1.CronTab],
        scope: :Namespaces
      }
    ]
  end
end

We're going to look at version manifest declaration in more detail in just a moment. For now, let's just define a simple API version v1 for the CronTab custom resource with just defaults for all the fields. You do this by defining a module that starts with the API declared in the application configuration (YourOperator.API.V1), followed by the CRD name (CronTab). The module must use Bonny.API.Version which expects you to implement manifest/0.

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

  @impl Bonny.API.Version
  def manifest() do
    defaults()
    |> struct!(name: "v1", storage: true)
  end
end

YourOperator.API.V1.CronTab.manifest()

Now, if we define a CronTabController, Bonny finds this version and add it to the CRD manifest.

crds =
  [MyOperator.Operator]
  |> Bonny.Mix.Operator.crds()
  |> Ymlr.documents!()

IO.puts(crds)

Version Manifest Declaration

Our V1.CronTab module called the defaults/0 macro from manifest/0. This macro helps initializing a generic version with no schema, subresources or additional printer columns. The storage flag is set to false (see Multi-Version APIs further down). For the other flags :served and :deprecated such as the field :deprecatedWarning, assumptions are made.

An operator run in production might want to define at least a :schema, probably :additionalPrinterColumns and maybe :subresources. All these fields such as flags can be overriden in manifest/0.

defmodule YourOperator.API.V1Alpha1.Widget do
  use Bonny.API.Version

  @impl true
  def manifest() do
    struct!(
      defaults(),
      storage: true,
      schema: %{
        openAPIV3Schema: %{
          type: :object,
          properties: %{
            spec: %{
              type: :object,
              properties: %{
                foos_requested: %{type: :integer}
              }
            },
            status: %{
              type: :object,
              properties: %{
                foos_implemented: %{type: :integer}
              }
            }
          }
        }
      },
      additionalPrinterColumns: [
        %{
          name: "requested_foos",
          type: :integer,
          description: "Number of foos requested",
          jsonPath: ".spec.foos_requested"
        },
        %{
          name: "implemented_foos",
          type: :integer,
          description: "Number of foos implemented",
          jsonPath: ".status.foos_implemented"
        }
      ],
      subresources: %{
        status: %{}
      }
    )
  end
end

YourOperator.API.V1Alpha1.Widget.manifest()

The Status Subresource

In the controllers guide we explain the importance of using of the status subresource. In order to use the status subresource, it has to be enabled and its schema has to be defined in the CRD. You can see an example in the code snipped above in the YourOperator.API.V1Alpha1.Widget.

Two special use cases of the status subresource are skipping obeserved generations and conditions. In order to use these concepts with custom resources, they have to be configured on the CRD. When useing Bonny.API.Version, helper functions add_observed_generation_status/1 and add_conditions/1 are imported to your module. Use them to add the required fields to your manifest:

defmodule YourOperator.API.V1Alpha1.Feature do
  use Bonny.API.Version

  @impl true
  def manifest() do
    defaults()
    |> struct!(
      schema:
        %{
          # your openAPIV3schema
        }
    )
    # initialize the usage of the observed generations pluggable step
    |> add_observed_generation_status()
    # initialize the usage of conditions.
    |> add_conditions()
  end
end

YourOperator.API.V1Alpha1.Widget.manifest()

Multi-Version APIs

There is some documentation about multi-version apis for the kubebuilder. Obviousely, that one is for creating a kubernetes controller in Go, but it's a good read nontheless. This is how it begins:

Most projects start out with an alpha API that changes release to release. However, eventually, most projects will need to move to a more stable API. Once your API is stable though, you can't make breaking changes to it. That's where API versions come into play.

Conversion

Webhooks are currently not implemented in Bonny. There is the module bonny_plug that can be used to implement them. There might be a neater integration of the two in the future, though.

Bonny already lets you define a version as the hub. The only thing this does right now is it sets the storage flag to true in the generated manifest.

defmodule YourOperator.API.V1.CronTab do
  use Bonny.API.Version,
    hub: true

  @impl Bonny.API.Version
  # storage: true not needed here.
  def manifest(), do: defaults(name: "v1")
end

YourOperator.API.V1.CronTab.manifest()

Storage Versions

Even if you define multiple versions for the same resource, Kubernetes is only going to store the data in one version - the storage version.

Note that multiple versions may exist in storage if they were written before the storage version changes -- changing the storage version only affects how objects are created/updated after the change.

As mentioned above, there are to ways to define the storage version, by passing hub: true as an option to use Bonny.API.Version or by setting storage: true in manifest/0.