Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(reconciler): Shared reconciliation loop logic for controllers #623

Merged
merged 77 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
3f31bf8
(chore): common clean up finalizer
abhijith-darshan Oct 7, 2024
6ce79cd
(chore): adds deletion condition
abhijith-darshan Oct 7, 2024
1c3324b
(chore): adds reconcile context helpers
abhijith-darshan Oct 7, 2024
2ed036b
(chore): adds reconcile utilities
abhijith-darshan Oct 7, 2024
5f57aca
(chore): adds common reconcile interface
abhijith-darshan Oct 7, 2024
3973edc
Automatic generation of CRD API Docs
Oct 7, 2024
82f1d60
Automatic application of license header
Oct 7, 2024
898b89b
Merge branch 'main' into feat/reconcile_standardization
abhijith-darshan Oct 7, 2024
5d82e61
(fix): go fmt!
abhijith-darshan Oct 7, 2024
0778314
Automatic generation of CRD API Docs
Oct 7, 2024
0101a0c
(fix): address lint issues
abhijith-darshan Oct 7, 2024
7dcedcf
Merge remote-tracking branch 'origin/feat/reconcile_standardization' …
abhijith-darshan Oct 7, 2024
49c242d
Automatic generation of CRD API Docs
Oct 7, 2024
423d693
(chore): move to lifecycle pkg
abhijith-darshan Oct 7, 2024
53eee91
Automatic generation of CRD API Docs
Oct 7, 2024
530b585
(chore): remove redundant break
abhijith-darshan Oct 7, 2024
a65d192
Merge remote-tracking branch 'origin/feat/reconcile_standardization' …
abhijith-darshan Oct 7, 2024
dd9374f
Automatic generation of CRD API Docs
Oct 7, 2024
9f74696
(chore): go fmt
abhijith-darshan Oct 7, 2024
e0e1f1d
Automatic generation of CRD API Docs
Oct 7, 2024
732507f
Merge branch 'main' into feat/reconcile_standardization
abhijith-darshan Oct 8, 2024
38e32d1
Merge branch 'main' into feat/reconcile_standardization
abhijith-darshan Oct 8, 2024
e7132dd
(chore): implements statusFunc and adds go docs
abhijith-darshan Oct 8, 2024
5580908
(chore): remove redundant break
abhijith-darshan Oct 8, 2024
c1a1fd2
Automatic generation of CRD API Docs
Oct 8, 2024
4a43f9d
Apply suggestions from code review
abhijith-darshan Oct 8, 2024
eb2b410
Automatic generation of CRD API Docs
Oct 8, 2024
a1e6c51
(chore): removes adding type information
abhijith-darshan Oct 8, 2024
f7247b0
(chore): add go docs
abhijith-darshan Oct 8, 2024
798da5c
Automatic generation of CRD API Docs
Oct 8, 2024
72b7a4e
(fix): go fmt!
abhijith-darshan Oct 8, 2024
6b8dfc6
Merge branch 'feat/reconcile_standardization' of https://github.com/c…
abhijith-darshan Oct 8, 2024
f2b58ff
Automatic generation of CRD API Docs
Oct 8, 2024
27e03ce
Merge branch 'main' into feat/reconcile_standardization
abhijith-darshan Oct 9, 2024
435f590
(chore): adds gvk info to dummy types
abhijith-darshan Oct 10, 2024
e4b4616
(chore): adds finalizer check
abhijith-darshan Oct 10, 2024
617771b
(chore): uses clientutil finalizer methods
abhijith-darshan Oct 10, 2024
c57fa8d
Automatic generation of CRD API Docs
Oct 10, 2024
4d49d9b
Automatic application of license header
Oct 10, 2024
464ee01
(chore): remove gvk for dummy due to regression
abhijith-darshan Oct 10, 2024
d398e1f
(chore): fix lint issues
abhijith-darshan Oct 10, 2024
ee6e57f
Merge branch 'feat/reconcile_standardization' of https://github.com/c…
abhijith-darshan Oct 10, 2024
76157e8
Automatic generation of CRD API Docs
Oct 10, 2024
72b6fc4
(chore): fix lint issues
abhijith-darshan Oct 10, 2024
c289a87
Automatic generation of CRD API Docs
Oct 10, 2024
ed17132
Merge branch 'main' into feat/reconcile_standardization
abhijith-darshan Oct 14, 2024
9fe8b1e
(chore): tidy up!
abhijith-darshan Oct 14, 2024
0ac0d2c
Automatic generation of CRD API Docs
Oct 14, 2024
bd72557
(chore): remove finalizer tests
abhijith-darshan Oct 14, 2024
a415830
(chore): remove unused conditions
abhijith-darshan Oct 14, 2024
03df985
(chore): simplify reconcile
abhijith-darshan Oct 14, 2024
d613091
Automatic generation of CRD API Docs
Oct 14, 2024
30c2a9f
(chore): generate mocks
abhijith-darshan Oct 14, 2024
a64beaa
Automatic application of license header
Oct 14, 2024
53016b6
(chore): fix lint
abhijith-darshan Oct 14, 2024
f5ec06b
Automatic generation of CRD API Docs
Oct 14, 2024
2bf848d
Update pkg/lifecycle/reconcile.go
abhijith-darshan Oct 14, 2024
e29399f
Automatic generation of CRD API Docs
Oct 14, 2024
cedac7d
(chore): re-generates mocks and adds reconcile test
abhijith-darshan Oct 14, 2024
45e852e
Automatic application of license header
Oct 14, 2024
b395af1
Automatic generation of CRD API Docs
Oct 14, 2024
2262d9b
(chore): adds unit test for context
abhijith-darshan Oct 15, 2024
51c7057
(chore): refactor reconcile_test.go
abhijith-darshan Oct 15, 2024
3c7c9b2
Automatic generation of CRD API Docs
Oct 15, 2024
9711125
(fix): go fmt
abhijith-darshan Oct 15, 2024
95b5012
Merge branch 'feat/reconcile_standardization' of https://github.com/c…
abhijith-darshan Oct 15, 2024
e6104b2
Automatic generation of CRD API Docs
Oct 15, 2024
f26af17
(fix): use correct assertions
abhijith-darshan Oct 15, 2024
1ed9009
Merge branch 'feat/reconcile_standardization' of https://github.com/c…
abhijith-darshan Oct 15, 2024
0e0f10a
Automatic generation of CRD API Docs
Oct 15, 2024
94230f7
Update pkg/lifecycle/reconcile.go
abhijith-darshan Oct 16, 2024
d6e6872
Merge branch 'main' into feat/reconcile_standardization
abhijith-darshan Oct 16, 2024
c93b6da
Automatic generation of CRD API Docs
Oct 16, 2024
54f4e21
(chore): use exports_test to expose pvt func
abhijith-darshan Oct 16, 2024
aecb95b
(chore): fmt
abhijith-darshan Oct 16, 2024
dbca39c
Automatic generation of CRD API Docs
Oct 16, 2024
dd1fc19
Automatic application of license header
Oct 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

