diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go index bb1fe497f8..36a3af972e 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go @@ -202,6 +202,10 @@ func (r *AtlasDatabaseUserReconciler) ready(ctx *workflow.Context, atlasDatabase EnsureStatusOption(status.AtlasDatabaseUserNameOption(atlasDatabaseUser.Spec.Username)). EnsureStatusOption(status.AtlasDatabaseUserPasswordVersion(passwordVersion)) + if atlasDatabaseUser.Spec.ExternalProjectRef != nil { + return workflow.Requeue(workflow.StandaloneResourceRequeuePeriod).ReconcileResult() + } + return workflow.OK().ReconcileResult() } diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go index eeb1d334b0..ea0a64e7ae 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go @@ -5,7 +5,10 @@ import ( "errors" "reflect" "testing" + "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.mongodb.org/atlas-sdk/v20231115008/admin" @@ -283,6 +286,172 @@ func TestTerminate(t *testing.T) { } } +func TestReady(t *testing.T) { + tests := map[string]struct { + dbUser *akov2.AtlasDatabaseUser + passwordVersion string + interceptors interceptor.Funcs + + expectedResult ctrl.Result + expectedConditions []api.Condition + }{ + "fail to set finalizer": { + dbUser: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + passwordVersion: "1", + interceptors: interceptor.Funcs{ + Patch: func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + return errors.New("failed to set finalizer") + }, + }, + expectedResult: workflow.Terminate(workflow.AtlasFinalizerNotSet, "").ReconcileResult(), + expectedConditions: []api.Condition{ + api.FalseCondition(api.DatabaseUserReadyType). + WithReason(string(workflow.AtlasFinalizerNotSet)). + WithMessageRegexp("failed to set finalizer"), + }, + }, + "fail to set last applied config": { + dbUser: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + passwordVersion: "1", + interceptors: interceptor.Funcs{ + Patch: func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + if patch.Type() == types.JSONPatchType { + return nil + } + + return errors.New("failed to set last applied config") + }, + }, + expectedResult: workflow.Terminate(workflow.Internal, "").ReconcileResult(), + expectedConditions: []api.Condition{ + api.FalseCondition(api.DatabaseUserReadyType). + WithReason(string(workflow.Internal)). + WithMessageRegexp("failed to set last applied config"), + }, + }, + "don't requeue when it's a linked resource": { + dbUser: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + passwordVersion: "1", + expectedResult: workflow.OK().ReconcileResult(), + expectedConditions: []api.Condition{ + api.TrueCondition(api.ReadyType), + api.TrueCondition(api.DatabaseUserReadyType), + }, + }, + "don't requeue when it's a standalone resource": { + dbUser: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + ExternalProjectRef: &akov2.ExternalProjectReference{ + ID: "project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "user-creds", + }, + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + passwordVersion: "1", + expectedResult: workflow.Requeue(15 * time.Minute).ReconcileResult(), + expectedConditions: []api.Condition{ + api.TrueCondition(api.ReadyType), + api.TrueCondition(api.DatabaseUserReadyType), + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testScheme := runtime.NewScheme() + assert.NoError(t, akov2.AddToScheme(testScheme)) + assert.NoError(t, corev1.AddToScheme(testScheme)) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(tt.dbUser). + WithStatusSubresource(tt.dbUser). + WithInterceptorFuncs(tt.interceptors). + Build() + + logger := zaptest.NewLogger(t).Sugar() + c := &AtlasDatabaseUserReconciler{ + Client: k8sClient, + Log: logger, + } + ctx := &workflow.Context{ + Context: context.Background(), + Log: logger, + } + + assert.Equal(t, tt.expectedResult, c.ready(ctx, tt.dbUser, tt.passwordVersion)) + assert.True( + t, + cmp.Equal( + tt.expectedConditions, + ctx.Conditions(), + cmpopts.IgnoreFields(api.Condition{}, "LastTransitionTime"), + ), + ) + }) + } +} + func TestFindAtlasDatabaseUserForSecret(t *testing.T) { for _, tc := range []struct { name string diff --git a/pkg/controller/workflow/result.go b/pkg/controller/workflow/result.go index 11221c3d46..d5c81ed111 100644 --- a/pkg/controller/workflow/result.go +++ b/pkg/controller/workflow/result.go @@ -7,8 +7,9 @@ import ( ) const ( - DefaultRetry = time.Second * 10 - DefaultTimeout = time.Minute * 20 + DefaultRetry = time.Second * 10 + StandaloneResourceRequeuePeriod = time.Minute * 15 + DefaultTimeout = time.Minute * 20 ) type Result struct { @@ -30,6 +31,13 @@ func OK() Result { } } +func Requeue(period time.Duration) Result { + return Result{ + terminated: false, + requeueAfter: period, + } +} + // Terminate indicates that the reconciliation logic cannot proceed and needs to be finished (and possibly requeued). // This is not an expected termination of the reconciliation process so 'warning' flag is set to 'true'. // 'reason' and 'message' indicate the error state and are supposed to be reflected in the `conditions` for the