Skip to content

Commit

Permalink
Prettier API
Browse files Browse the repository at this point in the history
  • Loading branch information
roivaz committed Dec 12, 2023
1 parent b21a747 commit 42c4f59
Show file tree
Hide file tree
Showing 12 changed files with 127 additions and 95 deletions.
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.24
ENVTEST_K8S_VERSION = 1.27

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
Expand Down Expand Up @@ -45,7 +45,10 @@ KUBEBUILDER_ASSETS = "$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)"
TEST_PKG = ./...

test: manifests generate fmt vet envtest ginkgo ## Run tests.
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -p -v -r $(TEST_PKG)
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -p -r $(TEST_PKG)

test-debug: manifests generate fmt vet envtest ginkgo ## Run tests.
KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) $(GINKGO) -v -r $(TEST_PKG)

manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
$(CONTROLLER_GEN) crd paths="./test/..." output:crd:artifacts:config="./test/api/v1alpha1"
Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
# basereconciler
# basereconciler

Basereconciler is an attempt to create a reconciler that can be imported an used in any controller-runtime based controller to perform the most common tasks a controller usually performs. It's a bunch of code that it's typically written again and again for every and each controller and that can be abstracted to work in a more generic way to avoid the repetition and improve code mantainability.
At the moment basereconciler can perform the following tasks:

* Get the custom resource and perform some common tasks on it:
* Management of resource finalizer: some custom resources required more complex finalization logic. For this to happen a finalizer must be in place. Basereconciler can keep this finalizer in place and remove it when necessary during resource finalization.
* Management of finalization logic: it checks if the resource is being finalized and executed the finalization logic passed to it if that is the case. When all finalization logic is completed it removes the finalizer on the custom resource.
* Reconcile resources owned by the custom resource: basreconciler can keep the owned resources of a custom resource in it's desired state. It works for any resource type, and only requires that the user configures how each specific resource type has to be configured. The resource reconciler only works in "update mode" right now, so any operation to transition a given resource from its live state to its desired state will be an Update. We might add a "patch mode" in the future.
* Reconcile custom resource status: if the custom resource implements a certain interface, basereconciler can also be in charge of reconciling the status.
* Resource pruner: when the reconciler stops seeing a certain resource, owned by the custom resource, it will prune them as it understands that the resource is no logner required. The resource pruner can be disabled globally or enabled/disabled on a per resource basis based on an annotation.

## Basic Usage

The following example is a kubebuilder bootstrapped controller that uses basereconciler to reconcile several resources owned by a custom resource. Explanations inline in the code.

```go

}
```
3 changes: 1 addition & 2 deletions config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package config