# .mockery.yaml
with-expecter: false
filename: "mock_{{.InterfaceName}}.go"
outpkg: mocks
dir: pkg/mocks
packages:
github.com/cloudoperators/greenhouse/pkg/lifecycle:
interfaces:
Reconciler:
sigs.k8s.io/controller-runtime/pkg/client:
interfaces:
Client:
SubResourceWriter:
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,9 @@ WEBHOOK_DEV ?= false
.PHONY: setup-webhook
setup-webhook: cli
$(CLI) dev setup webhook --name $(ADMIN_CLUSTER) --namespace $(ADMIN_NAMESPACE) --release $(ADMIN_RELEASE) --chart-path $(ADMIN_CHART_PATH) --dockerfile ./ --dev-mode=$(WEBHOOK_DEV)

# Download and install mockery locally via `brew install mockery`
MOCKERY := $(shell which mockery)
mockery:
# will look into .mockery.yaml for configuration
$(MOCKERY)
2 changes: 1 addition & 1 deletion docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Greenhouse
version: bd165f9
version: aecb95b
description: PlusOne operations platform
paths:
/TeamMembership:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/vladimirvivien/gexe v0.3.0
github.com/wI2L/jsondiff v0.6.0
go.uber.org/zap v1.27.0
Expand Down Expand Up @@ -69,6 +70,7 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tidwall/gjson v1.17.3 // indirect
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/greenhouse/v1alpha1/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const (
// ReadyCondition reflects the overall readiness status of a resource.
ReadyCondition ConditionType = "Ready"

// DeleteCondition reflects that the resource has finished it's cleanup process.
DeleteCondition ConditionType = "Deleted"

// ClusterListEmpty is set when the resources ClusterSelector results in an empty ClusterList.
ClusterListEmpty ConditionType = "ClusterListEmpty"

Expand Down
6 changes: 0 additions & 6 deletions pkg/apis/greenhouse/v1alpha1/conditions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,6 @@ var _ = Describe("Test conditions util functions", func() {
LastTransitionTime: timeNow,
Message: "test",
}, false),
Entry("should return false if the Ready condition is not set", greenhousev1alpha1.Condition{
Type: greenhousev1alpha1.KubeconfigReadyCondition,
Status: metav1.ConditionFalse,
LastTransitionTime: timeNow,
Message: "test",
}, false),
Entry("should return false if no conditions are set", nil, false),
)

