This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Usage

How to use the component-operator-runtime framework

1 - Overview

General implementation principles (doing it the hard way)

This framework is based on the controller-runtime project. Therefore one way of consuming it would be to bootstrap a kubebuilder project, such as

kubebuilder init \
  --domain kyma-project.io \
  --repo github.com/myorg/mycomponent-operator \
  --project-name=mycomponent-operator

kubebuilder create api \
  --group operator \
  --version v1alpha1 \
  --kind MyComponent \
  --resource \
  --controller \
  --make

First you will enhance the custom resource type, as generated by kubebuilder in the api/v1alpha1 folder:

// MyComponentSpec defines the desired state of MyComponent
type MyComponentSpec struct {
  // Add fields here ...
}

// MyComponentStatus defines the observed state of MyComponent
type MyComponentStatus struct {
  component.Status `json:",inline"`
}

In many cases, it makes sense to embed one or more of the following structs into the spec:

  • component.PlacementSpec if instances should be allowed to specify target namespace and name different from the component’s namespace and name
  • component.ClientSpec if (remote) deployments via a specified kubeconfig shall be possible
  • component.ImpersonationSpec if you want to support impersonation of the deployment (e.g. via a service account).

Most likely you will add own attributes to the spec, allowing to parameterize the deployment of your component. Including component.Status into the status is mandatory, but you are free to add further fields if needed.

In order to make the custom resource type implement the Component interface, add the following methods:

var _ component.Component = &MyComponent{}

func (s *MyComponentSpec) ToUnstructured() map[string]any {
  result, err := runtime.DefaultUnstructuredConverter.ToUnstructured(s)
  if err != nil {
    panic(err)
  }
  return result
}

func (c *MyComponent) GetSpec() runtimetypes.Unstructurable {
  return &c.Spec
}

func (c *MyComponent) GetStatus() *component.Status {
  return &c.Status.Status
}

Now we are settled to replace the controller generated by kubebuilder with the component-operator-runtime reconciler in the scaffolded main.go:

// Replace this by a real resource generator (e.g. HelmGenerator or KustomizeGenerator, or your own one).
resourceGenerator, err := manifests.NewDummyGenerator()
if err != nil {
  setupLog.Error(err, "error initializing resource generator")
  os.Exit(1)
}

if err := component.NewReconciler[*operatorv1alpha1.MyComponent](
  "mycomponent-operator.kyma-project.io",
  nil,
  nil,
  nil,
  nil,
  resourceGenerator,
).SetupWithManager(mgr); err != nil {
  setupLog.Error(err, "unable to create controller", "controller", "MyComponent")
  os.Exit(1)
}

In addition, you have to add the apiextensions.k8s.io/v1 and apiregistration.k8s.io/v1 groups to the used scheme, such that the kubebuilder generated init() function looks like this:

func init() {
  utilruntime.Must(clientgoscheme.AddToScheme(scheme))
  utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
  utilruntime.Must(apiregistrationv1.AddToScheme(scheme))
  utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
}

Furthermore, pay attention to bypass informer caching in the client at least for the following types:

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
  // ...
  Client: client.Options{
    Cache: &client.CacheOptions{
      DisableFor: []client.Object{
        &operatorv1alpha1.MyComponent{},
        &apiextensionsv1.CustomResourceDefinition{},
        &apiregistrationv1.APIService{},
      },
    },
  },
  // ...
})

Now the actual work starts, which means that you will tailor the custom resource type’s spec according to the needs of the managed component, and implement a meaningful resource generator, replacing manifests.NewDummyGenerator().

2 - Scaffolder

Generate a component operator by the comonent-operator-runtime scaffolder

The recommended way to get started with the implementation of a new component operator is to use the included scaffolding tool, which can be downloaded from the releases page.

After installing the scaffolder, a new project can be created like this:

scaffold \
  --group-name operator.kyma-project.io \
  --kind MyComponent \
  --operator-name mycomponent-operator.kyma-project.io \
  --go-module github.com/myorg/mycomponent-operator \
  --image mycomponent-operator:latest \
  <output-directory>

In this example, some options were left out, using the according default values; the full option list is as follows:

Usage: scaffold [options] [output directory]
  [output directory]: Target directory for the generated scaffold; must exist
  [options]:
      --version                                    Show version
      --owner string                               Owner of this project, as written to the license header (default "SAP SE")
      --spdx-license-headers                       Whether to write license headers in SPDX format
      --group-name string                          API group name
      --group-version string                       API group version (default "v1alpha1")
      --kind string                                API kind for the component
      --resource string                            API resource (plural) for the component; if empty, it will be the pluralized kind
      --operator-name string                       Unique name for this operator, used e.g. for leader election and labels; should be a valid DNS hostname
      --with-validating-webhook                    Whether to scaffold validating webhook
      --with-mutating-webhook                      Whether to scaffold mutating webhook
      --go-version string                          Go version to be used (default "1.21")
      --go-module string                           Name of the Go module, as written to the go.mod file
      --kubernetes-version string                  Kubernetes go-client version to be used (default "v0.28.1")
      --controller-runtime-version string          Controller-runtime version to be used (default "v0.16.0")
      --controller-tools-version string            Controller-tools version to be used (default "v0.13.0")
      --code-generator-version string              Code-generator version to be used (default "v0.28.1")
      --admission-webhook-runtime-version string   Admission-webhook-runtime version to be used (default "v0.1.0")
      --envtest-kubernetes-version string          Kubernetes version to be used by envtest (default "1.27.1")
      --image string                               Name of the Docker/OCI image produced by this project (default "controller:latest")
      --skip-post-processing                       Skip post-processing

After generating the scaffold, the next steps are:

  • Enhance the spec type of the generated custom resource type (in api/<group-version>/types.go) according to the needs of your component
  • Implement a meaningful resource generator and use it in main.go instead of manifests.NewDummyGenerator(); to do so you can either implement your own generator, or reuse one of the generic generators shipped with this repository.