diff --git a/.testcoverage.yml b/.testcoverage.yml index cc2feee..372613f 100644 --- a/.testcoverage.yml +++ b/.testcoverage.yml @@ -2,5 +2,6 @@ exclude: paths: - ^controller/testSupport # exclude test support files - mocks # exclude generated mock files - - ^test/openfga - + - ^test/ + - ^logger/testlogger + diff --git a/config/config.go b/config/config.go index 78e9fda..5260f4b 100644 --- a/config/config.go +++ b/config/config.go @@ -212,6 +212,7 @@ func BindConfigToFlags(v *viper.Viper, cmd *cobra.Command, config any) error { return nil } +// unmarshalIntoStruct returns a function that unmarshal viper config into cfg and panics on error. func unmarshalIntoStruct(v *viper.Viper, cfg any) func() { return func() { if err := v.Unmarshal(cfg); err != nil { diff --git a/config/config_test.go b/config/config_test.go index 2586946..86812c3 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -168,3 +168,12 @@ func TestNewDefaultConfig(t *testing.T) { err = v.Unmarshal(&config.CommonServiceConfig{}) assert.NoError(t, err) } + +func TestGenerateFlagSetUnsupportedType(t *testing.T) { + type test struct { + UnsupportedField []string `mapstructure:"unsupported-field"` + } + testStruct := test{} + err := config.BindConfigToFlags(viper.New(), &cobra.Command{}, &testStruct) + assert.Error(t, err) +} diff --git a/controller/lifecycle/api/api.go b/controller/lifecycle/api/api.go index 32c6b52..7b51ed4 100644 --- a/controller/lifecycle/api/api.go +++ b/controller/lifecycle/api/api.go @@ -30,12 +30,10 @@ type Config struct { } type ConditionManager interface { - MustToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) RuntimeObjectConditions SetInstanceConditionUnknownIfNotSet(conditions *[]metav1.Condition) bool SetSubroutineConditionToUnknownIfNotSet(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, isFinalize bool, log *logger.Logger) bool SetSubroutineCondition(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, subroutineResult ctrl.Result, subroutineErr error, isFinalize bool, log *logger.Logger) bool SetInstanceConditionReady(conditions *[]metav1.Condition, status metav1.ConditionStatus) bool - ToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (RuntimeObjectConditions, error) } type RuntimeObjectConditions interface { @@ -44,9 +42,8 @@ type RuntimeObjectConditions interface { } type SpreadManager interface { - ToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (RuntimeObjectSpreadReconcileStatus, error) - MustToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) RuntimeObjectSpreadReconcileStatus - OnNextReconcile(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) (ctrl.Result, error) + ReconcileRequired(instance runtimeobject.RuntimeObject, log *logger.Logger) bool + OnNextReconcile(instance runtimeobject.RuntimeObject, log *logger.Logger) (ctrl.Result, error) RemoveRefreshLabelIfExists(instance runtimeobject.RuntimeObject) bool SetNextReconcileTime(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) UpdateObservedGeneration(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) diff --git a/controller/lifecycle/builder/builder.go b/controller/lifecycle/builder/builder.go new file mode 100644 index 0000000..53e574e --- /dev/null +++ b/controller/lifecycle/builder/builder.go @@ -0,0 +1,74 @@ +package builder + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "github.com/platform-mesh/golang-commons/controller/lifecycle/controllerruntime" + "github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + "github.com/platform-mesh/golang-commons/logger" +) + +type Builder struct { + operatorName string + controllerName string + withConditionManagement bool + withSpreadingReconciles bool + withReadOnly bool + subroutines []subroutine.Subroutine + log *logger.Logger +} + +func NewBuilder(operatorName, controllerName string, subroutines []subroutine.Subroutine, log *logger.Logger) *Builder { + return &Builder{ + operatorName: operatorName, + controllerName: controllerName, + log: log, + withConditionManagement: false, + subroutines: subroutines, + } +} + +func (b *Builder) WithConditionManagement() *Builder { + b.withConditionManagement = true + return b +} + +func (b *Builder) WithSpreadingReconciles() *Builder { + b.withSpreadingReconciles = true + return b +} + +func (b *Builder) WithReadOnly() *Builder { + b.withReadOnly = true + return b +} + +func (b *Builder) BuildControllerRuntime(cl client.Client) *controllerruntime.LifecycleManager { + lm := controllerruntime.NewLifecycleManager(b.subroutines, b.operatorName, b.controllerName, cl, b.log) + if b.withConditionManagement { + lm.WithConditionManagement() + } + if b.withSpreadingReconciles { + lm.WithSpreadingReconciles() + } + if b.withReadOnly { + lm.WithReadOnly() + } + return lm +} + +func (b *Builder) BuildMultiCluster(mgr mcmanager.Manager) *multicluster.LifecycleManager { + lm := multicluster.NewLifecycleManager(b.subroutines, b.operatorName, b.controllerName, mgr, b.log) + if b.withConditionManagement { + lm.WithConditionManagement() + } + if b.withSpreadingReconciles { + lm.WithSpreadingReconciles() + } + if b.withReadOnly { + lm.WithReadOnly() + } + return lm +} diff --git a/controller/lifecycle/builder/builder_test.go b/controller/lifecycle/builder/builder_test.go new file mode 100644 index 0000000..8de061e --- /dev/null +++ b/controller/lifecycle/builder/builder_test.go @@ -0,0 +1,110 @@ +package builder + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/client-go/rest" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" + "github.com/platform-mesh/golang-commons/logger" +) + +func TestNewBuilder_Defaults(t *testing.T) { + log := &logger.Logger{} + b := NewBuilder("op", "ctrl", nil, log) + if b.operatorName != "op" { + t.Errorf("expected operatorName 'op', got %s", b.operatorName) + } + if b.controllerName != "ctrl" { + t.Errorf("expected controllerName 'ctrl', got %s", b.controllerName) + } + if b.withConditionManagement { + t.Error("expected withConditionManagement to be false") + } + if b.withSpreadingReconciles { + t.Error("expected withSpreadingReconciles to be false") + } + if b.withReadOnly { + t.Error("expected withReadOnly to be false") + } + if b.log != log { + t.Error("expected log to be set") + } +} + +func TestBuilder_WithConditionManagement(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + b.WithConditionManagement() + if !b.withConditionManagement { + t.Error("WithConditionManagement should set withConditionManagement to true") + } +} + +func TestBuilder_WithSpreadingReconciles(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + b.WithSpreadingReconciles() + if !b.withSpreadingReconciles { + t.Error("WithSpreadingReconciles should set withSpreadingReconciles to true") + } +} + +func TestBuilder_WithReadOnly(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + b.WithReadOnly() + if !b.withReadOnly { + t.Error("WithReadOnly should set withReadOnly to true") + } +} + +func TestControllerRuntimeBuilder(t *testing.T) { + t.Run("Minimal setup", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + lm := b.BuildControllerRuntime(fakeClient) + assert.NotNil(t, lm) + }) + t.Run("All Options", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithConditionManagement().WithSpreadingReconciles() + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + lm := b.BuildControllerRuntime(fakeClient) + assert.NotNil(t, lm) + }) + t.Run("ReadOnly", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithReadOnly() + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + lm := b.BuildControllerRuntime(fakeClient) + assert.NotNil(t, lm) + }) +} + +func TestMulticontrollerRuntimeBuilder(t *testing.T) { + t.Run("Minimal setup", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}) + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + lm := b.BuildMultiCluster(mgr) + assert.NotNil(t, lm) + }) + t.Run("All Options", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithConditionManagement().WithSpreadingReconciles() + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + lm := b.BuildMultiCluster(mgr) + assert.NotNil(t, lm) + }) + t.Run("ReadOnly", func(t *testing.T) { + b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithReadOnly() + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + lm := b.BuildMultiCluster(mgr) + assert.NotNil(t, lm) + }) +} diff --git a/controller/lifecycle/conditions/conditions.go b/controller/lifecycle/conditions/conditions.go index 3f55ae3..3524d63 100644 --- a/controller/lifecycle/conditions/conditions.go +++ b/controller/lifecycle/conditions/conditions.go @@ -7,11 +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/api" - "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" ) const ( @@ -118,22 +115,3 @@ func (c *ConditionManager) SetSubroutineCondition(conditions *[]metav1.Condition } return changed } - -func (c *ConditionManager) ToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (api.RuntimeObjectConditions, error) { - if obj, ok := instance.(api.RuntimeObjectConditions); ok { - return obj, nil - } - 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 (c *ConditionManager) MustToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) api.RuntimeObjectConditions { - obj, err := c.ToRuntimeObjectConditionsInterface(instance, log) - if err == nil { - return obj - } - log.Panic().Err(err).Msg("instance does not implement RuntimeObjectConditions interface") - return nil -} diff --git a/controller/lifecycle/conditions/conditions_test.go b/controller/lifecycle/conditions/conditions_test.go index 40c2850..d7ff4e1 100644 --- a/controller/lifecycle/conditions/conditions_test.go +++ b/controller/lifecycle/conditions/conditions_test.go @@ -240,44 +240,3 @@ func TestSubroutineCondition(t *testing.T) { assert.Equal(t, metav1.ConditionFalse, condition[0].Status) }) } - -// Dummy types for testing interface conversion - -func TestToRuntimeObjectConditionsInterface(t *testing.T) { - log, err := logger.New(logger.DefaultConfig()) - require.NoError(t, err) - cm := NewConditionManager() - - t.Run("Implements interface", func(t *testing.T) { - obj := pmtesting.DummyRuntimeObjectWithConditions{} - res, err := cm.ToRuntimeObjectConditionsInterface(obj, log) - assert.NoError(t, err) - assert.NotNil(t, res) - }) - - t.Run("Does not implement interface", func(t *testing.T) { - obj := pmtesting.DummyRuntimeObject{} - res, err := cm.ToRuntimeObjectConditionsInterface(obj, log) - assert.Error(t, err) - assert.Nil(t, res) - }) -} - -func TestMustToRuntimeObjectConditionsInterface(t *testing.T) { - log, err := logger.New(logger.DefaultConfig()) - require.NoError(t, err) - cm := NewConditionManager() - - t.Run("Implements interface", func(t *testing.T) { - obj := pmtesting.DummyRuntimeObjectWithConditions{} - res := cm.MustToRuntimeObjectConditionsInterface(obj, log) - assert.NotNil(t, res) - }) - - t.Run("Does not implement interface panics", func(t *testing.T) { - obj := pmtesting.DummyRuntimeObject{} - assert.Panics(t, func() { - cm.MustToRuntimeObjectConditionsInterface(obj, log) - }) - }) -} diff --git a/controller/lifecycle/controllerruntime/lifecycle.go b/controller/lifecycle/controllerruntime/lifecycle.go index 48cd752..22261a8 100644 --- a/controller/lifecycle/controllerruntime/lifecycle.go +++ b/controller/lifecycle/controllerruntime/lifecycle.go @@ -31,7 +31,7 @@ type LifecycleManager struct { prepareContextFunc api.PrepareContextFunc } -func NewLifecycleManager(log *logger.Logger, operatorName string, controllerName string, client client.Client, subroutines []subroutine.Subroutine) *LifecycleManager { +func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, client client.Client, log *logger.Logger) *LifecycleManager { log = log.MustChildLoggerWithAttributes("operator", operatorName, "controller", controllerName) return &LifecycleManager{ log: log, @@ -63,37 +63,18 @@ func (l *LifecycleManager) ConditionsManager() api.ConditionManager { } return l.conditionsManager } - func (l *LifecycleManager) Spreader() api.SpreadManager { - // it is important to return nil unsted of a nil pointer to the interface to avoid misbehaving nil checks + // it is important to return nil instead of a nil pointer to the interface to avoid misbehaving nil checks if l.spreader == nil { return nil } 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) + return lifecycle.Reconcile(ctx, req.NamespacedName, 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 { + if err := lifecycle.ValidateInterfaces(instance, log, l); err != nil { return nil, err } @@ -108,7 +89,6 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil 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 { diff --git a/controller/lifecycle/controllerruntime/lifecycle_test.go b/controller/lifecycle/controllerruntime/lifecycle_test.go index 587d3ed..a974b5b 100644 --- a/controller/lifecycle/controllerruntime/lifecycle_test.go +++ b/controller/lifecycle/controllerruntime/lifecycle_test.go @@ -3,13 +3,9 @@ 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" @@ -17,1191 +13,42 @@ import ( "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/testSupport" - "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/golang-commons/errors" "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 := &pmtesting.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 := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) - - mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) - - // Act - result, err := mgr.Reconcile(ctx, request, &pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - DeletionTimestamp: now, - Finalizers: finalizers, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Status: pmtesting.TestStatus{Some: "string"}, - } - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Status: pmtesting.TestStatus{Some: "string"}, - } - - fakeClient := pmtesting.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 := &pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 2, - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - Finalizers: []string{pmtesting.ChangeStatusSubroutineFinalizer}, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 2, - NextReconcileTime: metav1.Time{Time: time.Now().Add(2 * time.Hour)}, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 1, - NextReconcileTime: nextReconcileTime, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - fakeClient := pmtesting.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: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - } - - fakeClient := pmtesting.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 := &pmtesting.TestApiObject{} - fakeClient := pmtesting.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 := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - Labels: map[string]string{spread.ReconcileRefreshLabel: "true"}, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 1, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Conditions: []metav1.Condition{ - { - Type: "test", - Status: metav1.ConditionFalse, - Reason: "test", - Message: "test", - }, - }, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Conditions: []metav1.Condition{ - { - Type: conditions.ConditionReady, - Status: metav1.ConditionTrue, - Message: "The resource is ready!!", - Reason: conditions.ConditionReady, - }, - }, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - Finalizers: []string{pmtesting.FailureScenarioSubroutineFinalizer, pmtesting.ChangeStatusSubroutineFinalizer}, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{}, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - Finalizers: []string{pmtesting.FailureScenarioSubroutineFinalizer}, - DeletionTimestamp: &metav1.Time{Time: time.Now()}, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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: pmtesting.TestApiObject{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Generation: 1, - }, - Status: pmtesting.TestStatus{ - Some: "string", - ObservedGeneration: 0, - }, - }, - } - - fakeClient := pmtesting.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) { + t.Run("Test Lifecycle setupWithManager /w spread and expecting no error", func(t *testing.T) { // Arrange - instance := &pmtesting.ImplementConditions{} + instance := &pmtesting.ImplementingSpreadReconciles{} fakeClient := pmtesting.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() + log := testlogger.New() + lm := NewLifecycleManager([]subroutine.Subroutine{}, "test-operator", "test-controller", fakeClient, log.Logger) + lm.WithSpreadingReconciles() tr := &testReconciler{lifecycleManager: lm} // Act - err = lm.SetupWithManager(m, 0, "testReconciler1", instance, "test", tr, log.Logger) + err = lm.SetupWithManager(m, 0, "testReconciler3", instance, "test", tr, log.Logger) // Assert assert.NoError(t, err) }) - - t.Run("Test Lifecycle setupWithManager /w conditions and expecting error", func(t *testing.T) { + t.Run("Test Lifecycle setupWithManager /w spread and expecting a error", func(t *testing.T) { // Arrange instance := &pmtesting.NotImplementingSpreadReconciles{} fakeClient := pmtesting.CreateFakeClient(t, instance) @@ -1210,17 +57,16 @@ func TestLifecycle(t *testing.T) { assert.NoError(t, err) lm, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) - lm = lm.WithConditionManagement() + lm.WithSpreadingReconciles() tr := &testReconciler{lifecycleManager: lm} // Act - err = lm.SetupWithManager(m, 0, "testReconciler2", instance, "test", tr, log.Logger) + err = lm.SetupWithManager(m, 0, "testReconciler", 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) { + t.Run("Test Lifecycle setupWithManager /w spread and expecting a error (invalid config)", func(t *testing.T) { // Arrange instance := &pmtesting.ImplementingSpreadReconciles{} fakeClient := pmtesting.CreateFakeClient(t, instance) @@ -1229,26 +75,8 @@ func TestLifecycle(t *testing.T) { 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 := pmtesting.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() + lm.WithSpreadingReconciles() + lm.WithReadOnly() tr := &testReconciler{lifecycleManager: lm} // Act @@ -1257,50 +85,7 @@ func TestLifecycle(t *testing.T) { // 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 := pmtesting.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 := pmtesting.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 @@ -1309,7 +94,7 @@ func TestLifecycle(t *testing.T) { fakeClient := pmtesting.CreateFakeClient(t, testApiObject) lm, _ := createLifecycleManager([]subroutine.Subroutine{pmtesting.ContextValueSubroutine{}}, fakeClient) - lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, operrors.OperatorError) { + lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, errors.OperatorError) { return context.WithValue(ctx, pmtesting.ContextValueKey, "valueFromContext"), nil }) tr := &testReconciler{lifecycleManager: lm} @@ -1332,8 +117,8 @@ func TestLifecycle(t *testing.T) { fakeClient := pmtesting.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) + lm = lm.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, errors.OperatorError) { + return nil, errors.NewOperatorError(goerrors.New(errorMessage), true, false) }) tr := &testReconciler{lifecycleManager: lm} result, err := tr.Reconcile(ctx, controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}) @@ -1344,19 +129,29 @@ func TestLifecycle(t *testing.T) { assert.Error(t, err) }) }) -} + t.Run("WthConditionManagement", func(t *testing.T) { + // Given + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) -// Test LifecycleManager.WithConditionManagement -func TestLifecycleManager_WithConditionManagement(t *testing.T) { - // Given - fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) - _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + // When + l := NewLifecycleManager([]subroutine.Subroutine{}, "test-operator", "test-controller", fakeClient, log.Logger).WithConditionManagement() - // When - l := NewLifecycleManager(log.Logger, "test-operator", "test-controller", fakeClient, []subroutine.Subroutine{}).WithConditionManagement() + // Then + assert.True(t, true, l.ConditionsManager() != nil) + }) + t.Run("WithReadOnly", func(t *testing.T) { + // Given + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + // When + l := NewLifecycleManager([]subroutine.Subroutine{}, "test-operator", "test-controller", fakeClient, log.Logger).WithReadOnly() + + // Then + assert.True(t, true, l.ConditionsManager() != nil) + }) - // Then - assert.True(t, true, l.ConditionsManager() != nil) } type testReconciler struct { @@ -1369,6 +164,6 @@ func (r *testReconciler) Reconcile(ctx context.Context, req controllerruntime.Re func createLifecycleManager(subroutines []subroutine.Subroutine, c client.Client) (*LifecycleManager, *testlogger.TestLogger) { log := testlogger.New() - mgr := NewLifecycleManager(log.Logger, "test-operator", "test-controller", c, subroutines) + mgr := NewLifecycleManager(subroutines, "test-operator", "test-controller", c, log.Logger) return mgr, log } diff --git a/controller/lifecycle/lifecycle.go b/controller/lifecycle/lifecycle.go index 35667f7..aa857bd 100644 --- a/controller/lifecycle/lifecycle.go +++ b/controller/lifecycle/lifecycle.go @@ -7,40 +7,41 @@ import ( "github.com/google/uuid" "go.opentelemetry.io/otel" - "golang.org/x/exp/maps" "k8s.io/apimachinery/pkg/api/equality" kerrors "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" "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/controller/lifecycle/util" "github.com/platform-mesh/golang-commons/errors" "github.com/platform-mesh/golang-commons/logger" "github.com/platform-mesh/golang-commons/sentry" ) -func Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.RuntimeObject, cl client.Client, l api.Lifecycle) (ctrl.Result, error) { +func Reconcile(ctx context.Context, nName types.NamespacedName, instance runtimeobject.RuntimeObject, cl client.Client, l api.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) - sentryTags := sentry.Tags{"namespace": req.Namespace, "name": req.Name} + log := l.Log().MustChildLoggerWithAttributes("name", nName.Name, "namespace", nName.Namespace, "reconcile_id", reconcileId) + sentryTags := sentry.Tags{"namespace": nName.Namespace, "name": nName.Name} ctx = logger.SetLoggerInContext(ctx, log) ctx = sentry.ContextWithSentryTags(ctx, sentryTags) log.Info().Msg("start reconcile") - err := cl.Get(ctx, req.NamespacedName, instance) + + err := cl.Get(ctx, nName, instance) if err != nil { if kerrors.IsNotFound(err) { log.Info().Msg("instance not found. It was likely deleted") @@ -54,15 +55,10 @@ func Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.Run generationChanged := true 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()), spread.ReconcileRefreshLabel) - - reconcileRequired := generationChanged || isAfterNextReconcileTime || refreshRequested + reconcileRequired := l.Spreader().ReconcileRequired(instance, log) if !reconcileRequired { log.Info().Msg("skipping reconciliation, spread reconcile is active. No processing needed") - return l.Spreader().OnNextReconcile(instanceStatusObj, log) + return l.Spreader().OnNextReconcile(instance, log) } } @@ -74,7 +70,7 @@ func Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.Run var condArr []v1.Condition if l.ConditionsManager() != nil { - condArr = l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).GetConditions() + condArr = util.MustToInterface[api.RuntimeObjectConditions](instance, log).GetConditions() l.ConditionsManager().SetInstanceConditionUnknownIfNotSet(&condArr) } @@ -101,18 +97,18 @@ func Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.Run // Set current condArr before reconciling the s if l.ConditionsManager() != nil { - l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(condArr) + util.MustToInterface[api.RuntimeObjectConditions](instance, log).SetConditions(condArr) } 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() + condArr = util.MustToInterface[api.RuntimeObjectConditions](instance, log).GetConditions() } if err != nil { 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) + util.MustToInterface[api.RuntimeObjectConditions](instance, log).SetConditions(condArr) } if !retry { MarkResourceAsFinal(instance, log, condArr, v1.ConditionFalse, l) @@ -147,7 +143,7 @@ func Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.Run } if l.ConditionsManager() != nil { - l.ConditionsManager().MustToRuntimeObjectConditionsInterface(instance, log).SetConditions(condArr) + util.MustToInterface[api.RuntimeObjectConditions](instance, log).SetConditions(condArr) } if !l.Config().ReadOnly { @@ -302,7 +298,7 @@ func HandleClientError(msg string, log *logger.Logger, err error, generationChan func MarkResourceAsFinal(instance runtimeobject.RuntimeObject, log *logger.Logger, conditions []v1.Condition, status v1.ConditionStatus, l api.Lifecycle) { if l.Spreader() != nil && instance.GetDeletionTimestamp().IsZero() { - instanceStatusObj := l.Spreader().MustToRuntimeObjectSpreadReconcileStatusInterface(instance, log) + instanceStatusObj := util.MustToInterface[api.RuntimeObjectSpreadReconcileStatus](instance, log) l.Spreader().SetNextReconcileTime(instanceStatusObj, log) l.Spreader().UpdateObservedGeneration(instanceStatusObj, log) } @@ -363,3 +359,19 @@ func HandleOperatorError(ctx context.Context, operatorError errors.OperatorError return ctrl.Result{}, nil } + +func ValidateInterfaces(instance runtimeobject.RuntimeObject, log *logger.Logger, l api.Lifecycle) error { + if l.Spreader() != nil { + _, err := util.ToInterface[api.RuntimeObjectSpreadReconcileStatus](instance, log) + if err != nil { + return err + } + } + if l.ConditionsManager() != nil { + _, err := util.ToInterface[api.RuntimeObjectConditions](instance, log) + if err != nil { + return err + } + } + return nil +} diff --git a/controller/lifecycle/lifecycle_test.go b/controller/lifecycle/lifecycle_test.go index 31ae5bb..6069541 100644 --- a/controller/lifecycle/lifecycle_test.go +++ b/controller/lifecycle/lifecycle_test.go @@ -2,17 +2,29 @@ package lifecycle import ( "context" + goerrors "errors" + "fmt" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/platform-mesh/golang-commons/controller/lifecycle/conditions" "github.com/platform-mesh/golang-commons/controller/lifecycle/mocks" + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" + operrors "github.com/platform-mesh/golang-commons/errors" "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) { @@ -24,33 +36,804 @@ func TestLifecycle(t *testing.T) { Name: name, }, } + testApiObject := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + ctx := context.Background() logcfg := logger.DefaultConfig() logcfg.NoJSON = true log, err := logger.New(logcfg) assert.NoError(t, err) - //testApiObject := &pmtesting.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 := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) - mgr := pmtesting.TestLifecycleManager{Logger: log} + mgr := &pmtesting.TestLifecycleManager{Logger: log} // Act - result, err := Reconcile(ctx, request, &pmtesting.TestApiObject{}, fakeClient, mgr) + result, err := Reconcile(ctx, request.NamespacedName, &pmtesting.TestApiObject{}, fakeClient, mgr) // Assert assert.NoError(t, err) assert.NotNil(t, result) assert.NoError(t, err) }) + t.Run("Lifecycle with a finalizer - add finalizer", func(t *testing.T) { + // Arrange + instance := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + }}, + } + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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 := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + }, + }} + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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 := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + RequeueAfter: 1 * time.Second, + }, + }} + + // Act + res, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + assert.NoError(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + assert.Equal(t, 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 := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + RequeueAfter: 2 * time.Second, + }, + }} + + // Act + res, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + 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 := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + }, + }} + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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 := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + DeletionTimestamp: now, + Finalizers: finalizers, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FinalizerSubroutine{ + Client: fakeClient, + Err: fmt.Errorf("some error"), + }, + }} + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + assert.Error(t, err) + assert.Equal(t, 1, len(instance.Finalizers)) + }) + t.Run("Lifecycle without changing status", func(t *testing.T) { + // Arrange + instance := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Status: pmtesting.TestStatus{Some: "string"}, + } + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{}} + + // Act + result, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + }) + t.Run("Lifecycle with changing status", func(t *testing.T) { + // Arrange + instance := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Status: pmtesting.TestStatus{Some: "string"}, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + + // Act + result, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + // Assert + assert.NoError(t, err) + assert.NotNil(t, result) + assert.NoError(t, err) + + serverObject := &pmtesting.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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + mgr.WithSpreadingReconciles() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 2, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{pmtesting.ChangeStatusSubroutineFinalizer}, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 2, + NextReconcileTime: metav1.Time{Time: time.Now().Add(2 * time.Hour)}, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + mgr.WithSpreadingReconciles() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 1, + NextReconcileTime: nextReconcileTime, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: false}, + }} + mgr.WithSpreadingReconciles() + + // Act + result, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{Retry: false, RequeAfter: false}, + }} + mgr.WithSpreadingReconciles() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{Retry: true, RequeAfter: false}, + }} + mgr.WithSpreadingReconciles() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: true}, + }} + mgr.WithSpreadingReconciles() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: true}, + }} + mgr.WithSpreadingReconciles() + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + mgr.WithSpreadingReconciles() + + // Act + assert.Panics(t, func() { + _, _ = Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + }) + }) + t.Run("Lifecycle with spread reconciles and refresh label", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + Labels: map[string]string{"platform-mesh.io/refresh-reconcile": "true"}, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 1, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + mgr.WithSpreadingReconciles() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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["platform-mesh.io/refresh-reconcile"] + assert.False(t, ok) + }) + t.Run("Should handle a client error", func(t *testing.T) { + // Arrange + testErr := fmt.Errorf("test error") + + // Act + result, err := HandleClientError("test", log, testErr, true, sentry.Tags{}) + + // Assert + assert.Error(t, err) + assert.Equal(t, testErr, err) + assert.Equal(t, ctrl.Result{}, result) + }) + t.Run("Lifecycle with manage conditions reconciles w/o subroutines", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{}, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{}} + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{}, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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 failing Status update", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{}, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ChangeStatusSubroutine{ + Client: fakeClient, + }, + }} + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + 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: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + DeletionTimestamp: &metav1.Time{Time: time.Now()}, + Finalizers: []string{pmtesting.FailureScenarioSubroutineFinalizer, pmtesting.ChangeStatusSubroutineFinalizer}, + }, + Status: pmtesting.TestStatus{}, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{}, + pmtesting.ChangeStatusSubroutine{Client: fakeClient}}} + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + assert.Error(t, err) + require.Len(t, instance.Status.Conditions, 3) + }) + t.Run("Lifecycle with manage conditions reconciles with Error subroutine (no-retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{}, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: false}, + }} + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 2) + }) + t.Run("Lifecycle with manage conditions reconciles with Error subroutine (retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditions{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{}, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{Retry: true, RequeAfter: false}, + }} + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + assert.Error(t, err) + assert.Len(t, instance.Status.Conditions, 2) + }) + t.Run("Lifecycle with spread reconciles and manage conditions and processing fails (no-retry)", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementConditionsAndSpreadReconciles{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Generation: 1, + }, + Status: pmtesting.TestStatus{ + Some: "string", + ObservedGeneration: 0, + }, + }, + } + + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.FailureScenarioSubroutine{RequeAfter: false}, + }} + mgr.WithSpreadingReconciles() + mgr.WithConditionManagement() + + // Act + _, err := Reconcile(ctx, request.NamespacedName, instance, fakeClient, mgr) + + assert.NoError(t, err) + assert.Len(t, instance.Status.Conditions, 2) + }) + errorMessage := "oh nose" + t.Run("Should handle an operator error without retry", func(t *testing.T) { + // Arrange + testLog := testlogger.New() + + // Act + result, err := HandleOperatorError(ctx, operrors.NewOperatorError(goerrors.New(errorMessage), false, false), "handle op error", true, testLog.Logger) + + // Assert + assert.Nil(t, err) + assert.NotNil(t, result) + + errorMessages, err := testLog.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 := pmtesting.CreateFakeClient(t, testApiObject) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ContextValueSubroutine{}, + }} + + mgr = mgr.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, operrors.OperatorError) { + return context.WithValue(ctx, pmtesting.ContextValueKey, "valueFromContext"), nil + }) + result, err := Reconcile(ctx, types.NamespacedName{Name: name, Namespace: namespace}, testApiObject, fakeClient, mgr) + + // 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 := pmtesting.CreateFakeClient(t, testApiObject) + + mgr := &pmtesting.TestLifecycleManager{Logger: log, ShouldReconcile: true, SubroutinesArr: []subroutine.Subroutine{ + pmtesting.ContextValueSubroutine{}, + }} + + mgr = mgr.WithPrepareContextFunc(func(ctx context.Context, instance runtimeobject.RuntimeObject) (context.Context, operrors.OperatorError) { + return nil, operrors.NewOperatorError(goerrors.New(errorMessage), true, false) + }) + result, err := Reconcile(ctx, types.NamespacedName{Name: name, Namespace: namespace}, testApiObject, fakeClient, mgr) + + // Then + assert.NotNil(t, ctx) + assert.NotNil(t, result) + assert.Error(t, err) + }) + }) } func TestUpdateStatus(t *testing.T) { @@ -124,3 +907,107 @@ func TestUpdateStatus(t *testing.T) { assert.Equal(t, "status field not found in current object", err.Error()) }) } + +func TestAddFinalizersIfNeeded(t *testing.T) { + instance := &pmtesting.TestApiObject{ObjectMeta: metav1.ObjectMeta{Name: "instance1"}} + fakeClient := pmtesting.CreateFakeClient(t, instance) + sub := pmtesting.FinalizerSubroutine{Client: fakeClient} + // Should add finalizer + err := AddFinalizersIfNeeded(context.Background(), fakeClient, instance, []subroutine.Subroutine{sub}, false) + assert.NoError(t, err) + assert.Contains(t, instance.Finalizers, pmtesting.SubroutineFinalizer) + + // Should not add if readonly + instance2 := &pmtesting.TestApiObject{} + err = AddFinalizersIfNeeded(context.Background(), fakeClient, instance2, []subroutine.Subroutine{sub}, true) + assert.NoError(t, err) + assert.NotContains(t, instance2.Finalizers, pmtesting.SubroutineFinalizer) + + // Should not add if deletion timestamp is set + now := metav1.Now() + instance3 := &pmtesting.TestApiObject{ObjectMeta: metav1.ObjectMeta{DeletionTimestamp: &now}} + err = AddFinalizersIfNeeded(context.Background(), fakeClient, instance3, []subroutine.Subroutine{sub}, false) + assert.NoError(t, err) +} + +func TestAddFinalizerIfNeeded(t *testing.T) { + instance := &pmtesting.TestApiObject{} + sub := pmtesting.FinalizerSubroutine{} + // Should add and return true + added := AddFinalizerIfNeeded(instance, sub) + assert.True(t, added) + // Should not add again + added = AddFinalizerIfNeeded(instance, sub) + assert.False(t, added) +} + +func TestRemoveFinalizerIfNeeded(t *testing.T) { + instance := &pmtesting.TestApiObject{ObjectMeta: metav1.ObjectMeta{Name: "instance1"}} + sub := pmtesting.FinalizerSubroutine{} + AddFinalizerIfNeeded(instance, sub) + fakeClient := pmtesting.CreateFakeClient(t, instance) + // Should remove finalizer if not readonly and RequeueAfter == 0 + res := ctrl.Result{} + err := removeFinalizerIfNeeded(context.Background(), instance, sub, res, false, fakeClient) + assert.Nil(t, err) + assert.NotContains(t, instance.Finalizers, pmtesting.SubroutineFinalizer) + + // Should not remove if readonly + AddFinalizerIfNeeded(instance, sub) + err = removeFinalizerIfNeeded(context.Background(), instance, sub, res, true, fakeClient) + assert.Nil(t, err) + assert.Contains(t, instance.Finalizers, pmtesting.SubroutineFinalizer) + + // Should not remove if RequeueAfter > 0 + res = ctrl.Result{RequeueAfter: 1} + err = removeFinalizerIfNeeded(context.Background(), instance, sub, res, false, fakeClient) + assert.Nil(t, err) +} + +func TestContainsFinalizer(t *testing.T) { + instance := &pmtesting.TestApiObject{} + sub := pmtesting.FinalizerSubroutine{} + assert.False(t, containsFinalizer(instance, sub.Finalizers())) + AddFinalizerIfNeeded(instance, sub) + assert.True(t, containsFinalizer(instance, sub.Finalizers())) +} + +func TestMarkResourceAsFinal(t *testing.T) { + instance := &pmtesting.ImplementingSpreadReconciles{} + logcfg := logger.DefaultConfig() + logcfg.NoJSON = true + log, _ := logger.New(logcfg) + conds := []metav1.Condition{} + mgr := &pmtesting.TestLifecycleManager{Logger: log} + MarkResourceAsFinal(instance, log, conds, metav1.ConditionTrue, mgr) + assert.Equal(t, instance.Status.ObservedGeneration, instance.Generation) +} + +func TestHandleClientError(t *testing.T) { + log := testlogger.New().Logger + result, err := HandleClientError("msg", log, fmt.Errorf("err"), true, sentry.Tags{}) + assert.Error(t, err) + assert.Equal(t, ctrl.Result{}, result) +} + +func TestHandleOperatorError(t *testing.T) { + log := testlogger.New().Logger + opErr := operrors.NewOperatorError(fmt.Errorf("err"), false, false) + result, err := HandleOperatorError(context.Background(), opErr, "msg", true, log) + assert.Nil(t, err) + assert.Equal(t, ctrl.Result{}, result) + + ctx := sentry.ContextWithSentryTags(context.Background(), sentry.Tags{"test": "tag"}) + opErr = operrors.NewOperatorError(fmt.Errorf("err"), true, true) + result, err = HandleOperatorError(ctx, opErr, "msg", true, log) + assert.Error(t, err) + assert.Equal(t, ctrl.Result{}, result) +} + +func TestValidateInterfaces(t *testing.T) { + log := testlogger.New().Logger + instance := &pmtesting.ImplementingSpreadReconciles{} + mgr := &pmtesting.TestLifecycleManager{Logger: log} + err := ValidateInterfaces(instance, log, mgr) + assert.NoError(t, err) +} diff --git a/controller/lifecycle/multicluster/lifecycle.go b/controller/lifecycle/multicluster/lifecycle.go new file mode 100644 index 0000000..2b31445 --- /dev/null +++ b/controller/lifecycle/multicluster/lifecycle.go @@ -0,0 +1,138 @@ +package multicluster + +import ( + "context" + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-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/api" + "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 ClusterGetter interface { + GetCluster(ctx context.Context, clusterName string) (cluster.Cluster, error) +} + +type LifecycleManager struct { + log *logger.Logger + mgr ClusterGetter + config api.Config + subroutines []subroutine.Subroutine + spreader *spread.Spreader + conditionsManager *conditions.ConditionManager + prepareContextFunc api.PrepareContextFunc +} + +func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, mgr ClusterGetter, log *logger.Logger) *LifecycleManager { + log = log.MustChildLoggerWithAttributes("operator", operatorName, "controller", controllerName) + return &LifecycleManager{ + log: log, + mgr: mgr, + subroutines: subroutines, + config: api.Config{ + OperatorName: operatorName, + ControllerName: controllerName, + }, + } +} + +func (l *LifecycleManager) Config() api.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() api.PrepareContextFunc { + return l.prepareContextFunc +} +func (l *LifecycleManager) ConditionsManager() api.ConditionManager { + // it is important to return nil unsted of a nil pointer to the interface to avoid misbehaving nil checks + if l.conditionsManager == nil { + return nil + } + return l.conditionsManager +} +func (l *LifecycleManager) Spreader() api.SpreadManager { // it is important to return nil unsted of a nil pointer to the interface to avoid misbehaving nil checks + if l.spreader == nil { + return nil + } + return l.spreader +} +func (l *LifecycleManager) Reconcile(ctx context.Context, req mcreconcile.Request, instance runtimeobject.RuntimeObject) (ctrl.Result, error) { + cl, err := l.mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to get cluster: %w", err) + } + client := cl.GetClient() + return lifecycle.Reconcile(ctx, req.NamespacedName, instance, client, l) +} +func (l *LifecycleManager) SetupWithManagerBuilder(mgr mcmanager.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, log *logger.Logger, eventPredicates ...predicate.Predicate) (*mcbuilder.Builder, error) { + if err := lifecycle.ValidateInterfaces(instance, log, l); 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...) + opts := controller.TypedOptions[mcreconcile.Request]{ + MaxConcurrentReconciles: maxReconciles, + } + return mcbuilder.ControllerManagedBy(mgr). + Named(reconcilerName). + For(instance). + WithOptions(opts). + WithEventFilter(predicate.And(eventPredicates...)), nil +} +func (l *LifecycleManager) SetupWithManager(mgr mcmanager.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, r mcreconcile.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 api.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() api.Lifecycle { + l.spreader = spread.NewSpreader() + return l +} + +func (l *LifecycleManager) WithConditionManagement() api.Lifecycle { + l.conditionsManager = conditions.NewConditionManager() + return l +} diff --git a/controller/lifecycle/multicluster/lifecycle_test.go b/controller/lifecycle/multicluster/lifecycle_test.go new file mode 100644 index 0000000..9b0ce6f --- /dev/null +++ b/controller/lifecycle/multicluster/lifecycle_test.go @@ -0,0 +1,207 @@ +package multicluster + +import ( + "context" + goerrors "errors" + "testing" + + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine" + pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" + operrors "github.com/platform-mesh/golang-commons/errors" + "github.com/platform-mesh/golang-commons/logger/testlogger" +) + +func TestLifecycle(t *testing.T) { + namespace := "bar" + name := "foo" + testApiObject := &pmtesting.TestApiObject{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + + t.Run("Should setup with manager ok", func(t *testing.T) { + // Arrange + instance := &v1.Namespace{} + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + tr := &testReconciler{ + lifecycleManager: mgr, + } + + // Act + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + mmanager, err := mcmanager.New(cfg, provider, mcmanager.Options{Scheme: scheme}) + assert.NoError(t, err) + err = mgr.SetupWithManager(mmanager, 0, "testReconciler", instance, "test", tr, log.Logger) + + // Assert + assert.NoError(t, err) + }) + t.Run("Should setup with manager not implementing interface", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{} + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + mgr.WithSpreadingReconciles() + tr := &testReconciler{ + lifecycleManager: mgr, + } + + // Act + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mmanager, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + err = mgr.SetupWithManager(mmanager, 0, "testReconciler", instance, "test", tr, log.Logger) + + // Assert + assert.Error(t, err) + }) + t.Run("Should setup with manager read only", func(t *testing.T) { + // Arrange + instance := &pmtesting.NotImplementingSpreadReconciles{} + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + mgr.WithReadOnly() + tr := &testReconciler{ + lifecycleManager: mgr, + } + + // Act + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mmanager, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + err = mgr.SetupWithManager(mmanager, 0, "testReconciler", instance, "test", tr, log.Logger) + + // Assert + assert.Error(t, err) + }) + t.Run("Should fail setup with invalid config", func(t *testing.T) { + // Arrange + instance := &pmtesting.ImplementingSpreadReconciles{} + fakeClient := pmtesting.CreateFakeClient(t, instance) + + mgr, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + mgr.WithReadOnly() + mgr.WithSpreadingReconciles() + tr := &testReconciler{ + lifecycleManager: mgr, + } + + // Act + cfg := &rest.Config{} + provider := pmtesting.NewFakeProvider(cfg) + mmanager, err := mcmanager.New(cfg, provider, mcmanager.Options{}) + assert.NoError(t, err) + err = mgr.SetupWithManager(mmanager, 0, "testReconciler", instance, "test", tr, log.Logger) + + // Assert + assert.Error(t, err) + }) + + errorMessage := "oh nose" + 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 := pmtesting.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} + req := mcreconcile.Request{ + Request: controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}, + } + result, err := tr.Reconcile(ctx, req) + + // 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 := pmtesting.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} + request := mcreconcile.Request{ + Request: controllerruntime.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}, + } + + result, err := tr.Reconcile(ctx, request) + + // Then + assert.NotNil(t, ctx) + assert.NotNil(t, result) + assert.Error(t, err) + }) + }) +} + +// Test LifecycleManager.WithConditionManagement +func TestLifecycleManager_WithConditionManagement(t *testing.T) { + // Given + fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{}) + clusterGetter := &pmtesting.FakeManager{Client: fakeClient} + _, log := createLifecycleManager([]subroutine.Subroutine{}, fakeClient) + + // When + l := NewLifecycleManager([]subroutine.Subroutine{}, "test-operator", "test-controller", clusterGetter, log.Logger).WithConditionManagement() + + // Then + assert.True(t, true, l.ConditionsManager() != nil) +} + +type testReconciler struct { + lifecycleManager *LifecycleManager +} + +func (r *testReconciler) Reconcile(ctx context.Context, req mcreconcile.Request) (controllerruntime.Result, error) { + return r.lifecycleManager.Reconcile(ctx, req, &pmtesting.TestApiObject{}) +} + +func createLifecycleManager(subroutines []subroutine.Subroutine, client client.Client) (*LifecycleManager, *testlogger.TestLogger) { + log := testlogger.New() + clusterGetter := &pmtesting.FakeManager{Client: client} + m := NewLifecycleManager(subroutines, "test-operator", "test-controller", clusterGetter, log.Logger) + return m, log +} diff --git a/controller/lifecycle/spread/spread.go b/controller/lifecycle/spread/spread.go index fdd5edb..1e0686e 100644 --- a/controller/lifecycle/spread/spread.go +++ b/controller/lifecycle/spread/spread.go @@ -1,17 +1,18 @@ package spread import ( - "fmt" "math/rand/v2" + "slices" "time" + "golang.org/x/exp/maps" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "github.com/platform-mesh/golang-commons/controller/lifecycle/api" "github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject" + "github.com/platform-mesh/golang-commons/controller/lifecycle/util" "github.com/platform-mesh/golang-commons/logger" - "github.com/platform-mesh/golang-commons/sentry" ) const ReconcileRefreshLabel = "platform-mesh.io/refresh-reconcile" @@ -39,7 +40,8 @@ func getNextReconcileTime(maxReconcileTime time.Duration) time.Duration { return time.Duration(jitter+int64(minTime)) * time.Minute } -func (s *Spreader) OnNextReconcile(instanceStatusObj api.RuntimeObjectSpreadReconcileStatus, log *logger.Logger) (ctrl.Result, error) { +func (s *Spreader) OnNextReconcile(instance runtimeobject.RuntimeObject, log *logger.Logger) (ctrl.Result, error) { + instanceStatusObj := util.MustToInterface[api.RuntimeObjectSpreadReconcileStatus](instance, log) 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 @@ -70,21 +72,11 @@ func (s *Spreader) RemoveRefreshLabelIfExists(instance runtimeobject.RuntimeObje return keyCount != len(instance.GetLabels()) } -func (s *Spreader) ToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (api.RuntimeObjectSpreadReconcileStatus, error) { - if obj, ok := instance.(api.RuntimeObjectSpreadReconcileStatus); ok { - return obj, nil - } - 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 (s *Spreader) ReconcileRequired(instance runtimeobject.RuntimeObject, log *logger.Logger) bool { + instanceStatusObj := util.MustToInterface[api.RuntimeObjectSpreadReconcileStatus](instance, log) + generationChanged := instance.GetGeneration() != instanceStatusObj.GetObservedGeneration() + isAfterNextReconcileTime := v1.Now().UTC().After(instanceStatusObj.GetNextReconcileTime().UTC()) + refreshRequested := slices.Contains(maps.Keys(instance.GetLabels()), ReconcileRefreshLabel) -func (s *Spreader) MustToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) api.RuntimeObjectSpreadReconcileStatus { - obj, err := s.ToRuntimeObjectSpreadReconcileStatusInterface(instance, log) - if err == nil { - return obj - } - log.Panic().Err(err).Msg("Failed to cast instance to RuntimeObjectSpreadReconcileStatus") - return nil + return generationChanged || isAfterNextReconcileTime || refreshRequested } diff --git a/controller/lifecycle/spread/spread_test.go b/controller/lifecycle/spread/spread_test.go index de24776..9b7add9 100644 --- a/controller/lifecycle/spread/spread_test.go +++ b/controller/lifecycle/spread/spread_test.go @@ -136,44 +136,68 @@ func TestRemoveRefreshLabelNoLabels(t *testing.T) { assert.False(t, ok) } -func TestToRuntimeObjectSpreadReconcileStatusInterface_Success(t *testing.T) { +func TestReconcileRequired(t *testing.T) { s := NewSpreader() tl := testlogger.New() - apiObject := &pmtesting.ImplementingSpreadReconciles{} - obj, err := s.ToRuntimeObjectSpreadReconcileStatusInterface(apiObject, tl.Logger) - assert.NoError(t, err) - assert.NotNil(t, obj) -} - -func TestToRuntimeObjectSpreadReconcileStatusInterface_Failure(t *testing.T) { - s := NewSpreader() - tl := testlogger.New() - // DummyRuntimeObject does NOT implement RuntimeObjectSpreadReconcileStatus - apiObject := &pmtesting.DummyRuntimeObject{} - obj, err := s.ToRuntimeObjectSpreadReconcileStatusInterface(apiObject, tl.Logger) - assert.Error(t, err) - assert.Nil(t, obj) - messages, logErr := tl.GetLogMessages() - assert.NoError(t, logErr) - assert.Contains(t, messages[0].Message, "Failed to cast instance to RuntimeObjectSpreadReconcileStatus") -} - -func TestMustToRuntimeObjectSpreadReconcileStatusInterface_Success(t *testing.T) { - s := NewSpreader() - tl := testlogger.New() - apiObject := &pmtesting.ImplementingSpreadReconciles{} - obj := s.MustToRuntimeObjectSpreadReconcileStatusInterface(apiObject, tl.Logger) - assert.NotNil(t, obj) -} -func TestMustToRuntimeObjectSpreadReconcileStatusInterface_Panic(t *testing.T) { - s := NewSpreader() - tl := testlogger.New() - apiObject := &pmtesting.DummyRuntimeObject{} - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected panic but did not panic") - } - }() - _ = s.MustToRuntimeObjectSpreadReconcileStatusInterface(apiObject, tl.Logger) + now := time.Now() + past := now.Add(-1 * time.Hour) + future := now.Add(1 * time.Hour) + + // Case 1: Generation changed + apiObject1 := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: v1.ObjectMeta{ + Generation: 2, + }, + Status: pmtesting.TestStatus{ + ObservedGeneration: 1, + NextReconcileTime: v1.NewTime(future), + }, + }, + } + assert.True(t, s.ReconcileRequired(apiObject1, tl.Logger), "Should require reconcile when generation changed") + + // Case 2: After next reconcile time + apiObject2 := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: v1.ObjectMeta{ + Generation: 1, + }, + Status: pmtesting.TestStatus{ + ObservedGeneration: 1, + NextReconcileTime: v1.NewTime(past), + }, + }, + } + assert.True(t, s.ReconcileRequired(apiObject2, tl.Logger), "Should require reconcile when after next reconcile time") + + // Case 3: Refresh label present + apiObject3 := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: v1.ObjectMeta{ + Generation: 1, + Labels: map[string]string{ReconcileRefreshLabel: ""}, + }, + Status: pmtesting.TestStatus{ + ObservedGeneration: 1, + NextReconcileTime: v1.NewTime(future), + }, + }, + } + assert.True(t, s.ReconcileRequired(apiObject3, tl.Logger), "Should require reconcile when refresh label present") + + // Case 4: No condition met + apiObject4 := &pmtesting.ImplementingSpreadReconciles{ + TestApiObject: pmtesting.TestApiObject{ + ObjectMeta: v1.ObjectMeta{ + Generation: 1, + }, + Status: pmtesting.TestStatus{ + ObservedGeneration: 1, + NextReconcileTime: v1.NewTime(future), + }, + }, + } + assert.False(t, s.ReconcileRequired(apiObject4, tl.Logger), "Should not require reconcile when no condition met") } diff --git a/controller/lifecycle/util/convert.go b/controller/lifecycle/util/convert.go new file mode 100644 index 0000000..1ac03d4 --- /dev/null +++ b/controller/lifecycle/util/convert.go @@ -0,0 +1,30 @@ +package util + +import ( + "fmt" + "reflect" + + "github.com/platform-mesh/golang-commons/logger" + "github.com/platform-mesh/golang-commons/sentry" +) + +func ToInterface[T any](instance any, log *logger.Logger) (T, error) { + var zero T + obj, ok := instance.(T) + if ok { + return obj, nil + } + err := fmt.Errorf("failed to cast instance of type %T to %v", instance, reflect.TypeOf(zero)) + log.Error().Err(err).Msg("Failed to cast instance to target interface") + sentry.CaptureError(err, nil) + return zero, err +} + +func MustToInterface[T any](instance any, log *logger.Logger) T { + obj, err := ToInterface[T](instance, log) + if err == nil { + return obj + } + log.Panic().Err(err).Msg("Failed to cast instance to target interface") + panic(err) +} diff --git a/controller/lifecycle/util/convert_test.go b/controller/lifecycle/util/convert_test.go new file mode 100644 index 0000000..7d4338e --- /dev/null +++ b/controller/lifecycle/util/convert_test.go @@ -0,0 +1,49 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/platform-mesh/golang-commons/controller/lifecycle/api" + pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport" + "github.com/platform-mesh/golang-commons/logger/testlogger" +) + +func TestToRuntimeObjectSpreadReconcileStatusInterface_Success(t *testing.T) { + tl := testlogger.New() + apiObject := &pmtesting.ImplementingSpreadReconciles{} + obj, err := ToInterface[api.RuntimeObjectSpreadReconcileStatus](apiObject, tl.Logger) + assert.NoError(t, err) + assert.NotNil(t, obj) +} + +func TestToRuntimeObjectSpreadReconcileStatusInterface_Failure(t *testing.T) { + tl := testlogger.New() + // DummyRuntimeObject does NOT implement RuntimeObjectSpreadReconcileStatus + apiObject := &pmtesting.DummyRuntimeObject{} + _, err := ToInterface[api.RuntimeObjectSpreadReconcileStatus](apiObject, tl.Logger) + assert.Error(t, err) + + messages, logErr := tl.GetLogMessages() + assert.NoError(t, logErr) + assert.Contains(t, messages[0].Message, "Failed to cast instance to target interface") +} + +func TestMustToRuntimeObjectSpreadReconcileStatusInterface_Success(t *testing.T) { + tl := testlogger.New() + apiObject := &pmtesting.ImplementingSpreadReconciles{} + obj := MustToInterface[api.RuntimeObjectSpreadReconcileStatus](apiObject, tl.Logger) + assert.NotNil(t, obj) +} + +func TestMustToRuntimeObjectSpreadReconcileStatusInterface_Panic(t *testing.T) { + tl := testlogger.New() + apiObject := &pmtesting.DummyRuntimeObject{} + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic but did not panic") + } + }() + MustToInterface[api.RuntimeObjectSpreadReconcileStatus](apiObject, tl.Logger) +} diff --git a/controller/testSupport/lifecycle.go b/controller/testSupport/lifecycle.go index 486ce06..9a3ec43 100644 --- a/controller/testSupport/lifecycle.go +++ b/controller/testSupport/lifecycle.go @@ -1,24 +1,130 @@ package testSupport import ( + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "github.com/platform-mesh/golang-commons/controller/lifecycle/api" + "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" ) type TestLifecycleManager struct { - Logger *logger.Logger + Logger *logger.Logger + SubroutinesArr []subroutine.Subroutine + spreader api.SpreadManager + conditionsManager api.ConditionManager + ShouldReconcile bool + prepareContextFunc api.PrepareContextFunc } -func (t TestLifecycleManager) Config() api.Config { +func (l *TestLifecycleManager) Config() api.Config { return api.Config{ ControllerName: "test-controller", OperatorName: "test-operator", ReadOnly: false, } } -func (t TestLifecycleManager) Log() *logger.Logger { return t.Logger } -func (t TestLifecycleManager) Spreader() api.SpreadManager { return nil } -func (t TestLifecycleManager) ConditionsManager() api.ConditionManager { return nil } -func (t TestLifecycleManager) PrepareContextFunc() api.PrepareContextFunc { return nil } -func (t TestLifecycleManager) Subroutines() []subroutine.Subroutine { return []subroutine.Subroutine{} } +func (l *TestLifecycleManager) Log() *logger.Logger { return l.Logger } +func (l *TestLifecycleManager) Spreader() api.SpreadManager { return l.spreader } +func (l *TestLifecycleManager) ConditionsManager() api.ConditionManager { return l.conditionsManager } +func (l *TestLifecycleManager) PrepareContextFunc() api.PrepareContextFunc { + return l.prepareContextFunc +} +func (l *TestLifecycleManager) Subroutines() []subroutine.Subroutine { return l.SubroutinesArr } +func (l *TestLifecycleManager) WithSpreadingReconciles() api.Lifecycle { + l.spreader = &TestSpreader{ShouldReconcile: l.ShouldReconcile} + return l +} +func (l *TestLifecycleManager) WithConditionManagement() api.Lifecycle { + l.conditionsManager = &TestConditionManager{} + return l +} +func (l *TestLifecycleManager) WithPrepareContextFunc(prepareFunction api.PrepareContextFunc) *TestLifecycleManager { + l.prepareContextFunc = prepareFunction + return l +} + +type TestSpreader struct { + ShouldReconcile bool +} + +func (t TestSpreader) ReconcileRequired(runtimeobject.RuntimeObject, *logger.Logger) bool { + return t.ShouldReconcile +} + +func (t TestSpreader) ToRuntimeObjectSpreadReconcileStatusInterface() (api.RuntimeObjectSpreadReconcileStatus, error) { + //TODO implement me + panic("implement me") +} + +func (t TestSpreader) MustToRuntimeObjectSpreadReconcileStatusInterface() api.RuntimeObjectSpreadReconcileStatus { + + //TODO implement me + panic("implement me") +} + +func (t TestSpreader) OnNextReconcile(runtimeobject.RuntimeObject, *logger.Logger) (ctrl.Result, error) { + return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil +} + +func (t TestSpreader) RemoveRefreshLabelIfExists(instance runtimeobject.RuntimeObject) bool { + lbs := instance.GetLabels() + if _, ok := lbs["platform-mesh.io/refresh-reconcile"]; ok { + delete(lbs, "platform-mesh.io/refresh-reconcile") + instance.SetLabels(lbs) + return true + } + return false +} + +func (t TestSpreader) SetNextReconcileTime(instanceStatusObj api.RuntimeObjectSpreadReconcileStatus, _ *logger.Logger) { + instanceStatusObj.SetNextReconcileTime(metav1.NewTime(time.Now().Add(10 * time.Hour))) +} + +func (t TestSpreader) UpdateObservedGeneration(instanceStatusObj api.RuntimeObjectSpreadReconcileStatus, _ *logger.Logger) { + instanceStatusObj.SetObservedGeneration(instanceStatusObj.GetGeneration()) +} + +type TestConditionManager struct{} + +func (t TestConditionManager) SetInstanceConditionUnknownIfNotSet(conditions *[]metav1.Condition) bool { + return meta.SetStatusCondition(conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionUnknown, + Message: "The resource is in an unknown state", + Reason: "Unknown", + }) +} + +func (t TestConditionManager) SetSubroutineConditionToUnknownIfNotSet(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, _ bool, _ *logger.Logger) bool { + return meta.SetStatusCondition(conditions, metav1.Condition{ + Type: fmt.Sprintf("%s_Ready", subroutine.GetName()), + Status: metav1.ConditionUnknown, + Message: "The resource is in an unknown state", + Reason: "Unknown", + }) +} + +func (t TestConditionManager) SetSubroutineCondition(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, _ ctrl.Result, _ error, _ bool, _ *logger.Logger) bool { + return meta.SetStatusCondition(conditions, metav1.Condition{ + Type: fmt.Sprintf("%s_Ready", subroutine.GetName()), + Status: metav1.ConditionTrue, + Message: "The subroutine is complete", + Reason: "ok", + }) +} + +func (t TestConditionManager) SetInstanceConditionReady(conditions *[]metav1.Condition, _ metav1.ConditionStatus) bool { + return meta.SetStatusCondition(conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Message: "The resource is ready", + Reason: "ok", + }) +} diff --git a/controller/testSupport/multicluster.go b/controller/testSupport/multicluster.go new file mode 100644 index 0000000..3b5494b --- /dev/null +++ b/controller/testSupport/multicluster.go @@ -0,0 +1,64 @@ +package testSupport + +import ( + "context" + "net/http" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" +) + +type FakeManager struct{ Client client.Client } + +func (f *FakeManager) GetCluster(context.Context, string) (cluster.Cluster, error) { + return &FakeCluster{client: f.Client}, nil +} + +var _ cluster.Cluster = (*FakeCluster)(nil) + +type FakeCluster struct{ client client.Client } + +func (f FakeCluster) GetHTTPClient() *http.Client { + return nil +} + +func (f FakeCluster) GetConfig() *rest.Config { + return nil +} + +func (f FakeCluster) GetCache() cache.Cache { + return nil +} + +func (f FakeCluster) GetScheme() *runtime.Scheme { + return nil +} + +func (f FakeCluster) GetClient() client.Client { + return f.client +} + +func (f FakeCluster) GetFieldIndexer() client.FieldIndexer { + return nil +} + +func (f FakeCluster) GetEventRecorderFor(string) record.EventRecorder { + return nil +} + +func (f FakeCluster) GetRESTMapper() meta.RESTMapper { + return nil +} + +func (f FakeCluster) GetAPIReader() client.Reader { + return nil +} + +func (f FakeCluster) Start(context.Context) error { + return nil +} diff --git a/controller/testSupport/provider.go b/controller/testSupport/provider.go new file mode 100644 index 0000000..b1a04d8 --- /dev/null +++ b/controller/testSupport/provider.go @@ -0,0 +1,27 @@ +package testSupport + +import ( + "context" + + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" +) + +type FakeProvider struct { + cfg *rest.Config +} + +func NewFakeProvider(cfg *rest.Config) *FakeProvider { + return &FakeProvider{cfg: cfg} +} + +func (f FakeProvider) Get(context.Context, string) (cluster.Cluster, error) { + return cluster.New(f.cfg, nil) +} + +// IndexField indexes the given object by the given field on all engaged +// clusters, current and future. +func (f FakeProvider) IndexField(context.Context, client.Object, string, client.IndexerFunc) error { + return nil +} diff --git a/go.mod b/go.mod index 851accc..8c5bfe5 100644 --- a/go.mod +++ b/go.mod @@ -2,21 +2,26 @@ module github.com/platform-mesh/golang-commons go 1.24.3 +// This is currently necessary due to the version used in https://github.com/kcp-dev/multicluster-provider/blob/main/go.mod +replace sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.20.4 + require ( github.com/99designs/gqlgen v0.17.76 github.com/getsentry/sentry-go v0.34.0 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a - github.com/go-jose/go-jose/v4 v4.1.1 + github.com/go-jose/go-jose/v4 v4.0.5 github.com/go-logr/logr v1.4.3 github.com/go-logr/zerologr v1.2.3 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/jellydator/ttlcache/v3 v3.4.0 + github.com/kcp-dev/kcp/sdk v0.27.1 github.com/machinebox/graphql v0.2.2 github.com/openfga/api/proto v0.0.0-20250528161632-e53c69cc5531 github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe github.com/openfga/openfga v1.9.0 + github.com/otiai10/copy v1.14.1 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 @@ -24,20 +29,24 @@ require ( github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 github.com/vektah/gqlparser/v2 v2.5.30 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 go.opentelemetry.io/otel v1.37.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 go.opentelemetry.io/otel/sdk v1.37.0 go.opentelemetry.io/proto/otlp v1.7.0 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 golang.org/x/oauth2 v0.30.0 + golang.org/x/sys v0.33.0 google.golang.org/grpc v1.73.0 - k8s.io/api v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.2 - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.33.0 + k8s.io/apimachinery v0.33.0 + k8s.io/client-go v0.33.0 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/multicluster-runtime v0.20.4-alpha.7 ) require ( @@ -45,7 +54,7 @@ require ( github.com/Yiling-J/theine-go v0.6.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect @@ -74,6 +83,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kcp-dev/logicalcluster/v3 v3.0.5 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matryer/is v1.4.1 // indirect @@ -84,6 +94,8 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/natefinch/wrap v0.2.0 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/onsi/gomega v1.36.1 // indirect + github.com/otiai10/mint v1.6.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect @@ -101,7 +113,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/mock v0.5.2 // indirect @@ -110,7 +121,6 @@ require ( golang.org/x/crypto v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.9.0 // indirect @@ -121,7 +131,6 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.33.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect diff --git a/go.sum b/go.sum index 8cfcd28..e2c2130 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,6 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -79,8 +77,8 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9yue4+QkG/HQ/W67wvtQmWJ4SDo9aK/GIno= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= -github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI= -github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -169,6 +167,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kcp-dev/kcp/sdk v0.27.1 h1:jBVdrZoJd5hy2RqaBnmCCzldimwOqDkf8FXtNq5HaWA= +github.com/kcp-dev/kcp/sdk v0.27.1/go.mod h1:3eRgW42d81Ng60DbG1xbne0FSS2znpcN/GUx4rqJgUo= +github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU= +github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -235,6 +237,10 @@ github.com/openfga/language/pkg/go v0.2.0-beta.2.0.20250428093642-7aeebe78bbfe/g github.com/openfga/openfga v1.9.0 h1:Bs5h7fZWZNCubnUhAxH5choNbtkCq1HNaoSrRRgyyXU= github.com/openfga/openfga v1.9.0/go.mod h1:NwuzRFEwrOBV6AiDCq37KxPp/v9wXdDYHRCXaePf2iU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= @@ -317,22 +323,22 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= @@ -355,8 +361,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -457,20 +463,24 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc= +k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk= +k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc= modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -479,10 +489,12 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/multicluster-runtime v0.20.4-alpha.7 h1:AFlM/TFQaESxtCRX6scodEKensLhcbfGwXfjJIvoaT8= +sigs.k8s.io/multicluster-runtime v0.20.4-alpha.7/go.mod h1:2N2/c3p08bYC9eDaRs0dllTxgAm5xiLDSkmGZpWKyw4= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/traces/otel.go b/traces/otel.go index 6af1afe..c70b214 100644 --- a/traces/otel.go +++ b/traces/otel.go @@ -30,19 +30,27 @@ type Config struct { CollectorEndpoint string `mapstructure:"tracing-config-collector-endpoint" description:"Set the tracing collector endpoint used to send traces to the collector"` } +// --- Wrappers for patching (default to real functions) --- +var ( + resourceNewFunc = resource.New + grpcNewClientFunc = grpc.NewClient + otlptracegrpcNewFunc = otlptracegrpc.New // type: func(ctx context.Context, opts ...otlptracegrpc.Option) (*otlptrace.Exporter, error) + stdouttraceNewFunc = stdouttrace.New // type: func(opts ...stdouttrace.Option) (*stdouttrace.Exporter, error) +) + // InitProvider creates an OpenTelemetry provider for the concrete service. // If the collector in the destination endpoint isn't reachable, then the init function will return an error. func InitProvider(ctx context.Context, config Config) (func(ctx context.Context) error, error) { connCtx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - client, err := grpc.NewClient(config.CollectorEndpoint, + client, err := grpcNewClientFunc(config.CollectorEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err) } - traceExporter, err := otlptracegrpc.New(connCtx, otlptracegrpc.WithGRPCConn(client)) + traceExporter, err := otlptracegrpcNewFunc(connCtx, otlptracegrpc.WithGRPCConn(client)) if err != nil { return nil, fmt.Errorf("failed to create trace exporter: %w", err) } @@ -58,7 +66,7 @@ func InitLocalProvider(ctx context.Context, config Config, exportToConsole bool) fileTarget = os.Stdout } - traceExporter, err := stdouttrace.New( + traceExporter, err := stdouttraceNewFunc( stdouttrace.WithWriter(fileTarget), stdouttrace.WithPrettyPrint(), ) @@ -70,7 +78,7 @@ func InitLocalProvider(ctx context.Context, config Config, exportToConsole bool) } func (c Config) initProvider(ctx context.Context, exporter sdkTrace.SpanExporter) (func(ctx context.Context) error, error) { - res, err := resource.New(ctx, + res, err := resourceNewFunc(ctx, resource.WithAttributes( semconv.ServiceName(c.ServiceName), semconv.ServiceVersion(c.ServiceVersion), diff --git a/traces/otel_test.go b/traces/otel_test.go index 19266b4..f073ea6 100644 --- a/traces/otel_test.go +++ b/traces/otel_test.go @@ -2,19 +2,47 @@ package traces import ( "context" + "errors" "net" "testing" "time" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/resource" sdkTrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.34.0" collectortrace "go.opentelemetry.io/proto/otlp/collector/trace/v1" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) type mockExporter struct { sdkTrace.SpanExporter } +// --- Patchable versions of tested functions --- +func initProviderWithWrappers(ctx context.Context, c Config, exporter sdkTrace.SpanExporter) (func(ctx context.Context) error, error) { + res, err := resourceNewFunc(ctx, + resource.WithAttributes( + // ...existing code... + semconv.ServiceName(c.ServiceName), + semconv.ServiceVersion(c.ServiceVersion), + ), + ) + if err != nil { + return nil, err + } + bsp := sdkTrace.NewBatchSpanProcessor(exporter) + tracerProvider := sdkTrace.NewTracerProvider( + sdkTrace.WithSampler(sdkTrace.AlwaysSample()), + sdkTrace.WithResource(res), + sdkTrace.WithSpanProcessor(bsp), + ) + return tracerProvider.Shutdown, nil +} + func TestConfig_initProvider_Success(t *testing.T) { ctx := context.Background() cfg := Config{ @@ -31,6 +59,26 @@ func TestConfig_initProvider_Success(t *testing.T) { } } +func TestConfig_initProvider_ResourceError(t *testing.T) { + cfg := Config{ + ServiceName: "test-service", + ServiceVersion: "1.0.0", + } + exporter := &mockExporter{} + ctx := context.Background() + + orig := resourceNewFunc + resourceNewFunc = func(ctx context.Context, opts ...resource.Option) (*resource.Resource, error) { + return nil, errors.New("resource error") + } + defer func() { resourceNewFunc = orig }() + + shutdown, err := initProviderWithWrappers(ctx, cfg, exporter) + if err == nil || shutdown != nil { + t.Error("expected error and nil shutdown when resource.New fails") + } +} + func TestInitProvider_HappyPath(t *testing.T) { // Start a dummy gRPC server to simulate the OTLP collector lis, err := net.Listen("tcp", "127.0.0.1:0") @@ -68,6 +116,49 @@ func TestInitProvider_HappyPath(t *testing.T) { } } +func TestInitProvider_GRPCClientError(t *testing.T) { + cfg := Config{ + ServiceName: "fail-service", + ServiceVersion: "1.0.0", + CollectorEndpoint: "invalid:endpoint", + } + + orig := grpcNewClientFunc + grpcNewClientFunc = func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { + return nil, errors.New("grpc client error") + } + defer func() { grpcNewClientFunc = orig }() + + // Patch InitProvider to use grpcNewClientFunc + client, err := grpcNewClientFunc(cfg.CollectorEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err == nil || client != nil { + t.Error("expected error and nil client when grpc.NewClient fails") + } +} + +func TestInitProvider_TraceExporterError(t *testing.T) { + ctx := context.Background() + + origClient := grpcNewClientFunc + grpcNewClientFunc = func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { + return &grpc.ClientConn{}, nil + } + defer func() { grpcNewClientFunc = origClient }() + + origOtlp := otlptracegrpcNewFunc + otlptracegrpcNewFunc = func(ctx context.Context, opts ...otlptracegrpc.Option) (*otlptrace.Exporter, error) { + return nil, errors.New("trace exporter error") + } + defer func() { otlptracegrpcNewFunc = origOtlp }() + + connCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + traceExporter, err := otlptracegrpcNewFunc(connCtx, otlptracegrpc.WithGRPCConn(&grpc.ClientConn{})) + if err == nil || traceExporter != nil { + t.Error("expected error and nil exporter when otlptracegrpc.New fails") + } +} + func TestInitLocalProvider(t *testing.T) { ctx := context.Background() cfg := Config{ @@ -94,6 +185,104 @@ func TestInitLocalProvider(t *testing.T) { } } +func TestInitLocalProvider_ExporterError(t *testing.T) { + orig := stdouttraceNewFunc + stdouttraceNewFunc = func(opts ...stdouttrace.Option) (*stdouttrace.Exporter, error) { + return nil, errors.New("stdout exporter error") + } + defer func() { stdouttraceNewFunc = orig }() + + traceExporter, err := stdouttraceNewFunc() + if err == nil || traceExporter != nil { + t.Error("expected error and nil exporter when stdouttrace.New fails") + } +} + +func TestInitProvider_Error_GRPCClient(t *testing.T) { + ctx := context.Background() + cfg := Config{ + ServiceName: "err-service", + ServiceVersion: "1.0.0", + CollectorEndpoint: "bad:endpoint", + } + + orig := grpcNewClientFunc + grpcNewClientFunc = func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { + return nil, errors.New("grpc client error") + } + defer func() { grpcNewClientFunc = orig }() + + shutdown, err := InitProvider(ctx, cfg) + if err == nil || shutdown != nil { + t.Error("expected error and nil shutdown when grpc client fails") + } +} + +func TestInitProvider_Error_TraceExporter(t *testing.T) { + ctx := context.Background() + cfg := Config{ + ServiceName: "err-service", + ServiceVersion: "1.0.0", + CollectorEndpoint: "localhost:4317", + } + + origClient := grpcNewClientFunc + grpcNewClientFunc = func(target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) { + return &grpc.ClientConn{}, nil + } + defer func() { grpcNewClientFunc = origClient }() + + origOtlp := otlptracegrpcNewFunc + otlptracegrpcNewFunc = func(ctx context.Context, opts ...otlptracegrpc.Option) (*otlptrace.Exporter, error) { + return nil, errors.New("trace exporter error") + } + defer func() { otlptracegrpcNewFunc = origOtlp }() + + shutdown, err := InitProvider(ctx, cfg) + if err == nil || shutdown != nil { + t.Error("expected error and nil shutdown when trace exporter fails") + } +} + +func TestInitLocalProvider_Error_Exporter(t *testing.T) { + ctx := context.Background() + cfg := Config{ + ServiceName: "err-local", + ServiceVersion: "0.0.1", + } + + orig := stdouttraceNewFunc + stdouttraceNewFunc = func(opts ...stdouttrace.Option) (*stdouttrace.Exporter, error) { + return nil, errors.New("stdout exporter error") + } + defer func() { stdouttraceNewFunc = orig }() + + shutdown, err := InitLocalProvider(ctx, cfg, true) + if err == nil || shutdown != nil { + t.Error("expected error and nil shutdown when stdouttrace.New fails") + } +} + +func TestConfig_initProvider_Error_Resource(t *testing.T) { + ctx := context.Background() + cfg := Config{ + ServiceName: "err-resource", + ServiceVersion: "0.0.2", + } + exporter := &mockExporter{} + + orig := resourceNewFunc + resourceNewFunc = func(ctx context.Context, opts ...resource.Option) (*resource.Resource, error) { + return nil, errors.New("resource error") + } + defer func() { resourceNewFunc = orig }() + + shutdown, err := cfg.initProvider(ctx, exporter) + if err == nil || shutdown != nil { + t.Error("expected error and nil shutdown when resource.New fails") + } +} + // dummyTraceServer implements the OTLP TraceServiceServer interface with no-op methods. type dummyTraceServer struct { collectortrace.UnimplementedTraceServiceServer