diff --git a/controller/lifecycle/conditions.go b/controller/lifecycle/conditions/conditions.go similarity index 72% rename from controller/lifecycle/conditions.go rename to controller/lifecycle/conditions/conditions.go index 101cffa..45784e1 100644 --- a/controller/lifecycle/conditions.go +++ b/controller/lifecycle/conditions/conditions.go @@ -1,4 +1,4 @@ -package lifecycle +package conditions import ( "fmt" @@ -7,6 +7,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" ) @@ -30,9 +32,10 @@ const ( subroutineMessageErrorFormatString = "The %s has an error: %s" ) -func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { - l.manageConditions = true - return l +type ConditionManager struct{} + +func NewConditionManager() *ConditionManager { + return &ConditionManager{} } type RuntimeObjectConditions interface { @@ -41,7 +44,7 @@ type RuntimeObjectConditions interface { } // Set the Condition of the instance to be ready -func setInstanceConditionReady(conditions *[]metav1.Condition, status metav1.ConditionStatus) bool { +func (c *ConditionManager) SetInstanceConditionReady(conditions *[]metav1.Condition, status metav1.ConditionStatus) bool { var msg string switch status { case metav1.ConditionTrue: @@ -60,15 +63,15 @@ func setInstanceConditionReady(conditions *[]metav1.Condition, status metav1.Con } // Set the Condition to be Unknown in case it is not set yet -func setInstanceConditionUnknownIfNotSet(conditions *[]metav1.Condition) bool { +func (c *ConditionManager) SetInstanceConditionUnknownIfNotSet(conditions *[]metav1.Condition) bool { existingCondition := meta.FindStatusCondition(*conditions, ConditionReady) if existingCondition == nil { - return setInstanceConditionReady(conditions, metav1.ConditionUnknown) + return c.SetInstanceConditionReady(conditions, metav1.ConditionUnknown) } return false } -func setSubroutineConditionToUnknownIfNotSet(conditions *[]metav1.Condition, subroutine Subroutine, isFinalize bool, log *logger.Logger) bool { +func (c *ConditionManager) SetSubroutineConditionToUnknownIfNotSet(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, isFinalize bool, log *logger.Logger) bool { conditionName, conditionMessage := getConditionNameAndMessage(subroutine, isFinalize) existingCondition := meta.FindStatusCondition(*conditions, conditionName) @@ -83,7 +86,7 @@ func setSubroutineConditionToUnknownIfNotSet(conditions *[]metav1.Condition, sub return false } -func getConditionNameAndMessage(subroutine Subroutine, isFinalize bool) (string, string) { +func getConditionNameAndMessage(subroutine subroutine.Subroutine, isFinalize bool) (string, string) { conditionName := fmt.Sprintf(subroutineReadyConditionFormatString, subroutine.GetName()) conditionMessage := "subroutine" if isFinalize { @@ -94,7 +97,7 @@ func getConditionNameAndMessage(subroutine Subroutine, isFinalize bool) (string, } // Set Subroutines Conditions -func setSubroutineCondition(conditions *[]metav1.Condition, subroutine Subroutine, subroutineResult ctrl.Result, subroutineErr error, isFinalize bool, log *logger.Logger) bool { +func (c *ConditionManager) SetSubroutineCondition(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, subroutineResult ctrl.Result, subroutineErr error, isFinalize bool, log *logger.Logger) bool { conditionName, conditionMessage := getConditionNameAndMessage(subroutine, isFinalize) // processing complete @@ -120,18 +123,18 @@ func setSubroutineCondition(conditions *[]metav1.Condition, subroutine Subroutin return changed } -func toRuntimeObjectConditionsInterface(instance RuntimeObject, log *logger.Logger) (RuntimeObjectConditions, error) { +func (c *ConditionManager) ToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (RuntimeObjectConditions, error) { if obj, ok := instance.(RuntimeObjectConditions); ok { return obj, nil } - err := fmt.Errorf("manageConditions is enabled, but instance does not implement RuntimeObjectConditions interface. This is a programming error") + err := fmt.Errorf("ManageConditions is enabled, but instance does not implement RuntimeObjectConditions interface. This is a programming error") log.Error().Err(err).Msg("instance does not implement RuntimeObjectConditions interface") sentry.CaptureError(err, nil) return nil, err } -func MustToRuntimeObjectConditionsInterface(instance RuntimeObject, log *logger.Logger) RuntimeObjectConditions { - obj, err := toRuntimeObjectConditionsInterface(instance, log) +func (c *ConditionManager) MustToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) RuntimeObjectConditions { + obj, err := c.ToRuntimeObjectConditionsInterface(instance, log) if err == nil { return obj } diff --git a/controller/lifecycle/conditions_test.go b/controller/lifecycle/conditions/conditions_test.go similarity index 71% rename from controller/lifecycle/conditions_test.go rename to controller/lifecycle/conditions/conditions_test.go index fcf0291..1be5d49 100644 --- a/controller/lifecycle/conditions_test.go +++ b/controller/lifecycle/conditions/conditions_test.go @@ -1,4 +1,4 @@ -package lifecycle +package conditions import ( "errors" @@ -11,32 +11,19 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" controllerruntime "sigs.k8s.io/controller-runtime" - "github.com/platform-mesh/golang-commons/controller/testSupport" + pmtesting "github.com/platform-mesh/golang-commons/controller/lifecycle/testing" "github.com/platform-mesh/golang-commons/logger" ) -// Test LifecycleManager.WithConditionManagement -func TestLifecycleManager_WithConditionManagement(t *testing.T) { - // Given - fakeClient := testSupport.CreateFakeClient(t, &testSupport.TestApiObject{}) - _, log := createLifecycleManager([]Subroutine{}, fakeClient) - - // When - l := NewLifecycleManager(log.Logger, "test-operator", "test-controller", fakeClient, []Subroutine{}).WithConditionManagement() - - // Then - assert.True(t, true, l.manageConditions) -} - // Test the setReady function with an empty array func TestSetReady(t *testing.T) { t.Run("TestSetReady with empty array", func(t *testing.T) { // Given condition := []metav1.Condition{} - + cm := NewConditionManager() // When - setInstanceConditionReady(&condition, metav1.ConditionTrue) + cm.SetInstanceConditionReady(&condition, metav1.ConditionTrue) // Then assert.Equal(t, 1, len(condition)) @@ -45,12 +32,13 @@ func TestSetReady(t *testing.T) { t.Run("TestSetReady with existing condition", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{ {Type: "test", Status: metav1.ConditionFalse}, } // When - setInstanceConditionReady(&condition, metav1.ConditionTrue) + cm.SetInstanceConditionReady(&condition, metav1.ConditionTrue) // Then assert.Equal(t, 2, len(condition)) @@ -62,10 +50,11 @@ func TestSetUnknown(t *testing.T) { t.Run("TestSetUnknown with empty array", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{} // When - setInstanceConditionUnknownIfNotSet(&condition) + cm.SetInstanceConditionUnknownIfNotSet(&condition) // Then assert.Equal(t, 1, len(condition)) @@ -74,12 +63,13 @@ func TestSetUnknown(t *testing.T) { t.Run("TestSetUnknown with existing ready condition", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{ {Type: ConditionReady, Status: metav1.ConditionTrue}, } // When - setInstanceConditionUnknownIfNotSet(&condition) + cm.SetInstanceConditionUnknownIfNotSet(&condition) // Then assert.Equal(t, 1, len(condition)) @@ -111,9 +101,10 @@ func TestSetSubroutineConditionToUnknownIfNotSet(t *testing.T) { t.Run(tt.Name, func(t *testing.T) { // Given condition := []metav1.Condition{} + cm := NewConditionManager() // When - setSubroutineConditionToUnknownIfNotSet(&condition, changeStatusSubroutine{}, tt.IsFinalize, log) + cm.SetSubroutineConditionToUnknownIfNotSet(&condition, pmtesting.ChangeStatusSubroutine{}, tt.IsFinalize, log) // Then assert.Equal(t, 1, len(condition)) @@ -124,12 +115,13 @@ func TestSetSubroutineConditionToUnknownIfNotSet(t *testing.T) { t.Run("TestSetSubroutineConditionToUnknownIfNotSet with existing condition", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{ {Type: "test", Status: metav1.ConditionFalse}, } // When - setSubroutineConditionToUnknownIfNotSet(&condition, changeStatusSubroutine{}, false, log) + cm.SetSubroutineConditionToUnknownIfNotSet(&condition, pmtesting.ChangeStatusSubroutine{}, false, log) // Then assert.Equal(t, 2, len(condition)) @@ -138,14 +130,15 @@ func TestSetSubroutineConditionToUnknownIfNotSet(t *testing.T) { t.Run("TestSetSubroutineConditionToUnknownIfNotSet with existing ready", func(t *testing.T) { // Given - subroutine := changeStatusSubroutine{} + cm := NewConditionManager() + subroutine := pmtesting.ChangeStatusSubroutine{} condition := []metav1.Condition{ {Type: "test", Status: metav1.ConditionFalse}, {Type: fmt.Sprintf("%s_Ready", subroutine.GetName()), Status: metav1.ConditionTrue}, } // When - setSubroutineConditionToUnknownIfNotSet(&condition, subroutine, false, log) + cm.SetSubroutineConditionToUnknownIfNotSet(&condition, subroutine, false, log) // Then assert.Equal(t, 2, len(condition)) @@ -160,11 +153,12 @@ func TestSubroutineCondition(t *testing.T) { // Add a test case to set a subroutine condition to ready if it was successfull t.Run("TestSetSubroutineConditionReady", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{} - subroutine := changeStatusSubroutine{} + subroutine := pmtesting.ChangeStatusSubroutine{} // When - setSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, nil, false, log) + cm.SetSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, nil, false, log) // Then assert.Equal(t, 1, len(condition)) @@ -174,11 +168,12 @@ func TestSubroutineCondition(t *testing.T) { // Add a test case to set a subroutine condition to unknown if it is still processing t.Run("TestSetSubroutineConditionProcessing", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{} - subroutine := changeStatusSubroutine{} + subroutine := pmtesting.ChangeStatusSubroutine{} // When - setSubroutineCondition(&condition, subroutine, controllerruntime.Result{RequeueAfter: 1 * time.Second}, nil, false, log) + cm.SetSubroutineCondition(&condition, subroutine, controllerruntime.Result{RequeueAfter: 1 * time.Second}, nil, false, log) // Then assert.Equal(t, 1, len(condition)) @@ -189,10 +184,11 @@ func TestSubroutineCondition(t *testing.T) { t.Run("TestSetSubroutineConditionError", func(t *testing.T) { // Given condition := []metav1.Condition{} - subroutine := changeStatusSubroutine{} + cm := NewConditionManager() + subroutine := pmtesting.ChangeStatusSubroutine{} // When - setSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, errors.New("failed"), false, log) + cm.SetSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, errors.New("failed"), false, log) // Then assert.Equal(t, 1, len(condition)) @@ -202,11 +198,12 @@ func TestSubroutineCondition(t *testing.T) { // Add a test case to set a subroutine condition for isFinalize true t.Run("TestSetSubroutineFinalizeConditionReady", func(t *testing.T) { // Given + cm := NewConditionManager() condition := []metav1.Condition{} - subroutine := changeStatusSubroutine{} + subroutine := pmtesting.ChangeStatusSubroutine{} // When - setSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, nil, true, log) + cm.SetSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, nil, true, log) // Then assert.Equal(t, 1, len(condition)) @@ -217,10 +214,11 @@ func TestSubroutineCondition(t *testing.T) { t.Run("TestSetSubroutineFinalizeConditionProcessing", func(t *testing.T) { // Given condition := []metav1.Condition{} - subroutine := changeStatusSubroutine{} + cm := NewConditionManager() + subroutine := pmtesting.ChangeStatusSubroutine{} // When - setSubroutineCondition(&condition, subroutine, controllerruntime.Result{RequeueAfter: 1 * time.Second}, nil, true, log) + cm.SetSubroutineCondition(&condition, subroutine, controllerruntime.Result{RequeueAfter: 1 * time.Second}, nil, true, log) // Then assert.Equal(t, 1, len(condition)) @@ -231,10 +229,11 @@ func TestSubroutineCondition(t *testing.T) { t.Run("TestSetSubroutineFinalizeConditionError", func(t *testing.T) { // Given condition := []metav1.Condition{} - subroutine := changeStatusSubroutine{} + cm := NewConditionManager() + subroutine := pmtesting.ChangeStatusSubroutine{} // When - setSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, errors.New("failed"), true, log) + cm.SetSubroutineCondition(&condition, subroutine, controllerruntime.Result{}, errors.New("failed"), true, log) // Then assert.Equal(t, 1, len(condition)) diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go new file mode 100644 index 0000000..695339e --- /dev/null +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -0,0 +1,136 @@ +package controllerruntime + +import ( + "context" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/platform-mesh/golang-commons/controller/filter" + "github.com/platform-mesh/golang-commons/controller/lifecycle" + "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/logger" +) + +type LifecycleManager struct { + log *logger.Logger + client client.Client + config lifecycle.Config + subroutines []subroutine.Subroutine + spreader *spread.Spreader + conditionsManager *conditions.ConditionManager + prepareContextFunc lifecycle.PrepareContextFunc +} + +func NewLifecycleManager(log *logger.Logger, operatorName string, controllerName string, client client.Client, subroutines []subroutine.Subroutine) *LifecycleManager { + log = log.MustChildLoggerWithAttributes("operator", operatorName, "controller", controllerName) + return &LifecycleManager{ + log: log, + client: client, + subroutines: subroutines, + config: lifecycle.Config{ + OperatorName: operatorName, + ControllerName: controllerName, + }, + } +} + +func (l *LifecycleManager) Config() lifecycle.Config { + return l.config +} +func (l *LifecycleManager) Log() *logger.Logger { + return l.log +} +func (l *LifecycleManager) Subroutines() []subroutine.Subroutine { + return l.subroutines +} +func (l *LifecycleManager) PrepareContextFunc() lifecycle.PrepareContextFunc { + return l.prepareContextFunc +} +func (l *LifecycleManager) ConditionsManager() *conditions.ConditionManager { + return l.conditionsManager +} + +func (l *LifecycleManager) Spreader() *spread.Spreader { + return l.spreader +} + +func (l *LifecycleManager) Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.RuntimeObject) (ctrl.Result, error) { + return lifecycle.Reconcile(ctx, req, instance, l.client, l) +} + +func (l *LifecycleManager) validateInterfaces(instance runtimeobject.RuntimeObject, log *logger.Logger) error { + if l.Spreader() != nil { + _, err := l.Spreader().ToRuntimeObjectSpreadReconcileStatusInterface(instance, log) + if err != nil { + return err + } + } + if l.ConditionsManager() != nil { + _, err := l.ConditionsManager().ToRuntimeObjectConditionsInterface(instance, log) + if err != nil { + return err + } + } + return nil +} + +func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, log *logger.Logger, eventPredicates ...predicate.Predicate) (*builder.Builder, error) { + if err := l.validateInterfaces(instance, log); err != nil { + return nil, err + } + + if (l.ConditionsManager() != nil || l.Spreader() != nil) && l.Config().ReadOnly { + return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") + } + + eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) + return ctrl.NewControllerManagedBy(mgr). + Named(reconcilerName). + For(instance). + WithOptions(controller.Options{MaxConcurrentReconciles: maxReconciles}). + WithEventFilter(predicate.And(eventPredicates...)), nil +} + +func (l *LifecycleManager) SetupWithManager(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, r reconcile.Reconciler, log *logger.Logger, eventPredicates ...predicate.Predicate) error { + b, err := l.SetupWithManagerBuilder(mgr, maxReconciles, reconcilerName, instance, debugLabelValue, log, eventPredicates...) + if err != nil { + return err + } + + return b.Complete(r) +} + +// WithPrepareContextFunc allows to set a function that prepares the context before each reconciliation +// This can be used to add additional information to the context that is needed by the subroutines +// You need to return a new context and an OperatorError in case of an error +func (l *LifecycleManager) WithPrepareContextFunc(prepareFunction lifecycle.PrepareContextFunc) *LifecycleManager { + l.prepareContextFunc = prepareFunction + return l +} + +// WithReadOnly allows to set the controller to read-only mode +// In read-only mode, the controller will not update the status of the instance +func (l *LifecycleManager) WithReadOnly() *LifecycleManager { + l.config.ReadOnly = true + return l +} + +// WithSpreadingReconciles sets the LifecycleManager to spread out the reconciles +func (l *LifecycleManager) WithSpreadingReconciles() *LifecycleManager { + l.spreader = spread.NewSpreader() + return l +} + +func (l *LifecycleManager) WithConditionManagement() *LifecycleManager { + l.conditionsManager = conditions.NewConditionManager() + return l +} diff --git a/controller/lifecycle/controllerruntime/lifecycle_test.go b/controller/lifecycle/controllerruntime/lifecycle_test.go new file mode 100644 index 0000000..12e1ff6 --- /dev/null +++ b/controller/lifecycle/controllerruntime/lifecycle_test.go @@ -0,0 +1,1375 @@ +package controllerruntime + +import ( + "context" + goerrors "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/platform-mesh/golang-commons/controller/lifecycle" + "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + operrors "github.com/platform-mesh/golang-commons/errors" + + pmtesting "github.com/platform-mesh/golang-commons/controller/lifecycle/testing" + "github.com/platform-mesh/golang-commons/controller/testSupport" + "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/golang-commons/logger/testlogger" + "github.com/platform-mesh/golang-commons/sentry" +) + +func TestLifecycle(t *testing.T) { + namespace := "bar" + name := "foo" + request := controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: name, + }, + } + testApiObject := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + ctx := context.Background() + + t.Run("Lifecycle with a not found object", func(t *testing.T) { + // Arrange + fakeClient := testSupport.CreateFakeClient(t, &testSupport.TestApiObject{}) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + // Act + result, err := mgr.Reconcile(ctx, request, &testSupport.TestApiObject{}) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + logMessages, err := log.GetLogMessages() + assert.NoError(t, err) + assert.Equal(t, len(logMessages), 2) + assert.Equal(t, logMessages[0].Message, "start reconcile") + assert.Contains(t, logMessages[1].Message, "instance not found") + }) + + t.Run("Lifecycle with a finalizer - add finalizer", func(t *testing.T) { + // Arrange + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + }) + + t.Run("Lifecycle with a finalizer - finalization", func(t *testing.T) { + // Arrange + now := &metav1.Time{Time: time.Now()} + finalizers := []string{pmtesting.SubroutineFinalizer} + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, 0, len(instance.Finalizers)) + }) + + t.Run("Lifecycle with a finalizer - finalization(requeue)", func(t *testing.T) { + // Arrange + now := &metav1.Time{Time: time.Now()} + finalizers := []string{pmtesting.SubroutineFinalizer} + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + RequeueAfter: 1 * time.Second, + }, + }, fakeClient) + + // Act + res, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + assert.Equal(t, time.Duration(1*time.Second), res.RequeueAfter) + }) + + t.Run("Lifecycle with a finalizer - finalization(requeueAfter)", func(t *testing.T) { + // Arrange + now := &metav1.Time{Time: time.Now()} + finalizers := []string{pmtesting.SubroutineFinalizer} + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + RequeueAfter: 2 * time.Second, + }, + }, fakeClient) + + // Act + res, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + assert.Equal(t, 2*time.Second, res.RequeueAfter) + }) + + t.Run("Lifecycle with a finalizer - skip finalization if the finalizer is not in there", func(t *testing.T) { + // Arrange + now := &metav1.Time{Time: time.Now()} + finalizers := []string{"other-finalizer"} + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + }) + t.Run("Lifecycle with a finalizer - failing finalization subroutine", func(t *testing.T) { + // Arrange + now := &metav1.Time{Time: time.Now()} + finalizers := []string{pmtesting.SubroutineFinalizer} + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + Err: fmt.Errorf("some error"), + }, + }, fakeClient) + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.Error(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + }) + + t.Run("Lifecycle without changing status", func(t *testing.T) { + // Arrange + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Status: testSupport.TestStatus{Some: "string"}, + } + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + // Act + result, err := mgr.Reconcile(ctx, request, instance) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + logMessages, err := log.GetLogMessages() + assert.NoError(t, err) + assert.Equal(t, len(logMessages), 3) + assert.Equal(t, logMessages[0].Message, "start reconcile") + assert.Equal(t, logMessages[1].Message, "skipping status update, since they are equal") + assert.Equal(t, logMessages[2].Message, "end reconcile") + }) + + t.Run("Lifecycle with changing status", func(t *testing.T) { + // Arrange + instance := &testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Status: testSupport.TestStatus{Some: "string"}, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + + // Act + result, err := mgr.Reconcile(ctx, request, instance) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + logMessages, err := log.GetLogMessages() + assert.NoError(t, err) + assert.Equal(t, len(logMessages), 7) + assert.Equal(t, logMessages[0].Message, "start reconcile") + assert.Equal(t, logMessages[1].Message, "start subroutine") + assert.Equal(t, logMessages[2].Message, "processing instance") + assert.Equal(t, logMessages[3].Message, "processed instance") + assert.Equal(t, logMessages[4].Message, "end subroutine") + + serverObject := &testSupport.TestApiObject{} + err = fakeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, serverObject) + assert.NoError(t, err) + assert.Equal(t, serverObject.Status.Some, "other string") + }) + + t.Run("Lifecycle with spread reconciles", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, instance.Generation, instance.Status.ObservedGeneration) + }) + + t.Run("Lifecycle with spread reconciles on deleted object", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 2, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{pmtesting.ChangeStatusSubroutineFinalizer}, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 2, + NextReconcileTime: metav1.Time{Time: time.Now().Add(2 * time.Hour)}, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + assert.NoError(t, err) + assert.Len(t, instance.Finalizers, 0) + + }) + + t.Run("Lifecycle with spread reconciles skips if the generation is the same", func(t *testing.T) { + // Arrange + nextReconcileTime := metav1.NewTime(time.Now().Add(1 * time.Hour)) + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 1, + NextReconcileTime: nextReconcileTime, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{RequeAfter: false}}, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + result, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, int64(1), instance.Status.ObservedGeneration) + assert.GreaterOrEqual(t, 12*time.Hour, result.RequeueAfter) + }) + + t.Run("Lifecycle with spread reconciles and processing fails (no-retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{Retry: false, RequeAfter: false}}, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, int64(1), instance.Status.ObservedGeneration) + }) + + t.Run("Lifecycle with spread reconciles and processing fails (retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.Error(t, err) + assert.Equal(t, int64(0), instance.Status.ObservedGeneration) + }) + + t.Run("Lifecycle with spread reconciles and processing needs requeue", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{RequeAfter: true}}, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, int64(0), instance.Status.ObservedGeneration) + }) + + t.Run("Lifecycle with spread reconciles and processing needs requeueAfter", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{RequeAfter: true}}, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, int64(0), instance.Status.ObservedGeneration) + }) + + t.Run("Lifecycle with spread not implementing the interface", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + mgr.WithSpreadingReconciles() + + // Act + assert.Panics(t, func() { + _, _ = mgr.Reconcile(ctx, request, instance) + }) + }) + + t.Run("Should setup with manager", func(t *testing.T) { + // Arrange + instance := &testSupport.TestApiObject{} + fakeClient := testSupport.CreateFakeClient(t, instance) + log, err := logger.New(logger.DefaultConfig()) + assert.NoError(t, err) + m, err := manager.New(&rest.Config{}, manager.Options{ + Scheme: fakeClient.Scheme(), + }) + assert.NoError(t, err) + + lm, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{RequeAfter: true}}, fakeClient) + tr := &testReconciler{ + lifecycleManager: lm, + } + + // Act + err = lm.SetupWithManager(m, 0, "testReconciler", instance, "test", tr, log) + + // Assert + assert.NoError(t, err) + }) + + t.Run("Should setup with manager not implementing interface", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{} + fakeClient := testSupport.CreateFakeClient(t, instance) + log, err := logger.New(logger.DefaultConfig()) + assert.NoError(t, err) + m, err := manager.New(&rest.Config{}, manager.Options{ + Scheme: fakeClient.Scheme(), + }) + assert.NoError(t, err) + + lm, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{RequeAfter: true}}, fakeClient) + lm.WithSpreadingReconciles() + tr := &testReconciler{ + lifecycleManager: lm, + } + + // Act + err = lm.SetupWithManager(m, 0, "testReconciler", instance, "test", tr, log) + + // Assert + assert.Error(t, err) + }) + + t.Run("Lifecycle with spread reconciles and refresh label", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + Labels: map[string]string{spread.ReconcileRefreshLabel: "true"}, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 1, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + lm, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + lm.WithSpreadingReconciles() + + // Act + _, err := lm.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Equal(t, int64(1), instance.Status.ObservedGeneration) + + serverObject := &pmtesting.ImplementingSpreadReconciles{} + err = fakeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, serverObject) + assert.NoError(t, err) + assert.Equal(t, serverObject.Status.Some, "other string") + _, ok := serverObject.Labels[spread.ReconcileRefreshLabel] + assert.False(t, ok) + }) + + t.Run("Should handle a client error", func(t *testing.T) { + // Arrange + _, log := createLifecycleManager([]subroutine.Subroutine{}, nil) + testErr := fmt.Errorf("test error") + + // Act + result, err := lifecycle.HandleClientError("test", log.Logger, testErr, true, sentry.Tags{}) + + // Assert + assert.Error(t, err) + assert.Equal(t, testErr, err) + assert.Equal(t, controllerruntime.Result{}, result) + }) + + t.Run("Lifecycle with manage conditions reconciles w/o subroutines", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 1) + assert.Equal(t, instance.Status.Conditions[0].Type, conditions.ConditionReady) + assert.Equal(t, instance.Status.Conditions[0].Status, metav1.ConditionTrue) + assert.Equal(t, instance.Status.Conditions[0].Message, "The resource is ready") + }) + + t.Run("Lifecycle with manage conditions reconciles with subroutine", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + require.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "changeStatus_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) + }) + + t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.AddConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + require.Len(t, instance.Status.Conditions, 3) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) + assert.Equal(t, "test", instance.Status.Conditions[2].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) + assert.Equal(t, "test", instance.Status.Conditions[2].Message) + + }) + + t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.AddConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + require.Len(t, instance.Status.Conditions, 3) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) + assert.Equal(t, "test", instance.Status.Conditions[2].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) + assert.Equal(t, "test", instance.Status.Conditions[2].Message) + + }) + + t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition with preexisting conditions (update)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Conditions: []metav1.Condition{ + { + Type: "test", + Status: metav1.ConditionFalse, + Reason: "test", + Message: "test", + }, + }, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.AddConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + require.Len(t, instance.Status.Conditions, 3) + assert.Equal(t, "test", instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "test", instance.Status.Conditions[0].Message) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) + assert.Equal(t, "The resource is ready", instance.Status.Conditions[1].Message) + assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[2].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) + assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[2].Message) + + }) + + t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition with preexisting conditions", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Conditions: []metav1.Condition{ + { + Type: conditions.ConditionReady, + Status: metav1.ConditionTrue, + Message: "The resource is ready!!", + Reason: conditions.ConditionReady, + }, + }, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.AddConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + require.Len(t, instance.Status.Conditions, 3) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) + assert.Equal(t, "test", instance.Status.Conditions[2].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) + assert.Equal(t, "test", instance.Status.Conditions[2].Message) + + }) + + t.Run("Lifecycle w/o manage conditions reconciles with subroutine that adds a condition", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.AddConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + require.Len(t, instance.Status.Conditions, 1) + assert.Equal(t, "test", instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "test", instance.Status.Conditions[0].Message) + + }) + + t.Run("Lifecycle with manage conditions reconciles with subroutine failing Status update", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "changeStatus_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) + }) + + t.Run("Lifecycle with manage conditions finalizes with multiple subroutines partially succeeding", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{pmtesting.FailureScenarioSubroutineFinalizer, pmtesting.ChangeStatusSubroutineFinalizer}, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{}, + pmtesting.ChangeStatusSubroutine{Client: fakeClient}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.Error(t, err) + require.Len(t, instance.Status.Conditions, 3) + assert.Equal(t, "changeStatus_Finalize", instance.Status.Conditions[0].Type, "") + assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) + assert.Equal(t, "The subroutine finalization is complete", instance.Status.Conditions[0].Message) + assert.Equal(t, "FailureScenarioSubroutine_Finalize", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine finalization has an error: FailureScenarioSubroutine", instance.Status.Conditions[1].Message) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[2].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[2].Status) + assert.Equal(t, "The resource is not ready", instance.Status.Conditions[2].Message) + }) + + t.Run("Lifecycle with manage conditions reconciles with ReqeueAfter subroutine", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: true}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is not ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "FailureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionUnknown, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine is processing", instance.Status.Conditions[1].Message) + }) + + t.Run("Lifecycle with manage conditions reconciles with Error subroutine (no-retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: false}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is not ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "FailureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine has an error: FailureScenarioSubroutine", instance.Status.Conditions[1].Message) + }) + + t.Run("Lifecycle with manage conditions reconciles with Error subroutine (retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{}, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.Error(t, err) + assert.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[0].Status) + assert.Equal(t, "The resource is not ready", instance.Status.Conditions[0].Message) + assert.Equal(t, "FailureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[1].Status) + assert.Equal(t, "The subroutine has an error: FailureScenarioSubroutine", instance.Status.Conditions[1].Message) + }) + + t.Run("Lifecycle with manage conditions not implementing the interface", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }, fakeClient) + mgr.WithConditionManagement() + + // Act + // So the validation is already happening in SetupWithManager. So we can panic in the reconcile. + assert.Panics(t, func() { + _, _ = mgr.Reconcile(ctx, request, instance) + }) + }) + + t.Run("Lifecycle with manage conditions failing finalize", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + Finalizers: []string{pmtesting.FailureScenarioSubroutineFinalizer}, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{}}, fakeClient) + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.Error(t, err) + assert.Equal(t, "FailureScenarioSubroutine", err.Error()) + }) + + t.Run("Lifecycle with spread reconciles and manage conditions and processing fails (retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditionsAndSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) + mgr.WithSpreadingReconciles() + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.Error(t, err) + assert.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[0].Status)) + assert.Equal(t, "FailureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[1].Status)) + assert.Equal(t, int64(0), instance.Status.ObservedGeneration) + }) + + t.Run("Lifecycle with spread reconciles and manage conditions and processing fails (no-retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditionsAndSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: testSupport.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := testSupport.CreateFakeClient(t, instance) + + mgr, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.FailureScenarioSubroutine{RequeAfter: false}}, fakeClient) + mgr.WithSpreadingReconciles() + mgr.WithConditionManagement() + + // Act + _, err := mgr.Reconcile(ctx, request, instance) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 2) + assert.Equal(t, conditions.ConditionReady, instance.Status.Conditions[0].Type) + assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[0].Status)) + assert.Equal(t, "FailureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) + assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[1].Status)) + assert.Equal(t, int64(1), instance.Status.ObservedGeneration) + }) + + t.Run("Test Lifecycle setupWithManager /w conditions and expecting no error", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{} + fakeClient := testSupport.CreateFakeClient(t, instance) + + m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) + assert.NoError(t, err) + + lm, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + lm = lm.WithConditionManagement() + tr := &testReconciler{lifecycleManager: lm} + + // Act + err = lm.SetupWithManager(m, 0, "testReconciler1", instance, "test", tr, log.Logger) + + // Assert + assert.NoError(t, err) + }) + + t.Run("Test Lifecycle setupWithManager /w conditions and expecting error", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{} + fakeClient := testSupport.CreateFakeClient(t, instance) + + m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) + assert.NoError(t, err) + + lm, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + lm = lm.WithConditionManagement() + tr := &testReconciler{lifecycleManager: lm} + + // Act + err = lm.SetupWithManager(m, 0, "testReconciler2", instance, "test", tr, log.Logger) + + // Assert + assert.Error(t, err) + }) + + t.Run("Test Lifecycle setupWithManager /w spread and expecting no error", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{} + fakeClient := testSupport.CreateFakeClient(t, instance) + + m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) + assert.NoError(t, err) + + lm, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + lm = lm.WithSpreadingReconciles() + tr := &testReconciler{lifecycleManager: lm} + + // Act + err = lm.SetupWithManager(m, 0, "testReconciler3", instance, "test", tr, log.Logger) + + // Assert + assert.NoError(t, err) + }) + + t.Run("Test Lifecycle setupWithManager /w spread and expecting a error", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{} + fakeClient := testSupport.CreateFakeClient(t, instance) + + m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) + assert.NoError(t, err) + + lm, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + lm = lm.WithSpreadingReconciles() + tr := &testReconciler{lifecycleManager: lm} + + // Act + err = lm.SetupWithManager(m, 0, "testReconciler", instance, "test", tr, log.Logger) + + // Assert + assert.Error(t, err) + }) + + errorMessage := "oh nose" + t.Run("handleOperatorError", func(t *testing.T) { + t.Run("Should handle an operator error with retry and sentry", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{} + fakeClient := testSupport.CreateFakeClient(t, instance) + + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + ctx = sentry.ContextWithSentryTags(ctx, map[string]string{}) + + // Act + result, err := lifecycle.HandleOperatorError(ctx, operrors.NewOperatorError(goerrors.New(errorMessage), true, true), "handle op error", true, log.Logger) + + // Assert + assert.Error(t, err) + assert.NotNil(t, result) + assert.Equal(t, errorMessage, err.Error()) + + errorMessages, err := log.GetErrorMessages() + assert.NoError(t, err) + assert.Equal(t, errorMessage, *errorMessages[0].Error) + }) + + t.Run("Should handle an operator error without retry", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{} + fakeClient := testSupport.CreateFakeClient(t, instance) + + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + // Act + result, err := lifecycle.HandleOperatorError(ctx, operrors.NewOperatorError(goerrors.New(errorMessage), false, false), "handle op error", true, log.Logger) + + // Assert + assert.Nil(t, err) + assert.NotNil(t, result) + + errorMessages, err := log.GetErrorMessages() + assert.NoError(t, err) + assert.Equal(t, errorMessage, *errorMessages[0].Error) + }) + }) + + t.Run("Prepare Context", func(t *testing.T) { + t.Run("Sets a context that can be used in the subroutine", func(t *testing.T) { + // Arrange + ctx := context.Background() + + fakeClient := testSupport.CreateFakeClient(t, testApiObject) + + lm, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.ContextValueSubroutine{}}, fakeClient) + lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, operrors.OperatorError) { + return context.WithValue(ctx, pmtesting.ContextValueKey, "valueFromContext"), nil + }) + tr := &testReconciler{lifecycleManager: lm} + result, err := tr.Reconcile(ctx, controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}) + + // Then + assert.NotNil(t, ctx) + assert.NotNil(t, result) + assert.NoError(t, err) + + err = fakeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, testApiObject) + assert.NoError(t, err) + assert.Equal(t, "valueFromContext", testApiObject.Status.Some) + }) + + t.Run("Handles the errors correctly", func(t *testing.T) { + // Arrange + ctx := context.Background() + + fakeClient := testSupport.CreateFakeClient(t, testApiObject) + + lm, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.ContextValueSubroutine{}}, fakeClient) + lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, operrors.OperatorError) { + return nil, operrors.NewOperatorError(goerrors.New(errorMessage), true, false) + }) + tr := &testReconciler{lifecycleManager: lm} + result, err := tr.Reconcile(ctx, controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}) + + // Then + assert.NotNil(t, ctx) + assert.NotNil(t, result) + assert.Error(t, err) + }) + }) +} + +// Test LifecycleManager.WithConditionManagement +func TestLifecycleManager_WithConditionManagement(t *testing.T) { + // Given + fakeClient := testSupport.CreateFakeClient(t, &testSupport.TestApiObject{}) + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + // When + l := NewLifecycleManager(log.Logger, "test-operator", "test-controller", fakeClient, []subroutine.Subroutine{}).WithConditionManagement() + + // Then + assert.True(t, true, l.ConditionsManager() != nil) +} + +type testReconciler struct { + lifecycleManager *LifecycleManager +} + +func (r *testReconciler) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) { + return r.lifecycleManager.Reconcile(ctx, req, &testSupport.TestApiObject{}) +} + +func createLifecycleManager(subroutines []subroutine.Subroutine, c client.Client) (*LifecycleManager, *testlogger.TestLogger) { + log := testlogger.New() + mgr := NewLifecycleManager(log.Logger, "test-operator", "test-controller", c, subroutines) + return mgr, log +} diff --git a/controller/lifecycle/finalizerSubroutine_test.go b/controller/lifecycle/finalizerSubroutine_test.go deleted file mode 100644 index 806e8b9..0000000 --- a/controller/lifecycle/finalizerSubroutine_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package lifecycle - -import ( - "context" - "time" - - controllerruntime "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/platform-mesh/golang-commons/controller/testSupport" - "github.com/platform-mesh/golang-commons/errors" -) - -const subroutineFinalizer = "finalizer" - -type finalizerSubroutine struct { - client client.Client - err error - requeueAfter time.Duration -} - -func (c finalizerSubroutine) Process(_ context.Context, runtimeObj RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - instance := runtimeObj.(*testSupport.TestApiObject) - instance.Status.Some = "other string" - return controllerruntime.Result{}, nil -} - -func (c finalizerSubroutine) Finalize(_ context.Context, _ RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - if c.err != nil { - return controllerruntime.Result{}, errors.NewOperatorError(c.err, true, true) - } - if c.requeueAfter > 0 { - return controllerruntime.Result{RequeueAfter: c.requeueAfter}, nil - } - - return controllerruntime.Result{}, nil -} - -func (c finalizerSubroutine) GetName() string { - return "changeStatus" -} - -func (c finalizerSubroutine) Finalizers() []string { - return []string{ - subroutineFinalizer, - } -} diff --git a/controller/lifecycle/lifecycle.go b/controller/lifecycle/lifecycle.go index 1647797..215e474 100644 --- a/controller/lifecycle/lifecycle.go +++ b/controller/lifecycle/lifecycle.go @@ -14,149 +14,128 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/platform-mesh/golang-commons/controller/filter" + "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/spread" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" ) -type LifecycleManager struct { - log *logger.Logger - client client.Client - subroutines []Subroutine - operatorName string - controllerName string - spreadReconciles bool - manageConditions bool - readOnly bool - prepareContextFunc PrepareContextFunc +type Lifecycle interface { + Config() Config + Log() *logger.Logger + Spreader() *spread.Spreader + ConditionsManager() *conditions.ConditionManager + PrepareContextFunc() PrepareContextFunc + Subroutines() []subroutine.Subroutine } -type RuntimeObject interface { - runtime.Object - v1.Object +type Config struct { + OperatorName string + ControllerName string + ReadOnly bool } -type Subroutine interface { - Process(ctx context.Context, instance RuntimeObject) (ctrl.Result, errors.OperatorError) - Finalize(ctx context.Context, instance RuntimeObject) (ctrl.Result, errors.OperatorError) - GetName() string - Finalizers() []string -} - -func NewLifecycleManager(log *logger.Logger, operatorName string, controllerName string, client client.Client, subroutines []Subroutine) *LifecycleManager { - - log = log.MustChildLoggerWithAttributes("operator", operatorName, "controller", controllerName) - return &LifecycleManager{ - log: log, - client: client, - subroutines: subroutines, - operatorName: operatorName, - controllerName: controllerName, - spreadReconciles: false, - } -} +type PrepareContextFunc func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, errors.OperatorError) -func (l *LifecycleManager) Reconcile(ctx context.Context, req ctrl.Request, instance RuntimeObject) (ctrl.Result, error) { - ctx, span := otel.Tracer(l.operatorName).Start(ctx, fmt.Sprintf("%s.Reconcile", l.controllerName)) +func Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.RuntimeObject, cl client.Client, l Lifecycle) (ctrl.Result, error) { + ctx, span := otel.Tracer(l.Config().OperatorName).Start(ctx, fmt.Sprintf("%s.Reconcile", l.Config().ControllerName)) defer span.End() result := ctrl.Result{} reconcileId := uuid.New().String() - log := l.log.MustChildLoggerWithAttributes("name", req.Name, "namespace", req.Namespace, "reconcile_id", reconcileId) + log := l.Log().MustChildLoggerWithAttributes("name", req.Name, "namespace", req.Namespace, "reconcile_id", reconcileId) sentryTags := sentry.Tags{"namespace": req.Namespace, "name": req.Name} ctx = logger.SetLoggerInContext(ctx, log) ctx = sentry.ContextWithSentryTags(ctx, sentryTags) log.Info().Msg("start reconcile") - generationChanged := true - err := l.client.Get(ctx, req.NamespacedName, instance) + err := cl.Get(ctx, req.NamespacedName, instance) if err != nil { if kerrors.IsNotFound(err) { log.Info().Msg("instance not found. It was likely deleted") return ctrl.Result{}, nil } - return l.handleClientError("failed to retrieve instance", log, err, generationChanged, sentryTags) + return HandleClientError("failed to retrieve instance", log, err, true, sentryTags) } originalCopy := instance.DeepCopyObject() inDeletion := instance.GetDeletionTimestamp() != nil + generationChanged := true - if l.spreadReconciles && instance.GetDeletionTimestamp().IsZero() { - instanceStatusObj := MustToRuntimeObjectSpreadReconcileStatusInterface(instance, log) + if l.Spreader() != nil && instance.GetDeletionTimestamp().IsZero() { + instanceStatusObj := l.Spreader().MustToRuntimeObjectSpreadReconcileStatusInterface(instance, log) generationChanged = instance.GetGeneration() != instanceStatusObj.GetObservedGeneration() isAfterNextReconcileTime := v1.Now().UTC().After(instanceStatusObj.GetNextReconcileTime().UTC()) - refreshRequested := slices.Contains(maps.Keys(instance.GetLabels()), SpreadReconcileRefreshLabel) + refreshRequested := slices.Contains(maps.Keys(instance.GetLabels()), spread.ReconcileRefreshLabel) reconcileRequired := generationChanged || isAfterNextReconcileTime || refreshRequested if !reconcileRequired { log.Info().Msg("skipping reconciliation, spread reconcile is active. No processing needed") - return onNextReconcile(instanceStatusObj, log) + return l.Spreader().OnNextReconcile(instanceStatusObj, log) } } // Manage Finalizers - ferr := l.addFinalizersIfNeeded(ctx, instance) + ferr := AddFinalizersIfNeeded(ctx, cl, instance, l.Subroutines(), l.Config().ReadOnly) if ferr != nil { return ctrl.Result{}, ferr } - var conditions []v1.Condition - if l.manageConditions { - conditions = MustToRuntimeObjectConditionsInterface(instance, log).GetConditions() - setInstanceConditionUnknownIfNotSet(&conditions) + var condArr []v1.Condition + if l.ConditionsManager() != nil { + condArr = l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).GetConditions() + l.ConditionsManager().SetInstanceConditionUnknownIfNotSet(&condArr) } - if l.prepareContextFunc != nil { - localCtx, oErr := l.prepareContextFunc(ctx, instance) + if l.PrepareContextFunc() != nil { + localCtx, oErr := l.PrepareContextFunc()(ctx, instance) if oErr != nil { - return l.handleOperatorError(ctx, oErr, "failed to prepare context", generationChanged) + return HandleOperatorError(ctx, oErr, "failed to prepare context", generationChanged, l.Log()) } ctx = localCtx } // In case of deletion execute the finalize subroutines in the reverse order as subroutine processing - subroutines := make([]Subroutine, len(l.subroutines)) - copy(subroutines, l.subroutines) + subroutines := make([]subroutine.Subroutine, len(l.Subroutines())) + copy(subroutines, l.Subroutines()) if inDeletion { slices.Reverse(subroutines) } // Continue with reconciliation - for _, subroutine := range subroutines { - if l.manageConditions { - setSubroutineConditionToUnknownIfNotSet(&conditions, subroutine, inDeletion, log) + for _, s := range subroutines { + if l.ConditionsManager() != nil { + l.ConditionsManager().SetSubroutineConditionToUnknownIfNotSet(&condArr, s, inDeletion, log) } - // Set current conditions before reconciling the subroutine - if l.manageConditions { - MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(conditions) + // Set current condArr before reconciling the s + if l.ConditionsManager() != nil { + l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(condArr) } - subResult, retry, err := l.reconcileSubroutine(ctx, instance, subroutine, log, generationChanged, sentryTags) - // Update conditions with any changes the subroutine did - if l.manageConditions { - conditions = MustToRuntimeObjectConditionsInterface(instance, log).GetConditions() + subResult, retry, err := reconcileSubroutine(ctx, instance, s, cl, l, log, generationChanged, sentryTags) + // Update condArr with any changes the s did + if l.ConditionsManager() != nil { + condArr = l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).GetConditions() } if err != nil { - if l.manageConditions { - setSubroutineCondition(&conditions, subroutine, result, err, inDeletion, log) - setInstanceConditionReady(&conditions, v1.ConditionFalse) - MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(conditions) + if l.ConditionsManager() != nil { + l.ConditionsManager().SetSubroutineCondition(&condArr, s, result, err, inDeletion, log) + l.ConditionsManager().SetInstanceConditionReady(&condArr, v1.ConditionFalse) + l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(condArr) } if !retry { - l.markResourceAsFinal(instance, log, conditions, v1.ConditionFalse) + MarkResourceAsFinal(instance, log, condArr, v1.ConditionFalse, l) } - if !l.readOnly { - _ = updateStatus(ctx, l.client, originalCopy, instance, log, generationChanged, sentryTags) + if !l.Config().ReadOnly { + _ = updateStatus(ctx, cl, originalCopy, instance, log, generationChanged, sentryTags) } if !retry { return ctrl.Result{}, nil @@ -168,40 +147,40 @@ func (l *LifecycleManager) Reconcile(ctx context.Context, req ctrl.Request, inst result.RequeueAfter = subResult.RequeueAfter } } - if l.manageConditions { + if l.ConditionsManager() != nil { if subResult.RequeueAfter == 0 { - setSubroutineCondition(&conditions, subroutine, subResult, err, inDeletion, log) + l.ConditionsManager().SetSubroutineCondition(&condArr, s, subResult, err, inDeletion, log) } } } if result.RequeueAfter == 0 { // Reconciliation was successful - l.markResourceAsFinal(instance, log, conditions, v1.ConditionTrue) + MarkResourceAsFinal(instance, log, condArr, v1.ConditionTrue, l) } else { - if l.manageConditions { - setInstanceConditionReady(&conditions, v1.ConditionFalse) + if l.ConditionsManager() != nil { + l.ConditionsManager().SetInstanceConditionReady(&condArr, v1.ConditionFalse) } } - if l.manageConditions { - MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(conditions) + if l.ConditionsManager() != nil { + l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(condArr) } - if !l.readOnly { - err = updateStatus(ctx, l.client, originalCopy, instance, log, generationChanged, sentryTags) + if !l.Config().ReadOnly { + err = updateStatus(ctx, cl, originalCopy, instance, log, generationChanged, sentryTags) if err != nil { return result, err } } - if l.spreadReconciles && instance.GetDeletionTimestamp().IsZero() { + if l.Spreader() != nil && instance.GetDeletionTimestamp().IsZero() { original := instance.DeepCopyObject().(client.Object) - removed := removeRefreshLabelIfExists(instance) + removed := l.Spreader().RemoveRefreshLabelIfExists(instance) if removed { - updateErr := l.client.Patch(ctx, instance, client.MergeFrom(original)) + updateErr := cl.Patch(ctx, instance, client.MergeFrom(original)) if updateErr != nil { - return l.handleClientError("failed to update instance", log, err, generationChanged, sentryTags) + return HandleClientError("failed to update instance", log, err, generationChanged, sentryTags) } } } @@ -210,35 +189,78 @@ func (l *LifecycleManager) Reconcile(ctx context.Context, req ctrl.Request, inst return result, nil } -func (l *LifecycleManager) markResourceAsFinal(instance RuntimeObject, log *logger.Logger, conditions []v1.Condition, status v1.ConditionStatus) { - if l.spreadReconciles && instance.GetDeletionTimestamp().IsZero() { - instanceStatusObj := MustToRuntimeObjectSpreadReconcileStatusInterface(instance, log) - setNextReconcileTime(instanceStatusObj, log) - updateObservedGeneration(instanceStatusObj, log) +func reconcileSubroutine(ctx context.Context, instance runtimeobject.RuntimeObject, subroutine subroutine.Subroutine, cl client.Client, l Lifecycle, log *logger.Logger, generationChanged bool, sentryTags map[string]string) (ctrl.Result, bool, error) { + subroutineLogger := log.ChildLogger("subroutine", subroutine.GetName()) + ctx = logger.SetLoggerInContext(ctx, subroutineLogger) + subroutineLogger.Debug().Msg("start subroutine") + + ctx, span := otel.Tracer(l.Config().OperatorName).Start(ctx, fmt.Sprintf("%s.reconcileSubroutine.%s", l.Config().ControllerName, subroutine.GetName())) + defer span.End() + var result ctrl.Result + var err errors.OperatorError + if instance.GetDeletionTimestamp() != nil { + if containsFinalizer(instance, subroutine.Finalizers()) { + subroutineLogger.Debug().Msg("finalizing instance") + result, err = subroutine.Finalize(ctx, instance) + subroutineLogger.Debug().Any("result", result).Msg("finalized instance") + if err == nil { + // Remove finalizers unless requeue is requested + err = removeFinalizerIfNeeded(ctx, instance, subroutine, result, l.Config().ReadOnly, cl) + } + } + } else { + subroutineLogger.Debug().Msg("processing instance") + result, err = subroutine.Process(ctx, instance) + subroutineLogger.Debug().Any("result", result).Msg("processed instance") } - if l.manageConditions { - setInstanceConditionReady(&conditions, status) + if err != nil { + if generationChanged && err.Sentry() { + sentry.CaptureError(err.Err(), sentryTags) + } + subroutineLogger.Error().Err(err.Err()).Bool("retry", err.Retry()).Msg("subroutine ended with error") + return result, err.Retry(), err.Err() } + + subroutineLogger.Debug().Msg("end subroutine") + return result, false, nil } -func (l *LifecycleManager) validateInterfaces(instance RuntimeObject, log *logger.Logger) error { - if l.spreadReconciles { - _, err := toRuntimeObjectSpreadReconcileStatusInterface(instance, log) - if err != nil { - return err +func containsFinalizer(o client.Object, subroutineFinalizers []string) bool { + for _, subroutineFinalizer := range subroutineFinalizers { + if controllerutil.ContainsFinalizer(o, subroutineFinalizer) { + return true } } - if l.manageConditions { - _, err := toRuntimeObjectConditionsInterface(instance, log) - if err != nil { - return err + return false +} + +func removeFinalizerIfNeeded(ctx context.Context, instance runtimeobject.RuntimeObject, subroutine subroutine.Subroutine, result ctrl.Result, readonly bool, cl client.Client) errors.OperatorError { + if readonly { + return nil + } + + if result.RequeueAfter == 0 { + update := false + original := instance.DeepCopyObject().(client.Object) + for _, f := range subroutine.Finalizers() { + needsUpdate := controllerutil.RemoveFinalizer(instance, f) + if needsUpdate { + update = true + } + } + if update { + err := cl.Patch(ctx, instance, client.MergeFrom(original)) + if err != nil { + return errors.NewOperatorError(errors.Wrap(err, "failed to update instance"), true, false) + } } } + return nil } -func updateStatus(ctx context.Context, cl client.Client, original runtime.Object, current RuntimeObject, log *logger.Logger, generationChanged bool, sentryTags sentry.Tags) error { +func updateStatus(ctx context.Context, cl client.Client, original runtime.Object, current runtimeobject.RuntimeObject, log *logger.Logger, generationChanged bool, sentryTags sentry.Tags) error { currentUn, err := runtime.DefaultUnstructuredConverter.ToUnstructured(current) if err != nil { return err @@ -286,20 +308,7 @@ func updateStatus(ctx context.Context, cl client.Client, original runtime.Object return nil } -func (l *LifecycleManager) handleOperatorError(ctx context.Context, operatorError errors.OperatorError, msg string, generationChanged bool) (ctrl.Result, error) { - l.log.Error().Bool("retry", operatorError.Retry()).Bool("sentry", operatorError.Sentry()).Err(operatorError.Err()).Msg(msg) - if generationChanged && operatorError.Sentry() { - sentry.CaptureError(operatorError.Err(), sentry.GetSentryTagsFromContext(ctx)) - } - - if operatorError.Retry() { - return ctrl.Result{}, operatorError.Err() - } - - return ctrl.Result{}, nil -} - -func (l *LifecycleManager) handleClientError(msg string, log *logger.Logger, err error, generationChanged bool, sentryTags sentry.Tags) (ctrl.Result, error) { +func HandleClientError(msg string, log *logger.Logger, err error, generationChanged bool, sentryTags sentry.Tags) (ctrl.Result, error) { log.Error().Err(err).Msg(msg) if generationChanged { sentry.CaptureError(err, sentryTags) @@ -308,54 +317,20 @@ func (l *LifecycleManager) handleClientError(msg string, log *logger.Logger, err return ctrl.Result{}, err } -func containsFinalizer(o client.Object, subroutineFinalizers []string) bool { - for _, subroutineFinalizer := range subroutineFinalizers { - if controllerutil.ContainsFinalizer(o, subroutineFinalizer) { - return true - } - } - return false -} - -func (l *LifecycleManager) reconcileSubroutine(ctx context.Context, instance RuntimeObject, subroutine Subroutine, log *logger.Logger, generationChanged bool, sentryTags map[string]string) (ctrl.Result, bool, error) { - subroutineLogger := log.ChildLogger("subroutine", subroutine.GetName()) - ctx = logger.SetLoggerInContext(ctx, subroutineLogger) - subroutineLogger.Debug().Msg("start subroutine") - - ctx, span := otel.Tracer(l.operatorName).Start(ctx, fmt.Sprintf("%s.reconcileSubroutine.%s", l.controllerName, subroutine.GetName())) - defer span.End() - var result ctrl.Result - var err errors.OperatorError - if instance.GetDeletionTimestamp() != nil { - if containsFinalizer(instance, subroutine.Finalizers()) { - subroutineLogger.Debug().Msg("finalizing instance") - result, err = subroutine.Finalize(ctx, instance) - subroutineLogger.Debug().Any("result", result).Msg("finalized instance") - if err == nil { - // Remove finalizers unless requeue is requested - err = l.removeFinalizerIfNeeded(ctx, instance, subroutine, result) - } - } - } else { - subroutineLogger.Debug().Msg("processing instance") - result, err = subroutine.Process(ctx, instance) - subroutineLogger.Debug().Any("result", result).Msg("processed instance") +func MarkResourceAsFinal(instance runtimeobject.RuntimeObject, log *logger.Logger, conditions []v1.Condition, status v1.ConditionStatus, l Lifecycle) { + if l.Spreader() != nil && instance.GetDeletionTimestamp().IsZero() { + instanceStatusObj := l.Spreader().MustToRuntimeObjectSpreadReconcileStatusInterface(instance, log) + l.Spreader().SetNextReconcileTime(instanceStatusObj, log) + l.Spreader().UpdateObservedGeneration(instanceStatusObj, log) } - if err != nil { - if generationChanged && err.Sentry() { - sentry.CaptureError(err.Err(), sentryTags) - } - subroutineLogger.Error().Err(err.Err()).Bool("retry", err.Retry()).Msg("subroutine ended with error") - return result, err.Retry(), err.Err() + if l.ConditionsManager() != nil { + l.ConditionsManager().SetInstanceConditionReady(&conditions, status) } - - subroutineLogger.Debug().Msg("end subroutine") - return result, false, nil } -func (l *LifecycleManager) addFinalizersIfNeeded(ctx context.Context, instance RuntimeObject) error { - if l.readOnly { +func AddFinalizersIfNeeded(ctx context.Context, cl client.Client, instance runtimeobject.RuntimeObject, subroutines []subroutine.Subroutine, readonly bool) error { + if readonly { return nil } @@ -365,16 +340,16 @@ func (l *LifecycleManager) addFinalizersIfNeeded(ctx context.Context, instance R update := false original := instance.DeepCopyObject().(client.Object) - for _, subroutine := range l.subroutines { - if len(subroutine.Finalizers()) > 0 { - needsUpdate := l.addFinalizerIfNeeded(instance, subroutine) + for _, s := range subroutines { + if len(s.Finalizers()) > 0 { + needsUpdate := AddFinalizerIfNeeded(instance, s) if needsUpdate { update = true } } } if update { - err := l.client.Patch(ctx, instance, client.MergeFrom(original)) + err := cl.Patch(ctx, instance, client.MergeFrom(original)) if err != nil { return err } @@ -382,7 +357,7 @@ func (l *LifecycleManager) addFinalizersIfNeeded(ctx context.Context, instance R return nil } -func (l *LifecycleManager) addFinalizerIfNeeded(instance RuntimeObject, subroutine Subroutine) bool { +func AddFinalizerIfNeeded(instance runtimeobject.RuntimeObject, subroutine subroutine.Subroutine) bool { update := false for _, f := range subroutine.Finalizers() { needsUpdate := controllerutil.AddFinalizer(instance, f) @@ -393,70 +368,15 @@ func (l *LifecycleManager) addFinalizerIfNeeded(instance RuntimeObject, subrouti return update } -func (l *LifecycleManager) removeFinalizerIfNeeded(ctx context.Context, instance RuntimeObject, subroutine Subroutine, result ctrl.Result) errors.OperatorError { - if l.readOnly { - return nil - } - - if result.RequeueAfter == 0 { - update := false - original := instance.DeepCopyObject().(client.Object) - for _, f := range subroutine.Finalizers() { - needsUpdate := controllerutil.RemoveFinalizer(instance, f) - if needsUpdate { - update = true - } - } - if update { - err := l.client.Patch(ctx, instance, client.MergeFrom(original)) - if err != nil { - return errors.NewOperatorError(errors.Wrap(err, "failed to update instance"), true, false) - } - } - } - - return nil -} - -func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance RuntimeObject, debugLabelValue string, log *logger.Logger, eventPredicates ...predicate.Predicate) (*builder.Builder, error) { - if err := l.validateInterfaces(instance, log); err != nil { - return nil, err - } - - if (l.manageConditions || l.spreadReconciles) && l.readOnly { - return nil, fmt.Errorf("cannot use conditions or spread reconciles in read-only mode") +func HandleOperatorError(ctx context.Context, operatorError errors.OperatorError, msg string, generationChanged bool, log *logger.Logger) (ctrl.Result, error) { + log.Error().Bool("retry", operatorError.Retry()).Bool("sentry", operatorError.Sentry()).Err(operatorError.Err()).Msg(msg) + if generationChanged && operatorError.Sentry() { + sentry.CaptureError(operatorError.Err(), sentry.GetSentryTagsFromContext(ctx)) } - eventPredicates = append([]predicate.Predicate{filter.DebugResourcesBehaviourPredicate(debugLabelValue)}, eventPredicates...) - return ctrl.NewControllerManagedBy(mgr). - Named(reconcilerName). - For(instance). - WithOptions(controller.Options{MaxConcurrentReconciles: maxReconciles}). - WithEventFilter(predicate.And(eventPredicates...)), nil -} - -func (l *LifecycleManager) SetupWithManager(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance RuntimeObject, debugLabelValue string, r reconcile.Reconciler, log *logger.Logger, eventPredicates ...predicate.Predicate) error { - bldr, err := l.SetupWithManagerBuilder(mgr, maxReconciles, reconcilerName, instance, debugLabelValue, log, eventPredicates...) - if err != nil { - return err + if operatorError.Retry() { + return ctrl.Result{}, operatorError.Err() } - return bldr.Complete(r) -} - -type PrepareContextFunc func(ctx context.Context, instance RuntimeObject) (context.Context, errors.OperatorError) - -// WithPrepareContextFunc allows to set a function that prepares the context before each reconciliation -// This can be used to add additional information to the context that is needed by the subroutines -// You need to return a new context and an OperatorError in case of an error -func (l *LifecycleManager) WithPrepareContextFunc(prepareFunction PrepareContextFunc) *LifecycleManager { - l.prepareContextFunc = prepareFunction - return l -} - -// WithReadOnly allows to set the controller to read-only mode -// In read-only mode, the controller will not update the status of the instance -func (l *LifecycleManager) WithReadOnly() *LifecycleManager { - l.readOnly = true - return l + return ctrl.Result{}, nil } diff --git a/controller/lifecycle/lifecycle_test.go b/controller/lifecycle/lifecycle_test.go index 1c23558..076d1ae 100644 --- a/controller/lifecycle/lifecycle_test.go +++ b/controller/lifecycle/lifecycle_test.go @@ -2,1349 +2,18 @@ package lifecycle import ( "context" - goerrors "errors" - "fmt" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/rest" - controllerruntime "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/manager" - - operrors "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/controller/lifecycle/mocks" + pmtesting "github.com/platform-mesh/golang-commons/controller/lifecycle/testing" "github.com/platform-mesh/golang-commons/controller/testSupport" "github.com/platform-mesh/golang-commons/logger" - "github.com/platform-mesh/golang-commons/logger/testlogger" - "github.com/platform-mesh/golang-commons/sentry" ) -func TestLifecycle(t *testing.T) { - namespace := "bar" - name := "foo" - request := controllerruntime.Request{ - NamespacedName: types.NamespacedName{ - Namespace: namespace, - Name: name, - }, - } - testApiObject := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - ctx := context.Background() - - t.Run("Lifecycle with a not found object", func(t *testing.T) { - // Arrange - fakeClient := testSupport.CreateFakeClient(t, &testSupport.TestApiObject{}) - - mgr, log := createLifecycleManager([]Subroutine{}, fakeClient) - - // Act - result, err := mgr.Reconcile(ctx, request, &testSupport.TestApiObject{}) - - // Assert - assert.NoError(t, err) - assert.NotNil(t, result) - logMessages, err := log.GetLogMessages() - assert.NoError(t, err) - assert.Equal(t, len(logMessages), 2) - assert.Equal(t, logMessages[0].Message, "start reconcile") - assert.Contains(t, logMessages[1].Message, "instance not found") - }) - - t.Run("Lifecycle with a finalizer - add finalizer", func(t *testing.T) { - // Arrange - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - finalizerSubroutine{ - client: fakeClient, - }, - }, fakeClient) - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, 1, len(instance.Finalizers)) - }) - - t.Run("Lifecycle with a finalizer - finalization", func(t *testing.T) { - // Arrange - now := &metav1.Time{Time: time.Now()} - finalizers := []string{subroutineFinalizer} - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - finalizerSubroutine{ - client: fakeClient, - }, - }, fakeClient) - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, 0, len(instance.Finalizers)) - }) - - t.Run("Lifecycle with a finalizer - finalization(requeue)", func(t *testing.T) { - // Arrange - now := &metav1.Time{Time: time.Now()} - finalizers := []string{subroutineFinalizer} - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - finalizerSubroutine{ - client: fakeClient, - requeueAfter: 1 * time.Second, - }, - }, fakeClient) - - // Act - res, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, 1, len(instance.Finalizers)) - assert.Equal(t, time.Duration(1*time.Second), res.RequeueAfter) - }) - - t.Run("Lifecycle with a finalizer - finalization(requeueAfter)", func(t *testing.T) { - // Arrange - now := &metav1.Time{Time: time.Now()} - finalizers := []string{subroutineFinalizer} - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - finalizerSubroutine{ - client: fakeClient, - requeueAfter: 2 * time.Second, - }, - }, fakeClient) - - // Act - res, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, 1, len(instance.Finalizers)) - assert.Equal(t, 2*time.Second, res.RequeueAfter) - }) - - t.Run("Lifecycle with a finalizer - skip finalization if the finalizer is not in there", func(t *testing.T) { - // Arrange - now := &metav1.Time{Time: time.Now()} - finalizers := []string{"other-finalizer"} - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - finalizerSubroutine{ - client: fakeClient, - }, - }, fakeClient) - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, 1, len(instance.Finalizers)) - }) - t.Run("Lifecycle with a finalizer - failing finalization subroutine", func(t *testing.T) { - // Arrange - now := &metav1.Time{Time: time.Now()} - finalizers := []string{subroutineFinalizer} - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - finalizerSubroutine{ - client: fakeClient, - err: fmt.Errorf("some error"), - }, - }, fakeClient) - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - assert.Equal(t, 1, len(instance.Finalizers)) - }) - - t.Run("Lifecycle without changing status", func(t *testing.T) { - // Arrange - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Status: testSupport.TestStatus{Some: "string"}, - } - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, log := createLifecycleManager([]Subroutine{}, fakeClient) - - // Act - result, err := mgr.Reconcile(ctx, request, instance) - - // Assert - assert.NoError(t, err) - assert.NotNil(t, result) - logMessages, err := log.GetLogMessages() - assert.NoError(t, err) - assert.Equal(t, len(logMessages), 3) - assert.Equal(t, logMessages[0].Message, "start reconcile") - assert.Equal(t, logMessages[1].Message, "skipping status update, since they are equal") - assert.Equal(t, logMessages[2].Message, "end reconcile") - }) - - t.Run("Lifecycle with changing status", func(t *testing.T) { - // Arrange - instance := &testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Status: testSupport.TestStatus{Some: "string"}, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, log := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }, - }, fakeClient) - - // Act - result, err := mgr.Reconcile(ctx, request, instance) - - // Assert - assert.NoError(t, err) - assert.NotNil(t, result) - logMessages, err := log.GetLogMessages() - assert.NoError(t, err) - assert.Equal(t, len(logMessages), 7) - assert.Equal(t, logMessages[0].Message, "start reconcile") - assert.Equal(t, logMessages[1].Message, "start subroutine") - assert.Equal(t, logMessages[2].Message, "processing instance") - assert.Equal(t, logMessages[3].Message, "processed instance") - assert.Equal(t, logMessages[4].Message, "end subroutine") - - serverObject := &testSupport.TestApiObject{} - err = fakeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, serverObject) - assert.NoError(t, err) - assert.Equal(t, serverObject.Status.Some, "other string") - }) - - t.Run("Lifecycle with spread reconciles", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }, - }, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, instance.Generation, instance.Status.ObservedGeneration) - }) - - t.Run("Lifecycle with spread reconciles on deleted object", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 2, - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - Finalizers: []string{changeStatusSubroutineFinalizer}, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 2, - NextReconcileTime: metav1.Time{Time: time.Now().Add(2 * time.Hour)}, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }, - }, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - assert.NoError(t, err) - assert.Len(t, instance.Finalizers, 0) - - }) - - t.Run("Lifecycle with spread reconciles skips if the generation is the same", func(t *testing.T) { - // Arrange - nextReconcileTime := metav1.NewTime(time.Now().Add(1 * time.Hour)) - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 1, - NextReconcileTime: nextReconcileTime, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: false, RequeAfter: false}}, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - result, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, int64(1), instance.Status.ObservedGeneration) - assert.GreaterOrEqual(t, 12*time.Hour, result.RequeueAfter) - }) - - t.Run("Lifecycle with spread reconciles and processing fails (no-retry)", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: false, RequeAfter: false}}, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, int64(1), instance.Status.ObservedGeneration) - }) - - t.Run("Lifecycle with spread reconciles and processing fails (retry)", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - assert.Equal(t, int64(0), instance.Status.ObservedGeneration) - }) - - t.Run("Lifecycle with spread reconciles and processing needs requeue", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - assert.Equal(t, int64(0), instance.Status.ObservedGeneration) - }) - - t.Run("Lifecycle with spread reconciles and processing needs requeueAfter", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: false, RequeAfter: true}}, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, int64(0), instance.Status.ObservedGeneration) - }) - - t.Run("Lifecycle with spread not implementing the interface", func(t *testing.T) { - // Arrange - instance := ¬ImplementingSpreadReconciles{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }, - }, fakeClient) - mgr.WithSpreadingReconciles() - - // Act - assert.Panics(t, func() { - _, _ = mgr.Reconcile(ctx, request, instance) - }) - }) - - t.Run("Should setup with manager", func(t *testing.T) { - // Arrange - instance := &testSupport.TestApiObject{} - fakeClient := testSupport.CreateFakeClient(t, instance) - log, err := logger.New(logger.DefaultConfig()) - assert.NoError(t, err) - m, err := manager.New(&rest.Config{}, manager.Options{ - Scheme: fakeClient.Scheme(), - }) - assert.NoError(t, err) - - lm, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: false, RequeAfter: true}}, fakeClient) - tr := &testReconciler{ - lifecycleManager: lm, - } - - // Act - err = lm.SetupWithManager(m, 0, "testReconciler", instance, "test", tr, log) - - // Assert - assert.NoError(t, err) - }) - - t.Run("Should setup with manager not implementing interface", func(t *testing.T) { - // Arrange - instance := ¬ImplementingSpreadReconciles{} - fakeClient := testSupport.CreateFakeClient(t, instance) - log, err := logger.New(logger.DefaultConfig()) - assert.NoError(t, err) - m, err := manager.New(&rest.Config{}, manager.Options{ - Scheme: fakeClient.Scheme(), - }) - assert.NoError(t, err) - - lm, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: false, RequeAfter: true}}, fakeClient) - lm.WithSpreadingReconciles() - tr := &testReconciler{ - lifecycleManager: lm, - } - - // Act - err = lm.SetupWithManager(m, 0, "testReconciler", instance, "test", tr, log) - - // Assert - assert.Error(t, err) - }) - - t.Run("Lifecycle with spread reconciles and refresh label", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - Labels: map[string]string{SpreadReconcileRefreshLabel: "true"}, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 1, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - lm, _ := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }, - }, fakeClient) - lm.WithSpreadingReconciles() - - // Act - _, err := lm.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Equal(t, int64(1), instance.Status.ObservedGeneration) - - serverObject := &implementingSpreadReconciles{} - err = fakeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, serverObject) - assert.NoError(t, err) - assert.Equal(t, serverObject.Status.Some, "other string") - _, ok := serverObject.Labels[SpreadReconcileRefreshLabel] - assert.False(t, ok) - }) - - t.Run("Should handle a client error", func(t *testing.T) { - // Arrange - lm, log := createLifecycleManager([]Subroutine{}, nil) - - testErr := fmt.Errorf("test error") - - // Act - result, err := lm.handleClientError("test", log.Logger, testErr, true, sentry.Tags{}) - - // Assert - assert.Error(t, err) - assert.Equal(t, testErr, err) - assert.Equal(t, controllerruntime.Result{}, result) - }) - - t.Run("Lifecycle with manage conditions reconciles w/o subroutines", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Len(t, instance.Status.Conditions, 1) - assert.Equal(t, instance.Status.Conditions[0].Type, ConditionReady) - assert.Equal(t, instance.Status.Conditions[0].Status, metav1.ConditionTrue) - assert.Equal(t, instance.Status.Conditions[0].Message, "The resource is ready") - }) - - t.Run("Lifecycle with manage conditions reconciles with subroutine", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{changeStatusSubroutine{ - client: fakeClient, - }}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - require.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "changeStatus_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) - }) - - t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{addConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - require.Len(t, instance.Status.Conditions, 3) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) - assert.Equal(t, "test", instance.Status.Conditions[2].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) - assert.Equal(t, "test", instance.Status.Conditions[2].Message) - - }) - - t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{addConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - require.Len(t, instance.Status.Conditions, 3) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) - assert.Equal(t, "test", instance.Status.Conditions[2].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) - assert.Equal(t, "test", instance.Status.Conditions[2].Message) - - }) - - t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition with preexisting conditions (update)", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Conditions: []metav1.Condition{ - { - Type: "test", - Status: metav1.ConditionFalse, - Reason: "test", - Message: "test", - }, - }, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{addConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - require.Len(t, instance.Status.Conditions, 3) - assert.Equal(t, "test", instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "test", instance.Status.Conditions[0].Message) - assert.Equal(t, ConditionReady, instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) - assert.Equal(t, "The resource is ready", instance.Status.Conditions[1].Message) - assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[2].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) - assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[2].Message) - - }) - - t.Run("Lifecycle with manage conditions reconciles with subroutine that adds a condition with preexisting conditions", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Conditions: []metav1.Condition{ - { - Type: ConditionReady, - Status: metav1.ConditionTrue, - Message: "The resource is ready!!", - Reason: ConditionReady, - }, - }, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{addConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - require.Len(t, instance.Status.Conditions, 3) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "addCondition_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) - assert.Equal(t, "test", instance.Status.Conditions[2].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[2].Status) - assert.Equal(t, "test", instance.Status.Conditions[2].Message) - - }) - - t.Run("Lifecycle w/o manage conditions reconciles with subroutine that adds a condition", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{addConditionSubroutine{Ready: metav1.ConditionTrue}}, fakeClient) - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - require.Len(t, instance.Status.Conditions, 1) - assert.Equal(t, "test", instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "test", instance.Status.Conditions[0].Message) - - }) - - t.Run("Lifecycle with manage conditions reconciles with subroutine failing Status update", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "changeStatus_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine is complete", instance.Status.Conditions[1].Message) - }) - - t.Run("Lifecycle with manage conditions finalizes with multiple subroutines partially succeeding", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - Finalizers: []string{failureScenarioSubroutineFinalizer, changeStatusSubroutineFinalizer}, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - failureScenarioSubroutine{}, - changeStatusSubroutine{client: fakeClient}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - require.Len(t, instance.Status.Conditions, 3) - assert.Equal(t, "changeStatus_Finalize", instance.Status.Conditions[0].Type, "") - assert.Equal(t, metav1.ConditionTrue, instance.Status.Conditions[0].Status) - assert.Equal(t, "The subroutine finalization is complete", instance.Status.Conditions[0].Message) - assert.Equal(t, "failureScenarioSubroutine_Finalize", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine finalization has an error: failureScenarioSubroutine", instance.Status.Conditions[1].Message) - assert.Equal(t, ConditionReady, instance.Status.Conditions[2].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[2].Status) - assert.Equal(t, "The resource is not ready", instance.Status.Conditions[2].Message) - }) - - t.Run("Lifecycle with manage conditions reconciles with RequeAfter subroutine", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - failureScenarioSubroutine{Retry: false, RequeAfter: true}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is not ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "failureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionUnknown, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine is processing", instance.Status.Conditions[1].Message) - }) - - t.Run("Lifecycle with manage conditions reconciles with Error subroutine (no-retry)", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - failureScenarioSubroutine{Retry: false, RequeAfter: false}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is not ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "failureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine has an error: failureScenarioSubroutine", instance.Status.Conditions[1].Message) - }) - - t.Run("Lifecycle with manage conditions reconciles with Error subroutine (retry)", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{}, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - failureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - assert.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[0].Status) - assert.Equal(t, "The resource is not ready", instance.Status.Conditions[0].Message) - assert.Equal(t, "failureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, metav1.ConditionFalse, instance.Status.Conditions[1].Status) - assert.Equal(t, "The subroutine has an error: failureScenarioSubroutine", instance.Status.Conditions[1].Message) - }) - - t.Run("Lifecycle with manage conditions not implementing the interface", func(t *testing.T) { - // Arrange - instance := ¬ImplementingSpreadReconciles{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{ - changeStatusSubroutine{ - client: fakeClient, - }, - }, fakeClient) - mgr.WithConditionManagement() - - // Act - // So the validation is already happening in SetupWithManager. So we can panic in the reconcile. - assert.Panics(t, func() { - _, _ = mgr.Reconcile(ctx, request, instance) - }) - }) - - t.Run("Lifecycle with manage conditions failing finalize", func(t *testing.T) { - // Arrange - instance := &implementConditions{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - Finalizers: []string{failureScenarioSubroutineFinalizer}, - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{}}, fakeClient) - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - assert.Equal(t, "failureScenarioSubroutine", err.Error()) - }) - - t.Run("Lifecycle with spread reconciles and manage conditions and processing fails (retry)", func(t *testing.T) { - // Arrange - instance := &implementConditionsAndSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: true, RequeAfter: false}}, fakeClient) - mgr.WithSpreadingReconciles() - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.Error(t, err) - assert.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[0].Status)) - assert.Equal(t, "failureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[1].Status)) - assert.Equal(t, int64(0), instance.Status.ObservedGeneration) - }) - - t.Run("Lifecycle with spread reconciles and manage conditions and processing fails (no-retry)", func(t *testing.T) { - // Arrange - instance := &implementConditionsAndSpreadReconciles{ - testSupport.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: testSupport.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := testSupport.CreateFakeClient(t, instance) - - mgr, _ := createLifecycleManager([]Subroutine{failureScenarioSubroutine{Retry: false, RequeAfter: false}}, fakeClient) - mgr.WithSpreadingReconciles() - mgr.WithConditionManagement() - - // Act - _, err := mgr.Reconcile(ctx, request, instance) - - assert.NoError(t, err) - assert.Len(t, instance.Status.Conditions, 2) - assert.Equal(t, ConditionReady, instance.Status.Conditions[0].Type) - assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[0].Status)) - assert.Equal(t, "failureScenarioSubroutine_Ready", instance.Status.Conditions[1].Type) - assert.Equal(t, string(v1.ConditionFalse), string(instance.Status.Conditions[1].Status)) - assert.Equal(t, int64(1), instance.Status.ObservedGeneration) - }) - - t.Run("Test Lifecycle setupWithManager /w conditions and expecting no error", func(t *testing.T) { - // Arrange - instance := &implementConditions{} - fakeClient := testSupport.CreateFakeClient(t, instance) - - m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) - assert.NoError(t, err) - - lm, log := createLifecycleManager([]Subroutine{}, fakeClient) - lm = lm.WithConditionManagement() - tr := &testReconciler{lifecycleManager: lm} - - // Act - err = lm.SetupWithManager(m, 0, "testReconciler1", instance, "test", tr, log.Logger) - - // Assert - assert.NoError(t, err) - }) - - t.Run("Test Lifecycle setupWithManager /w conditions and expecting error", func(t *testing.T) { - // Arrange - instance := ¬ImplementingSpreadReconciles{} - fakeClient := testSupport.CreateFakeClient(t, instance) - - m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) - assert.NoError(t, err) - - lm, log := createLifecycleManager([]Subroutine{}, fakeClient) - lm = lm.WithConditionManagement() - tr := &testReconciler{lifecycleManager: lm} - - // Act - err = lm.SetupWithManager(m, 0, "testReconciler2", instance, "test", tr, log.Logger) - - // Assert - assert.Error(t, err) - }) - - t.Run("Test Lifecycle setupWithManager /w spread and expecting no error", func(t *testing.T) { - // Arrange - instance := &implementingSpreadReconciles{} - fakeClient := testSupport.CreateFakeClient(t, instance) - - m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) - assert.NoError(t, err) - - lm, log := createLifecycleManager([]Subroutine{}, fakeClient) - lm = lm.WithSpreadingReconciles() - tr := &testReconciler{lifecycleManager: lm} - - // Act - err = lm.SetupWithManager(m, 0, "testReconciler3", instance, "test", tr, log.Logger) - - // Assert - assert.NoError(t, err) - }) - - t.Run("Test Lifecycle setupWithManager /w spread and expecting a error", func(t *testing.T) { - // Arrange - instance := ¬ImplementingSpreadReconciles{} - fakeClient := testSupport.CreateFakeClient(t, instance) - - m, err := manager.New(&rest.Config{}, manager.Options{Scheme: fakeClient.Scheme()}) - assert.NoError(t, err) - - lm, log := createLifecycleManager([]Subroutine{}, fakeClient) - lm = lm.WithSpreadingReconciles() - tr := &testReconciler{lifecycleManager: lm} - - // Act - err = lm.SetupWithManager(m, 0, "testReconciler", instance, "test", tr, log.Logger) - - // Assert - assert.Error(t, err) - }) - - errorMessage := "oh nose" - t.Run("handleOperatorError", func(t *testing.T) { - t.Run("Should handle an operator error with retry and sentry", func(t *testing.T) { - // Arrange - instance := &implementConditions{} - fakeClient := testSupport.CreateFakeClient(t, instance) - - lm, log := createLifecycleManager([]Subroutine{}, fakeClient) - ctx = sentry.ContextWithSentryTags(ctx, map[string]string{}) - - // Act - result, err := lm.handleOperatorError(ctx, operrors.NewOperatorError(goerrors.New(errorMessage), true, true), "handle op error", true) - - // Assert - assert.Error(t, err) - assert.NotNil(t, result) - assert.Equal(t, errorMessage, err.Error()) - - errorMessages, err := log.GetErrorMessages() - assert.NoError(t, err) - assert.Equal(t, errorMessage, *errorMessages[0].Error) - }) - - t.Run("Should handle an operator error without retry", func(t *testing.T) { - // Arrange - instance := &implementConditions{} - fakeClient := testSupport.CreateFakeClient(t, instance) - - lm, log := createLifecycleManager([]Subroutine{}, fakeClient) - - // Act - result, err := lm.handleOperatorError(ctx, operrors.NewOperatorError(goerrors.New(errorMessage), false, false), "handle op error", true) - - // Assert - assert.Nil(t, err) - assert.NotNil(t, result) - - errorMessages, err := log.GetErrorMessages() - assert.NoError(t, err) - assert.Equal(t, errorMessage, *errorMessages[0].Error) - }) - }) - - t.Run("Prepare Context", func(t *testing.T) { - t.Run("Sets a context that can be used in the subroutine", func(t *testing.T) { - // Arrange - ctx := context.Background() - - fakeClient := testSupport.CreateFakeClient(t, testApiObject) - - lm, _ := createLifecycleManager([]Subroutine{contextValueSubroutine{}}, fakeClient) - lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance RuntimeObject) (context.Context, operrors.OperatorError) { - return context.WithValue(ctx, contextValueKey, "valueFromContext"), nil - }) - tr := &testReconciler{lifecycleManager: lm} - result, err := tr.Reconcile(ctx, controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}) - - // Then - assert.NotNil(t, ctx) - assert.NotNil(t, result) - assert.NoError(t, err) - - err = fakeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, testApiObject) - assert.NoError(t, err) - assert.Equal(t, "valueFromContext", testApiObject.Status.Some) - }) - - t.Run("Handles the errors correctly", func(t *testing.T) { - // Arrange - ctx := context.Background() - - fakeClient := testSupport.CreateFakeClient(t, testApiObject) - - lm, _ := createLifecycleManager([]Subroutine{contextValueSubroutine{}}, fakeClient) - lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance RuntimeObject) (context.Context, operrors.OperatorError) { - return nil, operrors.NewOperatorError(goerrors.New(errorMessage), true, false) - }) - tr := &testReconciler{lifecycleManager: lm} - result, err := tr.Reconcile(ctx, controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}) - - // Then - assert.NotNil(t, ctx) - assert.NotNil(t, result) - assert.Error(t, err) - }) - }) -} - func TestUpdateStatus(t *testing.T) { clientMock := new(mocks.Client) subresourceClient := new(mocks.SubResourceClient) @@ -1355,8 +24,8 @@ func TestUpdateStatus(t *testing.T) { assert.NoError(t, err) t.Run("Test UpdateStatus with no changes", func(t *testing.T) { - original := &implementingSpreadReconciles{ - testSupport.TestApiObject{ + original := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ Status: testSupport.TestStatus{ Some: "string", }, @@ -1370,14 +39,14 @@ func TestUpdateStatus(t *testing.T) { }) t.Run("Test UpdateStatus with update error", func(t *testing.T) { - original := &implementingSpreadReconciles{ - testSupport.TestApiObject{ + original := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ Status: testSupport.TestStatus{ Some: "string", }, }} - current := &implementingSpreadReconciles{ - testSupport.TestApiObject{ + current := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: testSupport.TestApiObject{ Status: testSupport.TestStatus{ Some: "string1", }, @@ -1397,7 +66,7 @@ func TestUpdateStatus(t *testing.T) { t.Run("Test UpdateStatus with no status object (original)", func(t *testing.T) { original := &testSupport.TestNoStatusApiObject{} - current := &implementConditions{} + current := &pmtesting.ImplementConditions{} // When err := updateStatus(context.Background(), clientMock, original, current, log, true, nil) @@ -1406,7 +75,7 @@ func TestUpdateStatus(t *testing.T) { assert.Equal(t, "status field not found in current object", err.Error()) }) t.Run("Test UpdateStatus with no status object (current)", func(t *testing.T) { - original := &implementConditions{} + original := &pmtesting.ImplementConditions{} current := &testSupport.TestNoStatusApiObject{} // When err := updateStatus(context.Background(), clientMock, original, current, log, true, nil) @@ -1416,17 +85,3 @@ func TestUpdateStatus(t *testing.T) { assert.Equal(t, "status field not found in current object", err.Error()) }) } - -type testReconciler struct { - lifecycleManager *LifecycleManager -} - -func (r *testReconciler) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) { - return r.lifecycleManager.Reconcile(ctx, req, &testSupport.TestApiObject{}) -} - -func createLifecycleManager(subroutines []Subroutine, c client.Client) (*LifecycleManager, *testlogger.TestLogger) { - log := testlogger.New() - mgr := NewLifecycleManager(log.Logger, "test-operator", "test-controller", c, subroutines) - return mgr, log -} diff --git a/controller/lifecycle/runtimeobject/runtimeobject.go b/controller/lifecycle/runtimeobject/runtimeobject.go new file mode 100644 index 0000000..a9c1c9c --- /dev/null +++ b/controller/lifecycle/runtimeobject/runtimeobject.go @@ -0,0 +1,11 @@ +package runtimeobject + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type RuntimeObject interface { + runtime.Object + v1.Object +} diff --git a/controller/lifecycle/spread.go b/controller/lifecycle/spread/spread.go similarity index 64% rename from controller/lifecycle/spread.go rename to controller/lifecycle/spread/spread.go index 19ad737..87e5f44 100644 --- a/controller/lifecycle/spread.go +++ b/controller/lifecycle/spread/spread.go @@ -1,4 +1,4 @@ -package lifecycle +package spread import ( "fmt" @@ -8,16 +8,18 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" ) -const SpreadReconcileRefreshLabel = "platform-mesh.io/refresh-reconcile" +const ReconcileRefreshLabel = "platform-mesh.io/refresh-reconcile" -// WithSpreadingReconciles sets the LifecycleManager to spread out the reconciles -func (l *LifecycleManager) WithSpreadingReconciles() *LifecycleManager { - l.spreadReconciles = true - return l +type Spreader struct { +} + +func NewSpreader() *Spreader { + return &Spreader{} } type RuntimeObjectSpreadReconcileStatus interface { @@ -44,15 +46,14 @@ func getNextReconcileTime(maxReconcileTime time.Duration) time.Duration { return time.Duration(jitter+int64(minTime)) * time.Minute } -// onNextReconcile is a helper function to set the next reconcile time and return the requeueAfter time -func onNextReconcile(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) (ctrl.Result, error) { +func (s *Spreader) OnNextReconcile(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) (ctrl.Result, error) { requeueAfter := time.Until(instanceStatusObj.GetNextReconcileTime().UTC()) log.Debug().Int64("minutes-till-next-execution", int64(requeueAfter.Minutes())).Msg("Completed reconciliation, no processing needed") return ctrl.Result{RequeueAfter: requeueAfter}, nil } -// setNextReconcileTime calculates and sets the next reconcile time for the instance -func setNextReconcileTime(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) { +// SetNextReconcileTime calculates and sets the next reconcile time for the instance +func (s *Spreader) SetNextReconcileTime(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) { var border = defaultMaxReconcileDuration if in, ok := instanceStatusObj.(GenerateNextReconcileTimer); ok { @@ -65,29 +66,29 @@ func setNextReconcileTime(instanceStatusObj RuntimeObjectSpreadReconcileStatus, instanceStatusObj.SetNextReconcileTime(v1.NewTime(time.Now().Add(nextReconcileTime))) } -// updateObservedGeneration updates the observed generation of the instance struct -func updateObservedGeneration(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) { +// UpdateObservedGeneration updates the observed generation of the instance struct +func (s *Spreader) UpdateObservedGeneration(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) { log.Debug().Int64("observed-generation", instanceStatusObj.GetObservedGeneration()).Int64("generation", instanceStatusObj.GetGeneration()).Msg("Updating observed generation") instanceStatusObj.SetObservedGeneration(instanceStatusObj.GetGeneration()) } -func removeRefreshLabelIfExists(instance RuntimeObject) bool { +func (s *Spreader) RemoveRefreshLabelIfExists(instance runtimeobject.RuntimeObject) bool { keyCount := len(instance.GetLabels()) - delete(instance.GetLabels(), SpreadReconcileRefreshLabel) + delete(instance.GetLabels(), ReconcileRefreshLabel) return keyCount != len(instance.GetLabels()) } -func toRuntimeObjectSpreadReconcileStatusInterface(instance RuntimeObject, log *logger.Logger) (RuntimeObjectSpreadReconcileStatus, error) { +func (s *Spreader) ToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (RuntimeObjectSpreadReconcileStatus, error) { if obj, ok := instance.(RuntimeObjectSpreadReconcileStatus); ok { return obj, nil } - err := fmt.Errorf("spreadReconciles is enabled, but instance does not implement RuntimeObjectSpreadReconcileStatus interface. This is a programming error") + err := fmt.Errorf("SpreadReconciles is enabled, but instance does not implement RuntimeObjectSpreadReconcileStatus interface. This is a programming error") log.Error().Err(err).Msg("Failed to cast instance to RuntimeObjectSpreadReconcileStatus") sentry.CaptureError(err, nil) return nil, err } -func MustToRuntimeObjectSpreadReconcileStatusInterface(instance RuntimeObject, log *logger.Logger) RuntimeObjectSpreadReconcileStatus { - obj, err := toRuntimeObjectSpreadReconcileStatusInterface(instance, log) +func (s *Spreader) MustToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) RuntimeObjectSpreadReconcileStatus { + obj, err := s.ToRuntimeObjectSpreadReconcileStatusInterface(instance, log) if err == nil { return obj } diff --git a/controller/lifecycle/spread_test.go b/controller/lifecycle/spread/spread_test.go similarity index 71% rename from controller/lifecycle/spread_test.go rename to controller/lifecycle/spread/spread_test.go index 309f724..0ad5122 100644 --- a/controller/lifecycle/spread_test.go +++ b/controller/lifecycle/spread/spread_test.go @@ -1,4 +1,4 @@ -package lifecycle +package spread import ( "testing" @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/mock" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + pmtesting "github.com/platform-mesh/golang-commons/controller/lifecycle/testing" "github.com/platform-mesh/golang-commons/controller/testSupport" "github.com/platform-mesh/golang-commons/logger/testlogger" ) @@ -36,10 +37,11 @@ func TestOnNextReconcile(t *testing.T) { instanceStatusObj := testSupport.TestStatus{ NextReconcileTime: v1.NewTime(nextReconcile), } - apiObject := &implementingSpreadReconciles{testSupport.TestApiObject{Status: instanceStatusObj}} + s := NewSpreader() + apiObject := &pmtesting.ImplementingSpreadReconciles{TestApiObject: testSupport.TestApiObject{Status: instanceStatusObj}} tl := testlogger.New() - requeueAfter, err := onNextReconcile(apiObject, tl.Logger) + requeueAfter, err := s.OnNextReconcile(apiObject, tl.Logger) if err != nil { t.Errorf("Expected no error, but got %v", err) } @@ -55,7 +57,7 @@ func TestOnNextReconcile(t *testing.T) { type testInstance struct { mock.Mock - *implementingSpreadReconciles + *pmtesting.ImplementingSpreadReconciles } func (t *testInstance) GenerateNextReconcileTime() time.Duration { @@ -65,21 +67,22 @@ func (t *testInstance) GenerateNextReconcileTime() time.Duration { func TestGenerateNextReconcileTimer(t *testing.T) { instance := &testInstance{ - implementingSpreadReconciles: &implementingSpreadReconciles{testSupport.TestApiObject{}}, + ImplementingSpreadReconciles: &pmtesting.ImplementingSpreadReconciles{}, } - + s := NewSpreader() instance.On("GenerateNextReconcileTime").Return(10 * time.Minute) - setNextReconcileTime(instance, testlogger.New().Logger) + s.SetNextReconcileTime(instance, testlogger.New().Logger) assert.True(t, instance.AssertCalled(t, "GenerateNextReconcileTime")) } func TestUpdateObservedGeneration(t *testing.T) { + s := NewSpreader() instanceStatusObj := testSupport.TestStatus{ ObservedGeneration: 0, } - apiObject := &implementingSpreadReconciles{testSupport.TestApiObject{ + apiObject := &pmtesting.ImplementingSpreadReconciles{TestApiObject: testSupport.TestApiObject{ Status: instanceStatusObj, ObjectMeta: v1.ObjectMeta{ Generation: 1, @@ -87,7 +90,7 @@ func TestUpdateObservedGeneration(t *testing.T) { }, } tl := testlogger.New() - updateObservedGeneration(apiObject, tl.Logger) + s.UpdateObservedGeneration(apiObject, tl.Logger) assert.Equal(t, apiObject.GetObservedGeneration(), apiObject.GetGeneration()) messages, err := tl.GetLogMessages() @@ -96,37 +99,40 @@ func TestUpdateObservedGeneration(t *testing.T) { } func TestRemoveRefreshLabel(t *testing.T) { + s := NewSpreader() apiObject := &testSupport.TestApiObject{ ObjectMeta: v1.ObjectMeta{ - Labels: map[string]string{SpreadReconcileRefreshLabel: ""}, + Labels: map[string]string{ReconcileRefreshLabel: ""}, }, } - removeRefreshLabelIfExists(apiObject) + s.RemoveRefreshLabelIfExists(apiObject) - _, ok := apiObject.GetLabels()[SpreadReconcileRefreshLabel] + _, ok := apiObject.GetLabels()[ReconcileRefreshLabel] assert.False(t, ok) } func TestRemoveRefreshLabelFilledWithValue(t *testing.T) { + s := NewSpreader() apiObject := &testSupport.TestApiObject{ ObjectMeta: v1.ObjectMeta{ - Labels: map[string]string{SpreadReconcileRefreshLabel: "true"}, + Labels: map[string]string{ReconcileRefreshLabel: "true"}, }, } - removeRefreshLabelIfExists(apiObject) + s.RemoveRefreshLabelIfExists(apiObject) - _, ok := apiObject.GetLabels()[SpreadReconcileRefreshLabel] + _, ok := apiObject.GetLabels()[ReconcileRefreshLabel] assert.False(t, ok) } func TestRemoveRefreshLabelNoLabels(t *testing.T) { + s := NewSpreader() apiObject := &testSupport.TestApiObject{ ObjectMeta: v1.ObjectMeta{}, } - removeRefreshLabelIfExists(apiObject) + s.RemoveRefreshLabelIfExists(apiObject) - _, ok := apiObject.GetLabels()[SpreadReconcileRefreshLabel] + _, ok := apiObject.GetLabels()[ReconcileRefreshLabel] assert.False(t, ok) } diff --git a/controller/lifecycle/subroutine/subroutine.go b/controller/lifecycle/subroutine/subroutine.go new file mode 100644 index 0000000..54a22e7 --- /dev/null +++ b/controller/lifecycle/subroutine/subroutine.go @@ -0,0 +1,17 @@ +package subroutine + +import ( + "context" + + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/errors" +) + +type Subroutine interface { + Process(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) + Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, errors.OperatorError) + GetName() string + Finalizers() []string +} diff --git a/controller/lifecycle/testSupport_test.go b/controller/lifecycle/testSupport_test.go deleted file mode 100644 index 16623ed..0000000 --- a/controller/lifecycle/testSupport_test.go +++ /dev/null @@ -1,233 +0,0 @@ -package lifecycle - -import ( - "context" - "fmt" - "time" - - "k8s.io/apimachinery/pkg/api/meta" - - "github.com/platform-mesh/golang-commons/context/keys" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - controllerruntime "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/platform-mesh/golang-commons/controller/testSupport" - "github.com/platform-mesh/golang-commons/errors" -) - -const failureScenarioSubroutineFinalizer = "failuresubroutine" -const changeStatusSubroutineFinalizer = "changestatus" - -type implementConditions struct { - testSupport.TestApiObject `json:",inline"` -} - -func (m *implementConditions) GetConditions() []metav1.Condition { - return m.Status.Conditions -} - -func (m *implementConditions) SetConditions(conditions []metav1.Condition) { - m.Status.Conditions = conditions -} - -type implementingSpreadReconciles struct { - testSupport.TestApiObject `json:",inline"` -} - -func (m *implementingSpreadReconciles) GetGeneration() int64 { - return m.Generation -} - -func (m *implementingSpreadReconciles) GetObservedGeneration() int64 { - return m.Status.ObservedGeneration -} - -func (m *implementingSpreadReconciles) SetObservedGeneration(g int64) { - m.Status.ObservedGeneration = g -} - -func (m *implementingSpreadReconciles) GetNextReconcileTime() metav1.Time { - return m.Status.NextReconcileTime -} - -func (m *implementingSpreadReconciles) SetNextReconcileTime(time metav1.Time) { - m.Status.NextReconcileTime = time -} - -type notImplementingSpreadReconciles struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Status testSupport.TestStatus `json:"status,omitempty"` -} - -func (m *notImplementingSpreadReconciles) DeepCopyObject() runtime.Object { - if c := m.DeepCopy(); c != nil { - return c - } - return nil -} - -func (m *notImplementingSpreadReconciles) DeepCopy() *notImplementingSpreadReconciles { - if m == nil { - return nil - } - out := new(notImplementingSpreadReconciles) - m.DeepCopyInto(out) - return out -} -func (m *notImplementingSpreadReconciles) DeepCopyInto(out *notImplementingSpreadReconciles) { - *out = *m - out.TypeMeta = m.TypeMeta - m.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Status = m.Status -} - -type changeStatusSubroutine struct { - client client.Client -} - -func (c changeStatusSubroutine) Process(_ context.Context, runtimeObj RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - if instance, ok := runtimeObj.(*testSupport.TestApiObject); ok { - instance.Status.Some = "other string" - } - if instance, ok := runtimeObj.(*implementingSpreadReconciles); ok { - instance.Status.Some = "other string" - } - - if instance, ok := runtimeObj.(*implementConditions); ok { - instance.Status.Some = "other string" - } - return controllerruntime.Result{}, nil -} - -func (c changeStatusSubroutine) Finalize(_ context.Context, _ RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - return controllerruntime.Result{}, nil -} - -func (c changeStatusSubroutine) GetName() string { - return "changeStatus" -} - -func (c changeStatusSubroutine) Finalizers() []string { - return []string{"changestatus"} -} - -type addConditionSubroutine struct { - Ready metav1.ConditionStatus -} - -func (c addConditionSubroutine) Process(_ context.Context, runtimeObj RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - if instance, ok := runtimeObj.(*implementConditions); ok { - instance.Status.Some = "other string" - meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ - Type: "test", - Status: c.Ready, - Reason: "test", - Message: "test", - }) - } - - return controllerruntime.Result{}, nil -} - -func (c addConditionSubroutine) Finalize(_ context.Context, _ RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - return controllerruntime.Result{}, nil -} - -func (c addConditionSubroutine) GetName() string { - return "addCondition" -} - -func (c addConditionSubroutine) Finalizers() []string { - return []string{} -} - -type failureScenarioSubroutine struct { - Retry bool - RequeAfter bool - FinalizeRetry bool - FinalizeRequeAfter bool -} - -func (f failureScenarioSubroutine) Process(_ context.Context, _ RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - if f.RequeAfter { - return controllerruntime.Result{RequeueAfter: 10 * time.Second}, nil - } - - return controllerruntime.Result{}, errors.NewOperatorError(fmt.Errorf("failureScenarioSubroutine"), f.Retry, false) -} - -func (f failureScenarioSubroutine) Finalize(_ context.Context, _ RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - if f.Retry { - return controllerruntime.Result{Requeue: true}, nil - } - - if f.RequeAfter { - return controllerruntime.Result{RequeueAfter: 10 * time.Second}, nil - } - - return controllerruntime.Result{}, errors.NewOperatorError(fmt.Errorf("failureScenarioSubroutine"), true, false) -} - -func (f failureScenarioSubroutine) Finalizers() []string { - return []string{failureScenarioSubroutineFinalizer} -} - -func (c failureScenarioSubroutine) GetName() string { - return "failureScenarioSubroutine" -} - -type implementConditionsAndSpreadReconciles struct { - testSupport.TestApiObject `json:",inline"` -} - -func (m *implementConditionsAndSpreadReconciles) GetConditions() []metav1.Condition { - return m.Status.Conditions -} -func (m *implementConditionsAndSpreadReconciles) SetConditions(conditions []metav1.Condition) { - m.Status.Conditions = conditions -} -func (m *implementConditionsAndSpreadReconciles) GetGeneration() int64 { - return m.Generation -} -func (m *implementConditionsAndSpreadReconciles) GetObservedGeneration() int64 { - return m.Status.ObservedGeneration -} -func (m *implementConditionsAndSpreadReconciles) SetObservedGeneration(g int64) { - m.Status.ObservedGeneration = g -} - -func (m *implementConditionsAndSpreadReconciles) GetNextReconcileTime() metav1.Time { - return m.Status.NextReconcileTime -} -func (m *implementConditionsAndSpreadReconciles) SetNextReconcileTime(time metav1.Time) { - m.Status.NextReconcileTime = time -} - -type contextValueSubroutine struct { -} - -const contextValueKey = keys.ContextKey("contextValueKey") - -func (f contextValueSubroutine) Process(ctx context.Context, r RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - if instance, ok := r.(*testSupport.TestApiObject); ok { - instance.Status.Some = ctx.Value(contextValueKey).(string) - } - return controllerruntime.Result{}, nil -} - -func (f contextValueSubroutine) Finalize(_ context.Context, _ RuntimeObject) (controllerruntime.Result, errors.OperatorError) { - return controllerruntime.Result{}, nil -} - -func (f contextValueSubroutine) Finalizers() []string { - return []string{} -} - -func (c contextValueSubroutine) GetName() string { - return "contextValueSubroutine" -} diff --git a/controller/lifecycle/testing/finalizerSubroutine.go b/controller/lifecycle/testing/finalizerSubroutine.go new file mode 100644 index 0000000..d150580 --- /dev/null +++ b/controller/lifecycle/testing/finalizerSubroutine.go @@ -0,0 +1,48 @@ +package testing + +import ( + "context" + "time" + + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/testSupport" + "github.com/platform-mesh/golang-commons/errors" +) + +const SubroutineFinalizer = "finalizer" + +type FinalizerSubroutine struct { + Client client.Client + Err error + RequeueAfter time.Duration +} + +func (c FinalizerSubroutine) Process(_ context.Context, runtimeObj runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + instance := runtimeObj.(*testSupport.TestApiObject) + instance.Status.Some = "other string" + return controllerruntime.Result{}, nil +} + +func (c FinalizerSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + if c.Err != nil { + return controllerruntime.Result{}, errors.NewOperatorError(c.Err, true, true) + } + if c.RequeueAfter > 0 { + return controllerruntime.Result{RequeueAfter: c.RequeueAfter}, nil + } + + return controllerruntime.Result{}, nil +} + +func (c FinalizerSubroutine) GetName() string { + return "changeStatus" +} + +func (c FinalizerSubroutine) Finalizers() []string { + return []string{ + SubroutineFinalizer, + } +} diff --git a/controller/lifecycle/testing/testSupport.go b/controller/lifecycle/testing/testSupport.go new file mode 100644 index 0000000..4f12b70 --- /dev/null +++ b/controller/lifecycle/testing/testSupport.go @@ -0,0 +1,230 @@ +package testing + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + + "github.com/platform-mesh/golang-commons/context/keys" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/platform-mesh/golang-commons/controller/testSupport" + "github.com/platform-mesh/golang-commons/errors" +) + +const FailureScenarioSubroutineFinalizer = "failuresubroutine" +const ChangeStatusSubroutineFinalizer = "changestatus" + +type ImplementConditions struct { + testSupport.TestApiObject `json:",inline"` +} + +func (m *ImplementConditions) GetConditions() []metav1.Condition { + return m.Status.Conditions +} + +func (m *ImplementConditions) SetConditions(conditions []metav1.Condition) { + m.Status.Conditions = conditions +} + +type ImplementingSpreadReconciles struct { + testSupport.TestApiObject `json:",inline"` +} + +func (m *ImplementingSpreadReconciles) GetGeneration() int64 { + return m.Generation +} + +func (m *ImplementingSpreadReconciles) GetObservedGeneration() int64 { + return m.Status.ObservedGeneration +} + +func (m *ImplementingSpreadReconciles) SetObservedGeneration(g int64) { + m.Status.ObservedGeneration = g +} + +func (m *ImplementingSpreadReconciles) GetNextReconcileTime() metav1.Time { + return m.Status.NextReconcileTime +} + +func (m *ImplementingSpreadReconciles) SetNextReconcileTime(time metav1.Time) { + m.Status.NextReconcileTime = time +} + +type NotImplementingSpreadReconciles struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Status testSupport.TestStatus `json:"status,omitempty"` +} + +func (m *NotImplementingSpreadReconciles) DeepCopyObject() runtime.Object { + if c := m.DeepCopy(); c != nil { + return c + } + return nil +} + +func (m *NotImplementingSpreadReconciles) DeepCopy() *NotImplementingSpreadReconciles { + if m == nil { + return nil + } + out := new(NotImplementingSpreadReconciles) + m.DeepCopyInto(out) + return out +} +func (m *NotImplementingSpreadReconciles) DeepCopyInto(out *NotImplementingSpreadReconciles) { + *out = *m + out.TypeMeta = m.TypeMeta + m.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Status = m.Status +} + +type ChangeStatusSubroutine struct { + Client client.Client +} + +func (c ChangeStatusSubroutine) Process(_ context.Context, runtimeObj runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + if instance, ok := runtimeObj.(*testSupport.TestApiObject); ok { + instance.Status.Some = "other string" + } + if instance, ok := runtimeObj.(*ImplementingSpreadReconciles); ok { + instance.Status.Some = "other string" + } + + if instance, ok := runtimeObj.(*ImplementConditions); ok { + instance.Status.Some = "other string" + } + return controllerruntime.Result{}, nil +} + +func (c ChangeStatusSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + return controllerruntime.Result{}, nil +} + +func (c ChangeStatusSubroutine) GetName() string { + return "changeStatus" +} + +func (c ChangeStatusSubroutine) Finalizers() []string { + return []string{"changestatus"} +} + +type AddConditionSubroutine struct { + Ready metav1.ConditionStatus +} + +func (c AddConditionSubroutine) Process(_ context.Context, runtimeObj runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + if instance, ok := runtimeObj.(*ImplementConditions); ok { + instance.Status.Some = "other string" + meta.SetStatusCondition(&instance.Status.Conditions, metav1.Condition{ + Type: "test", + Status: c.Ready, + Reason: "test", + Message: "test", + }) + } + + return controllerruntime.Result{}, nil +} + +func (c AddConditionSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + return controllerruntime.Result{}, nil +} + +func (c AddConditionSubroutine) GetName() string { + return "addCondition" +} + +func (c AddConditionSubroutine) Finalizers() []string { + return []string{} +} + +type FailureScenarioSubroutine struct { + Retry bool + RequeAfter bool + FinalizeRetry bool + FinalizeRequeAfter bool +} + +func (f FailureScenarioSubroutine) Process(_ context.Context, _ runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + if f.RequeAfter { + return controllerruntime.Result{RequeueAfter: 10 * time.Second}, nil + } + + return controllerruntime.Result{}, errors.NewOperatorError(fmt.Errorf("FailureScenarioSubroutine"), f.Retry, false) +} + +func (f FailureScenarioSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + if f.RequeAfter { + return controllerruntime.Result{RequeueAfter: 10 * time.Second}, nil + } + + return controllerruntime.Result{}, errors.NewOperatorError(fmt.Errorf("FailureScenarioSubroutine"), true, false) +} + +func (f FailureScenarioSubroutine) Finalizers() []string { + return []string{FailureScenarioSubroutineFinalizer} +} + +func (c FailureScenarioSubroutine) GetName() string { + return "FailureScenarioSubroutine" +} + +type ImplementConditionsAndSpreadReconciles struct { + testSupport.TestApiObject `json:",inline"` +} + +func (m *ImplementConditionsAndSpreadReconciles) GetConditions() []metav1.Condition { + return m.Status.Conditions +} +func (m *ImplementConditionsAndSpreadReconciles) SetConditions(conditions []metav1.Condition) { + m.Status.Conditions = conditions +} +func (m *ImplementConditionsAndSpreadReconciles) GetGeneration() int64 { + return m.Generation +} +func (m *ImplementConditionsAndSpreadReconciles) GetObservedGeneration() int64 { + return m.Status.ObservedGeneration +} +func (m *ImplementConditionsAndSpreadReconciles) SetObservedGeneration(g int64) { + m.Status.ObservedGeneration = g +} + +func (m *ImplementConditionsAndSpreadReconciles) GetNextReconcileTime() metav1.Time { + return m.Status.NextReconcileTime +} +func (m *ImplementConditionsAndSpreadReconciles) SetNextReconcileTime(time metav1.Time) { + m.Status.NextReconcileTime = time +} + +type ContextValueSubroutine struct { +} + +const ContextValueKey = keys.ContextKey("ContextValueKey") + +func (f ContextValueSubroutine) Process(ctx context.Context, r runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + if instance, ok := r.(*testSupport.TestApiObject); ok { + instance.Status.Some = ctx.Value(ContextValueKey).(string) + } + return controllerruntime.Result{}, nil +} + +func (f ContextValueSubroutine) Finalize(_ context.Context, _ runtimeobject.RuntimeObject) (controllerruntime.Result, errors.OperatorError) { + return controllerruntime.Result{}, nil +} + +func (f ContextValueSubroutine) Finalizers() []string { + return []string{} +} + +func (c ContextValueSubroutine) GetName() string { + return "ContextValueSubroutine" +} diff --git a/fga/store/store.go b/fga/store/store.go index af694f2..35dfc73 100644 --- a/fga/store/store.go +++ b/fga/store/store.go @@ -19,14 +19,24 @@ type FGAStoreHelper interface { } type FgaTenantStore struct { - cache *expirable.LRU[string, string] + cache *expirable.LRU[string, string] + storePrefix string } var _ FGAStoreHelper = (*FgaTenantStore)(nil) +// Deprecated: Use NewWithPrefix instead. func New() *FgaTenantStore { return &FgaTenantStore{ - cache: expirable.NewLRU[string, string](10, nil, 10*time.Minute), + cache: expirable.NewLRU[string, string](10, nil, 10*time.Minute), + storePrefix: "tenant-", + } +} + +func NewWithPrefix(prefix string) *FgaTenantStore { + return &FgaTenantStore{ + cache: expirable.NewLRU[string, string](10, nil, 10*time.Minute), + storePrefix: prefix, } } @@ -43,9 +53,10 @@ func (c *FgaTenantStore) GetStoreIDForTenant(ctx context.Context, conn openfgav1 return "", err } - idx := slices.IndexFunc(res.GetStores(), func(s *openfgav1.Store) bool { return s.Name == cacheKey }) + storeName := c.storePrefix + tenantID + idx := slices.IndexFunc(res.GetStores(), func(s *openfgav1.Store) bool { return s.Name == storeName }) if idx < 0 { - return "", fmt.Errorf("could not find store matching key %q", cacheKey) + return "", fmt.Errorf("could not find store matching key %q", storeName) } store := res.GetStores()[idx] diff --git a/fga/store/store_test.go b/fga/store/store_test.go index 381f676..769aff6 100644 --- a/fga/store/store_test.go +++ b/fga/store/store_test.go @@ -121,7 +121,7 @@ func TestGetModelIDForTenant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := &mocks.OpenFGAServiceClient{} - cachedStore := fgastore.New() + cachedStore := fgastore.NewWithPrefix("tenant-") tt.setupMock(client, cachedStore) modelID, err := cachedStore.GetModelIDForTenant(ctx, client, tenantID) @@ -159,9 +159,7 @@ func TestIsDuplicateWriteError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - - cachedStore := fgastore.New() - + cachedStore := fgastore.NewWithPrefix("tenant-") result := cachedStore.IsDuplicateWriteError(tt.err) assert.Equal(t, tt.expected, result) })