Skip to content

Commit

Permalink
Merge pull request #10 from 3scale-ops/basereconciler-v0.4
Browse files Browse the repository at this point in the history
basereconciler v0.4
  • Loading branch information
3scale-robot authored Jan 8, 2024
2 parents 8931743 + e9a8f34 commit 211a3a8
Show file tree
Hide file tree
Showing 55 changed files with 3,644 additions and 4,036 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: "1.19"
go-version: "1.20"

- uses: actions/cache@v2
with:
Expand Down
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 @@ -42,11 +42,14 @@ vet: ## Run go vet against code.

##@ Test
KUBEBUILDER_ASSETS = "$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)"
TEST_PKG = ./test/...
TEST_PKG = ./...

test: manifests generate fmt vet envtest ginkgo ## Run tests.
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
165 changes: 164 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,164 @@
# basereconciler
# basereconciler

Basereconciler is an attempt to create a reconciler that can be imported and 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**: basereconciler 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 longer 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
package controllers

import (
"context"

"github.com/3scale-ops/basereconciler/config"
"github.com/3scale-ops/basereconciler/mutators"
"github.com/3scale-ops/basereconciler/reconciler"
"github.com/3scale-ops/basereconciler/resource"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
webappv1 "my.domain/guestbook/api/v1"
)

// Use the init function to configure the behavior of the controller. In this case we use
// "SetDefaultReconcileConfigForGVK" to specify the paths that need to be reconciled/ignored
// for each resource type. Check the "github.com/3scale-ops/basereconciler/config" for more
// configuration options
func init() {
config.SetDefaultReconcileConfigForGVK(
schema.FromAPIVersionAndKind("apps/v1", "Deployment"),
config.ReconcileConfigForGVK{
EnsureProperties: []string{
"metadata.annotations",
"metadata.labels",
"spec",
},
IgnoreProperties: []string{
"metadata.annotations['deployment.kubernetes.io/revision']",
},
})
config.SetDefaultReconcileConfigForGVK(
// specifying a config for an empty GVK will
// set a default fallback config for any gvk that is not
// explicitely declared in the configuration. Think of it
// as a wildcard.
schema.GroupVersionKind{},
config.ReconcileConfigForGVK{
EnsureProperties: []string{
"metadata.annotations",
"metadata.labels",
"spec",
},
})
}

// GuestbookReconciler reconciles a Guestbook object
type GuestbookReconciler struct {
*reconciler.Reconciler
}

// +kubebuilder:rbac:groups=webapp.my.domain,resources=guestbooks,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=webapp.my.domain,resources=guestbooks/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="core",namespace=placeholder,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="apps",namespace=placeholder,resources=deployments,verbs=get;list;watch;create;update;patch;delete

func (r *GuestbookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// configure the logger for the controller. The function also returns a modified
// copy of the context that includes the logger so it's easily passed around to other functions.
ctx, logger := r.Logger(ctx, "guestbook", req.NamespacedName)

// ManageResourceLifecycle will take care of retrieving the custom resoure from the API. It is also in charge of the resource
// lifecycle: initialization and finalization logic. In this example, we are configuring a finalizer in our custom resource and passing
// a finalization function that will casuse a log line to show when the resource is being deleted.
guestbook := &webappv1.Guestbook{}
result := r.ManageResourceLifecycle(ctx, req, guestbook,
reconciler.WithFinalizer("guestbook-finalizer"),
reconciler.WithFinalizationFunc(
func(context.Context, client.Client) error {
logger.Info("finalizing resource")
return nil
}),
)
if result.ShouldReturn() {
return result.Values()
}

// ReconcileOwnedResources creates/updates/deletes the resoures that our custom resource owns.
// It is a list of templates, in this case generated from the base of an object we provide.
// Modifiers can be added to the template to get live values from the k8s API, like in this example
// with the Service. Check the documentation of the "github.com/3scale-ops/basereconciler/resource"
// for more information on building templates.
result = r.ReconcileOwnedResources(ctx, guestbook, []resource.TemplateInterface{

resource.NewTemplateFromObjectFunction[*appsv1.Deployment](
func() *appsv1.Deployment {
return &appsv1.Deployment{
// define your object here
}
}),

resource.NewTemplateFromObjectFunction[*corev1.Service](
func() *corev1.Service {
return &corev1.Service{
// define your object here
}
}).
// Retrieve the live values that kube-controller-manager sets
// in the Service spec to avoid overwrting them
WithMutation(mutators.SetServiceLiveValues()).
// There are some useful mutations in the "github.com/3scale-ops/basereconciler/mutators"
// package or you can pass your own mutation functions
WithMutation(func(ctx context.Context, cl client.Client, desired client.Object) error {
// your mutation logic here
return nil
}).
// The templates are reconciled using the global config defined in the init() function
// but in this case we are passing a custom config that will apply
// only to the reconciliation of this template
WithEnsureProperties([]resource.Property{"spec"}).
WithIgnoreProperties([]resource.Property{"spec.clusterIP", "spec.clusterIPs"}),
})

if result.ShouldReturn() {
return result.Values()
}

return ctrl.Result{}, nil
}

