pkg/controller/common/reconciler/reconciler.go (138 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package reconciler
import (
"context"
"fmt"
"reflect"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
)
// Params is a parameter object for the ReconcileResources function
type Params struct {
// Context to be used in API requests
Context context.Context
// Client k8s client to use
Client k8s.Client
// Owner will be set as the controller reference
Owner client.Object
// Expected the expected state of the resource going into reconciliation.
Expected client.Object
// Reconciled will contain the final state of the resource after reconciliation containing the
// unification of remote and expected state.
Reconciled client.Object
// NeedsUpdate returns true when the object to be reconciled has changes that are not persisted remotely.
NeedsUpdate func() bool
// NeedsRecreate returns true when the object to be reconciled needs to be deleted and re-created because it cannot be updated.
NeedsRecreate func() bool
// UpdateReconciled modifies the resource pointed to by Reconciled to reflect the state of Expected
UpdateReconciled func()
// PreCreate is called just before the creation of the resource.
PreCreate func() error
// PreUpdate is called just before the update of the resource.
PreUpdate func() error
// PostUpdate is called immediately after the resource is successfully updated.
PostUpdate func()
}
const resourceVersionKey = "resourceVersion"
func (p Params) CheckNilValues() error {
if p.Reconciled == nil {
return errors.New("Reconciled must not be nil")
}
if p.UpdateReconciled == nil {
return errors.New("UpdateReconciled must not be nil")
}
if p.NeedsUpdate == nil {
return errors.New("NeedsUpdate must not be nil")
}
if p.Expected == nil {
return errors.New("Expected must not be nil")
}
return nil
}
// ReconcileResource is a generic reconciliation function for resources that need to
// implement runtime.Object and meta/v1.Object.
func ReconcileResource(params Params) error {
err := params.CheckNilValues()
if err != nil {
return err
}
gvk, err := apiutil.GVKForObject(params.Expected, scheme.Scheme)
if err != nil {
return err
}
if params.Owner != nil {
if err := controllerutil.SetControllerReference(params.Owner, params.Expected, scheme.Scheme); err != nil {
return err
}
}
kind := gvk.Kind
namespace := params.Expected.GetNamespace()
name := params.Expected.GetName()
log := ulog.FromContext(params.Context).WithValues("kind", kind, "namespace", namespace, "name", name)
create := func() error {
log.Info("Creating resource")
if params.PreCreate != nil {
if err := params.PreCreate(); err != nil {
return err
}
}
// Copy the content of params.Expected into params.Reconciled.
// Unfortunately it's not straightforward to change the value of an interface underlying pointer,
// so we need a small bit of reflection here.
// This will panic if params.Expected and params.Reconciled don't have the same underlying type.
expectedCopyValue := reflect.ValueOf(params.Expected.DeepCopyObject()).Elem()
reflect.ValueOf(params.Reconciled).Elem().Set(expectedCopyValue)
// Create the object, which modifies params.Reconciled in-place
err = params.Client.Create(params.Context, params.Reconciled)
if err != nil {
return err
}
log.Info("Created resource successfully", resourceVersionKey, params.Reconciled.GetResourceVersion())
return nil
}
// Check if already exists
err = params.Client.Get(params.Context, types.NamespacedName{Name: name, Namespace: namespace}, params.Reconciled)
if err != nil && apierrors.IsNotFound(err) {
return create()
} else if err != nil {
log.Error(err, fmt.Sprintf("Generic GET for %s %s/%s failed with error", kind, namespace, name))
return fmt.Errorf("failed to get %s %s/%s: %w", kind, namespace, name, err)
}
if params.NeedsRecreate != nil && params.NeedsRecreate() {
log.Info("Deleting resource as it cannot be updated, it will be recreated")
reconciledMeta, err := meta.Accessor(params.Reconciled)
if err != nil {
return err
}
// Using a precondition here to make sure we delete the version of the resource we intend to delete and
// to avoid accidentally deleting a resource already recreated for example
uidToDelete := reconciledMeta.GetUID()
resourceVersionToDelete := reconciledMeta.GetResourceVersion()
opt := client.Preconditions{
UID: &uidToDelete,
ResourceVersion: &resourceVersionToDelete,
}
err = params.Client.Delete(params.Context, params.Expected, opt)
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to delete %s %s/%s: %w", kind, namespace, name, err)
}
log.Info("Deleted resource successfully")
return create()
}
//nolint:nestif
// Update if needed
if params.NeedsUpdate() {
log.Info("Updating resource")
if params.PreUpdate != nil {
if err := params.PreUpdate(); err != nil {
return err
}
}
reconciledMeta, err := meta.Accessor(params.Reconciled)
if err != nil {
return err
}
// retain the resource version to avoid unconditional updates
resourceVersion := reconciledMeta.GetResourceVersion()
params.UpdateReconciled()
// and set the resource version back into the resource to indicate the state we are basing the update off of
reconciledMeta.SetResourceVersion(resourceVersion)
// also keep the owner references up to date
expectedMeta, err := meta.Accessor(params.Expected)
if err != nil {
return err
}
expectedOwners := expectedMeta.GetOwnerReferences()
if expectedOwners != nil {
// we can safely assume we have just one reference here given that it was created just above
// but we don't want to replace wholesale in case a user has set an additional reference
k8s.OverrideControllerReference(reconciledMeta, expectedOwners[0])
}
err = params.Client.Update(params.Context, params.Reconciled)
if err != nil {
return err
}
if params.PostUpdate != nil {
params.PostUpdate()
}
log.Info("Updated resource successfully", resourceVersionKey, params.Reconciled.GetResourceVersion())
}
return nil
}