Architecture

  • Based on Operator SDK, meanwhile a tiny wrapper around Kubebuilder, and therefore uses controller-runtime as the library under the hood

  • Controller watches for changes on Service Binding resources and process them in reconcile loop

  • Reconciliation logic is modeled as a pipeline, moving the incoming Service Binding instance through the following stages:

    • Gather bindings from Provisioned Service

    • Gather bindings from Direct Secret Reference

    • Gather binding definitions (Secret Generation Extension)

    • Extract binding data from services and/or referred resources using binding definitions

    • Generate synthetic binding data, whose definitions are part of Service Binding

    • Adjust binding item names if required through submitted Service Binding

    • If needed, create intermediate binding secret from the gathered binding data

    • If requested, project binding data as a set of environment variables in application container

    • If requested, project binding data as a set of files mounted inside application container

    • Persist changes in cluster

reconcile pipeline

The pipeline implements a form of Intercepting Filter pattern to give a developer full control over how a change on Service Binding is handled and how the handlers in the pipeline interact with each other:

  • A handler can decide if processing stops or the context is passed down to the next handler

  • Handlers communicate with each other only via reading/writing data to the context

  • Communication with cluster is abstracted via context

For details around Pipeline API, please check github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline package.
Benefits
  • Separation of concern - each stage does only one thing and does it good

  • Improved code readability/maintainability

  • Easy unit testing/integration testing

  • Single communication point with cluster allow easy mocking of cluster API

  • Possible reconciliation of service bindings outside of operator scope - use it as a library and allow tighter integration with other projects.

  • Easy support for various similar service binding APIs - same pipeline can be reused, just a new context provider need to be implemented

  • Easy support for different environments - e.g. binding services and application on plain Docker engine is possible by just providing proper context provider

  • Opens up an opportunity for better logging

  • Opens up an opportunity for better error handling

Both ServiceBinding APIs are supported using the same pipeline mechanism, but configured with a slightly different sets of stages and different context implementation. Pipelines reconciling binding.operators.coreos.com service bindings are instantiated with the help of DefaultBuilder from github.com/redhat-developer/service-binding-operator/pkg/reconcile/pipeline/builder package, whereas servicebinding.io service bindings are processed by pipelines created by SpecBuilder from the same package. These pipelines are created during startup of the appropriate controllers and used within Reconcile functions.

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the ServiceBinding object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
func (r *BindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("serviceBinding", req.NamespacedName)
	serviceBinding := r.ReconcilingObject()

	err := r.Get(ctx, req.NamespacedName, serviceBinding)
	if err != nil {
		if errors.IsNotFound(err) {
			// Request object not found, could have been deleted after reconcile request.
			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
			// Return and don't requeue
			log.Info("ServiceBinding resource not found. Ignoring since object must be deleted", "name", req.NamespacedName, "err", err)
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		log.Error(err, "Failed to get ServiceBinding", "name", req.NamespacedName, "err", err)
		return ctrl.Result{}, err
	}
	if !serviceBinding.HasDeletionTimestamp() && apis.MaybeAddFinalizer(serviceBinding) {
		if err = r.Update(ctx, serviceBinding); err != nil {
			return ctrl.Result{}, err
		}
	}
	if !serviceBinding.HasDeletionTimestamp() {
		log.Info("Reconciling")
	} else {
		log.Info("Deleted, unbind the application")
	}
	retry, delay, err := r.pipeline.Process(serviceBinding) (1)
	if !retry && err == nil {
		if serviceBinding.HasDeletionTimestamp() {
			if apis.MaybeRemoveFinalizer(serviceBinding) {
				if err = r.Update(ctx, serviceBinding); err != nil {
					return ctrl.Result{}, err
				}
			}
		}
	}
	result := ctrl.Result{Requeue: retry, RequeueAfter: delay}
	log.Info("Done", "retry", retry, "error", err)
	if retry {
		return result, err
	}
	return result, nil
}
1 Pushing service binding to the pipeline