import (
"fmt"
"reflect"

"k8s.io/apimachinery/pkg/runtime/schema"
)
Expand Down Expand Up @@ -78,7 +77,7 @@ func GetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind) (ReconcileConf
// If the passed GVK is an empty one ("schema.GroupVersionKind{}"), the function will set the wildcard instead, which
// is a default set of basic reconclie rules that the reconciler will try to use when no other configuration is available.
func SetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind, cfg ReconcileConfigForGVK) {
if reflect.DeepEqual(gvk, schema.GroupVersionKind{}) {
if gvk.Empty() {
config.defaultResourceReconcileConfig["*"] = cfg
} else {
config.defaultResourceReconcileConfig[gvk.String()] = cfg
Expand Down
5 changes: 2 additions & 3 deletions mutators/mutators.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"

"github.com/3scale-ops/basereconciler/resource"
"github.com/3scale-ops/basereconciler/util"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -35,7 +34,7 @@ func SetDeploymentReplicas(enforce bool) resource.TemplateMutationFunction {
}

live := &appsv1.Deployment{}
if err := cl.Get(ctx, util.ObjectKey(desired), live); err != nil {
if err := cl.Get(ctx, client.ObjectKeyFromObject(desired), live); err != nil {
if errors.IsNotFound(err) {
return nil
}
Expand Down Expand Up @@ -71,7 +70,7 @@ func SetServiceLiveValues() resource.TemplateMutationFunction {

svc := desired.(*corev1.Service)
live := &corev1.Service{}
if err := cl.Get(ctx, util.ObjectKey(desired), live); err != nil {
if err := cl.Get(ctx, client.ObjectKeyFromObject(desired), live); err != nil {
if errors.IsNotFound(err) {
return nil
}
Expand Down
3 changes: 1 addition & 2 deletions reconciler/pruner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"testing"

"github.com/3scale-ops/basereconciler/config"
"github.com/3scale-ops/basereconciler/util"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -177,7 +176,7 @@ func TestReconciler_pruneOrphaned(t *testing.T) {
return
}
for _, check := range tt.want {
err := tt.fields.Client.Get(tt.args.ctx, util.ObjectKey(check.obj), check.obj)
err := tt.fields.Client.Get(tt.args.ctx, client.ObjectKeyFromObject(check.obj), check.obj)
if (err != nil && errors.IsNotFound(err)) != check.absent {
t.Errorf("Reconciler.pruneOrphaned() want %s to be absent=%v", check.obj.GetName(), check.absent)
}
Expand Down
112 changes: 67 additions & 45 deletions reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,25 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

type ReconcileResult struct {
Requeue bool
Error error
}

func (result ReconcileResult) IsReturnAndRequeue() bool {
return result.Requeue || result.Error != nil
}

func (result ReconcileResult) Values() (ctrl.Result, error) {
return ctrl.Result{Requeue: result.Requeue}, result.Error
}

// Reconciler computes a list of resources that it needs to keep in place
type Reconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
gvk schema.GroupVersionKind
typeTracker typeTracker
}

Expand All @@ -33,23 +47,28 @@ func NewFromManager(mgr manager.Manager) *Reconciler {
return &Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logr.Discard()}
}

func (r *Reconciler) WithGVK(apiVersion, kind string) *Reconciler {
r.gvk = schema.FromAPIVersionAndKind(apiVersion, kind)
return r
}

// WithLogger sets the Reconciler logger
func (r *Reconciler) WithLogger(logger logr.Logger) *Reconciler {
r.Log = logger
return r
}

func (r *Reconciler) GetLogger(ctx context.Context) logr.Logger {
if logger, err := logr.FromContext(ctx); err != nil {
return r.Log
// Logger returns the Reconciler logger and a copy of the context that also includes the logger inside to pass it around easily.
func (r *Reconciler) Logger(ctx context.Context, keysAndValues ...interface{}) (context.Context, logr.Logger) {
var logger logr.Logger
if !r.Log.IsZero() {
// get the logger configured in the Reconciler
logger = r.Log.WithValues(keysAndValues...)
} else {
return logger
// try to get a logger from the context
logger = logr.FromContextOrDiscard(ctx).WithValues(keysAndValues...)
}
}

func (r *Reconciler) SetLogger(ctx *context.Context, keysAndValues ...interface{}) logr.Logger {
logger := r.GetLogger(*ctx).WithValues(keysAndValues)
*ctx = logr.NewContext(*ctx, logger)
return logger
return logr.NewContext(ctx, logger), logger
}

// GetInstance tries to retrieve the custom resource instance and perform some standard
Expand All @@ -60,72 +79,68 @@ func (r *Reconciler) SetLogger(ctx *context.Context, keysAndValues ...interface{
// - cleanupFns: variadic parameter that allows passing cleanup functions that will be
// run when the custom resource is being deleted. Only works with a non-nil finalizer, otherwise
// the custom resource will be immediately deleted and the functions won't run.
func (r *Reconciler) GetInstance(ctx context.Context, key types.NamespacedName,
instance client.Object, finalizer *string, cleanupFns ...func()) (*ctrl.Result, error) {
logger := logr.FromContextOrDiscard(ctx)
func (r *Reconciler) GetInstance(ctx context.Context, req reconcile.Request, obj client.Object,
finalizer *string, cleanupFns ...func()) ReconcileResult {

err := r.Client.Get(ctx, key, instance)
ctx, logger := r.Logger(ctx)
err := r.Client.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, obj)
if err != nil {
if errors.IsNotFound(err) {
// Return and don't requeue
return &ctrl.Result{}, nil
return ReconcileResult{Requeue: false, Error: nil}
}
return &ctrl.Result{}, err
return ReconcileResult{Requeue: false, Error: err}
}

if util.IsBeingDeleted(instance) {
if util.IsBeingDeleted(obj) {

// finalizer logic is only triggered if the controller
// sets a finalizer, otherwise there's notihng to be done
if finalizer != nil {
// sets a finalizer and the finalizer is still present in the
// resource
if finalizer != nil && controllerutil.ContainsFinalizer(obj, *finalizer) {

if !controllerutil.ContainsFinalizer(instance, *finalizer) {
return &ctrl.Result{}, nil
}
err := r.ManageCleanupLogic(instance, cleanupFns, logger)
err := r.ManageCleanupLogic(obj, cleanupFns, logger)
if err != nil {
logger.Error(err, "unable to delete instance")
result, err := ctrl.Result{}, err
return &result, err
return ReconcileResult{Requeue: false, Error: err}
}
controllerutil.RemoveFinalizer(instance, *finalizer)
err = r.Client.Update(ctx, instance)
controllerutil.RemoveFinalizer(obj, *finalizer)
err = r.Client.Update(ctx, obj)
if err != nil {
logger.Error(err, "unable to update instance")
result, err := ctrl.Result{}, err
return &result, err
return ReconcileResult{Requeue: false, Error: err}
}

}
return &ctrl.Result{}, nil
// no finalizer, just return without doing anything
return ReconcileResult{Requeue: false, Error: nil}
}

if ok := r.IsInitialized(instance, finalizer); !ok {
err := r.Client.Update(ctx, instance)
if ok := r.IsInitialized(obj, finalizer); !ok {
err := r.Client.Update(ctx, obj)
if err != nil {
logger.Error(err, "unable to initialize instance")
result, err := ctrl.Result{}, err
return &result, err
return ReconcileResult{Requeue: false, Error: err}
}
return &ctrl.Result{}, nil
return ReconcileResult{Requeue: true, Error: nil}
}
return nil, nil
return ReconcileResult{Requeue: false, Error: nil}
}

// IsInitialized can be used to check if instance is correctly initialized.
// Returns false if it isn't.
func (r *Reconciler) IsInitialized(instance client.Object, finalizer *string) bool {
func (r *Reconciler) IsInitialized(obj client.Object, finalizer *string) bool {
ok := true
if finalizer != nil && !controllerutil.ContainsFinalizer(instance, *finalizer) {
controllerutil.AddFinalizer(instance, *finalizer)
if finalizer != nil && !controllerutil.ContainsFinalizer(obj, *finalizer) {
controllerutil.AddFinalizer(obj, *finalizer)
ok = false
}

return ok
}

// ManageCleanupLogic contains finalization logic for the Reconciler
func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), log logr.Logger) error {
func (r *Reconciler) ManageCleanupLogic(obj client.Object, fns []func(), log logr.Logger) error {
// Call any cleanup functions passed
for _, fn := range fns {
fn()
Expand All @@ -143,13 +158,16 @@ func (r *Reconciler) ManageCleanupLogic(instance client.Object, fns []func(), lo
// - If the resource pruner is enabled any resource owned by the custom resource not present in the list of managed
// resources is deleted. The resource pruner must be enabled in the global config (see package config) and also not
// explicitely disabled in the resource by the '<annotations-domain>/prune: true/false' annotation.
func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) error {
func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.Object, list []resource.TemplateInterface) ReconcileResult {
managedResources := []corev1.ObjectReference{}

for _, template := range list {
ref, err := resource.CreateOrUpdate(ctx, r.Client, r.Scheme, owner, template)
if err != nil {
return fmt.Errorf("unable to CreateOrUpdate resource: %w", err)
return ReconcileResult{
Requeue: false,
Error: fmt.Errorf("unable to CreateOrUpdate resource: %w", err),
}
}
if ref != nil {
managedResources = append(managedResources, *ref)
Expand All @@ -159,11 +177,15 @@ func (r *Reconciler) ReconcileOwnedResources(ctx context.Context, owner client.O

if isPrunerEnabled(owner) {
if err := r.pruneOrphaned(ctx, owner, managedResources); err != nil {
return fmt.Errorf("unable to prune orphaned resources: %w", err)

return ReconcileResult{
Requeue: false,
Error: fmt.Errorf("unable to prune orphaned resources: %w", err),
}
}
}

return nil
return ReconcileResult{Requeue: false, Error: nil}
}

// SecretEventHandler returns an EventHandler for the specific client.ObjectList
Expand All @@ -186,7 +208,7 @@ func (r *Reconciler) SecretEventHandler(ol client.ObjectList, logger logr.Logger
// TODO: pass a function that can decide if the event is of interest for a given resource
req := make([]reconcile.Request, 0, len(items))
for _, item := range items {
req = append(req, reconcile.Request{NamespacedName: util.ObjectKey(item)})
req = append(req, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(item)})
}
return req
},
Expand Down
10 changes: 5 additions & 5 deletions reconciler/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
// status of the custom resource. It also accepts functions with signature "func() bool" that can
// reconcile the status of the custom resource and return whether update is required or not.
func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithAppStatus,
deployments, statefulsets []types.NamespacedName, mutators ...func() bool) error {
deployments, statefulsets []types.NamespacedName, mutators ...func() bool) ReconcileResult {
logger := logr.FromContextOrDiscard(ctx)
update := false
status := instance.GetStatus()
Expand All @@ -27,7 +27,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp
deployment := &appsv1.Deployment{}
deploymentStatus := status.GetDeploymentStatus(key)
if err := r.Client.Get(ctx, key, deployment); err != nil {
return err
return ReconcileResult{Requeue: false, Error: err}
}

if !equality.Semantic.DeepEqual(deploymentStatus, deployment.Status) {
Expand All @@ -42,7 +42,7 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp
sts := &appsv1.StatefulSet{}
stsStatus := status.GetStatefulSetStatus(key)
if err := r.Client.Get(ctx, key, sts); err != nil {
return err
return ReconcileResult{Requeue: false, Error: err}
}

if !equality.Semantic.DeepEqual(stsStatus, sts.Status) {
Expand All @@ -63,11 +63,11 @@ func (r *Reconciler) ReconcileStatus(ctx context.Context, instance ObjectWithApp
if update {
if err := r.Client.Status().Update(ctx, instance); err != nil {
logger.Error(err, "unable to update status")
return err
return ReconcileResult{Requeue: false, Error: err}
}
}

return nil
return ReconcileResult{Requeue: false, Error: nil}
}

// ObjectWithAppStatus is an interface that implements
Expand Down
5 changes: 3 additions & 2 deletions resource/create_or_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import (
)

// CreateOrUpdate cretes or updates resources. The function receives several paremters:
// - ctx: the context
// - ctx: the context. The logger is expected to be within the context, otherwise the function won't
// produce any logs.
// - cl: the kubernetes API client
// - scheme: the kubernetes API scheme
// - owner: the object that owns the resource. Used to set the OwnerReference in the resource
Expand All @@ -37,7 +38,7 @@ func CreateOrUpdate(ctx context.Context, cl client.Client, scheme *runtime.Schem
return nil, fmt.Errorf("unable to build template: %w", err)
}

key := util.ObjectKey(desired)
key := client.ObjectKeyFromObject(desired)
gvk, err := apiutil.GVKForObject(desired, scheme)
if err != nil {
return nil, err
Expand Down
3 changes: 1 addition & 2 deletions test/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ var _ = BeforeSuite(func() {

// Add controllers for testing
err = (&Reconciler{
Reconciler: *reconciler.NewFromManager(mgr).
WithLogger(ctrl.Log.WithName("controllers").WithName("Test")),
Reconciler: reconciler.NewFromManager(mgr).WithLogger(ctrl.Log.WithName("controllers").WithName("Test")),
}).SetupWithManager(mgr)
Expect(err).ToNot(HaveOccurred())

Expand Down
Loading

0 comments on commit 42c4f59

Please sign in to comment.