func (r *GuestbookReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&webappv1.Guestbook{}).
// add the watches for the specific resource types that the
// custom resource owns to watch for changes on those
Owns(&appsv1.Deployment{}).
Owns(&corev1.Service{}).
Complete(r)
}
```

Then you just need to register the controller with the controller-runtime manager and you are all set!

```go
[...]
if err = (&controllers.GuestbookReconciler{
Reconciler: reconciler.NewFromManager(mgr).WithLogger(ctrl.Log.WithName("controllers").WithName("Guestbook")),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Guestbook")
os.Exit(1)
}
[...]
```
85 changes: 85 additions & 0 deletions config/global.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Package config provides global configuration for the basereconciler. The package
// provides some barebones configuration, but in most cases the user will want to
// tailor this configuration to the needs and requirements of the specific controller/s.
package config

import (
"fmt"

"k8s.io/apimachinery/pkg/runtime/schema"
)

type ReconcileConfigForGVK struct {
EnsureProperties []string
IgnoreProperties []string
}

var config = struct {
annotationsDomain string
resourcePruner bool
defaultResourceReconcileConfig map[string]ReconcileConfigForGVK
}{
annotationsDomain: "basereconciler.3cale.net",
resourcePruner: true,
defaultResourceReconcileConfig: map[string]ReconcileConfigForGVK{
"*": {
EnsureProperties: []string{
"metadata.annotations",
"metadata.labels",
"spec",
},
IgnoreProperties: []string{},
},
},
}

// GetAnnotationsDomain returns the globally configured annotations domain. The annotations
// domain is used for the rollout trigger annotations (see the mutators package) and the resource
// finalizers
func GetAnnotationsDomain() string { return config.annotationsDomain }

// SetAnnotationsDomain globally configures the annotations domain. The annotations
// domain is used for the rollout trigger annotations (see the mutators package) and the resource
// finalizers
func SetAnnotationsDomain(domain string) { config.annotationsDomain = domain }

// EnableResourcePruner enables the resource pruner. The resource pruner keeps track of
// the owned resources of a given custom resource and deletes all that are not present in the list
// of managed resoures to reconcile.
func EnableResourcePruner() { config.resourcePruner = true }

// DisableResourcePruner disables the resource pruner. The resource pruner keeps track of
// the owned resources of a given custom resource and deletes all that are not present in the list
// of managed resoures to reconcile.
func DisableResourcePruner() { config.resourcePruner = false }

// IsResourcePrunerEnabled returs a boolean indicating wheter the resource pruner is enabled or not.
func IsResourcePrunerEnabled() bool { return config.resourcePruner }

// GetDefaultReconcileConfigForGVK returns the default configuration that instructs basereconciler how to reconcile
// a given kubernetes GVK (GroupVersionKind). This default config will be used if the "resource.Template" object (see
// the resource package) does not specify a configuration itself.
// When the passed GVK does not match any of the configured, this function returns the "wildcard", which is a default
// set of basic reconclie rules that the reconciler will try to use when no other configuration is available.
func GetDefaultReconcileConfigForGVK(gvk schema.GroupVersionKind) (ReconcileConfigForGVK, error) {
if cfg, ok := config.defaultResourceReconcileConfig[gvk.String()]; ok {
return cfg, nil
} else if defcfg, ok := config.defaultResourceReconcileConfig["*"]; ok {
return defcfg, nil
} else {
return ReconcileConfigForGVK{}, fmt.Errorf("no config registered for gvk %s", gvk)
}
}

// SetDefaultReconcileConfigForGVK sets the default configuration that instructs basereconciler how to reconcile
// a given kubernetes GVK (GroupVersionKind). This default config will be used if the "resource.Template" object (see
// the resource package) does not specify a configuration itself.
// 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 gvk.Empty() {
config.defaultResourceReconcileConfig["*"] = cfg
} else {
config.defaultResourceReconcileConfig[gvk.String()] = cfg
}
}
Loading

0 comments on commit 211a3a8

Please sign in to comment.