Expand Down
12 changes: 11 additions & 1 deletion pkg/controllers/fixtures/dummy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ type DummySpec struct {
}

// DummyStatus defines the observed state of Dummy
type DummyStatus struct{}
type DummyStatus struct {
v1alpha1.StatusConditions `json:"statusConditions,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
Expand Down Expand Up @@ -143,3 +145,11 @@ func (in *DummyStatus) DeepCopy() *DummyStatus {
in.DeepCopyInto(out)
return out
}

func (in *Dummy) GetConditions() v1alpha1.StatusConditions {
return in.Status.StatusConditions
}

func (in *Dummy) SetCondition(condition v1alpha1.Condition) {
in.Status.StatusConditions.SetConditions(condition)
}
42 changes: 42 additions & 0 deletions pkg/lifecycle/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package lifecycle

import (
"context"
"errors"
)

type reconcileRunKey struct{}

type reconcileRun struct {
objectCopy RuntimeObject
}

// createContextFromRuntimeObject create a new context with a copy of the object attached.
func createContextFromRuntimeObject(ctx context.Context, object RuntimeObject) context.Context {
return context.WithValue(ctx, reconcileRunKey{}, &reconcileRun{
objectCopy: object.DeepCopyObject().(RuntimeObject),
})
}

func getRunFromContext(ctx context.Context) (*reconcileRun, error) {
val, ok := ctx.Value(reconcileRunKey{}).(*reconcileRun)
if !ok {
return nil, errors.New("could not extract *reconcileRun from given context")
}

return val, nil
}

// getOriginalResourceFromContext - returns the unmodified version of the RuntimeObject
func getOriginalResourceFromContext(ctx context.Context) (RuntimeObject, error) {
reconcileRun, err := getRunFromContext(ctx)
if err != nil {
return nil, err
}

// create another copy so that context can not be modified by accident
return reconcileRun.objectCopy.DeepCopyObject().(RuntimeObject), nil
}
9 changes: 9 additions & 0 deletions pkg/lifecycle/context_exports_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package lifecycle

var (
CreateContextFromRuntimeObject = createContextFromRuntimeObject
GetOriginalResourceFromContext = getOriginalResourceFromContext
)
46 changes: 46 additions & 0 deletions pkg/lifecycle/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package lifecycle_test

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/cloudoperators/greenhouse/pkg/controllers/fixtures"
"github.com/cloudoperators/greenhouse/pkg/lifecycle"
)

var _ = Describe("Context", func() {
Describe("ReceiveObjectCopy", func() {
It("should receive the old copy", func() {
testResource := &fixtures.Dummy{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-resource",
Namespace: "default",
Labels: map[string]string{
"key1": "value1",
},
Annotations: map[string]string{
"annotation1": "value1",
},
},
}

ctx := lifecycle.CreateContextFromRuntimeObject(context.Background(), testResource)
testResource.GetLabels()["key1"] = "value2"
origResource, err := lifecycle.GetOriginalResourceFromContext(ctx)

Expect(err).ToNot(HaveOccurred())
Expect(origResource.GetLabels()["key1"]).To(Equal("value1"))
Expect(testResource.GetLabels()["key1"]).To(Equal("value2"))
})
})
})
184 changes: 184 additions & 0 deletions pkg/lifecycle/reconcile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
// SPDX-License-Identifier: Apache-2.0

package lifecycle

import (
"context"

"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

"github.com/cloudoperators/greenhouse/pkg/clientutil"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

greenhousev1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1"
)

type ReconcileResult string

const (
CreatedReason greenhousev1alpha1.ConditionReason = "Created"
PendingCreationReason greenhousev1alpha1.ConditionReason = "PendingCreation"
FailingCreationReason greenhousev1alpha1.ConditionReason = "FailingCreation"
// ScheduledDeletionReason is used to indicate that the resource is scheduled for deletion
ScheduledDeletionReason greenhousev1alpha1.ConditionReason = "ScheduledDeletion"
PendingDeletionReason greenhousev1alpha1.ConditionReason = "PendingDeletion"
FailingDeletionReason greenhousev1alpha1.ConditionReason = "FailingDeletion"
DeletedReason greenhousev1alpha1.ConditionReason = "Deleted"
CommonCleanupFinalizer = "greenhouse.sap/cleanup"

// Success should be returned in case the operator reached its target state
Success ReconcileResult = "Success"
// Failed should be returned in case the operator wasn't able to reach its target state and without external changes it's unlikely that this will succeed in the next try
Failed ReconcileResult = "Failed"
// Pending should be returned in case the operator is still trying to reach the target state (Requeue, waiting for remote resource to be cleaned up, etc.)
Pending ReconcileResult = "Pending"
)

// Conditioner is a function that can be used to set the status conditions of the object at a later point in the reconciliation process
// Provided by the caller of the Reconcile function
type Conditioner func(context.Context, RuntimeObject)

// RuntimeObject is an interface that generalizes the CR object that is being reconciled
type RuntimeObject interface {
runtime.Object
v1.Object
// GetConditions returns the status conditions of the object (must be implemented in respective types)
GetConditions() greenhousev1alpha1.StatusConditions
// SetCondition sets the status conditions of the object (must be implemented in respective types)
SetCondition(greenhousev1alpha1.Condition)
}

// Reconciler is the interface that wraps the basic EnsureCreated and EnsureDeleted methods that a controller should implement
type Reconciler interface {
EnsureCreated(context.Context, RuntimeObject) (ctrl.Result, ReconcileResult, error)
EnsureDeleted(context.Context, RuntimeObject) (ctrl.Result, ReconcileResult, error)
}

// Reconcile - is a generic function that is used to reconcile the state of a resource
// It standardizes the reconciliation loop and provides a common way to set finalizers, remove finalizers, and update the status of the resource
// It splits the reconciliation into two phases: EnsureCreated and EnsureDeleted to keep the create / update and delete logic in controllers segregated
func Reconcile(ctx context.Context, kubeClient client.Client, namespacedName types.NamespacedName, runtimeObject RuntimeObject, reconciler Reconciler, statusFunc Conditioner) (ctrl.Result, error) {
logger := ctrl.LoggerFrom(ctx)
if err := kubeClient.Get(ctx, namespacedName, runtimeObject); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// store the original object in the context
ctx = createContextFromRuntimeObject(ctx, runtimeObject)

shouldBeDeleted := runtimeObject.GetDeletionTimestamp() != nil
hasFinalizer := controllerutil.ContainsFinalizer(runtimeObject, CommonCleanupFinalizer)

// check whether finalizer is set
if !shouldBeDeleted && !hasFinalizer {
return ctrl.Result{}, clientutil.EnsureFinalizer(ctx, kubeClient, runtimeObject, CommonCleanupFinalizer)
}

var (
result ctrl.Result
err error
)
if shouldBeDeleted && hasFinalizer {
// check if the resource is already deleted (a control state to decide whether to remove finalizer)
// at this point the remote resource is already cleaned up so garbage collection can be done
if isResourceDeleted(runtimeObject) {
err = clientutil.RemoveFinalizer(ctx, kubeClient, runtimeObject, CommonCleanupFinalizer)
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// if the resource is not deleted yet, we need to ensure it is deleted
result, err = ensureDeleted(ctx, logger, reconciler, runtimeObject)
} else {
// if it is not in deletion phase then we ensure it is in desired created state
result, err = ensureCreated(ctx, logger, reconciler, runtimeObject, statusFunc)
}

// patch the final status of the resource to end the reconciliation loop
return result, patchStatus(ctx, kubeClient, runtimeObject, err)
}

// isResourceDeleted - returns true if the resource has a true Deleted condition
// This is used to determine if the resource is in deletion phase has finished its cleanup
func isResourceDeleted(runtimeObject RuntimeObject) bool {
status := runtimeObject.GetConditions()
deleteCondition := status.GetConditionByType(greenhousev1alpha1.DeleteCondition)
if deleteCondition == nil {
return false
}
return deleteCondition.IsTrue() && deleteCondition.Reason == DeletedReason
}

// ensureCreated - invokes the controller's EnsureCreated method and invokes the statusFunc to update the status of the resource
func ensureCreated(ctx context.Context, logger logr.Logger, reconciler Reconciler, runtimeObject RuntimeObject, statusFunc Conditioner) (ctrl.Result, error) {
logger.Info("ensure created")
result, reconcileResult, err := reconciler.EnsureCreated(ctx, runtimeObject)
if statusFunc != nil {
statusFunc(ctx, runtimeObject)
} else {
setupCreateState(runtimeObject, reconcileResult, err)
}
return result, err
}

// ensureDeleted - invokes the controller's EnsureDeleted method and sets the status of the resource to deleted
func ensureDeleted(ctx context.Context, logger logr.Logger, reconciler Reconciler, runtimeObject RuntimeObject) (ctrl.Result, error) {
logger.Info("ensure deleted")
setupDeleteState(runtimeObject, Pending, nil)
result, reconcileResult, err := reconciler.EnsureDeleted(ctx, runtimeObject)
setupDeleteState(runtimeObject, reconcileResult, err)
return result, err
}

// setupDeleteState - converts the reconcile result to a condition and sets it in the runtimeObject for deletion phase
func setupDeleteState(runtimeObject RuntimeObject, reconcileResult ReconcileResult, err error) {
var condition greenhousev1alpha1.Condition
switch reconcileResult {
case Success:
condition = greenhousev1alpha1.TrueCondition(greenhousev1alpha1.DeleteCondition, DeletedReason, "resource is successfully deleted")
case Failed:
msg := ""
if err != nil {
msg = err.Error()
}
condition = greenhousev1alpha1.FalseCondition(greenhousev1alpha1.DeleteCondition, FailingDeletionReason, "resource deletion failed: "+msg)
default:
condition = greenhousev1alpha1.FalseCondition(greenhousev1alpha1.DeleteCondition, PendingDeletionReason, "resource deletion is pending")
}
runtimeObject.SetCondition(condition)
}

// setupCreateState - if statusFunc is not passed to reconciler then the default status conditions are set in runtimeObject
func setupCreateState(runtimeObject RuntimeObject, reconcileResult ReconcileResult, err error) {
var condition greenhousev1alpha1.Condition
switch reconcileResult {
case Success:
condition = greenhousev1alpha1.TrueCondition(greenhousev1alpha1.ReadyCondition, CreatedReason, "resource is successfully created")
case Failed:
msg := ""
if err != nil {
msg = err.Error()
}
condition = greenhousev1alpha1.FalseCondition(greenhousev1alpha1.ReadyCondition, FailingCreationReason, "resource creation failed"+msg)
default:
condition = greenhousev1alpha1.UnknownCondition(greenhousev1alpha1.ReadyCondition, PendingCreationReason, "resource creation is pending")
}
runtimeObject.SetCondition(condition)
}

// patchStatus - patches the status of the resource with the new status and returns the reconcile error
func patchStatus(ctx context.Context, kubeClient client.Client, newObject RuntimeObject, reconcileError error) error {
oldObject, err := getOriginalResourceFromContext(ctx)
if err != nil {
return err
}
err = kubeClient.Status().Patch(ctx, newObject, client.MergeFrom(oldObject))
if err != nil {
return err
}
return reconcileError
}
Loading