From c56374f3570e44818d6e7393dfd500e7808bf0f0 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Wed, 2 Oct 2024 11:21:11 +0200 Subject: [PATCH] CLOUDP-266544: DatabaseUser independent CRD (#1769) --- .../atlas.mongodb.com_atlasdatabaseusers.yaml | 20 +- internal/mocks/translation/project_service.go | 59 +++ internal/translation/dbuser/conversion.go | 4 +- .../translation/dbuser/conversion_test.go | 12 +- internal/translation/project/project.go | 10 + pkg/api/v1/atlasdatabaseuser_types.go | 10 +- pkg/api/v1/atlasdatabaseuser_types_test.go | 157 ++++++ pkg/api/v1/externalreference.go | 7 + pkg/api/v1/zz_generated.deepcopy.go | 26 +- .../atlasdatabaseuser_controller.go | 4 +- .../atlasdatabaseuser_controller_test.go | 11 +- .../atlasdatabaseuser/databaseuser.go | 123 +++-- .../atlasdatabaseuser/databaseuser_test.go | 486 +++++++++++++++--- .../atlasdeployment/advanced_deployment.go | 46 +- .../advanced_deployment_test.go | 72 --- .../atlasdeployment_controller_test.go | 18 +- .../serverless_deployment_test.go | 11 +- .../connectionsecret/connectionsecrets.go | 11 +- .../customresource/protection_test.go | 2 +- .../atlasdatabaseuserexternalprojects.go | 44 ++ .../atlasdatabaseuserexternalprojects_test.go | 55 ++ pkg/indexer/atlasdatabaseuserprojects.go | 44 ++ pkg/indexer/atlasdatabaseuserprojects_test.go | 78 +++ pkg/indexer/indexer.go | 2 + test/e2e/atlas_gov_test.go | 2 +- test/e2e/db_users_test.go | 25 +- .../e2e/actions/deploy/deploy_operator.go | 5 + test/helper/e2e/data/user.go | 19 +- test/helper/e2e/model/dbuser.go | 2 +- 29 files changed, 1113 insertions(+), 252 deletions(-) create mode 100644 pkg/api/v1/atlasdatabaseuser_types_test.go create mode 100644 pkg/api/v1/externalreference.go create mode 100644 pkg/indexer/atlasdatabaseuserexternalprojects.go create mode 100644 pkg/indexer/atlasdatabaseuserexternalprojects_test.go create mode 100644 pkg/indexer/atlasdatabaseuserprojects.go create mode 100644 pkg/indexer/atlasdatabaseuserprojects_test.go diff --git a/config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml b/config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml index 52b75b72d6..b7de9bf3dd 100644 --- a/config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml +++ b/config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml @@ -88,6 +88,16 @@ spec: DeleteAfterDate is a timestamp in ISO 8601 date and time format in UTC after which Atlas deletes the user. The specified date must be in the future and within one week. type: string + externalProjectRef: + description: ExternalProjectRef holds the Atlas project ID the user + belongs to + properties: + id: + description: ID is the Atlas project ID + type: string + required: + - id + type: object labels: description: |- Labels is an array containing key-value pairs that tag and categorize the database user. @@ -212,10 +222,18 @@ spec: - CUSTOMER type: string required: - - projectRef - roles - username type: object + x-kubernetes-validations: + - message: must define only one project reference through externalProjectRef + or projectRef + rule: (has(self.externalProjectRef) && !has(self.projectRef)) || (!has(self.externalProjectRef) + && has(self.projectRef)) + - message: must define a local connection secret when referencing an external + project + rule: (has(self.externalProjectRef) && has(self.connectionSecret)) || + !has(self.externalProjectRef) status: description: AtlasDatabaseUserStatus defines the observed state of AtlasProject properties: diff --git a/internal/mocks/translation/project_service.go b/internal/mocks/translation/project_service.go index e53fb24586..b416d46584 100644 --- a/internal/mocks/translation/project_service.go +++ b/internal/mocks/translation/project_service.go @@ -117,6 +117,65 @@ func (_c *ProjectServiceMock_DeleteProject_Call) RunAndReturn(run func(context.C return _c } +// GetProject provides a mock function with given fields: ctx, ID +func (_m *ProjectServiceMock) GetProject(ctx context.Context, ID string) (*project.Project, error) { + ret := _m.Called(ctx, ID) + + if len(ret) == 0 { + panic("no return value specified for GetProject") + } + + var r0 *project.Project + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*project.Project, error)); ok { + return rf(ctx, ID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *project.Project); ok { + r0 = rf(ctx, ID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*project.Project) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, ID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProjectServiceMock_GetProject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProject' +type ProjectServiceMock_GetProject_Call struct { + *mock.Call +} + +// GetProject is a helper method to define mock.On call +// - ctx context.Context +// - ID string +func (_e *ProjectServiceMock_Expecter) GetProject(ctx interface{}, ID interface{}) *ProjectServiceMock_GetProject_Call { + return &ProjectServiceMock_GetProject_Call{Call: _e.mock.On("GetProject", ctx, ID)} +} + +func (_c *ProjectServiceMock_GetProject_Call) Run(run func(ctx context.Context, ID string)) *ProjectServiceMock_GetProject_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *ProjectServiceMock_GetProject_Call) Return(_a0 *project.Project, _a1 error) *ProjectServiceMock_GetProject_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ProjectServiceMock_GetProject_Call) RunAndReturn(run func(context.Context, string) (*project.Project, error)) *ProjectServiceMock_GetProject_Call { + _c.Call.Return(run) + return _c +} + // GetProjectByName provides a mock function with given fields: ctx, name func (_m *ProjectServiceMock) GetProjectByName(ctx context.Context, name string) (*project.Project, error) { ret := _m.Called(ctx, name) diff --git a/internal/translation/dbuser/conversion.go b/internal/translation/dbuser/conversion.go index 4df4a1591e..b5969e1e1a 100644 --- a/internal/translation/dbuser/conversion.go +++ b/internal/translation/dbuser/conversion.go @@ -65,9 +65,9 @@ func (u *User) clearedSpecClone() *akov2.AtlasDatabaseUserSpec { return nil } clone := *u.AtlasDatabaseUserSpec - clone.Project.Name = "" - clone.Project.Namespace = "" + clone.Project = nil clone.PasswordSecret = nil + clone.ExternalProjectRef = nil clone.ConnectionSecret = nil return &clone } diff --git a/internal/translation/dbuser/conversion_test.go b/internal/translation/dbuser/conversion_test.go index 40eee7ad70..4c25bdbbf9 100644 --- a/internal/translation/dbuser/conversion_test.go +++ b/internal/translation/dbuser/conversion_test.go @@ -391,8 +391,10 @@ func TestDiffSpecs(t *testing.T) { {Name: "lake1", Type: "DATA_LAKE"}, {Name: "lake2", Type: "DATA_LAKE"}, } - spec.Project.Name = "some-project" - spec.Project.Namespace = "some-namespace" + spec.Project = &common.ResourceRefNamespaced{ + Name: "some-project", + Namespace: "some-namespace", + } spec.PasswordSecret = &common.ResourceRef{Name: "some-secret-ref"} spec.ConnectionSecret = &api.LocalObjectReference{Name: "some-local-secret-ref"} return spec @@ -407,8 +409,10 @@ func TestDiffSpecs(t *testing.T) { {Name: "lake1", Type: "DATA_LAKE"}, {Name: "lake2", Type: "DATA_LAKE"}, } - spec.Project.Name = "another-project" - spec.Project.Namespace = "another-namespace" + spec.Project = &common.ResourceRefNamespaced{ + Name: "another-project", + Namespace: "another-namespace", + } spec.PasswordSecret = &common.ResourceRef{Name: "another-secret-ref"} spec.ConnectionSecret = &api.LocalObjectReference{Name: "another-local-secret-ref"} return spec diff --git a/internal/translation/project/project.go b/internal/translation/project/project.go index 1d79d609d5..8679e024c3 100644 --- a/internal/translation/project/project.go +++ b/internal/translation/project/project.go @@ -8,6 +8,7 @@ import ( type ProjectService interface { GetProjectByName(ctx context.Context, name string) (*Project, error) + GetProject(ctx context.Context, ID string) (*Project, error) CreateProject(ctx context.Context, project *Project) error DeleteProject(ctx context.Context, project *Project) error } @@ -29,6 +30,15 @@ func (a *ProjectAPI) GetProjectByName(ctx context.Context, name string) (*Projec return fromAtlas(group), err } +func (a *ProjectAPI) GetProject(ctx context.Context, ID string) (*Project, error) { + group, _, err := a.projectAPI.GetProject(ctx, ID).Execute() + if err != nil { + return nil, err + } + + return fromAtlas(group), err +} + func (a *ProjectAPI) CreateProject(ctx context.Context, project *Project) error { group, _, err := a.projectAPI.CreateProject(ctx, toAtlas(project)).Execute() if err != nil { diff --git a/pkg/api/v1/atlasdatabaseuser_types.go b/pkg/api/v1/atlasdatabaseuser_types.go index bbbdcdf58e..c8a9dfeda6 100644 --- a/pkg/api/v1/atlasdatabaseuser_types.go +++ b/pkg/api/v1/atlasdatabaseuser_types.go @@ -51,11 +51,15 @@ const ( ) // AtlasDatabaseUserSpec defines the desired state of Database User in Atlas +// +kubebuilder:validation:XValidation:rule="(has(self.externalProjectRef) && !has(self.projectRef)) || (!has(self.externalProjectRef) && has(self.projectRef))",message="must define only one project reference through externalProjectRef or projectRef" +// +kubebuilder:validation:XValidation:rule="(has(self.externalProjectRef) && has(self.connectionSecret)) || !has(self.externalProjectRef)",message="must define a local connection secret when referencing an external project" type AtlasDatabaseUserSpec struct { api.LocalCredentialHolder `json:",inline"` // Project is a reference to AtlasProject resource the user belongs to - Project common.ResourceRefNamespaced `json:"projectRef"` + Project *common.ResourceRefNamespaced `json:"projectRef,omitempty"` + // ExternalProjectRef holds the Atlas project ID the user belongs to + ExternalProjectRef *ExternalProjectReference `json:"externalProjectRef,omitempty"` // DatabaseName is a Database against which Atlas authenticates the user. Default value is 'admin'. // +kubebuilder:default=admin @@ -113,8 +117,6 @@ type AtlasDatabaseUserSpec struct { X509Type string `json:"x509Type,omitempty"` } -var _ api.AtlasCustomResource = &AtlasDatabaseUser{} - // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:object:root=true // +kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name` @@ -255,7 +257,7 @@ func NewDBUser(namespace, name, dbUserName, projectName string) *AtlasDatabaseUs }, Spec: AtlasDatabaseUserSpec{ Username: dbUserName, - Project: common.ResourceRefNamespaced{Name: projectName}, + Project: &common.ResourceRefNamespaced{Name: projectName}, PasswordSecret: &common.ResourceRef{}, Roles: []RoleSpec{}, Scopes: []ScopeSpec{}, diff --git a/pkg/api/v1/atlasdatabaseuser_types_test.go b/pkg/api/v1/atlasdatabaseuser_types_test.go new file mode 100644 index 0000000000..8b6c7689f4 --- /dev/null +++ b/pkg/api/v1/atlasdatabaseuser_types_test.go @@ -0,0 +1,157 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cel" +) + +func TestProjectReference(t *testing.T) { + validator, err := cel.VersionValidatorFromFile(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml", "v1") + require.NoError(t, err) + + tests := map[string]struct { + dbUser *AtlasDatabaseUser + expectedErrors []string + }{ + "no project reference is set": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{}, + }, + expectedErrors: []string{"spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef"}, + }, + "both project references are set": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef", + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "external project references is set": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "kubernetes project references is set": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + unstructuredDBUser, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.dbUser) + require.NoError(t, err) + + errs := validator(unstructuredDBUser, nil) + + require.Equal(t, len(tt.expectedErrors), len(errs)) + + for i, err := range errs { + assert.Equal(t, tt.expectedErrors[i], err.Error()) + } + }) + } +} + +func TestExternalProjectReferenceConnectionSecret(t *testing.T) { + validator, err := cel.VersionValidatorFromFile(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml", "v1") + require.NoError(t, err) + + tests := map[string]struct { + dbUser *AtlasDatabaseUser + expectedErrors []string + }{ + "external project references is set without connection secret": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "external project references is set with connection secret": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + "kubernetes project references is set without connection secret": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + "kubernetes project references is set with connection secret": { + dbUser: &AtlasDatabaseUser{ + Spec: AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + unstructuredDBUser, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.dbUser) + require.NoError(t, err) + + errs := validator(unstructuredDBUser, nil) + + require.Equal(t, len(tt.expectedErrors), len(errs)) + + for i, err := range errs { + assert.Equal(t, tt.expectedErrors[i], err.Error()) + } + }) + } +} diff --git a/pkg/api/v1/externalreference.go b/pkg/api/v1/externalreference.go new file mode 100644 index 0000000000..8295807b2a --- /dev/null +++ b/pkg/api/v1/externalreference.go @@ -0,0 +1,7 @@ +package v1 + +type ExternalProjectReference struct { + // ID is the Atlas project ID + // +kubebuilder:validation:Required + ID string `json:"id"` +} diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index fdd4c48b46..938a892577 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -678,7 +678,16 @@ func (in *AtlasDatabaseUserList) DeepCopyObject() runtime.Object { func (in *AtlasDatabaseUserSpec) DeepCopyInto(out *AtlasDatabaseUserSpec) { *out = *in in.LocalCredentialHolder.DeepCopyInto(&out.LocalCredentialHolder) - out.Project = in.Project + if in.Project != nil { + in, out := &in.Project, &out.Project + *out = new(common.ResourceRefNamespaced) + **out = **in + } + if in.ExternalProjectRef != nil { + in, out := &in.ExternalProjectRef, &out.ExternalProjectRef + *out = new(ExternalProjectReference) + **out = **in + } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make([]common.LabelSpec, len(*in)) @@ -1993,6 +2002,21 @@ func (in *EndpointSpec) DeepCopy() *EndpointSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalProjectReference) DeepCopyInto(out *ExternalProjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalProjectReference. +func (in *ExternalProjectReference) DeepCopy() *ExternalProjectReference { + if in == nil { + return nil + } + out := new(ExternalProjectReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPEndpoint) DeepCopyInto(out *GCPEndpoint) { *out = *in diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go index 8c431cbfba..bb1fe497f8 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go @@ -147,8 +147,8 @@ func (r *AtlasDatabaseUserReconciler) terminate( } // unmanage remove finalizer and release resource -func (r *AtlasDatabaseUserReconciler) unmanage(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject) ctrl.Result { - err := connectionsecret.RemoveStaleSecretsByUserName(ctx.Context, r.Client, atlasProject.ID(), atlasDatabaseUser.Spec.Username, *atlasDatabaseUser, r.Log) +func (r *AtlasDatabaseUserReconciler) unmanage(ctx *workflow.Context, projectID string, atlasDatabaseUser *akov2.AtlasDatabaseUser) ctrl.Result { + err := connectionsecret.RemoveStaleSecretsByUserName(ctx.Context, r.Client, projectID, atlasDatabaseUser.Spec.Username, *atlasDatabaseUser, r.Log) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserConnectionSecretsNotDeleted, true, err) } diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go index 49f5e202d4..eeb1d334b0 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go @@ -27,10 +27,9 @@ import ( atlasmock "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" - akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/indexer" ) @@ -47,7 +46,7 @@ func TestReconcile(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", Namespace: "default", }, @@ -72,7 +71,7 @@ func TestReconcile(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", Namespace: "default", }, @@ -95,7 +94,7 @@ func TestReconcile(t *testing.T) { }, }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", Namespace: "default", }, @@ -115,7 +114,7 @@ func TestReconcile(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", Namespace: "default", }, diff --git a/pkg/controller/atlasdatabaseuser/databaseuser.go b/pkg/controller/atlasdatabaseuser/databaseuser.go index 0da3ced362..84042d2498 100644 --- a/pkg/controller/atlasdatabaseuser/databaseuser.go +++ b/pkg/controller/atlasdatabaseuser/databaseuser.go @@ -8,10 +8,12 @@ import ( corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/timeutil" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/connectionsecret" @@ -21,10 +23,10 @@ import ( ) func (r *AtlasDatabaseUserReconciler) handleDatabaseUser(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser) ctrl.Result { - valid, validationErr := customresource.ResourceVersionIsValid(atlasDatabaseUser) + valid, err := customresource.ResourceVersionIsValid(atlasDatabaseUser) switch { - case validationErr != nil: - return r.terminate(ctx, atlasDatabaseUser, api.ResourceVersionStatus, workflow.AtlasResourceVersionIsInvalid, true, validationErr) + case err != nil: + return r.terminate(ctx, atlasDatabaseUser, api.ResourceVersionStatus, workflow.AtlasResourceVersionIsInvalid, true, err) case !valid: return r.terminate( ctx, @@ -43,28 +45,21 @@ func (r *AtlasDatabaseUserReconciler) handleDatabaseUser(ctx *workflow.Context, return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.AtlasGovUnsupported, false, fmt.Errorf("the %T is not supported by Atlas for government", atlasDatabaseUser)) } - atlasProject := &akov2.AtlasProject{} - if err := r.Client.Get(ctx.Context, atlasDatabaseUser.AtlasProjectObjectKey(), atlasProject); err != nil { - return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) - } - - credentialsSecret, err := customresource.ComputeSecret(atlasProject, atlasDatabaseUser) - if err != nil { - return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.AtlasAPIAccessNotConfigured, true, err) + var atlasProject *project.Project + if atlasDatabaseUser.Spec.ExternalProjectRef != nil { + atlasProject, err = r.getProjectFromAtlas(ctx, atlasDatabaseUser) + } else { + atlasProject, err = r.getProjectFromKube(ctx, atlasDatabaseUser) } - sdkClient, _, err := r.AtlasProvider.SdkClient(ctx.Context, credentialsSecret, r.Log) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.AtlasAPIAccessNotConfigured, true, err) } - r.dbUserService = dbuser.NewAtlasUsers(sdkClient.DatabaseUsersApi) - r.deploymentService = deployment.NewAtlasDeployments(sdkClient.ClustersApi, sdkClient.ServerlessInstancesApi, r.AtlasProvider.IsCloudGov()) - return r.dbuLifeCycle(ctx, atlasDatabaseUser, atlasProject) } -func (r *AtlasDatabaseUserReconciler) dbuLifeCycle(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject) ctrl.Result { - databaseUserInAtlas, err := r.dbUserService.Get(ctx.Context, atlasDatabaseUser.Spec.DatabaseName, atlasProject.ID(), atlasDatabaseUser.Spec.Username) +func (r *AtlasDatabaseUserReconciler) dbuLifeCycle(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *project.Project) ctrl.Result { + databaseUserInAtlas, err := r.dbUserService.Get(ctx.Context, atlasDatabaseUser.Spec.DatabaseName, atlasProject.ID, atlasDatabaseUser.Spec.Username) if err != nil && !errors.Is(err, dbuser.ErrorNotFound) { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } @@ -74,7 +69,7 @@ func (r *AtlasDatabaseUserReconciler) dbuLifeCycle(ctx *workflow.Context, atlasD return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserInvalidSpec, false, err) } if expired { - err = connectionsecret.RemoveStaleSecretsByUserName(ctx.Context, r.Client, atlasProject.ID(), atlasDatabaseUser.Spec.Username, *atlasDatabaseUser, r.Log) + err = connectionsecret.RemoveStaleSecretsByUserName(ctx.Context, r.Client, atlasProject.ID, atlasDatabaseUser.Spec.Username, *atlasDatabaseUser, r.Log) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserConnectionSecretsNotDeleted, true, err) } @@ -82,7 +77,7 @@ func (r *AtlasDatabaseUserReconciler) dbuLifeCycle(ctx *workflow.Context, atlasD return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserExpired, false, errors.New("an expired user cannot be managed")) } - scopesAreValid, err := r.areDeploymentScopesValid(ctx, atlasDatabaseUser, atlasProject) + scopesAreValid, err := r.areDeploymentScopesValid(ctx, atlasProject.ID, atlasDatabaseUser) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserInvalidSpec, false, err) } @@ -95,17 +90,17 @@ func (r *AtlasDatabaseUserReconciler) dbuLifeCycle(ctx *workflow.Context, atlasD switch { case !dbUserExists && !wasDeleted: - return r.create(ctx, atlasDatabaseUser, atlasProject) + return r.create(ctx, atlasProject.ID, atlasDatabaseUser) case dbUserExists && !wasDeleted: - return r.update(ctx, atlasDatabaseUser, atlasProject, databaseUserInAtlas) + return r.update(ctx, atlasProject, atlasDatabaseUser, databaseUserInAtlas) case dbUserExists && wasDeleted: - return r.delete(ctx, atlasDatabaseUser, atlasProject) + return r.delete(ctx, atlasProject.ID, atlasDatabaseUser) default: - return r.unmanage(ctx, atlasDatabaseUser, atlasProject) + return r.unmanage(ctx, atlasProject.ID, atlasDatabaseUser) } } -func (r *AtlasDatabaseUserReconciler) create(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject) ctrl.Result { +func (r *AtlasDatabaseUserReconciler) create(ctx *workflow.Context, projectID string, atlasDatabaseUser *akov2.AtlasDatabaseUser) ctrl.Result { if !canManageOIDC(r.FeaturePreviewOIDCAuthEnabled, atlasDatabaseUser.Spec.OIDCAuthType) { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, false, ErrOIDCNotEnabled) } @@ -115,7 +110,7 @@ func (r *AtlasDatabaseUserReconciler) create(ctx *workflow.Context, atlasDatabas return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } - databaseUserInAKO, err := dbuser.NewUser(atlasDatabaseUser.Spec.DeepCopy(), atlasProject.ID(), userPassword) + databaseUserInAKO, err := dbuser.NewUser(atlasDatabaseUser.Spec.DeepCopy(), projectID, userPassword) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } @@ -126,13 +121,13 @@ func (r *AtlasDatabaseUserReconciler) create(ctx *workflow.Context, atlasDatabas } if wasRenamed(atlasDatabaseUser) { - err = connectionsecret.RemoveStaleSecretsByUserName(ctx.Context, r.Client, atlasProject.ID(), atlasDatabaseUser.Status.UserName, *atlasDatabaseUser, r.Log) + err = connectionsecret.RemoveStaleSecretsByUserName(ctx.Context, r.Client, projectID, atlasDatabaseUser.Status.UserName, *atlasDatabaseUser, r.Log) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserConnectionSecretsNotDeleted, true, err) } ctx.Log.Infow("'spec.username' has changed - removing the old user from Atlas", "newUserName", atlasDatabaseUser.Spec.Username, "oldUserName", atlasDatabaseUser.Status.UserName) - if err = r.removeOldUser(ctx.Context, atlasDatabaseUser, atlasProject); err != nil { + if err = r.removeOldUser(ctx.Context, projectID, atlasDatabaseUser); err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } } @@ -140,7 +135,7 @@ func (r *AtlasDatabaseUserReconciler) create(ctx *workflow.Context, atlasDatabas return r.inProgress(ctx, atlasDatabaseUser, passwordVersion, "Clusters are scheduled to handle database users updates") } -func (r *AtlasDatabaseUserReconciler) update(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject, databaseUserInAtlas *dbuser.User) ctrl.Result { +func (r *AtlasDatabaseUserReconciler) update(ctx *workflow.Context, atlasProject *project.Project, atlasDatabaseUser *akov2.AtlasDatabaseUser, databaseUserInAtlas *dbuser.User) ctrl.Result { if !canManageOIDC(r.FeaturePreviewOIDCAuthEnabled, atlasDatabaseUser.Spec.OIDCAuthType) { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, false, ErrOIDCNotEnabled) } @@ -150,13 +145,13 @@ func (r *AtlasDatabaseUserReconciler) update(ctx *workflow.Context, atlasDatabas return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } - databaseUserInAKO, err := dbuser.NewUser(atlasDatabaseUser.Spec.DeepCopy(), atlasProject.ID(), userPassword) + databaseUserInAKO, err := dbuser.NewUser(atlasDatabaseUser.Spec.DeepCopy(), atlasProject.ID, userPassword) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } if !hasChanged(databaseUserInAKO, databaseUserInAtlas, atlasDatabaseUser.Status.PasswordVersion, passwordVersion) { - return r.readiness(ctx, atlasDatabaseUser, atlasProject, passwordVersion) + return r.readiness(ctx, atlasProject, atlasDatabaseUser, passwordVersion) } r.Log.Debug(dbuser.DiffSpecs(databaseUserInAKO, databaseUserInAtlas)) @@ -168,14 +163,14 @@ func (r *AtlasDatabaseUserReconciler) update(ctx *workflow.Context, atlasDatabas return r.inProgress(ctx, atlasDatabaseUser, passwordVersion, "Clusters are scheduled to handle database users updates") } -func (r *AtlasDatabaseUserReconciler) delete(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject) ctrl.Result { +func (r *AtlasDatabaseUserReconciler) delete(ctx *workflow.Context, projectID string, atlasDatabaseUser *akov2.AtlasDatabaseUser) ctrl.Result { if customresource.IsResourcePolicyKeepOrDefault(atlasDatabaseUser, r.ObjectDeletionProtection) { r.Log.Info("Not removing Atlas database user from Atlas as per configuration") - return r.unmanage(ctx, atlasDatabaseUser, atlasProject) + return r.unmanage(ctx, projectID, atlasDatabaseUser) } - err := r.dbUserService.Delete(ctx.Context, atlasDatabaseUser.Spec.DatabaseName, atlasProject.ID(), atlasDatabaseUser.Spec.Username) + err := r.dbUserService.Delete(ctx.Context, atlasDatabaseUser.Spec.DatabaseName, projectID, atlasDatabaseUser.Spec.Username) if err != nil { if !errors.Is(err, dbuser.ErrorNotFound) { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserNotDeletedInAtlas, true, err) @@ -184,11 +179,11 @@ func (r *AtlasDatabaseUserReconciler) delete(ctx *workflow.Context, atlasDatabas r.Log.Info("Database user doesn't exist or is already deleted") } - return r.unmanage(ctx, atlasDatabaseUser, atlasProject) + return r.unmanage(ctx, projectID, atlasDatabaseUser) } -func (r *AtlasDatabaseUserReconciler) readiness(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject, passwordVersion string) ctrl.Result { - allDeploymentNames, err := r.deploymentService.ListClusterNames(ctx.Context, atlasProject.ID()) +func (r *AtlasDatabaseUserReconciler) readiness(ctx *workflow.Context, atlasProject *project.Project, atlasDatabaseUser *akov2.AtlasDatabaseUser, passwordVersion string) ctrl.Result { + allDeploymentNames, err := r.deploymentService.ListClusterNames(ctx.Context, atlasProject.ID) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } @@ -200,7 +195,7 @@ func (r *AtlasDatabaseUserReconciler) readiness(ctx *workflow.Context, atlasData readyDeployments := 0 for _, c := range deploymentsToCheck { - ready, err := r.deploymentService.DeploymentIsReady(ctx.Context, atlasProject.ID(), c) + ready, err := r.deploymentService.DeploymentIsReady(ctx.Context, atlasProject.ID, c) if err != nil { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.Internal, true, err) } @@ -219,7 +214,7 @@ func (r *AtlasDatabaseUserReconciler) readiness(ctx *workflow.Context, atlasData } // TODO refactor connectionsecret package to follow state machine approach - result := connectionsecret.CreateOrUpdateConnectionSecrets(ctx, r.Client, r.deploymentService, r.EventRecorder, *atlasProject, *atlasDatabaseUser) + result := connectionsecret.CreateOrUpdateConnectionSecrets(ctx, r.Client, r.deploymentService, r.EventRecorder, atlasProject, *atlasDatabaseUser) if !result.IsOk() { return r.terminate(ctx, atlasDatabaseUser, api.DatabaseUserReadyType, workflow.DatabaseUserConnectionSecretsNotCreated, true, errors.New(result.GetMessage())) } @@ -248,9 +243,9 @@ func (r *AtlasDatabaseUserReconciler) readPassword(ctx context.Context, atlasDat } } -func (r *AtlasDatabaseUserReconciler) areDeploymentScopesValid(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject) (bool, error) { +func (r *AtlasDatabaseUserReconciler) areDeploymentScopesValid(ctx *workflow.Context, projectID string, atlasDatabaseUser *akov2.AtlasDatabaseUser) (bool, error) { for _, s := range atlasDatabaseUser.GetScopes(akov2.DeploymentScopeType) { - exists, err := r.deploymentService.ClusterExists(ctx.Context, atlasProject.ID(), s) + exists, err := r.deploymentService.ClusterExists(ctx.Context, projectID, s) if err != nil { return false, err } @@ -262,11 +257,11 @@ func (r *AtlasDatabaseUserReconciler) areDeploymentScopesValid(ctx *workflow.Con return true, nil } -func (r *AtlasDatabaseUserReconciler) removeOldUser(ctx context.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser, atlasProject *akov2.AtlasProject) error { +func (r *AtlasDatabaseUserReconciler) removeOldUser(ctx context.Context, projectID string, atlasDatabaseUser *akov2.AtlasDatabaseUser) error { deleteAttempts := 3 var err error for i := 1; i <= deleteAttempts; i++ { - err = r.dbUserService.Delete(ctx, atlasDatabaseUser.Spec.DatabaseName, atlasProject.ID(), atlasDatabaseUser.Status.UserName) + err = r.dbUserService.Delete(ctx, atlasDatabaseUser.Spec.DatabaseName, projectID, atlasDatabaseUser.Status.UserName) if err == nil || errors.Is(err, dbuser.ErrorNotFound) { return nil } @@ -279,6 +274,50 @@ func (r *AtlasDatabaseUserReconciler) removeOldUser(ctx context.Context, atlasDa return err } +func (r *AtlasDatabaseUserReconciler) getProjectFromAtlas(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser) (*project.Project, error) { + sdkClient, _, err := r.AtlasProvider.SdkClient( + ctx.Context, + &client.ObjectKey{Namespace: atlasDatabaseUser.Namespace, Name: atlasDatabaseUser.Credentials().Name}, + r.Log, + ) + if err != nil { + return nil, err + } + + projectService := project.NewProjectAPIService(sdkClient.ProjectsApi) + r.dbUserService = dbuser.NewAtlasUsers(sdkClient.DatabaseUsersApi) + r.deploymentService = deployment.NewAtlasDeployments(sdkClient.ClustersApi, sdkClient.ServerlessInstancesApi, r.AtlasProvider.IsCloudGov()) + + atlasProject, err := projectService.GetProject(ctx.Context, atlasDatabaseUser.Spec.ExternalProjectRef.ID) + if err != nil { + return nil, err + } + + return atlasProject, nil +} + +func (r *AtlasDatabaseUserReconciler) getProjectFromKube(ctx *workflow.Context, atlasDatabaseUser *akov2.AtlasDatabaseUser) (*project.Project, error) { + atlasProject := &akov2.AtlasProject{} + if err := r.Client.Get(ctx.Context, atlasDatabaseUser.AtlasProjectObjectKey(), atlasProject); err != nil { + return nil, err + } + + credentialsSecret, err := customresource.ComputeSecret(atlasProject, atlasDatabaseUser) + if err != nil { + return nil, err + } + + sdkClient, orgID, err := r.AtlasProvider.SdkClient(ctx.Context, credentialsSecret, r.Log) + if err != nil { + return nil, err + } + + r.dbUserService = dbuser.NewAtlasUsers(sdkClient.DatabaseUsersApi) + r.deploymentService = deployment.NewAtlasDeployments(sdkClient.ClustersApi, sdkClient.ServerlessInstancesApi, r.AtlasProvider.IsCloudGov()) + + return project.NewProject(atlasProject, orgID), nil +} + func canManageOIDC(isEnabled bool, oidcType string) bool { if !isEnabled && (oidcType != "" && oidcType != "NONE") { return false diff --git a/pkg/controller/atlasdatabaseuser/databaseuser_test.go b/pkg/controller/atlasdatabaseuser/databaseuser_test.go index 9aa894428d..edd1a9bc6f 100644 --- a/pkg/controller/atlasdatabaseuser/databaseuser_test.go +++ b/pkg/controller/atlasdatabaseuser/databaseuser_test.go @@ -24,8 +24,10 @@ import ( atlasmock "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/translation" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" @@ -126,19 +128,20 @@ func TestHandleDatabaseUser(t *testing.T) { WithMessageRegexp("the *v1.AtlasDatabaseUser is not supported by Atlas for government"), }, }, - "failed to get project": { + "manage user with independent configuration": { dbUserInAKO: &akov2.AtlasDatabaseUser{ ObjectMeta: metav1.ObjectMeta{ Name: "user1", Namespace: "default", - Labels: map[string]string{ - "mongodb.com/atlas-resource-version": "2.4.1", - }, }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ - Name: "my-project", - Namespace: "default", + ExternalProjectRef: &akov2.ExternalProjectReference{ + ID: "project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "project-creds", + }, }, Username: "user1", PasswordSecret: &common.ResourceRef{ @@ -147,56 +150,45 @@ func TestHandleDatabaseUser(t *testing.T) { DatabaseName: "admin", }, }, - atlasProvider: &atlasmock.TestProvider{ - IsSupportedFunc: func() bool { - return true - }, - }, - expectedResult: ctrl.Result{RequeueAfter: workflow.DefaultRetry}, - expectedConditions: []api.Condition{ - api.TrueCondition(api.ResourceVersionStatus), - api.TrueCondition(api.ValidationSucceeded), - api.FalseCondition(api.DatabaseUserReadyType). - WithReason(string(workflow.Internal)). - WithMessageRegexp("atlasprojects.atlas.mongodb.com \"my-project\" not found"), - }, - }, - "failed to create atlas sdk": { - dbUserInAKO: &akov2.AtlasDatabaseUser{ + dbUserSecret: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "user1", + Name: "user-pass", Namespace: "default", Labels: map[string]string{ - "mongodb.com/atlas-resource-version": "2.4.1", - }, - }, - Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ - Name: "my-project", - Namespace: "default", - }, - Username: "user1", - PasswordSecret: &common.ResourceRef{ - Name: "user-pass", + "atlas.mongodb.com/type": "credentials", }, - DatabaseName: "admin", - }, - }, - atlasProject: &akov2.AtlasProject{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-project", - Namespace: "default", }, - Spec: akov2.AtlasProjectSpec{ - Name: "my-project", + Data: map[string][]byte{ + "password": []byte("Passw0rd!"), }, }, atlasProvider: &atlasmock.TestProvider{ IsSupportedFunc: func() bool { return true }, + IsCloudGovFunc: func() bool { + return false + }, SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { - return nil, "", errors.New("failed to create client") + projectAPI := mockadmin.NewProjectsApi(t) + projectAPI.EXPECT().GetProject(context.Background(), "project-id"). + Return(admin.GetProjectApiRequest{ApiService: projectAPI}) + projectAPI.EXPECT().GetProjectExecute(mock.AnythingOfType("admin.GetProjectApiRequest")). + Return(&admin.Group{Id: pointer.MakePtr("project-id")}, nil, nil) + + userAPI := mockadmin.NewDatabaseUsersApi(t) + userAPI.EXPECT().GetDatabaseUser(context.Background(), "project-id", "admin", "user1"). + Return(admin.GetDatabaseUserApiRequest{ApiService: userAPI}) + userAPI.EXPECT().GetDatabaseUserExecute(mock.AnythingOfType("admin.GetDatabaseUserApiRequest")). + Return(nil, nil, nil) + userAPI.EXPECT().CreateDatabaseUser(context.Background(), "project-id", mock.AnythingOfType("*admin.CloudDatabaseUser")). + Return(admin.CreateDatabaseUserApiRequest{ApiService: userAPI}) + userAPI.EXPECT().CreateDatabaseUserExecute(mock.AnythingOfType("admin.CreateDatabaseUserApiRequest")). + Return(&admin.CloudDatabaseUser{}, nil, nil) + + clusterAPI := mockadmin.NewClustersApi(t) + + return &admin.APIClient{ProjectsApi: projectAPI, ClustersApi: clusterAPI, DatabaseUsersApi: userAPI}, "", nil }, }, expectedResult: ctrl.Result{RequeueAfter: workflow.DefaultRetry}, @@ -204,11 +196,11 @@ func TestHandleDatabaseUser(t *testing.T) { api.TrueCondition(api.ResourceVersionStatus), api.TrueCondition(api.ValidationSucceeded), api.FalseCondition(api.DatabaseUserReadyType). - WithReason(string(workflow.AtlasAPIAccessNotConfigured)). - WithMessageRegexp("failed to create client"), + WithReason(string(workflow.DatabaseUserDeploymentAppliedChanges)). + WithMessageRegexp("Clusters are scheduled to handle database users updates"), }, }, - "manage user": { + "manage user with linked configuration": { dbUserInAKO: &akov2.AtlasDatabaseUser{ ObjectMeta: metav1.ObjectMeta{ Name: "user1", @@ -218,7 +210,7 @@ func TestHandleDatabaseUser(t *testing.T) { }, }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", Namespace: "default", }, @@ -820,7 +812,7 @@ func TestDbuLifeCycle(t *testing.T) { Log: logger, } - result := r.dbuLifeCycle(ctx, tt.dbUserInAKO, &akov2.AtlasProject{}) + result := r.dbuLifeCycle(ctx, tt.dbUserInAKO, &project.Project{}) assert.Equal(t, tt.expectedResult, result) assert.True( t, @@ -999,7 +991,7 @@ func TestCreate(t *testing.T) { dbUserService: func() dbuser.AtlasUsersService { service := translation.NewAtlasUsersServiceMock(t) service.EXPECT().Create(context.Background(), mock.AnythingOfType("*dbuser.User")).Return(nil) - service.EXPECT().Delete(context.Background(), "admin", "", "user1").Return(nil) + service.EXPECT().Delete(context.Background(), "admin", "project-id", "user1").Return(nil) return service }, @@ -1081,7 +1073,7 @@ func TestCreate(t *testing.T) { Log: logger, } - result := r.create(ctx, tt.dbUserInAKO, &akov2.AtlasProject{}) + result := r.create(ctx, "project-id", tt.dbUserInAKO) assert.Equal(t, tt.expectedResult, result) assert.True( t, @@ -1388,7 +1380,7 @@ func TestUpdate(t *testing.T) { Log: logger, } - result := r.update(ctx, tt.dbUserInAKO, &akov2.AtlasProject{}, tt.dbUserInAtlas) + result := r.update(ctx, &project.Project{}, tt.dbUserInAKO, tt.dbUserInAtlas) assert.Equal(t, tt.expectedResult, result) assert.True( t, @@ -1469,7 +1461,7 @@ func TestDelete(t *testing.T) { }, dbUserService: func() dbuser.AtlasUsersService { service := translation.NewAtlasUsersServiceMock(t) - service.EXPECT().Delete(context.Background(), "admin", "", "user1"). + service.EXPECT().Delete(context.Background(), "admin", "project-id", "user1"). Return(errors.New("failed to delete user")) return service @@ -1498,7 +1490,7 @@ func TestDelete(t *testing.T) { }, dbUserService: func() dbuser.AtlasUsersService { service := translation.NewAtlasUsersServiceMock(t) - service.EXPECT().Delete(context.Background(), "admin", "", "user1"). + service.EXPECT().Delete(context.Background(), "admin", "project-id", "user1"). Return(dbuser.ErrorNotFound) return service @@ -1523,7 +1515,7 @@ func TestDelete(t *testing.T) { }, dbUserService: func() dbuser.AtlasUsersService { service := translation.NewAtlasUsersServiceMock(t) - service.EXPECT().Delete(context.Background(), "admin", "", "user1"). + service.EXPECT().Delete(context.Background(), "admin", "project-id", "user1"). Return(nil) return service @@ -1556,7 +1548,7 @@ func TestDelete(t *testing.T) { Log: logger, } - result := r.delete(ctx, tt.dbUser, &akov2.AtlasProject{}) + result := r.delete(ctx, "project-id", tt.dbUser) assert.Equal(t, tt.expectedResult, result) assert.True( t, @@ -1770,7 +1762,7 @@ func TestReadiness(t *testing.T) { Log: logger, } - result := r.readiness(ctx, tt.dbUser, &akov2.AtlasProject{}, "999") + result := r.readiness(ctx, &project.Project{}, tt.dbUser, "999") assert.Equal(t, tt.expectedResult, result) assert.True( t, @@ -1911,10 +1903,10 @@ func TestReadPassword(t *testing.T) { Client: k8sClient.Build(), } - pass, version, err := r.readPassword(context.Background(), tt.dbUser) + pass, passVersion, err := r.readPassword(context.Background(), tt.dbUser) assert.Equal(t, tt.expectedErr, err) assert.Equal(t, tt.expectedPassword, pass) - assert.Equal(t, tt.expectedVersion, version) + assert.Equal(t, tt.expectedVersion, passVersion) }) } } @@ -2014,7 +2006,7 @@ func TestAreDeploymentScopesValid(t *testing.T) { t.Run(name, func(t *testing.T) { deploymentService := translation.NewAtlasDeploymentsServiceMock(t) if tt.call != nil { - deploymentService.EXPECT().ClusterExists(context.Background(), "", mock.AnythingOfType("string")). + deploymentService.EXPECT().ClusterExists(context.Background(), "project-id", mock.AnythingOfType("string")). RunAndReturn(tt.call) } r := AtlasDatabaseUserReconciler{ @@ -2023,7 +2015,7 @@ func TestAreDeploymentScopesValid(t *testing.T) { ctx := &workflow.Context{ Context: context.Background(), } - valid, err := r.areDeploymentScopesValid(ctx, tt.dbUser, &akov2.AtlasProject{}) + valid, err := r.areDeploymentScopesValid(ctx, "project-id", tt.dbUser) assert.Equal(t, tt.err, err) assert.Equal(t, tt.expected, valid) }) @@ -2127,14 +2119,380 @@ func TestRemoveOldUser(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { dbUserService := translation.NewAtlasUsersServiceMock(t) - dbUserService.EXPECT().Delete(context.Background(), "", "", ""). + dbUserService.EXPECT().Delete(context.Background(), "admin", "project-id", "old-name"). RunAndReturn(tt.call) r := &AtlasDatabaseUserReconciler{ Log: zaptest.NewLogger(t).Sugar(), dbUserService: dbUserService, } - assert.Equal(t, tt.err, r.removeOldUser(context.Background(), &akov2.AtlasDatabaseUser{}, &akov2.AtlasProject{})) + user := &akov2.AtlasDatabaseUser{ + Spec: akov2.AtlasDatabaseUserSpec{ + Username: "user1", + DatabaseName: "admin", + }, + Status: status.AtlasDatabaseUserStatus{ + UserName: "old-name", + }, + } + assert.Equal(t, tt.err, r.removeOldUser(context.Background(), "project-id", user)) + }) + } +} + +func TestGetProjectFromAtlas(t *testing.T) { + tests := map[string]struct { + dbUserInAKO *akov2.AtlasDatabaseUser + dbUserSecret *corev1.Secret + atlasProvider atlas.Provider + expectedErr error + }{ + "failed to create atlas client": { + dbUserInAKO: &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: "project-creds", + }, + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + atlasProvider: &atlasmock.TestProvider{ + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + return nil, "", errors.New("failed to create client") + }, + }, + expectedErr: errors.New("failed to create client"), + }, + "failed to get project from atlas": { + dbUserInAKO: &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: "project-creds", + }, + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsCloudGovFunc: func() bool { + return false + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + projectAPI := mockadmin.NewProjectsApi(t) + projectAPI.EXPECT().GetProject(context.Background(), "project-id"). + Return(admin.GetProjectApiRequest{ApiService: projectAPI}) + projectAPI.EXPECT().GetProjectExecute(mock.AnythingOfType("admin.GetProjectApiRequest")). + Return(nil, nil, errors.New("failed to get project")) + + return &admin.APIClient{ProjectsApi: projectAPI}, "", nil + }, + }, + expectedErr: errors.New("failed to get project"), + }, + "get project": { + dbUserInAKO: &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: "project-creds", + }, + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + dbUserSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-pass", + Namespace: "default", + Labels: map[string]string{ + "atlas.mongodb.com/type": "credentials", + }, + }, + Data: map[string][]byte{ + "password": []byte("Passw0rd!"), + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsCloudGovFunc: func() bool { + return false + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + projectAPI := mockadmin.NewProjectsApi(t) + projectAPI.EXPECT().GetProject(context.Background(), "project-id"). + Return(admin.GetProjectApiRequest{ApiService: projectAPI}) + projectAPI.EXPECT().GetProjectExecute(mock.AnythingOfType("admin.GetProjectApiRequest")). + Return(&admin.Group{Id: pointer.MakePtr("project-id")}, nil, nil) + + return &admin.APIClient{ProjectsApi: projectAPI}, "", nil + }, + }, + }, + } + + 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.dbUserInAKO). + WithStatusSubresource(tt.dbUserInAKO) + + if tt.dbUserSecret != nil { + k8sClient.WithObjects(tt.dbUserSecret) + } + + logger := zaptest.NewLogger(t).Sugar() + r := AtlasDatabaseUserReconciler{ + Client: k8sClient.Build(), + AtlasProvider: tt.atlasProvider, + Log: logger, + } + ctx := &workflow.Context{ + Context: context.Background(), + Log: logger, + } + version.Version = "2.4.1" + + _, err := r.getProjectFromAtlas(ctx, tt.dbUserInAKO) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestGetProjectFromKube(t *testing.T) { + tests := map[string]struct { + dbUserInAKO *akov2.AtlasDatabaseUser + project *akov2.AtlasProject + projectSecret *corev1.Secret + atlasProvider atlas.Provider + expectedErr error + }{ + "failed to get project": { + dbUserInAKO: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + Labels: map[string]string{ + "mongodb.com/atlas-resource-version": "2.4.1", + }, + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsSupportedFunc: func() bool { + return true + }, + }, + expectedErr: &k8serrors.StatusError{ + ErrStatus: metav1.Status{ + Status: "Failure", + Message: "atlasprojects.atlas.mongodb.com \"my-project\" not found", + Reason: "NotFound", + Code: 404, + Details: &metav1.StatusDetails{ + Group: "atlas.mongodb.com", + Kind: "atlasprojects", + Name: "my-project", + }, + }, + }, + }, + "failed to create atlas sdk": { + dbUserInAKO: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + Labels: map[string]string{ + "mongodb.com/atlas-resource-version": "2.4.1", + }, + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + project: &akov2.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-project", + Namespace: "default", + }, + Spec: akov2.AtlasProjectSpec{ + Name: "my-project", + ConnectionSecret: &common.ResourceRefNamespaced{ + Name: "project-secret", + }, + }, + }, + projectSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "project-secret", + Namespace: "default", + Labels: map[string]string{ + "atlas.mongodb.com/type": "credentials", + }, + }, + Data: map[string][]byte{ + "publicKey": []byte("publicKey"), + "privateKey": []byte("privateKey"), + "orgID": []byte("orgID"), + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsSupportedFunc: func() bool { + return true + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + return nil, "", errors.New("failed to create client") + }, + }, + expectedErr: errors.New("failed to create client"), + }, + "get project": { + dbUserInAKO: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "project-creds", + }, + }, + Username: "user1", + PasswordSecret: &common.ResourceRef{ + Name: "user-pass", + }, + DatabaseName: "admin", + }, + }, + project: &akov2.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-project", + Namespace: "default", + }, + Spec: akov2.AtlasProjectSpec{ + Name: "my-project", + ConnectionSecret: &common.ResourceRefNamespaced{ + Name: "project-secret", + }, + }, + }, + projectSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "project-secret", + Namespace: "default", + Labels: map[string]string{ + "atlas.mongodb.com/type": "credentials", + }, + }, + Data: map[string][]byte{ + "publicKey": []byte("publicKey"), + "privateKey": []byte("privateKey"), + "orgID": []byte("orgID"), + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsCloudGovFunc: func() bool { + return false + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + return &admin.APIClient{}, "", nil + }, + }, + }, + } + + 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.dbUserInAKO). + WithStatusSubresource(tt.dbUserInAKO) + + if tt.project != nil { + k8sClient.WithObjects(tt.project) + } + + if tt.projectSecret != nil { + k8sClient.WithObjects(tt.projectSecret) + } + + logger := zaptest.NewLogger(t).Sugar() + r := AtlasDatabaseUserReconciler{ + Client: k8sClient.Build(), + AtlasProvider: tt.atlasProvider, + Log: logger, + } + ctx := &workflow.Context{ + Context: context.Background(), + Log: logger, + } + version.Version = "2.4.1" + + _, err := r.getProjectFromKube(ctx, tt.dbUserInAKO) + assert.Equal(t, tt.expectedErr, err) }) } } diff --git a/pkg/controller/atlasdeployment/advanced_deployment.go b/pkg/controller/atlasdeployment/advanced_deployment.go index 704c4139c9..f5822b1e89 100644 --- a/pkg/controller/atlasdeployment/advanced_deployment.go +++ b/pkg/controller/atlasdeployment/advanced_deployment.go @@ -6,6 +6,7 @@ import ( "strings" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/fields" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -18,6 +19,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/connectionsecret" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/customresource" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/indexer" ) const FreeTier = "M0" @@ -112,20 +114,32 @@ func (r *AtlasDeploymentReconciler) handleAdvancedDeployment(ctx *workflow.Conte func (r *AtlasDeploymentReconciler) ensureConnectionSecrets(ctx *workflow.Context, deploymentInAKO deployment.Deployment, connection *status.ConnectionStrings) error { databaseUsers := akov2.AtlasDatabaseUserList{} - err := r.Client.List(ctx.Context, &databaseUsers, &client.ListOptions{}) + + // list using resource name + atlasDeployment := deploymentInAKO.GetCustomResource() + listOpts := &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(indexer.AtlasDatabaseUserByProjectsRefIndex, atlasDeployment.Spec.Project.GetObject(atlasDeployment.Namespace).String()), + } + err := r.Client.List(ctx.Context, &databaseUsers, listOpts) if err != nil { return err } - atlasDeployment := deploymentInAKO.GetCustomResource() - secrets := make([]string, 0) - for i := range databaseUsers.Items { - dbUser := databaseUsers.Items[i] + dbUsers := databaseUsers.Items - if !dbUserBelongsToProject(&dbUser, atlasDeployment.Spec.Project.GetObject(atlasDeployment.Namespace)) { - continue - } + // list using project id + listOpts = &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(indexer.AtlasDatabaseUserByExternalProjectsRefIndex, deploymentInAKO.GetProjectID()), + } + err = r.Client.List(ctx.Context, &databaseUsers, listOpts) + if err != nil { + return err + } + dbUsers = append(dbUsers, databaseUsers.Items...) + + secrets := make([]string, 0) + for _, dbUser := range dbUsers { found := false for _, c := range dbUser.Status.Conditions { if c.Type == api.ReadyType && c.Status == v1.ConditionTrue { @@ -212,19 +226,3 @@ func (r *AtlasDeploymentReconciler) ensureAdvancedOptions(ctx *workflow.Context, return nil } - -func dbUserBelongsToProject(dbUser *akov2.AtlasDatabaseUser, projectRef *client.ObjectKey) bool { - if dbUser.Spec.Project.Name != projectRef.Name { - return false - } - - if dbUser.Spec.Project.Namespace == "" && dbUser.Namespace != projectRef.Namespace { - return false - } - - if dbUser.Spec.Project.Namespace != "" && dbUser.Spec.Project.Namespace != projectRef.Namespace { - return false - } - - return true -} diff --git a/pkg/controller/atlasdeployment/advanced_deployment_test.go b/pkg/controller/atlasdeployment/advanced_deployment_test.go index 2c7b45b8fa..d4b639e1c6 100644 --- a/pkg/controller/atlasdeployment/advanced_deployment_test.go +++ b/pkg/controller/atlasdeployment/advanced_deployment_test.go @@ -15,7 +15,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/translation" @@ -23,7 +22,6 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" ) @@ -752,73 +750,3 @@ func TestHandleAdvancedDeployment(t *testing.T) { }) } } - -func TestDbUserBelongsToProjects(t *testing.T) { - t.Run("Database User refer to a different project name", func(*testing.T) { - dbUser := &akov2.AtlasDatabaseUser{ - Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ - Name: "project2", - }, - }, - } - project := &client.ObjectKey{ - Name: "project1", - } - - assert.False(t, dbUserBelongsToProject(dbUser, project)) - }) - - t.Run("Database User is no", func(*testing.T) { - dbUser := &akov2.AtlasDatabaseUser{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns-2", - }, - Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ - Name: "project1", - }, - }, - } - project := &client.ObjectKey{ - Name: "project1", - Namespace: "ns-1", - } - - assert.False(t, dbUserBelongsToProject(dbUser, project)) - }) - - t.Run("Database User refer to a project with same name but in another namespace", func(*testing.T) { - dbUser := &akov2.AtlasDatabaseUser{ - Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ - Name: "project1", - Namespace: "ns-2", - }, - }, - } - project := &client.ObjectKey{ - Name: "project1", - Namespace: "ns-1", - } - - assert.False(t, dbUserBelongsToProject(dbUser, project)) - }) - - t.Run("Database User refer to a valid project and namespace", func(*testing.T) { - dbUser := &akov2.AtlasDatabaseUser{ - Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ - Name: "project1", - Namespace: "ns-1", - }, - }, - } - project := &client.ObjectKey{ - Name: "project1", - Namespace: "ns-1", - } - - assert.True(t, dbUserBelongsToProject(dbUser, project)) - }) -} diff --git a/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go b/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go index fc76a19607..264ed504ab 100644 --- a/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go +++ b/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go @@ -363,19 +363,24 @@ func TestRegularClusterReconciliation(t *testing.T) { } d.Spec.DeploymentSpec.SearchNodes = searchNodes + logger := zaptest.NewLogger(t) + sch := runtime.NewScheme() require.NoError(t, akov2.AddToScheme(sch)) require.NoError(t, corev1.AddToScheme(sch)) // Subresources need to be explicitly set now since controller-runtime 1.15 // https://github.com/kubernetes-sigs/controller-runtime/issues/2362#issuecomment-1698194188 + projectsRefIndexer := indexer.NewAtlasDatabaseUserByProjectsRefIndexer(logger) + externalProjectsRefIndexer := indexer.NewAtlasDatabaseUserByExternalProjectsRefIndexer(logger) k8sClient := fake.NewClientBuilder(). WithScheme(sch). WithObjects(secret, project, bPolicy, bSchedule, d). WithStatusSubresource(bPolicy, bSchedule). + WithIndex(projectsRefIndexer.Object(), projectsRefIndexer.Name(), projectsRefIndexer.Keys). + WithIndex(externalProjectsRefIndexer.Object(), externalProjectsRefIndexer.Name(), externalProjectsRefIndexer.Keys). Build() orgID := "0987654321" - logger := zaptest.NewLogger(t).Sugar() atlasProvider := &atlasmock.TestProvider{ SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { clusterAPI := mockadmin.NewClustersApi(t) @@ -522,7 +527,7 @@ func TestRegularClusterReconciliation(t *testing.T) { reconciler := &AtlasDeploymentReconciler{ Client: k8sClient, - Log: logger, + Log: logger.Sugar(), AtlasProvider: atlasProvider, EventRecorder: record.NewFakeRecorder(10), ObjectDeletionProtection: false, @@ -576,18 +581,23 @@ func TestServerlessInstanceReconciliation(t *testing.T) { } d := akov2.NewDefaultAWSServerlessInstance(project.Namespace, project.Name) + logger := zaptest.NewLogger(t) + sch := runtime.NewScheme() require.NoError(t, akov2.AddToScheme(sch)) require.NoError(t, corev1.AddToScheme(sch)) // Subresources need to be explicitly set now since controller-runtime 1.15 // https://github.com/kubernetes-sigs/controller-runtime/issues/2362#issuecomment-1698194188 + projectsRefIndexer := indexer.NewAtlasDatabaseUserByProjectsRefIndexer(logger) + externalProjectsRefIndexer := indexer.NewAtlasDatabaseUserByExternalProjectsRefIndexer(logger) k8sClient := fake.NewClientBuilder(). WithScheme(sch). WithObjects(secret, project, d). + WithIndex(projectsRefIndexer.Object(), projectsRefIndexer.Name(), projectsRefIndexer.Keys). + WithIndex(externalProjectsRefIndexer.Object(), externalProjectsRefIndexer.Name(), externalProjectsRefIndexer.Keys). Build() orgID := "0987654321" - logger := zaptest.NewLogger(t).Sugar() atlasProvider := &atlasmock.TestProvider{ SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { err := &admin.GenericOpenAPIError{} @@ -646,7 +656,7 @@ func TestServerlessInstanceReconciliation(t *testing.T) { reconciler := &AtlasDeploymentReconciler{ Client: k8sClient, - Log: logger, + Log: logger.Sugar(), AtlasProvider: atlasProvider, EventRecorder: record.NewFakeRecorder(10), ObjectDeletionProtection: false, diff --git a/pkg/controller/atlasdeployment/serverless_deployment_test.go b/pkg/controller/atlasdeployment/serverless_deployment_test.go index 067dbbd604..ee4c30a97c 100644 --- a/pkg/controller/atlasdeployment/serverless_deployment_test.go +++ b/pkg/controller/atlasdeployment/serverless_deployment_test.go @@ -25,6 +25,7 @@ import ( akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/indexer" ) func TestHandleServerlessInstance(t *testing.T) { @@ -766,21 +767,25 @@ func TestHandleServerlessInstance(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { + logger := zaptest.NewLogger(t) testScheme := runtime.NewScheme() require.NoError(t, akov2.AddToScheme(testScheme)) + projectsRefIndexer := indexer.NewAtlasDatabaseUserByProjectsRefIndexer(logger) + externalProjectsRefIndexer := indexer.NewAtlasDatabaseUserByExternalProjectsRefIndexer(logger) k8sClient := fake.NewClientBuilder(). WithScheme(testScheme). WithObjects(tt.atlasDeployment). + WithIndex(projectsRefIndexer.Object(), projectsRefIndexer.Name(), projectsRefIndexer.Keys). + WithIndex(externalProjectsRefIndexer.Object(), externalProjectsRefIndexer.Name(), externalProjectsRefIndexer.Keys). Build() - logger := zaptest.NewLogger(t).Sugar() reconciler := &AtlasDeploymentReconciler{ Client: k8sClient, - Log: logger, + Log: logger.Sugar(), deploymentService: tt.deploymentService(), } ctx := &workflow.Context{ Context: context.Background(), - Log: logger, + Log: logger.Sugar(), SdkClient: tt.sdkMock(), } diff --git a/pkg/controller/connectionsecret/connectionsecrets.go b/pkg/controller/connectionsecret/connectionsecrets.go index 29efccd68b..6c332a48d4 100644 --- a/pkg/controller/connectionsecret/connectionsecrets.go +++ b/pkg/controller/connectionsecret/connectionsecrets.go @@ -13,14 +13,15 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/stringutil" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" ) const ConnectionSecretsEnsuredEvent = "ConnectionSecretsEnsured" -func CreateOrUpdateConnectionSecrets(ctx *workflow.Context, k8sClient client.Client, ds deployment.AtlasDeploymentsService, recorder record.EventRecorder, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser) workflow.Result { - conns, err := ds.ListDeploymentConnections(ctx.Context, project.ID()) +func CreateOrUpdateConnectionSecrets(ctx *workflow.Context, k8sClient client.Client, ds deployment.AtlasDeploymentsService, recorder record.EventRecorder, project *project.Project, dbUser akov2.AtlasDatabaseUser) workflow.Result { + conns, err := ds.ListDeploymentConnections(ctx.Context, project.ID) if err != nil { return workflow.Terminate(workflow.DatabaseUserConnectionSecretsNotCreated, err.Error()) } @@ -33,7 +34,7 @@ func CreateOrUpdateConnectionSecrets(ctx *workflow.Context, k8sClient client.Cli return workflow.OK() } -func createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx *workflow.Context, k8sClient client.Client, recorder record.EventRecorder, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser, conns []deployment.Connection) workflow.Result { +func createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx *workflow.Context, k8sClient client.Client, recorder record.EventRecorder, project *project.Project, dbUser akov2.AtlasDatabaseUser, conns []deployment.Connection) workflow.Result { requeue := false secrets := make([]string, 0) @@ -62,7 +63,7 @@ func createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx *workflow.Context, FillPrivateConns(di, &data) var secretName string - if secretName, err = Ensure(ctx.Context, k8sClient, dbUser.Namespace, project.Spec.Name, project.ID(), di.Name, data); err != nil { + if secretName, err = Ensure(ctx.Context, k8sClient, dbUser.Namespace, project.Name, project.ID, di.Name, data); err != nil { return workflow.Terminate(workflow.DatabaseUserConnectionSecretsNotCreated, err.Error()) } secrets = append(secrets, secretName) @@ -73,7 +74,7 @@ func createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx *workflow.Context, recorder.Eventf(&dbUser, "Normal", ConnectionSecretsEnsuredEvent, "Connection Secrets were created/updated: %s", strings.Join(secrets, ", ")) } - if err := cleanupStaleSecrets(ctx, k8sClient, project.ID(), dbUser); err != nil { + if err := cleanupStaleSecrets(ctx, k8sClient, project.ID, dbUser); err != nil { return workflow.Terminate(workflow.DatabaseUserStaleConnectionSecrets, err.Error()) } diff --git a/pkg/controller/customresource/protection_test.go b/pkg/controller/customresource/protection_test.go index dc863f8e37..7bea4583b4 100644 --- a/pkg/controller/customresource/protection_test.go +++ b/pkg/controller/customresource/protection_test.go @@ -149,7 +149,7 @@ func TestApplyLastConfigApplied(t *testing.T) { annot := resource.GetAnnotations() assert.NotEmpty(t, annot) - expectedConfig := `{"projectRef":{"name":"","namespace":""},"roles":null,"username":"test-user"}` + expectedConfig := `{"roles":null,"username":"test-user"}` assert.Equal(t, expectedConfig, annot[customresource.AnnotationLastAppliedConfiguration]) } diff --git a/pkg/indexer/atlasdatabaseuserexternalprojects.go b/pkg/indexer/atlasdatabaseuserexternalprojects.go new file mode 100644 index 0000000000..be2af92d5d --- /dev/null +++ b/pkg/indexer/atlasdatabaseuserexternalprojects.go @@ -0,0 +1,44 @@ +package indexer + +import ( + "go.uber.org/zap" + "sigs.k8s.io/controller-runtime/pkg/client" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +const ( + AtlasDatabaseUserByExternalProjectsRefIndex = "atlasdatabaseuser.spec.externalProjectRef" +) + +type AtlasDatabaseUserByExternalProjectsRefIndexer struct { + logger *zap.SugaredLogger +} + +func NewAtlasDatabaseUserByExternalProjectsRefIndexer(logger *zap.Logger) *AtlasDatabaseUserByExternalProjectsRefIndexer { + return &AtlasDatabaseUserByExternalProjectsRefIndexer{ + logger: logger.Named(AtlasDatabaseUserByExternalProjectsRefIndex).Sugar(), + } +} + +func (*AtlasDatabaseUserByExternalProjectsRefIndexer) Object() client.Object { + return &akov2.AtlasDatabaseUser{} +} + +func (*AtlasDatabaseUserByExternalProjectsRefIndexer) Name() string { + return AtlasDatabaseUserByExternalProjectsRefIndex +} + +func (a *AtlasDatabaseUserByExternalProjectsRefIndexer) Keys(object client.Object) []string { + user, ok := object.(*akov2.AtlasDatabaseUser) + if !ok { + a.logger.Errorf("expected *akov2.AtlasDatabaseUser but got %T", object) + return nil + } + + if user.Spec.ExternalProjectRef != nil && user.Spec.ExternalProjectRef.ID != "" { + return []string{user.Spec.ExternalProjectRef.ID} + } + + return nil +} diff --git a/pkg/indexer/atlasdatabaseuserexternalprojects_test.go b/pkg/indexer/atlasdatabaseuserexternalprojects_test.go new file mode 100644 index 0000000000..8e47a126dd --- /dev/null +++ b/pkg/indexer/atlasdatabaseuserexternalprojects_test.go @@ -0,0 +1,55 @@ +package indexer + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zaptest" + "sigs.k8s.io/controller-runtime/pkg/client" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +func TestAtlasDatabaseUserByExternalProjectsIndexer(t *testing.T) { + for _, tc := range []struct { + name string + object client.Object + wantKeys []string + }{ + { + name: "should return nil on wrong type", + object: &akov2.AtlasProject{}, + }, + { + name: "should return nil when there are no references", + object: &akov2.AtlasDatabaseUser{}, + }, + { + name: "should return nil when there is an empty reference for external project", + object: &akov2.AtlasDatabaseUser{ + Spec: akov2.AtlasDatabaseUserSpec{ + ExternalProjectRef: &akov2.ExternalProjectReference{}, + }, + }, + }, + { + name: "should return external project reference", + object: &akov2.AtlasDatabaseUser{ + Spec: akov2.AtlasDatabaseUserSpec{ + ExternalProjectRef: &akov2.ExternalProjectReference{ + ID: "project-id", + }, + }, + }, + wantKeys: []string{"project-id"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + indexer := NewAtlasDatabaseUserByExternalProjectsRefIndexer(zaptest.NewLogger(t)) + keys := indexer.Keys(tc.object) + sort.Strings(keys) + assert.Equal(t, tc.wantKeys, keys) + }) + } +} diff --git a/pkg/indexer/atlasdatabaseuserprojects.go b/pkg/indexer/atlasdatabaseuserprojects.go new file mode 100644 index 0000000000..834837dd81 --- /dev/null +++ b/pkg/indexer/atlasdatabaseuserprojects.go @@ -0,0 +1,44 @@ +package indexer + +import ( + "go.uber.org/zap" + "sigs.k8s.io/controller-runtime/pkg/client" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +const ( + AtlasDatabaseUserByProjectsRefIndex = "atlasdatabaseuser.spec.projectRef" +) + +type AtlasDatabaseUserByProjectsRefIndexer struct { + logger *zap.SugaredLogger +} + +func NewAtlasDatabaseUserByProjectsRefIndexer(logger *zap.Logger) *AtlasDatabaseUserByProjectsRefIndexer { + return &AtlasDatabaseUserByProjectsRefIndexer{ + logger: logger.Named(AtlasDatabaseUserByProjectsRefIndex).Sugar(), + } +} + +func (*AtlasDatabaseUserByProjectsRefIndexer) Object() client.Object { + return &akov2.AtlasDatabaseUser{} +} + +func (*AtlasDatabaseUserByProjectsRefIndexer) Name() string { + return AtlasDatabaseUserByProjectsRefIndex +} + +func (a *AtlasDatabaseUserByProjectsRefIndexer) Keys(object client.Object) []string { + user, ok := object.(*akov2.AtlasDatabaseUser) + if !ok { + a.logger.Errorf("expected *akov2.AtlasDatabaseUser but got %T", object) + return nil + } + + if user.Spec.Project != nil && user.Spec.Project.Name != "" { + return []string{user.Spec.Project.GetObject(user.Namespace).String()} + } + + return nil +} diff --git a/pkg/indexer/atlasdatabaseuserprojects_test.go b/pkg/indexer/atlasdatabaseuserprojects_test.go new file mode 100644 index 0000000000..c245549649 --- /dev/null +++ b/pkg/indexer/atlasdatabaseuserprojects_test.go @@ -0,0 +1,78 @@ +package indexer + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zaptest" + "sigs.k8s.io/controller-runtime/pkg/client" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" +) + +func TestAtlasDatabaseUserByProjectsIndexer(t *testing.T) { + for _, tc := range []struct { + name string + object client.Object + wantKeys []string + }{ + { + name: "should return nil on wrong type", + object: &akov2.AtlasProject{}, + }, + { + name: "should return nil when there are no references", + object: &akov2.AtlasDatabaseUser{}, + }, + { + name: "should return nil when there is an empty reference for project", + object: &akov2.AtlasDatabaseUser{ + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{}, + }, + }, + }, + { + name: "should return project reference with database user namespace", + object: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user", + Namespace: "ns", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "someProject", + }, + }, + }, + wantKeys: []string{"ns/someProject"}, + }, + { + name: "should return project reference", + object: &akov2.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user", + Namespace: "ns", + }, + Spec: akov2.AtlasDatabaseUserSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "someProject", + Namespace: "nsProject", + }, + }, + }, + wantKeys: []string{"nsProject/someProject"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + indexer := NewAtlasDatabaseUserByProjectsRefIndexer(zaptest.NewLogger(t)) + keys := indexer.Keys(tc.object) + sort.Strings(keys) + assert.Equal(t, tc.wantKeys, keys) + }) + } +} diff --git a/pkg/indexer/indexer.go b/pkg/indexer/indexer.go index dd0d5612f8..8a1cc254ea 100644 --- a/pkg/indexer/indexer.go +++ b/pkg/indexer/indexer.go @@ -33,6 +33,8 @@ func RegisterAll(ctx context.Context, mgr manager.Manager, logger *zap.Logger) e NewAtlasFederatedAuthBySecretsIndexer(logger), NewAtlasDatabaseUserBySecretsIndexer(logger), NewAtlasDatabaseUserByCredentialIndexer(logger), + NewAtlasDatabaseUserByProjectsRefIndexer(logger), + NewAtlasDatabaseUserByExternalProjectsRefIndexer(logger), ) } diff --git a/test/e2e/atlas_gov_test.go b/test/e2e/atlas_gov_test.go index 1110f1ac55..6c26c197fa 100644 --- a/test/e2e/atlas_gov_test.go +++ b/test/e2e/atlas_gov_test.go @@ -538,7 +538,7 @@ var _ = Describe("Atlas for Government", Label("atlas-gov"), func() { Namespace: testData.Resources.Namespace, }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: projectName, Namespace: testData.Resources.Namespace, }, diff --git a/test/e2e/db_users_test.go b/test/e2e/db_users_test.go index 12819029ce..75dfb3f9f4 100644 --- a/test/e2e/db_users_test.go +++ b/test/e2e/db_users_test.go @@ -15,6 +15,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/featureflags" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/actions" @@ -25,7 +26,6 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/k8s" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/model" "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/e2e/utils" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/resources" ) const ( @@ -113,20 +113,19 @@ var _ = Describe("Operator watch all namespace should create connection secrets return ctx }(testData.Context) }) - By("Creating project and database users resources", func() { + By("Creating the project", func() { deploy.CreateProject(testData) - deploy.CreateUsers(testData) - - Eventually(func(g Gomega) bool { - for i := range testData.Users { - dbUser := testData.Users[i] - - g.Expect(testData.K8SClient.Get(testData.Context, client.ObjectKeyFromObject(dbUser), dbUser)).To(Succeed()) - g.Expect(resources.CheckCondition(testData.K8SClient, dbUser, api.TrueCondition(api.ReadyType))).To(BeTrue()) - } + }) + By("Failing when an user has both project and atlas references are set", func() { + testData.Users[0].Spec.ExternalProjectRef = &akov2.ExternalProjectReference{ + ID: testData.Project.ID(), + } - return true - }).WithTimeout(5 * time.Minute).WithPolling(10 * time.Second).Should(BeTrue()) + Expect(testData.K8SClient.Create(testData.Context, testData.Users[0])).ToNot(Succeed()) + }) + By("Creating a linked and a standalone users", func() { + data.WithExternalProjectRef(testData.Project.ID(), localSecretName)(testData.Users[0]) + deploy.CreateUsers(testData) Expect(countConnectionSecrets(testData.K8SClient, testData.Project.Spec.Name)).To(Equal(0)) }) diff --git a/test/helper/e2e/actions/deploy/deploy_operator.go b/test/helper/e2e/actions/deploy/deploy_operator.go index aad58524a8..b6215b3e68 100644 --- a/test/helper/e2e/actions/deploy/deploy_operator.go +++ b/test/helper/e2e/actions/deploy/deploy_operator.go @@ -97,14 +97,19 @@ func CreateUsers(testData *model.TestDataProvider) { for _, user := range testData.Users { if user.Namespace == "" { user.Namespace = testData.Resources.Namespace + } + + if user.Spec.Project != nil { user.Spec.Project.Namespace = testData.Resources.Namespace } + if user.Spec.PasswordSecret != nil { secret := utils.UserSecretPassword() Expect(k8s.CreateUserSecret(testData.Context, testData.K8SClient, secret, user.Spec.PasswordSecret.Name, user.Namespace)).Should(Succeed(), "Create user secret failed") } + err := testData.K8SClient.Create(testData.Context, user) Expect(err).ShouldNot(HaveOccurred(), fmt.Sprintf("User was not created: %v", user)) Eventually(func(g Gomega) { diff --git a/test/helper/e2e/data/user.go b/test/helper/e2e/data/user.go index 27c9d848bb..6f7071990c 100644 --- a/test/helper/e2e/data/user.go +++ b/test/helper/e2e/data/user.go @@ -23,7 +23,7 @@ func BasicUser(crName, atlasUserName string, add ...func(user *akov2.AtlasDataba Name: crName, }, Spec: akov2.AtlasDatabaseUserSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, Username: atlasUserName, @@ -93,7 +93,8 @@ func WithOIDCEnabled() func(user *akov2.AtlasDatabaseUser) { func WithProject(project *akov2.AtlasProject) func(user *akov2.AtlasDatabaseUser) { return func(user *akov2.AtlasDatabaseUser) { - user.Spec.Project = common.ResourceRefNamespaced{ + user.Spec.ExternalProjectRef = nil + user.Spec.Project = &common.ResourceRefNamespaced{ Name: project.Name, Namespace: project.Namespace, } @@ -111,3 +112,17 @@ func WithCredentials(secretName string) func(user *akov2.AtlasDatabaseUser) { user.Spec.ConnectionSecret = &api.LocalObjectReference{Name: secretName} } } + +func WithExternalProjectRef(projectID, credentialsName string) func(user *akov2.AtlasDatabaseUser) { + return func(user *akov2.AtlasDatabaseUser) { + user.Spec.Project = nil + user.Spec.ExternalProjectRef = &akov2.ExternalProjectReference{ + ID: projectID, + } + user.Spec.LocalCredentialHolder = api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: credentialsName, + }, + } + } +} diff --git a/test/helper/e2e/model/dbuser.go b/test/helper/e2e/model/dbuser.go index 3f21d817f5..ef0c44e792 100644 --- a/test/helper/e2e/model/dbuser.go +++ b/test/helper/e2e/model/dbuser.go @@ -43,7 +43,7 @@ func NewDBUser(userName string) *DBUser { }, Spec: UserSpec{ Username: userName, - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", }, },