This is a collection of tools to speed up the implementation of Kubernetes Operators on top of controller-runtime.
GenericResourceReconciler
GenericResourceReconciler
reconciles a single Kubernetes object against the API Server.
It creates the object if it doesn't exist or removes it in case its desired state is absent.
It uses the ObjectMatcher library to be able to tell if an already
existing object needs to be updated or not.
It depends on logr logger and the controller-runtime client
that is available in a typical kubebuilder or operator-sdk project.
Example:
package main
import (
corev1 "k8s.io/api/core/v1"
github.com/go-logr/logr
"github.com/banzaicloud/operator-tools/pkg/reconciler"
runtimeClient "sigs.k8s.io/controller-runtime/pkg/client"
)
func example(client runtimeClient.Client, logger logr.Logger) {
resourceReconciler := reconciler.NewReconciler(client, logger, reconciler.ReconcilerOpts{})
serviceObject := &corev1.Service{
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Protocol: corev1.ProtocolTCP,
Name: "example",
Port: 80,
TargetPort: 8080,
},
},
Selector: map[string]string{
"app": "example",
},
Type: corev1.ServiceTypeClusterIP,
ClusterIP: "None",
},
}
result, err := resourceReconciler.ReconcileResource(serviceObject, reconciler.StatePresent)
}
This feature is currently only covered with tests in the logging operator,
but this is a subject to change soon.
Secret & SecretLoader
Secret
is a type to be used in CRDs to abstract the concept of loading a secret item instead of defining it with it's value directly.
Currently it support Kubernetes secrets only, but it can be extended to refer to secrets in custom secret stores as well.
There are two main approaches to load secrets and one for testing.
- Load the secrets and return with their value directly if
ValueFrom
is set.
- Load the secrets in the background if
MountFrom
is set, but return only the full path where they should be available in a container.
It's the callers responsibility to make those secrets available on that given path, e.g. by creating an aggregated secret with all
the referenced secrets and mount it into the container through a secret volume (this is how we use it).
- Load the value directly if
Value
is set. (This is only good for testing.)
Once you're done with configuration you can create the SecretLoader
and load your secrets through it.
mountSecrets := &secret.MountSecrets{}
secretLoader := secret.NewSecretLoader(client, namespace, "/path/to/mount", mountSecrets)
Then you can load the secrets. The following steps can be made more dynamic, like it is beeing used in the logging operator:
https://github.com/banzaicloud/logging-operator/blob/master/pkg/sdk/model/types/stringmaps.go
// get the secretValue and use it as you like in an application configuration template for example
secretValue, err := secretLoader.Load(yourCustomResourceType.Spec.ExampleSecretField)
// get the path to the mount secret and use it as you like in an application configuration template for example
secretPath, err := secretLoader.Load(yourCustomResourceType.Spec.ExampleMountSecretField)
// render the configuration template and create a new secret from it that will be mounted into the container
appConfigSecret := &corev1.Secret{}
renderedTemplate := renderTemplate(secretValue, secretPath)
appConfigSecret.Data["app.conf"] = renderedTemplate
// create the combined secret to be mounted to the container on "/path/to/mount"
combinedSecret := &corev1.Secret{}
for _, secret := range *mountSecrets {
combinedSecret.Data[secret.MappedKey] = secret.Value
}
For a full example please check out the logging operator code.
Also, this feature is currently only covered with tests in the logging operator,
but this is a subject to change soon.
KubernetesVolume
Configure volumes in custom types for underlying pods in a uniform way.
type SomeCustomApp struct {
BufferStorage *volume.KubernetesVolume `json:"bufferStorage,omitempty"`
}
And this is how you would configure it through yaml
someCustomApp:
bufferStorage:
hostPath:
path: "/buffer" # set this here, or provide a default in the code
In the operator you can use the GetVolume
method on the object in a generic way:
// provide a default path if hostPath is used but no actual path has been configured explicitly
someCustomApp.bufferStorage.WithDefaultHostPath("/opt/buffer")
volume := someCustomApp.bufferStorage.GetVolume(
"volumeName",
))