From dc8769248189301e40c00fbc71faacd0ce73975c Mon Sep 17 00:00:00 2001 From: "jose.vazquez" Date: Fri, 12 Jul 2024 08:44:46 +0200 Subject: [PATCH] Revert "DB User Translation Layer & SDK migration (#1543)" This reverts commit 5e59fdcc95512634245c6c4e7745e45a83394722. --- .licenses-gomod.sha256 | 2 +- .mockery.yaml | 2 - go.mod | 1 - go.sum | 2 - internal/cmp/normalize.go | 2 +- internal/cmp/sort.go | 12 - .../translation/atlas_deployments_service.go | 272 ------------ .../mocks/translation/atlas_users_service.go | 242 ----------- internal/translation/dbuser/conversion.go | 236 ---------- .../translation/dbuser/conversion_test.go | 408 ------------------ internal/translation/dbuser/dbuser.go | 82 ---- internal/translation/dbuser/dbuser_test.go | 46 -- internal/translation/dbuser/internal_test.go | 132 ------ internal/translation/deployment/conversion.go | 90 ---- .../translation/deployment/conversion_test.go | 42 -- internal/translation/deployment/deployment.go | 94 ---- licenses.csv | 1 - pkg/api/v1/atlasbackupschedule_types_test.go | 3 +- pkg/api/v1/atlasdatabaseuser_types.go | 15 + .../atlasdatabaseuser_controller.go | 32 +- .../atlasdatabaseuser_controller_test.go | 127 ------ .../atlasdatabaseuser/databaseuser.go | 138 +++--- .../atlasdatabaseuser/databaseuser_test.go | 285 +----------- .../atlasproject/alert_configurations.go | 2 +- .../connectionsecret/connectionsecrets.go | 89 ++-- scripts/int_local.sh | 2 +- test/int/clusterwide/dbuser_test.go | 1 + test/int/databaseuser_protected_test.go | 4 +- test/int/databaseuser_unprotected_test.go | 14 +- 29 files changed, 185 insertions(+), 2193 deletions(-) delete mode 100644 internal/mocks/translation/atlas_deployments_service.go delete mode 100644 internal/mocks/translation/atlas_users_service.go delete mode 100644 internal/translation/dbuser/conversion.go delete mode 100644 internal/translation/dbuser/conversion_test.go delete mode 100644 internal/translation/dbuser/dbuser.go delete mode 100644 internal/translation/dbuser/dbuser_test.go delete mode 100644 internal/translation/dbuser/internal_test.go delete mode 100644 internal/translation/deployment/conversion.go delete mode 100644 internal/translation/deployment/conversion_test.go delete mode 100644 internal/translation/deployment/deployment.go delete mode 100644 pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go diff --git a/.licenses-gomod.sha256 b/.licenses-gomod.sha256 index 8207952919..af04632b9d 100644 --- a/.licenses-gomod.sha256 +++ b/.licenses-gomod.sha256 @@ -1 +1 @@ -100644 c73f2c61e23f551b04db496892f2d197c1325d38 go.mod +100644 00dda197f010caa332e89410c3ffee1369343795 go.mod diff --git a/.mockery.yaml b/.mockery.yaml index 18ab27283d..0876e4672f 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -7,5 +7,3 @@ mockname: "{{.InterfaceName}}Mock" all: true packages: github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/ipaccesslist: - github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser: - github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment: diff --git a/go.mod b/go.mod index c73f2c61e2..00dda197f0 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/mongodb-forks/digest v1.1.0 - github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 github.com/sethvargo/go-password v0.3.1 diff --git a/go.sum b/go.sum index 620c22796c..74438a4e6f 100644 --- a/go.sum +++ b/go.sum @@ -203,8 +203,6 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= -github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= diff --git a/internal/cmp/normalize.go b/internal/cmp/normalize.go index 57917f179b..a76bdf4dd1 100644 --- a/internal/cmp/normalize.go +++ b/internal/cmp/normalize.go @@ -29,7 +29,7 @@ func NormalizeSlice[S ~[]E, E any](slice S, cmp func(a, b E) int) S { if len(slice) == 0 { return nil } - slices.SortStableFunc(slice, cmp) + slices.SortFunc(slice, cmp) return slice } diff --git a/internal/cmp/sort.go b/internal/cmp/sort.go index c5945d7a1f..d3bb8ea4ec 100644 --- a/internal/cmp/sort.go +++ b/internal/cmp/sort.go @@ -49,15 +49,3 @@ func marshalJSON[T any](obj T) (string, error) { } return string(jObj), nil } - -func JSON[T any](obj T) []byte { - jObj, err := json.MarshalIndent(obj, " ", " ") - if err != nil { - return ([]byte)(err.Error()) - } - return jObj -} - -func JSONize[T any](obj T) string { - return string(JSON(obj)) -} diff --git a/internal/mocks/translation/atlas_deployments_service.go b/internal/mocks/translation/atlas_deployments_service.go deleted file mode 100644 index 3d04e37bdb..0000000000 --- a/internal/mocks/translation/atlas_deployments_service.go +++ /dev/null @@ -1,272 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package translation - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - deployment "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" -) - -// AtlasDeploymentsServiceMock is an autogenerated mock type for the AtlasDeploymentsService type -type AtlasDeploymentsServiceMock struct { - mock.Mock -} - -type AtlasDeploymentsServiceMock_Expecter struct { - mock *mock.Mock -} - -func (_m *AtlasDeploymentsServiceMock) EXPECT() *AtlasDeploymentsServiceMock_Expecter { - return &AtlasDeploymentsServiceMock_Expecter{mock: &_m.Mock} -} - -// ClusterExists provides a mock function with given fields: ctx, projectID, clusterName -func (_m *AtlasDeploymentsServiceMock) ClusterExists(ctx context.Context, projectID string, clusterName string) (bool, error) { - ret := _m.Called(ctx, projectID, clusterName) - - if len(ret) == 0 { - panic("no return value specified for ClusterExists") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { - return rf(ctx, projectID, clusterName) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { - r0 = rf(ctx, projectID, clusterName) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, projectID, clusterName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AtlasDeploymentsServiceMock_ClusterExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClusterExists' -type AtlasDeploymentsServiceMock_ClusterExists_Call struct { - *mock.Call -} - -// ClusterExists is a helper method to define mock.On call -// - ctx context.Context -// - projectID string -// - clusterName string -func (_e *AtlasDeploymentsServiceMock_Expecter) ClusterExists(ctx interface{}, projectID interface{}, clusterName interface{}) *AtlasDeploymentsServiceMock_ClusterExists_Call { - return &AtlasDeploymentsServiceMock_ClusterExists_Call{Call: _e.mock.On("ClusterExists", ctx, projectID, clusterName)} -} - -func (_c *AtlasDeploymentsServiceMock_ClusterExists_Call) Run(run func(ctx context.Context, projectID string, clusterName string)) *AtlasDeploymentsServiceMock_ClusterExists_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_ClusterExists_Call) Return(_a0 bool, _a1 error) *AtlasDeploymentsServiceMock_ClusterExists_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_ClusterExists_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *AtlasDeploymentsServiceMock_ClusterExists_Call { - _c.Call.Return(run) - return _c -} - -// DeploymentIsReady provides a mock function with given fields: ctx, projectID, deploymentName -func (_m *AtlasDeploymentsServiceMock) DeploymentIsReady(ctx context.Context, projectID string, deploymentName string) (bool, error) { - ret := _m.Called(ctx, projectID, deploymentName) - - if len(ret) == 0 { - panic("no return value specified for DeploymentIsReady") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { - return rf(ctx, projectID, deploymentName) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { - r0 = rf(ctx, projectID, deploymentName) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, projectID, deploymentName) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AtlasDeploymentsServiceMock_DeploymentIsReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeploymentIsReady' -type AtlasDeploymentsServiceMock_DeploymentIsReady_Call struct { - *mock.Call -} - -// DeploymentIsReady is a helper method to define mock.On call -// - ctx context.Context -// - projectID string -// - deploymentName string -func (_e *AtlasDeploymentsServiceMock_Expecter) DeploymentIsReady(ctx interface{}, projectID interface{}, deploymentName interface{}) *AtlasDeploymentsServiceMock_DeploymentIsReady_Call { - return &AtlasDeploymentsServiceMock_DeploymentIsReady_Call{Call: _e.mock.On("DeploymentIsReady", ctx, projectID, deploymentName)} -} - -func (_c *AtlasDeploymentsServiceMock_DeploymentIsReady_Call) Run(run func(ctx context.Context, projectID string, deploymentName string)) *AtlasDeploymentsServiceMock_DeploymentIsReady_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string)) - }) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_DeploymentIsReady_Call) Return(_a0 bool, _a1 error) *AtlasDeploymentsServiceMock_DeploymentIsReady_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_DeploymentIsReady_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *AtlasDeploymentsServiceMock_DeploymentIsReady_Call { - _c.Call.Return(run) - return _c -} - -// ListClusterNames provides a mock function with given fields: ctx, projectID -func (_m *AtlasDeploymentsServiceMock) ListClusterNames(ctx context.Context, projectID string) ([]string, error) { - ret := _m.Called(ctx, projectID) - - if len(ret) == 0 { - panic("no return value specified for ListClusterNames") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]string, error)); ok { - return rf(ctx, projectID) - } - if rf, ok := ret.Get(0).(func(context.Context, string) []string); ok { - r0 = rf(ctx, projectID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, projectID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AtlasDeploymentsServiceMock_ListClusterNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListClusterNames' -type AtlasDeploymentsServiceMock_ListClusterNames_Call struct { - *mock.Call -} - -// ListClusterNames is a helper method to define mock.On call -// - ctx context.Context -// - projectID string -func (_e *AtlasDeploymentsServiceMock_Expecter) ListClusterNames(ctx interface{}, projectID interface{}) *AtlasDeploymentsServiceMock_ListClusterNames_Call { - return &AtlasDeploymentsServiceMock_ListClusterNames_Call{Call: _e.mock.On("ListClusterNames", ctx, projectID)} -} - -func (_c *AtlasDeploymentsServiceMock_ListClusterNames_Call) Run(run func(ctx context.Context, projectID string)) *AtlasDeploymentsServiceMock_ListClusterNames_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_ListClusterNames_Call) Return(_a0 []string, _a1 error) *AtlasDeploymentsServiceMock_ListClusterNames_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_ListClusterNames_Call) RunAndReturn(run func(context.Context, string) ([]string, error)) *AtlasDeploymentsServiceMock_ListClusterNames_Call { - _c.Call.Return(run) - return _c -} - -// ListDeploymentConnections provides a mock function with given fields: ctx, projectID -func (_m *AtlasDeploymentsServiceMock) ListDeploymentConnections(ctx context.Context, projectID string) ([]deployment.Connection, error) { - ret := _m.Called(ctx, projectID) - - if len(ret) == 0 { - panic("no return value specified for ListDeploymentConnections") - } - - var r0 []deployment.Connection - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]deployment.Connection, error)); ok { - return rf(ctx, projectID) - } - if rf, ok := ret.Get(0).(func(context.Context, string) []deployment.Connection); ok { - r0 = rf(ctx, projectID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]deployment.Connection) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, projectID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AtlasDeploymentsServiceMock_ListDeploymentConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListDeploymentConnections' -type AtlasDeploymentsServiceMock_ListDeploymentConnections_Call struct { - *mock.Call -} - -// ListDeploymentConnections is a helper method to define mock.On call -// - ctx context.Context -// - projectID string -func (_e *AtlasDeploymentsServiceMock_Expecter) ListDeploymentConnections(ctx interface{}, projectID interface{}) *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call { - return &AtlasDeploymentsServiceMock_ListDeploymentConnections_Call{Call: _e.mock.On("ListDeploymentConnections", ctx, projectID)} -} - -func (_c *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call) Run(run func(ctx context.Context, projectID string)) *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call) Return(_a0 []deployment.Connection, _a1 error) *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call) RunAndReturn(run func(context.Context, string) ([]deployment.Connection, error)) *AtlasDeploymentsServiceMock_ListDeploymentConnections_Call { - _c.Call.Return(run) - return _c -} - -// NewAtlasDeploymentsServiceMock creates a new instance of AtlasDeploymentsServiceMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewAtlasDeploymentsServiceMock(t interface { - mock.TestingT - Cleanup(func()) -}) *AtlasDeploymentsServiceMock { - mock := &AtlasDeploymentsServiceMock{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/mocks/translation/atlas_users_service.go b/internal/mocks/translation/atlas_users_service.go deleted file mode 100644 index 83e42f07b3..0000000000 --- a/internal/mocks/translation/atlas_users_service.go +++ /dev/null @@ -1,242 +0,0 @@ -// Code generated by mockery. DO NOT EDIT. - -package translation - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - dbuser "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser" -) - -// AtlasUsersServiceMock is an autogenerated mock type for the AtlasUsersService type -type AtlasUsersServiceMock struct { - mock.Mock -} - -type AtlasUsersServiceMock_Expecter struct { - mock *mock.Mock -} - -func (_m *AtlasUsersServiceMock) EXPECT() *AtlasUsersServiceMock_Expecter { - return &AtlasUsersServiceMock_Expecter{mock: &_m.Mock} -} - -// Create provides a mock function with given fields: ctx, au -func (_m *AtlasUsersServiceMock) Create(ctx context.Context, au *dbuser.User) error { - ret := _m.Called(ctx, au) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *dbuser.User) error); ok { - r0 = rf(ctx, au) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// AtlasUsersServiceMock_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' -type AtlasUsersServiceMock_Create_Call struct { - *mock.Call -} - -// Create is a helper method to define mock.On call -// - ctx context.Context -// - au *dbuser.User -func (_e *AtlasUsersServiceMock_Expecter) Create(ctx interface{}, au interface{}) *AtlasUsersServiceMock_Create_Call { - return &AtlasUsersServiceMock_Create_Call{Call: _e.mock.On("Create", ctx, au)} -} - -func (_c *AtlasUsersServiceMock_Create_Call) Run(run func(ctx context.Context, au *dbuser.User)) *AtlasUsersServiceMock_Create_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*dbuser.User)) - }) - return _c -} - -func (_c *AtlasUsersServiceMock_Create_Call) Return(_a0 error) *AtlasUsersServiceMock_Create_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *AtlasUsersServiceMock_Create_Call) RunAndReturn(run func(context.Context, *dbuser.User) error) *AtlasUsersServiceMock_Create_Call { - _c.Call.Return(run) - return _c -} - -// Delete provides a mock function with given fields: ctx, db, projectID, username -func (_m *AtlasUsersServiceMock) Delete(ctx context.Context, db string, projectID string, username string) error { - ret := _m.Called(ctx, db, projectID, username) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { - r0 = rf(ctx, db, projectID, username) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// AtlasUsersServiceMock_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' -type AtlasUsersServiceMock_Delete_Call struct { - *mock.Call -} - -// Delete is a helper method to define mock.On call -// - ctx context.Context -// - db string -// - projectID string -// - username string -func (_e *AtlasUsersServiceMock_Expecter) Delete(ctx interface{}, db interface{}, projectID interface{}, username interface{}) *AtlasUsersServiceMock_Delete_Call { - return &AtlasUsersServiceMock_Delete_Call{Call: _e.mock.On("Delete", ctx, db, projectID, username)} -} - -func (_c *AtlasUsersServiceMock_Delete_Call) Run(run func(ctx context.Context, db string, projectID string, username string)) *AtlasUsersServiceMock_Delete_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *AtlasUsersServiceMock_Delete_Call) Return(_a0 error) *AtlasUsersServiceMock_Delete_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *AtlasUsersServiceMock_Delete_Call) RunAndReturn(run func(context.Context, string, string, string) error) *AtlasUsersServiceMock_Delete_Call { - _c.Call.Return(run) - return _c -} - -// Get provides a mock function with given fields: ctx, db, projectID, username -func (_m *AtlasUsersServiceMock) Get(ctx context.Context, db string, projectID string, username string) (*dbuser.User, error) { - ret := _m.Called(ctx, db, projectID, username) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 *dbuser.User - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (*dbuser.User, error)); ok { - return rf(ctx, db, projectID, username) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) *dbuser.User); ok { - r0 = rf(ctx, db, projectID, username) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*dbuser.User) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, db, projectID, username) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// AtlasUsersServiceMock_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type AtlasUsersServiceMock_Get_Call struct { - *mock.Call -} - -// Get is a helper method to define mock.On call -// - ctx context.Context -// - db string -// - projectID string -// - username string -func (_e *AtlasUsersServiceMock_Expecter) Get(ctx interface{}, db interface{}, projectID interface{}, username interface{}) *AtlasUsersServiceMock_Get_Call { - return &AtlasUsersServiceMock_Get_Call{Call: _e.mock.On("Get", ctx, db, projectID, username)} -} - -func (_c *AtlasUsersServiceMock_Get_Call) Run(run func(ctx context.Context, db string, projectID string, username string)) *AtlasUsersServiceMock_Get_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) - }) - return _c -} - -func (_c *AtlasUsersServiceMock_Get_Call) Return(_a0 *dbuser.User, _a1 error) *AtlasUsersServiceMock_Get_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *AtlasUsersServiceMock_Get_Call) RunAndReturn(run func(context.Context, string, string, string) (*dbuser.User, error)) *AtlasUsersServiceMock_Get_Call { - _c.Call.Return(run) - return _c -} - -// Update provides a mock function with given fields: ctx, au -func (_m *AtlasUsersServiceMock) Update(ctx context.Context, au *dbuser.User) error { - ret := _m.Called(ctx, au) - - if len(ret) == 0 { - panic("no return value specified for Update") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *dbuser.User) error); ok { - r0 = rf(ctx, au) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// AtlasUsersServiceMock_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' -type AtlasUsersServiceMock_Update_Call struct { - *mock.Call -} - -// Update is a helper method to define mock.On call -// - ctx context.Context -// - au *dbuser.User -func (_e *AtlasUsersServiceMock_Expecter) Update(ctx interface{}, au interface{}) *AtlasUsersServiceMock_Update_Call { - return &AtlasUsersServiceMock_Update_Call{Call: _e.mock.On("Update", ctx, au)} -} - -func (_c *AtlasUsersServiceMock_Update_Call) Run(run func(ctx context.Context, au *dbuser.User)) *AtlasUsersServiceMock_Update_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(*dbuser.User)) - }) - return _c -} - -func (_c *AtlasUsersServiceMock_Update_Call) Return(_a0 error) *AtlasUsersServiceMock_Update_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *AtlasUsersServiceMock_Update_Call) RunAndReturn(run func(context.Context, *dbuser.User) error) *AtlasUsersServiceMock_Update_Call { - _c.Call.Return(run) - return _c -} - -// NewAtlasUsersServiceMock creates a new instance of AtlasUsersServiceMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewAtlasUsersServiceMock(t interface { - mock.TestingT - Cleanup(func()) -}) *AtlasUsersServiceMock { - mock := &AtlasUsersServiceMock{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/translation/dbuser/conversion.go b/internal/translation/dbuser/conversion.go deleted file mode 100644 index 470a631f1e..0000000000 --- a/internal/translation/dbuser/conversion.go +++ /dev/null @@ -1,236 +0,0 @@ -package dbuser - -import ( - "fmt" - "reflect" - "strings" - "time" - - "go.mongodb.org/atlas-sdk/v20231115008/admin" - - "github.com/nsf/jsondiff" - - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/cmp" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/timeutil" - akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" -) - -type User struct { - *akov2.AtlasDatabaseUserSpec - Password string - ProjectID string -} - -// NewUser wraps and normalizes a Kubernetes Atlas User Spec pointer augmenting it with projectID and password. -func NewUser(spec *akov2.AtlasDatabaseUserSpec, projectID, password string) (*User, error) { - if spec == nil { - return nil, nil - } - user := &User{AtlasDatabaseUserSpec: spec, ProjectID: projectID, Password: password} - if err := normalize(user.AtlasDatabaseUserSpec); err != nil { - return nil, fmt.Errorf("failed to create internal user type: %w", err) - } - return user, nil -} - -// EqualSpecs returns true if the given users have the same specs -func EqualSpecs(spec, atlas *User) bool { - if !spec.hasSpec() && !atlas.hasSpec() { // both missing spec are same - return true - } - if !spec.hasSpec() || !atlas.hasSpec() { // only one missing spec are different - return false - } - // note users are normalized at construction time - return reflect.DeepEqual(spec.clearedSpecClone(), atlas.clearedSpecClone()) -} - -func DiffSpecs(a, b *User) string { - opts := jsondiff.DefaultJSONOptions() - _, result := jsondiff.Compare( - cmp.JSON(a.clearedSpecClone()), - cmp.JSON(b.clearedSpecClone()), - &opts) - return result -} - -func (u *User) hasSpec() bool { - return u != nil && u.AtlasDatabaseUserSpec != nil -} - -func (u *User) clearedSpecClone() *akov2.AtlasDatabaseUserSpec { - if u == nil || u.AtlasDatabaseUserSpec == nil { - return nil - } - clone := *u.AtlasDatabaseUserSpec - clone.Project.Name = "" - clone.Project.Namespace = "" - clone.PasswordSecret = nil - return &clone -} - -func normalize(spec *akov2.AtlasDatabaseUserSpec) error { - cmp.NormalizeSlice(spec.Roles, func(a, b akov2.RoleSpec) int { - return strings.Compare( - a.RoleName+a.DatabaseName+a.CollectionName, - b.RoleName+b.DatabaseName+b.CollectionName) - }) - cmp.NormalizeSlice(spec.Scopes, func(a, b akov2.ScopeSpec) int { - return strings.Compare( - a.Name+string(a.Type), - b.Name+string(b.Type)) - }) - if spec.DeleteAfterDate != "" { // enforce date format - operatorDeleteDate, err := timeutil.ParseISO8601(spec.DeleteAfterDate) - if err != nil { - return fmt.Errorf("failed to parse %q to an ISO date: %w", spec.DeleteAfterDate, err) - } - spec.DeleteAfterDate = timeutil.FormatISO8601(operatorDeleteDate) - } - return nil -} - -func fromAtlas(dbUser *admin.CloudDatabaseUser) (*User, error) { - if dbUser == nil { - return nil, nil - } - scopes, err := scopesFromAtlas(dbUser.GetScopes()) - if err != nil { - return nil, fmt.Errorf("failed to parse scopes: %w", err) - } - u := &User{ - ProjectID: dbUser.GroupId, - Password: dbUser.GetPassword(), - AtlasDatabaseUserSpec: &akov2.AtlasDatabaseUserSpec{ - DatabaseName: dbUser.DatabaseName, - DeleteAfterDate: dateFromAtlas(dbUser.DeleteAfterDate), - Roles: rolesFromAtlas(dbUser.GetRoles()), - Scopes: scopes, - Username: dbUser.Username, - OIDCAuthType: dbUser.GetOidcAuthType(), - AWSIAMType: dbUser.GetAwsIAMType(), - X509Type: dbUser.GetX509Type(), - }, - } - if err := normalize(u.AtlasDatabaseUserSpec); err != nil { - return nil, fmt.Errorf("failed to normalize spec from Atlas: %w", err) - } - return u, nil -} - -func toAtlas(au *User) (*admin.CloudDatabaseUser, error) { - if au == nil || au.AtlasDatabaseUserSpec == nil { - return nil, nil - } - date, err := dateToAtlas(au.DeleteAfterDate) - if err != nil { - return nil, fmt.Errorf("failed to parse deleteAfterDate value: %w", err) - } - return &admin.CloudDatabaseUser{ - DatabaseName: au.DatabaseName, - DeleteAfterDate: date, - X509Type: pointer.MakePtrOrNil(au.X509Type), - AwsIAMType: pointer.MakePtrOrNil(au.AWSIAMType), - GroupId: au.ProjectID, - Roles: rolesToAtlas(au.Roles), - Scopes: scopesToAtlas(au.Scopes), - Username: au.Username, - Password: pointer.MakePtrOrNil(au.Password), - OidcAuthType: pointer.MakePtrOrNil(au.OIDCAuthType), - }, nil -} - -func dateToAtlas(d string) (*time.Time, error) { - if d == "" { - return nil, nil - } - date, err := timeutil.ParseISO8601(d) - if err != nil { - return nil, fmt.Errorf("failed to parse %q to an ISO date: %w", d, err) - } - return pointer.MakePtr(date), nil -} - -func rolesToAtlas(roles []akov2.RoleSpec) *[]admin.DatabaseUserRole { - if len(roles) == 0 { - return nil - } - atlasRoles := []admin.DatabaseUserRole{} - for _, role := range roles { - ar := admin.DatabaseUserRole{ - RoleName: role.RoleName, - DatabaseName: role.DatabaseName, - } - if role.CollectionName != "" { - ar.CollectionName = pointer.MakePtr(role.CollectionName) - } - atlasRoles = append(atlasRoles, ar) - } - return &atlasRoles -} - -func scopesToAtlas(scopes []akov2.ScopeSpec) *[]admin.UserScope { - if len(scopes) == 0 { - return nil - } - atlasScopes := []admin.UserScope{} - for _, scope := range scopes { - atlasScopes = append(atlasScopes, admin.UserScope{ - Name: scope.Name, - Type: string(scope.Type), - }) - } - return &atlasScopes -} - -func dateFromAtlas(date *time.Time) string { - if date == nil { - return "" - } - return timeutil.FormatISO8601(*date) -} - -func scopesFromAtlas(scopes []admin.UserScope) ([]akov2.ScopeSpec, error) { - if len(scopes) == 0 { - return nil, nil - } - specScopes := []akov2.ScopeSpec{} - for _, scope := range scopes { - scopeType, err := scopeTypeFromAtlas(scope.Type) - if err != nil { - return nil, fmt.Errorf("failed to parse atlas scopes: %w", err) - } - specScopes = append(specScopes, akov2.ScopeSpec{ - Name: scope.Name, - Type: scopeType, - }) - } - return specScopes, nil -} - -func scopeTypeFromAtlas(scopeType string) (akov2.ScopeType, error) { - switch akov2.ScopeType(scopeType) { - case akov2.DeploymentScopeType: - return akov2.DeploymentScopeType, nil - case akov2.DataLakeScopeType: - return akov2.DataLakeScopeType, nil - default: - return "", fmt.Errorf("unsupported scope type %s", scopeType) - } -} - -func rolesFromAtlas(roles []admin.DatabaseUserRole) []akov2.RoleSpec { - if len(roles) == 0 { - return nil - } - specRoles := []akov2.RoleSpec{} - for _, role := range roles { - specRoles = append(specRoles, akov2.RoleSpec{ - RoleName: role.RoleName, - DatabaseName: role.DatabaseName, - CollectionName: role.GetCollectionName(), - }) - } - return specRoles -} diff --git a/internal/translation/dbuser/conversion_test.go b/internal/translation/dbuser/conversion_test.go deleted file mode 100644 index 1f7bedc6fd..0000000000 --- a/internal/translation/dbuser/conversion_test.go +++ /dev/null @@ -1,408 +0,0 @@ -package dbuser_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/timeutil" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser" - akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" -) - -const ( - testProjectID = "project-id" - - testPassword = "something-secret-here" - - testDB = "test-db" - - testUsername = "testUsername" -) - -var ( - testDate = timeutil.FormatISO8601(time.Now()) - - testOtherDate = timeutil.FormatISO8601(time.Now().Add(time.Hour)) -) - -func TestNewUser(t *testing.T) { - for _, tc := range []struct { - title string - spec *akov2.AtlasDatabaseUserSpec - projectID string - password string - expectedUser *dbuser.User - expectedErrorMsg string - }{ - { - title: "Nil spec returns nil user", - }, - - { - title: "Empty spec returns empty user", - spec: &akov2.AtlasDatabaseUserSpec{}, - expectedUser: &dbuser.User{AtlasDatabaseUserSpec: &akov2.AtlasDatabaseUserSpec{}}, - }, - - { - title: "Basic user is properly created", - spec: defaultTestSpec(), - projectID: testProjectID, - password: testPassword, - expectedUser: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - ProjectID: testProjectID, - Password: testPassword, - }, - }, - - { - title: "Spec with bad date gets rejected", - spec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.DeleteAfterDate = "bad-date" - return spec - }(), - projectID: testProjectID, - password: testPassword, - expectedUser: nil, - expectedErrorMsg: "failed to parse \"bad-date\" to an ISO date", - }, - - { - title: "Spec with proper date gets created", - spec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.DeleteAfterDate = testDate - return spec - }(), - projectID: testProjectID, - password: testPassword, - expectedUser: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.DeleteAfterDate = testDate - return spec - }(), - ProjectID: testProjectID, - Password: testPassword, - }, - }, - - { - title: "Spec with unordered roles renders a normalized user with ordered entries", - spec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Roles = []akov2.RoleSpec{ - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - {RoleName: "role2", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - } - return spec - }(), - projectID: testProjectID, - password: testPassword, - expectedUser: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Roles = []akov2.RoleSpec{ - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role2", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - } - return spec - }(), - ProjectID: testProjectID, - Password: testPassword, - }, - }, - - { - title: "Spec with unordered scopes renders a normalized user with ordered entries", - spec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake2", Type: "DATA_LAKE"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "cluster1", Type: "CLUSTER"}, - } - return spec - }(), - projectID: testProjectID, - password: testPassword, - expectedUser: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - return spec - }(), - ProjectID: testProjectID, - Password: testPassword, - }, - }, - } { - t.Run(tc.title, func(t *testing.T) { - user, err := dbuser.NewUser(tc.spec, tc.projectID, tc.password) - if tc.expectedErrorMsg != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErrorMsg) - } else { - require.NoError(t, err) - } - assert.Equal(t, tc.expectedUser, user) - }) - } -} - -func TestDiffSpecs(t *testing.T) { - for _, tc := range []struct { - title string - spec *dbuser.User - atlas *dbuser.User - expectedDiffs []string - }{ - { - title: "Nil users are equal", - }, - - { - title: "Nil spec side is flagged", - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - }, - expectedDiffs: []string{"\"changed\":[null, {}]"}, - }, - - { - title: "Nil atlas side is flagged", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - }, - expectedDiffs: []string{"\"changed\":[{}, null]"}, - }, - - { - title: "Sample users have no diffs", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - }, - }, - - { - title: "Only the spec part of each user is compared", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - ProjectID: "", - Password: testPassword, - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - ProjectID: "", - Password: testPassword, - }, - }, - - { - title: "All simple field diffs are flagged", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: defaultTestSpec(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Username = spec.Username + "1" - spec.DatabaseName = spec.DatabaseName + "2" - spec.DeleteAfterDate = testOtherDate - spec.OIDCAuthType = "IDP_GROUP" - spec.AWSIAMType = "USER" - spec.X509Type = "MANAGED" - return spec - }(), - }, - expectedDiffs: []string{ - "username", - "databaseName", - "deleteAfterDate", - "oidcAuthType", - "awsIamType", - "x509Type", - }, - }, - - { - title: "Role diffs are flagged", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Roles = []akov2.RoleSpec{ - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - {RoleName: "role2", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - } - return spec - }(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Roles = []akov2.RoleSpec{ - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - } - return spec - }(), - }, - expectedDiffs: []string{"roles", `"prop-removed":{"roleName": "role1"}`}, - }, - - { - title: "Equal role lists show no diffs", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Roles = []akov2.RoleSpec{ - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - {RoleName: "role2", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - } - return spec - }(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Roles = []akov2.RoleSpec{ - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - {RoleName: "role2", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - } - return spec - }(), - }, - expectedDiffs: []string{}, - }, - - { - title: "Scope diffs are flagged", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - return spec - }(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - return spec - }(), - }, - expectedDiffs: []string{"scopes", `prop-added":{"name": "lake1",}`}, - }, - - { - title: "Equal scopes show no diffs", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - return spec - }(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - return spec - }(), - }, - expectedDiffs: []string{}, - }, - - { - title: "Scopes with different references show no diffs", - spec: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - spec.Project.Name = "some-project" - spec.Project.Namespace = "some-namespace" - spec.PasswordSecret = &common.ResourceRef{Name: "some-secret-ref"} - return spec - }(), - }, - atlas: &dbuser.User{ - AtlasDatabaseUserSpec: func() *akov2.AtlasDatabaseUserSpec { - spec := defaultTestSpec() - spec.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - spec.Project.Name = "another-project" - spec.Project.Namespace = "another-namespace" - spec.PasswordSecret = &common.ResourceRef{Name: "another-secret-ref"} - return spec - }(), - }, - expectedDiffs: []string{}, - }, - } { - t.Run(tc.title, func(t *testing.T) { - equal := dbuser.EqualSpecs(tc.spec, tc.atlas) - if tc.expectedDiffs == nil { - assert.Equal(t, true, equal) - } else { - diff := dbuser.DiffSpecs(tc.spec, tc.atlas) - for _, expected := range tc.expectedDiffs { - assert.Contains(t, diff, expected) - } - } - }) - } -} - -func defaultTestSpec() *akov2.AtlasDatabaseUserSpec { - return &akov2.AtlasDatabaseUserSpec{ - DatabaseName: testDB, - Username: testUsername, - } -} diff --git a/internal/translation/dbuser/dbuser.go b/internal/translation/dbuser/dbuser.go deleted file mode 100644 index 66f12428f3..0000000000 --- a/internal/translation/dbuser/dbuser.go +++ /dev/null @@ -1,82 +0,0 @@ -package dbuser - -import ( - "context" - "errors" - "fmt" - - "go.mongodb.org/atlas-sdk/v20231115008/admin" - "go.uber.org/zap" - "k8s.io/apimachinery/pkg/types" - - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" -) - -var ( - // ErrorNotFound is returned when an database user was not found - ErrorNotFound = errors.New("database user not found") -) - -type AtlasUsersService interface { - Get(ctx context.Context, db, projectID, username string) (*User, error) - Delete(ctx context.Context, db, projectID, username string) error - Create(ctx context.Context, au *User) error - Update(ctx context.Context, au *User) error -} - -type AtlasUsers struct { - usersAPI admin.DatabaseUsersApi -} - -func NewAtlasDatabaseUsersService(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*AtlasUsers, error) { - client, err := translation.NewVersionedClient(ctx, provider, secretRef, log) - if err != nil { - return nil, fmt.Errorf("failed to create versioned client: %w", err) - } - return NewAtlasUsers(client.DatabaseUsersApi), nil -} - -func NewAtlasUsers(api admin.DatabaseUsersApi) *AtlasUsers { - return &AtlasUsers{usersAPI: api} -} - -func (dus *AtlasUsers) Get(ctx context.Context, db, projectID, username string) (*User, error) { - atlasDBUser, _, err := dus.usersAPI.GetDatabaseUser(ctx, projectID, db, username).Execute() - if err != nil { - if admin.IsErrorCode(err, atlas.UsernameNotFound) { - return nil, errors.Join(ErrorNotFound, err) - } - return nil, fmt.Errorf("failed to get database user %q: %w", username, err) - } - return fromAtlas(atlasDBUser) -} - -func (dus *AtlasUsers) Delete(ctx context.Context, db, projectID, username string) error { - _, _, err := dus.usersAPI.DeleteDatabaseUser(ctx, projectID, db, username).Execute() - if err != nil { - if admin.IsErrorCode(err, atlas.UsernameNotFound) { - return errors.Join(ErrorNotFound, err) - } - return err - } - return nil -} - -func (dus *AtlasUsers) Create(ctx context.Context, au *User) error { - u, err := toAtlas(au) - if err != nil { - return err - } - _, _, err = dus.usersAPI.CreateDatabaseUser(ctx, au.ProjectID, u).Execute() - return err -} - -func (dus *AtlasUsers) Update(ctx context.Context, au *User) error { - u, err := toAtlas(au) - if err != nil { - return err - } - _, _, err = dus.usersAPI.UpdateDatabaseUser(ctx, au.ProjectID, au.DatabaseName, au.Username, u).Execute() - return err -} diff --git a/internal/translation/dbuser/dbuser_test.go b/internal/translation/dbuser/dbuser_test.go deleted file mode 100644 index 09aaeebce8..0000000000 --- a/internal/translation/dbuser/dbuser_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package dbuser_test - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.mongodb.org/atlas-sdk/v20231115008/admin" - "go.uber.org/zap" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser" -) - -func TestNewAtlasDatabaseUsersService(t *testing.T) { - ctx := context.Background() - provider := &atlas.TestProvider{ - SdkClientFunc: func(_ *client.ObjectKey, _ *zap.SugaredLogger) (*admin.APIClient, string, error) { - return &admin.APIClient{}, "", nil - }, - } - secretRef := &types.NamespacedName{} - log := zap.S() - users, err := dbuser.NewAtlasDatabaseUsersService(ctx, provider, secretRef, log) - require.NoError(t, err) - assert.Equal(t, &dbuser.AtlasUsers{}, users) -} - -func TestFailedNewAtlasDatabaseUsersService(t *testing.T) { - expectedErr := errors.New("fake error") - ctx := context.Background() - provider := &atlas.TestProvider{ - SdkClientFunc: func(_ *client.ObjectKey, _ *zap.SugaredLogger) (*admin.APIClient, string, error) { - return nil, "", expectedErr - }, - } - secretRef := &types.NamespacedName{} - log := zap.S() - users, err := dbuser.NewAtlasDatabaseUsersService(ctx, provider, secretRef, log) - require.Nil(t, users) - require.ErrorIs(t, err, expectedErr) -} diff --git a/internal/translation/dbuser/internal_test.go b/internal/translation/dbuser/internal_test.go deleted file mode 100644 index c6a3303599..0000000000 --- a/internal/translation/dbuser/internal_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package dbuser - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.mongodb.org/atlas-sdk/v20231115008/admin" - - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/timeutil" - akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" -) - -const ( - testProjectID = "project-id" - - testPassword = "something-secret-here" - - testDB = "test-db" - - testUsername = "testUsername" -) - -var ( - testDate = timeutil.FormatISO8601(time.Now()) -) - -func TestToAndFromAtlas(t *testing.T) { - for _, tc := range []struct { - title string - user *User - }{ - { - title: "Nil user renders nil atlas user", - }, - - { - title: "Nil user spec renders nil atlas user", - }, - - { - title: "Default user spec converts back and forth correctly", - user: defaultTestUser(), - }, - - { - title: "Default user spec with correct date succeeds", - user: func() *User { - u := defaultTestUser() - u.DeleteAfterDate = testDate - return u - }(), - }, - - { - title: "Default user spec with ordered roles succeeds", - user: func() *User { - u := defaultTestUser() - u.Roles = []akov2.RoleSpec{ - {RoleName: "role1", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role2", DatabaseName: "database1", CollectionName: "collection1"}, - {RoleName: "role2", DatabaseName: "database2", CollectionName: "collection2"}, - } - return u - }(), - }, - - { - title: "Default user spec with ordered scopes succeeds", - user: func() *User { - u := defaultTestUser() - u.Scopes = []akov2.ScopeSpec{ - {Name: "cluster1", Type: "CLUSTER"}, - {Name: "cluster2", Type: "CLUSTER"}, - {Name: "lake1", Type: "DATA_LAKE"}, - {Name: "lake2", Type: "DATA_LAKE"}, - } - return u - }(), - }, - } { - t.Run(tc.title, func(t *testing.T) { - atlasUser, err := toAtlas(tc.user) - require.NoError(t, err) - converted, err := fromAtlas(atlasUser) - require.NoError(t, err) - assert.Equal(t, tc.user, converted) - }) - } -} - -func TestToAtlasDateFailure(t *testing.T) { - user := defaultTestUser() - user.DeleteAfterDate = "bad-date-string" - expectedErrMsg := "failed to parse deleteAfterDate value" - - _, err := toAtlas(user) - require.Error(t, err) - assert.Contains(t, err.Error(), expectedErrMsg) -} - -func TestFromAtlasScopeFailure(t *testing.T) { - atlasUser := defaultTestAtlasUser() - atlasUser.Scopes = &[]admin.UserScope{{Name: "some-name", Type: "not-a-proper-cluster"}} - expectedErrMsg := "unsupported scope type not-a-proper-cluster" - - _, err := fromAtlas(atlasUser) - require.Error(t, err) - assert.Contains(t, err.Error(), expectedErrMsg) -} - -func defaultTestUser() *User { - return &User{ - AtlasDatabaseUserSpec: &akov2.AtlasDatabaseUserSpec{ - DatabaseName: testDB, - Username: testUsername, - }, - Password: testPassword, - ProjectID: testProjectID, - } -} - -func defaultTestAtlasUser() *admin.CloudDatabaseUser { - return &admin.CloudDatabaseUser{ - DatabaseName: testDB, - GroupId: testProjectID, - Password: pointer.MakePtr(testPassword), - Username: testUsername, - } -} diff --git a/internal/translation/deployment/conversion.go b/internal/translation/deployment/conversion.go deleted file mode 100644 index 2e10c39520..0000000000 --- a/internal/translation/deployment/conversion.go +++ /dev/null @@ -1,90 +0,0 @@ -package deployment - -import "go.mongodb.org/atlas-sdk/v20231115008/admin" - -type Connection struct { - Name string - ConnURL string - SrvConnURL string - PrivateURL string - SrvPrivateURL string - Serverless bool - PrivateEndpoints []PrivateEndpoint -} - -type PrivateEndpoint struct { - URL string - ServerURL string - ShardURL string -} - -func clustersToConnections(clusters []admin.AdvancedClusterDescription) []Connection { - conns := []Connection{} - for _, c := range clusters { - conns = append(conns, Connection{ - Name: c.GetName(), - ConnURL: c.ConnectionStrings.GetStandard(), - SrvConnURL: c.ConnectionStrings.GetStandardSrv(), - PrivateURL: c.ConnectionStrings.GetPrivate(), - SrvPrivateURL: c.ConnectionStrings.GetPrivateSrv(), - Serverless: false, - PrivateEndpoints: fillClusterPrivateEndpoints(c.ConnectionStrings.GetPrivateEndpoint()), - }) - } - return conns -} - -func fillClusterPrivateEndpoints(cpeList []admin.ClusterDescriptionConnectionStringsPrivateEndpoint) []PrivateEndpoint { - pes := []PrivateEndpoint{} - for _, cpe := range cpeList { - pes = append(pes, PrivateEndpoint{ - URL: cpe.GetConnectionString(), - ServerURL: cpe.GetSrvConnectionString(), - ShardURL: cpe.GetSrvShardOptimizedConnectionString(), - }) - } - return pes -} - -func serverlessToConnections(serverless []admin.ServerlessInstanceDescription) []Connection { - conns := []Connection{} - for _, s := range serverless { - conns = append(conns, Connection{ - Name: s.GetName(), - ConnURL: "", - SrvConnURL: s.ConnectionStrings.GetStandardSrv(), - Serverless: true, - PrivateEndpoints: fillServerlessPrivateEndpoints(s.ConnectionStrings.GetPrivateEndpoint()), - }) - } - return conns -} - -func fillServerlessPrivateEndpoints(cpeList []admin.ServerlessConnectionStringsPrivateEndpointList) []PrivateEndpoint { - pes := []PrivateEndpoint{} - for _, cpe := range cpeList { - pes = append(pes, PrivateEndpoint{ - ServerURL: cpe.GetSrvConnectionString(), - }) - } - return pes -} - -func connectionSet(conns ...[]Connection) []Connection { - return set(func(conn Connection) string { return conn.Name }, conns...) -} - -func set[T any](nameFn func(T) string, lists ...[]T) []T { - hash := map[string]struct{}{} - result := []T{} - for _, list := range lists { - for _, item := range list { - name := nameFn(item) - if _, found := hash[name]; !found { - hash[name] = struct{}{} - result = append(result, item) - } - } - } - return result -} diff --git a/internal/translation/deployment/conversion_test.go b/internal/translation/deployment/conversion_test.go deleted file mode 100644 index 49ba5ee89c..0000000000 --- a/internal/translation/deployment/conversion_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package deployment - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestConnSet(t *testing.T) { - testCases := []struct { - title string - inputs [][]Connection - expected []Connection - }{ - { - title: "Disjoint lists concatenate", - inputs: [][]Connection{ - {{Name: "A"}, {Name: "B"}, {Name: "C"}}, - {{Name: "D"}, {Name: "E"}, {Name: "F"}}, - }, - expected: []Connection{ - {Name: "A"}, {Name: "B"}, {Name: "C"}, {Name: "D"}, {Name: "E"}, {Name: "F"}, - }, - }, - { - title: "Common items get merged away", - inputs: [][]Connection{ - {{Name: "A"}, {Name: "B"}, {Name: "C"}}, - {{Name: "B"}, {Name: "C"}, {Name: "D"}}, - }, - expected: []Connection{ - {Name: "A"}, {Name: "B"}, {Name: "C"}, {Name: "D"}, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.title, func(t *testing.T) { - result := connectionSet(tc.inputs...) - assert.Equal(t, tc.expected, result) - }) - } -} diff --git a/internal/translation/deployment/deployment.go b/internal/translation/deployment/deployment.go deleted file mode 100644 index b585805744..0000000000 --- a/internal/translation/deployment/deployment.go +++ /dev/null @@ -1,94 +0,0 @@ -package deployment - -import ( - "context" - "fmt" - - "go.mongodb.org/atlas-sdk/v20231115008/admin" - "go.mongodb.org/atlas/mongodbatlas" - "go.uber.org/zap" - "k8s.io/apimachinery/pkg/types" - - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" -) - -type AtlasDeploymentsService interface { - ListClusterNames(ctx context.Context, projectID string) ([]string, error) - ListDeploymentConnections(ctx context.Context, projectID string) ([]Connection, error) - ClusterExists(ctx context.Context, projectID, clusterName string) (bool, error) - DeploymentIsReady(ctx context.Context, projectID, deploymentName string) (bool, error) -} - -type ProductionAtlasDeployments struct { - clustersAPI admin.ClustersApi - serverlessAPI admin.ServerlessInstancesApi -} - -func NewAtlasDeploymentsService(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*ProductionAtlasDeployments, error) { - client, err := translation.NewVersionedClient(ctx, provider, secretRef, log) - if err != nil { - return nil, fmt.Errorf("failed to create versioned client: %w", err) - } - return NewProductionAtlasDeployments(client.ClustersApi, client.ServerlessInstancesApi), nil -} - -func NewProductionAtlasDeployments(clusterService admin.ClustersApi, serverlessAPI admin.ServerlessInstancesApi) *ProductionAtlasDeployments { - return &ProductionAtlasDeployments{clustersAPI: clusterService, serverlessAPI: serverlessAPI} -} - -func (ds *ProductionAtlasDeployments) ListClusterNames(ctx context.Context, projectID string) ([]string, error) { - var deploymentNames []string - clusters, _, err := ds.clustersAPI.ListClusters(ctx, projectID).Execute() - if err != nil { - return nil, fmt.Errorf("failed to list cluster names for project %s: %w", projectID, err) - } - if clusters.Results == nil { - return deploymentNames, nil - } - - for _, d := range *clusters.Results { - name := pointer.GetOrDefault(d.Name, "") - if name != "" { - deploymentNames = append(deploymentNames, name) - } - } - return deploymentNames, nil -} - -func (ds *ProductionAtlasDeployments) ListDeploymentConnections(ctx context.Context, projectID string) ([]Connection, error) { - clusters, _, err := ds.clustersAPI.ListClusters(ctx, projectID).Execute() - if err != nil { - return nil, fmt.Errorf("failed to list clusters for project %s: %w", projectID, err) - } - clusterConns := clustersToConnections(clusters.GetResults()) - - serverless, _, err := ds.serverlessAPI.ListServerlessInstances(ctx, projectID).Execute() - if err != nil { - return nil, fmt.Errorf("failed to list serverless deployments for project %s: %w", projectID, err) - } - serverlessConns := serverlessToConnections(serverless.GetResults()) - - return connectionSet(clusterConns, serverlessConns), nil -} - -func (ds *ProductionAtlasDeployments) ClusterExists(ctx context.Context, projectID, clusterName string) (bool, error) { - _, _, err := ds.clustersAPI.GetCluster(ctx, projectID, clusterName).Execute() - if admin.IsErrorCode(err, atlas.ClusterNotFound) { - return false, nil - } - if err != nil { - return false, fmt.Errorf("failed to get cluster %q: %w", clusterName, err) - } - return true, nil -} - -func (ds *ProductionAtlasDeployments) DeploymentIsReady(ctx context.Context, projectID, deploymentName string) (bool, error) { - // although this is within the clusters API it seems to also reply for serverless deployments - clusterStatus, _, err := ds.clustersAPI.GetClusterStatus(ctx, projectID, deploymentName).Execute() - if err != nil { - return false, fmt.Errorf("failed to get cluster %q status %w", deploymentName, err) - } - return clusterStatus.GetChangeStatus() == string(mongodbatlas.ChangeStatusApplied), nil -} diff --git a/licenses.csv b/licenses.csv index 248700f255..68651b9a47 100644 --- a/licenses.csv +++ b/licenses.csv @@ -72,7 +72,6 @@ github.com/mongodb-forks/digest,https://github.com/mongodb-forks/digest/blob/v1. github.com/mongodb/mongodb-atlas-kubernetes/v2,https://github.com/mongodb/mongodb-atlas-kubernetes/blob/HEAD/LICENSE,Apache-2.0 github.com/montanaflynn/stats,https://github.com/montanaflynn/stats/blob/v0.7.1/LICENSE,MIT github.com/munnerz/goautoneg,https://github.com/munnerz/goautoneg/blob/a7dc8b61c822/LICENSE,BSD-3-Clause -github.com/nsf/jsondiff,https://github.com/nsf/jsondiff/blob/43f6cf3098c1/LICENSE,MIT github.com/onsi/ginkgo/v2,https://github.com/onsi/ginkgo/blob/v2.19.0/LICENSE,MIT github.com/onsi/gomega,https://github.com/onsi/gomega/blob/v1.33.1/LICENSE,MIT github.com/pkg/browser,https://github.com/pkg/browser/blob/5ac0b6a4141c/LICENSE,BSD-2-Clause diff --git a/pkg/api/v1/atlasbackupschedule_types_test.go b/pkg/api/v1/atlasbackupschedule_types_test.go index c55fea1532..9ba94bdcf2 100644 --- a/pkg/api/v1/atlasbackupschedule_types_test.go +++ b/pkg/api/v1/atlasbackupschedule_types_test.go @@ -3,9 +3,8 @@ package v1 import ( "testing" - "go.mongodb.org/atlas/mongodbatlas" - "github.com/go-test/deep" + "go.mongodb.org/atlas/mongodbatlas" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" ) diff --git a/pkg/api/v1/atlasdatabaseuser_types.go b/pkg/api/v1/atlasdatabaseuser_types.go index ac763bfe31..4e3d1cee15 100644 --- a/pkg/api/v1/atlasdatabaseuser_types.go +++ b/pkg/api/v1/atlasdatabaseuser_types.go @@ -22,6 +22,7 @@ import ( "go.mongodb.org/atlas-sdk/v20231115008/admin" + "go.mongodb.org/atlas/mongodbatlas" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -215,6 +216,20 @@ func (p *AtlasDatabaseUser) ReadPassword(ctx context.Context, kubeClient client. return "", nil } +// ToAtlas converts the AtlasDatabaseUser to native Atlas client format. Reads the password from the Secret +func (p AtlasDatabaseUser) ToAtlas(ctx context.Context, kubeClient client.Client) (*mongodbatlas.DatabaseUser, error) { + password, err := p.ReadPassword(ctx, kubeClient) + if err != nil { + return nil, err + } + + result := &mongodbatlas.DatabaseUser{} + err = compat.JSONCopy(result, p.Spec) + result.Password = password + + return result, err +} + // ToAtlasSDK is clone of ToAtlas used temporarily for test migrations func (p AtlasDatabaseUser) ToAtlasSDK(ctx context.Context, kubeClient client.Client) (*admin.CloudDatabaseUser, error) { password, err := p.ReadPassword(ctx, kubeClient) diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go index 8fe9882b57..5d05a4ef16 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" + "go.mongodb.org/atlas/mongodbatlas" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -32,8 +33,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/featureflags" - "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/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" @@ -126,15 +125,17 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(ctx context.Context, req ctrl.Re return result.ReconcileResult(), nil } - dus, err := dbuser.NewAtlasDatabaseUsersService(ctx, r.AtlasProvider, project.ConnectionSecretObjectKey(), log) + atlasClient, orgID, err := r.AtlasProvider.Client(ctx, project.ConnectionSecretObjectKey(), log) if err != nil { result = workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error()) workflowCtx.SetConditionFromResult(api.DatabaseUserReadyType, result) return result.ReconcileResult(), nil } + workflowCtx.OrgID = orgID + workflowCtx.Client = atlasClient - deletionRequest, result := r.handleDeletion(ctx, databaseUser, project, dus, log) + deletionRequest, result := r.handleDeletion(ctx, databaseUser, project, atlasClient, log) if deletionRequest { return result.ReconcileResult(), nil } @@ -156,15 +157,7 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(ctx context.Context, req ctrl.Re return result.ReconcileResult(), nil } - ds, err := deployment.NewAtlasDeploymentsService(ctx, r.AtlasProvider, project.ConnectionSecretObjectKey(), log) - if err != nil { - result = workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error()) - workflowCtx.SetConditionFromResult(api.DatabaseUserReadyType, result) - - return result.ReconcileResult(), nil - } - - result = r.ensureDatabaseUser(workflowCtx, dus, ds, *project, *databaseUser) + result = r.ensureDatabaseUser(workflowCtx, *project, *databaseUser) if !result.IsOk() { workflowCtx.SetConditionFromResult(api.DatabaseUserReadyType, result) @@ -219,7 +212,7 @@ func (r *AtlasDatabaseUserReconciler) handleDeletion( ctx context.Context, dbUser *akov2.AtlasDatabaseUser, project *akov2.AtlasProject, - dus dbuser.AtlasUsersService, + atlasClient *mongodbatlas.Client, log *zap.SugaredLogger, ) (bool, workflow.Result) { if dbUser.GetDeletionTimestamp().IsZero() { @@ -244,11 +237,14 @@ func (r *AtlasDatabaseUserReconciler) handleDeletion( return true, workflow.OK() } - err := dus.Delete(ctx, dbUser.Spec.DatabaseName, project.ID(), dbUser.Spec.Username) - if errors.Is(err, dbuser.ErrorNotFound) { + _, err := atlasClient.DatabaseUsers.Delete(ctx, dbUser.Spec.DatabaseName, project.ID(), dbUser.Spec.Username) + if err != nil { + var apiError *mongodbatlas.ErrorResponse + if errors.As(err, &apiError) && apiError.ErrorCode != atlas.UsernameNotFound { + return true, workflow.Terminate(workflow.DatabaseUserNotDeletedInAtlas, err.Error()) + } + log.Info("Database user doesn't exist or is already deleted") - } else if err != nil { - return true, workflow.Terminate(workflow.DatabaseUserNotDeletedInAtlas, err.Error()) } err = customresource.ManageFinalizer(ctx, r.Client, dbUser, customresource.UnsetFinalizer) diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go deleted file mode 100644 index bd8ef6bd41..0000000000 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package atlasdatabaseuser - -import ( - "context" - "errors" - "testing" - "time" - - mocked "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" - akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/zap" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -const ( - testProject = "project" - - testProjectID = "12345" - - testDatabase = "db" - - testUsername = "user" -) - -var ( - errRandom = errors.New("random error") -) - -func TestHandleDeletion(t *testing.T) { - ctx := context.Background() - scheme := runtime.NewScheme() - utilruntime.Must(corev1.AddToScheme(scheme)) - utilruntime.Must(akov2.AddToScheme(scheme)) - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - user := defaultTestUser() - require.NoError(t, fakeClient.Create(ctx, user)) - defer fakeClient.Delete(ctx, user) - log := zap.S() - r := &AtlasDatabaseUserReconciler{ - Client: fakeClient, - Log: log, - } - for _, tc := range []struct { - title string - skipDeletion bool - service dbuser.AtlasUsersService - expectedOk bool - expectedResult workflow.Result - }{ - { - title: "User without deletion timestamp is not deleted", - skipDeletion: true, - expectedOk: false, - expectedResult: workflow.OK(), - }, - - { - title: "Ready user gets deleted properly", - service: fakeUserDeletion(ctx, testDatabase, testProjectID, testUsername, nil), - expectedOk: true, - expectedResult: workflow.OK(), - }, - - { - title: "Missing user is already deleted", - service: fakeUserDeletion(ctx, testDatabase, testProjectID, testUsername, dbuser.ErrorNotFound), - expectedOk: true, - expectedResult: workflow.OK(), - }, - - { - title: "Fails to delete user when returned error is unexpected", - service: fakeUserDeletion(ctx, testDatabase, testProjectID, testUsername, errRandom), - expectedOk: true, - expectedResult: workflow.Terminate(workflow.DatabaseUserNotDeletedInAtlas, errRandom.Error()), - }, - } { - t.Run(tc.title, func(t *testing.T) { - if !tc.skipDeletion { - user.DeletionTimestamp = pointer.MakePtr(metav1.NewTime(time.Now())) - } - done, result := r.handleDeletion(ctx, user, defaultTestProject(), tc.service, log) - assert.Equal(t, tc.expectedOk, done) - assert.Equal(t, tc.expectedResult, result) - }) - } -} - -func fakeUserDeletion(ctx context.Context, db, projectID, username string, err error) *mocked.AtlasUsersServiceMock { - return withFakeUserDeletion(&mocked.AtlasUsersServiceMock{}, ctx, db, projectID, username, err) -} - -func withFakeUserDeletion(service *mocked.AtlasUsersServiceMock, ctx context.Context, db, projectID, username string, err error) *mocked.AtlasUsersServiceMock { - service.EXPECT().Delete(ctx, db, projectID, username).Return(err) - return service -} - -func defaultTestProject() *akov2.AtlasProject { - return &akov2.AtlasProject{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{Name: testProject}, - Status: status.AtlasProjectStatus{ - ID: testProjectID, - }, - } -} - -func defaultTestUser() *akov2.AtlasDatabaseUser { - return &akov2.AtlasDatabaseUser{ - ObjectMeta: metav1.ObjectMeta{Name: testUsername}, - Spec: akov2.AtlasDatabaseUserSpec{ - DatabaseName: testDatabase, - Username: testUsername, - }, - } -} diff --git a/pkg/controller/atlasdatabaseuser/databaseuser.go b/pkg/controller/atlasdatabaseuser/databaseuser.go index 378e66c4d5..f72b73550d 100644 --- a/pkg/controller/atlasdatabaseuser/databaseuser.go +++ b/pkg/controller/atlasdatabaseuser/databaseuser.go @@ -6,26 +6,25 @@ import ( "fmt" "time" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "go.mongodb.org/atlas/mongodbatlas" "go.uber.org/zap" - "go.uber.org/zap/zapcore" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/compat" "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" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlasdeployment" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/connectionsecret" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" ) -func (r *AtlasDatabaseUserReconciler) ensureDatabaseUser(ctx *workflow.Context, dus dbuser.AtlasUsersService, ds deployment.AtlasDeploymentsService, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser) workflow.Result { - password, err := dbUser.ReadPassword(ctx.Context, r.Client) - if err != nil { - return workflow.Terminate(workflow.Internal, err.Error()) - } - apiUser, err := dbuser.NewUser(dbUser.Spec.DeepCopy(), project.ID(), password) +func (r *AtlasDatabaseUserReconciler) ensureDatabaseUser(ctx *workflow.Context, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser) workflow.Result { + apiUser, err := dbUser.ToAtlas(ctx.Context, r.Client) if err != nil { return workflow.Terminate(workflow.Internal, err.Error()) } @@ -34,24 +33,24 @@ func (r *AtlasDatabaseUserReconciler) ensureDatabaseUser(ctx *workflow.Context, return result } - if err := validateScopes(ctx, ds, project.ID(), dbUser); err != nil { + if err = validateScopes(ctx, project.ID(), dbUser); err != nil { return workflow.Terminate(workflow.DatabaseUserInvalidSpec, err.Error()) } - if result := performUpdateInAtlas(ctx, r.Client, dus, project, &dbUser, apiUser); !result.IsOk() { + if result := performUpdateInAtlas(ctx, r.Client, project, dbUser, apiUser); !result.IsOk() { return result } - if result := checkDeploymentsHaveReachedGoalState(ctx, ds, project.ID(), dbUser); !result.IsOk() { + if result := checkDeploymentsHaveReachedGoalState(ctx, project.ID(), dbUser); !result.IsOk() { return result } - if result := connectionsecret.CreateOrUpdateConnectionSecrets(ctx, r.Client, ds, r.EventRecorder, project, dbUser); !result.IsOk() { + if result := connectionsecret.CreateOrUpdateConnectionSecrets(ctx, r.Client, r.EventRecorder, project, dbUser); !result.IsOk() { return result } // We need to remove the old Atlas User right after all the connection secrets are ensured if username has changed. - if result := handleUserNameChange(ctx, dus, project.ID(), dbUser); !result.IsOk() { + if result := handleUserNameChange(ctx, project.ID(), dbUser); !result.IsOk() { return result } @@ -61,23 +60,21 @@ func (r *AtlasDatabaseUserReconciler) ensureDatabaseUser(ctx *workflow.Context, return workflow.OK() } -func handleUserNameChange(ctx *workflow.Context, dus dbuser.AtlasUsersService, projectID string, dbUser akov2.AtlasDatabaseUser) workflow.Result { +func handleUserNameChange(ctx *workflow.Context, projectID string, dbUser akov2.AtlasDatabaseUser) workflow.Result { if dbUser.Spec.Username != dbUser.Status.UserName && dbUser.Status.UserName != "" { ctx.Log.Infow("'spec.username' has changed - removing the old user from Atlas", "newUserName", dbUser.Spec.Username, "oldUserName", dbUser.Status.UserName) deleteAttempts := 3 - var err error for i := 1; i <= deleteAttempts; i++ { - err = dus.Delete(ctx.Context, dbUser.Spec.DatabaseName, projectID, dbUser.Status.UserName) - if err == nil || errors.Is(err, dbuser.ErrorNotFound) { - return workflow.OK() + _, err := ctx.Client.DatabaseUsers.Delete(ctx.Context, dbUser.Spec.DatabaseName, projectID, dbUser.Status.UserName) + if err == nil { + break } // There may be some rare errors due to the databaseName change or maybe the user has already been removed - this // is not-critical (the stale connection secret has already been removed) and we shouldn't retry to avoid infinite retries ctx.Log.Errorf("Failed to remove user %s from Atlas (attempt %d/%d): %s", dbUser.Status.UserName, i, deleteAttempts, err) } - return workflow.Terminate(workflow.Internal, err.Error()) } return workflow.OK() } @@ -100,7 +97,7 @@ func checkUserExpired(ctx context.Context, log *zap.SugaredLogger, k8sClient cli return workflow.OK() } -func performUpdateInAtlas(ctx *workflow.Context, k8sClient client.Client, dus dbuser.AtlasUsersService, project akov2.AtlasProject, dbUser *akov2.AtlasDatabaseUser, apiUser *dbuser.User) workflow.Result { +func performUpdateInAtlas(ctx *workflow.Context, k8sClient client.Client, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser, apiUser *mongodbatlas.DatabaseUser) workflow.Result { log := ctx.Log secret := &corev1.Secret{} @@ -116,25 +113,27 @@ func performUpdateInAtlas(ctx *workflow.Context, k8sClient client.Client, dus db retryAfterUpdate := workflow.InProgress(workflow.DatabaseUserDeploymentAppliedChanges, "Clusters are scheduled to handle database users updates") // Try to find the user - au, err := dus.Get(ctx.Context, dbUser.Spec.DatabaseName, project.ID(), dbUser.Spec.Username) - if errors.Is(err, dbuser.ErrorNotFound) { - log.Debugw("User doesn't exist. Create new user", "dbUser", dbUser) - if err = dus.Create(ctx.Context, apiUser); err != nil { + u, _, err := ctx.Client.DatabaseUsers.Get(ctx.Context, dbUser.Spec.DatabaseName, project.ID(), dbUser.Spec.Username) + if err != nil { + var apiError *mongodbatlas.ErrorResponse + if errors.As(err, &apiError) && apiError.ErrorCode == atlas.UsernameNotFound { + log.Debugw("User doesn't exist. Create new user", "apiUser", apiUser) + if _, _, err = ctx.Client.DatabaseUsers.Create(ctx.Context, project.ID(), apiUser); err != nil { + return workflow.Terminate(workflow.DatabaseUserNotCreatedInAtlas, err.Error()) + } + ctx.EnsureStatusOption(status.AtlasDatabaseUserPasswordVersion(currentPasswordResourceVersion)) + + ctx.Log.Infow("Created Atlas Database User", "name", dbUser.Spec.Username) + return retryAfterUpdate + } else { return workflow.Terminate(workflow.DatabaseUserNotCreatedInAtlas, err.Error()) } - ctx.EnsureStatusOption(status.AtlasDatabaseUserPasswordVersion(currentPasswordResourceVersion)) - - ctx.Log.Infow("Created Atlas Database User", "name", dbUser.Spec.Username) - return retryAfterUpdate - } - if err != nil { - return workflow.Terminate(workflow.DatabaseUserNotCreatedInAtlas, err.Error()) } // Update if the spec has changed - if shouldUpdate, err := shouldUpdate(ctx.Log, au, dbUser, currentPasswordResourceVersion); err != nil { + if shouldUpdate, err := shouldUpdate(ctx.Log, u, dbUser, currentPasswordResourceVersion); err != nil { return workflow.Terminate(workflow.Internal, err.Error()) } else if shouldUpdate { - err = dus.Update(ctx.Context, apiUser) + _, _, err = ctx.Client.DatabaseUsers.Update(ctx.Context, project.ID(), dbUser.Spec.Username, apiUser) if err != nil { return workflow.Terminate(workflow.DatabaseUserNotUpdatedInAtlas, err.Error()) } @@ -149,21 +148,19 @@ func performUpdateInAtlas(ctx *workflow.Context, k8sClient client.Client, dus db return workflow.OK() } -func validateScopes(ctx *workflow.Context, ds deployment.AtlasDeploymentsService, projectID string, user akov2.AtlasDatabaseUser) error { +func validateScopes(ctx *workflow.Context, projectID string, user akov2.AtlasDatabaseUser) error { for _, s := range user.GetScopes(akov2.DeploymentScopeType) { - exists, err := ds.ClusterExists(ctx.Context, projectID, s) - if !exists { + var apiError *mongodbatlas.ErrorResponse + _, _, advancedErr := ctx.Client.AdvancedClusters.Get(ctx.Context, projectID, s) + if errors.As(advancedErr, &apiError) && apiError.ErrorCode == atlas.ClusterNotFound { return fmt.Errorf(`"scopes" field references deployment named "%s" but such deployment doesn't exist in Atlas'`, s) } - if err != nil { - return err - } } return nil } -func checkDeploymentsHaveReachedGoalState(ctx *workflow.Context, ds deployment.AtlasDeploymentsService, projectID string, user akov2.AtlasDatabaseUser) workflow.Result { - allDeploymentNames, err := ds.ListClusterNames(ctx.Context, projectID) +func checkDeploymentsHaveReachedGoalState(ctx *workflow.Context, projectID string, user akov2.AtlasDatabaseUser) workflow.Result { + allDeploymentNames, err := atlasdeployment.GetAllDeploymentNames(ctx.Context, ctx.Client, projectID) if err != nil { return workflow.Terminate(workflow.Internal, err.Error()) } @@ -178,7 +175,7 @@ func checkDeploymentsHaveReachedGoalState(ctx *workflow.Context, ds deployment.A readyDeployments := 0 for _, c := range deploymentsToCheck { - ready, err := ds.DeploymentIsReady(ctx.Context, projectID, c) + ready, err := deploymentIsReady(ctx.Context, ctx.Client, projectID, c) if err != nil { return workflow.Terminate(workflow.Internal, err.Error()) } @@ -196,6 +193,14 @@ func checkDeploymentsHaveReachedGoalState(ctx *workflow.Context, ds deployment.A return workflow.OK() } +func deploymentIsReady(ctx context.Context, client *mongodbatlas.Client, projectID, deploymentName string) (bool, error) { + resourceStatus, _, err := client.Clusters.Status(ctx, projectID, deploymentName) + if err != nil { + return false, err + } + return resourceStatus.ChangeStatus == mongodbatlas.ChangeStatusApplied, nil +} + func filterScopeDeployments(user akov2.AtlasDatabaseUser, allDeploymentsInProject []string) []string { scopeDeployments := user.GetScopes(akov2.DeploymentScopeType) var deploymentsToCheck []string @@ -213,21 +218,52 @@ func filterScopeDeployments(user akov2.AtlasDatabaseUser, allDeploymentsInProjec return deploymentsToCheck } -func shouldUpdate(log *zap.SugaredLogger, atlasUser *dbuser.User, operatorUser *akov2.AtlasDatabaseUser, currentPasswordResourceVersion string) (bool, error) { - userSpecCopy, err := dbuser.NewUser(operatorUser.Spec.DeepCopy(), "", "") // project and password will not be compared +func shouldUpdate(log *zap.SugaredLogger, atlasSpec *mongodbatlas.DatabaseUser, operatorDBUser akov2.AtlasDatabaseUser, currentPasswordResourceVersion string) (bool, error) { + matches, err := userMatchesSpec(log, atlasSpec, operatorDBUser.Spec) if err != nil { - return false, fmt.Errorf("failed to convert User Spec: %w", err) + return false, err } - if !dbuser.EqualSpecs(userSpecCopy, atlasUser) { - if log.Level() == zapcore.DebugLevel { - log.Debugf("Atlas Database user differs from Spec: %v", dbuser.DiffSpecs(userSpecCopy, atlasUser)) - } + if !matches { return true, nil } // We need to check if the password has changed since the last time - passwordsChanged := operatorUser.Status.PasswordVersion != currentPasswordResourceVersion + passwordsChanged := operatorDBUser.Status.PasswordVersion != currentPasswordResourceVersion if passwordsChanged { log.Debug("Database User password has changed - making the request to Atlas") } return passwordsChanged, nil } + +// TODO move to a separate utils (reuse from deployments) +func userMatchesSpec(log *zap.SugaredLogger, atlasSpec *mongodbatlas.DatabaseUser, operatorSpec akov2.AtlasDatabaseUserSpec) (bool, error) { + userMerged := mongodbatlas.DatabaseUser{} + if err := compat.JSONCopy(&userMerged, atlasSpec); err != nil { + return false, err + } + + if err := compat.JSONCopy(&userMerged, operatorSpec); err != nil { + return false, err + } + + // performing some normalization of dates + if atlasSpec.DeleteAfterDate != "" { + atlasDeleteDate, err := timeutil.ParseISO8601(atlasSpec.DeleteAfterDate) + if err != nil { + return false, err + } + atlasSpec.DeleteAfterDate = timeutil.FormatISO8601(atlasDeleteDate) + } + if operatorSpec.DeleteAfterDate != "" { + operatorDeleteDate, err := timeutil.ParseISO8601(operatorSpec.DeleteAfterDate) + if err != nil { + return false, err + } + userMerged.DeleteAfterDate = timeutil.FormatISO8601(operatorDeleteDate) + } + d := cmp.Diff(*atlasSpec, userMerged, cmpopts.EquateEmpty()) + if d != "" { + log.Debugf("Users differs from spec: %s", d) + } + + return d == "", nil +} diff --git a/pkg/controller/atlasdatabaseuser/databaseuser_test.go b/pkg/controller/atlasdatabaseuser/databaseuser_test.go index 923b040268..1de5868d16 100644 --- a/pkg/controller/atlasdatabaseuser/databaseuser_test.go +++ b/pkg/controller/atlasdatabaseuser/databaseuser_test.go @@ -3,41 +3,26 @@ package atlasdatabaseuser import ( "context" "fmt" + "net/http" "testing" "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.mongodb.org/atlas-sdk/v20231115008/admin" - "go.mongodb.org/atlas-sdk/v20231115008/mockadmin" + "go.mongodb.org/atlas/mongodbatlas" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" - mocked "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/translation" - "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/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/connectionsecret" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" ) -const ( - testUserPasswordName = "password-name" - - nonExistingCluster = "non-existing-cluster" - - testDeployment = "deployment" -) - func init() { logger, _ := zap.NewDevelopment() zap.ReplaceGlobals(logger) @@ -119,19 +104,12 @@ func TestCheckUserExpired(t *testing.T) { func TestHandleUserNameChange(t *testing.T) { t.Run("Only one user after name change", func(t *testing.T) { - projectID := "project1" - username := "theuser" - user := *akov2.DefaultDBUser("ns", "theuser", projectID) + user := *akov2.DefaultDBUser("ns", "theuser", "project1") user.Spec.Username = "differentuser" - user.Status.UserName = username + user.Status.UserName = "theuser" ctx := workflow.NewContext(zap.S(), []api.Condition{}, nil) - ctx.Context = context.Background() - testUserAPI := mockadmin.NewDatabaseUsersApi(t) - dus := dbuser.NewAtlasUsers(testUserAPI) - testUserAPI.EXPECT().DeleteDatabaseUser(ctx.Context, projectID, "", username).Return( - admin.DeleteDatabaseUserApiRequest{ApiService: testUserAPI}) - testUserAPI.EXPECT().DeleteDatabaseUserExecute(mock.Anything).Return(nil, nil, nil) - result := handleUserNameChange(ctx, dus, projectID, user) + ctx.Client = mongodbatlas.NewClient(&http.Client{}) + result := handleUserNameChange(ctx, "", user) assert.True(t, result.IsOk()) }) } @@ -144,254 +122,3 @@ func dataForSecret() connectionsecret.ConnectionData { Password: "m@gick%", } } - -func TestEnsureDatabaseUser(t *testing.T) { - ctx := context.Background() - scheme := runtime.NewScheme() - utilruntime.Must(corev1.AddToScheme(scheme)) - utilruntime.Must(akov2.AddToScheme(scheme)) - fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build() - project := defaultTestProject() - user := defaultTestUser() - user.Spec.PasswordSecret = &common.ResourceRef{Name: testUserPasswordName} - user.Status.PasswordVersion = "1" - require.NoError(t, fakeClient.Create(ctx, user)) - defer fakeClient.Delete(ctx, user) - conditions := akov2.InitCondition(user, api.FalseCondition(api.ReadyType)) - log := zap.S() - workflowCtx := workflow.NewContext(log, conditions, ctx) - differentUser := defaultTestUser() - differentUser.Spec.AWSIAMType = "USER" - r := &AtlasDatabaseUserReconciler{ - Client: fakeClient, - Log: log, - } - for _, tc := range []struct { - title string - password *corev1.Secret - dateOverride string - scopeOverrides []akov2.ScopeSpec - nameOverride string - dus dbuser.AtlasUsersService - ds deployment.AtlasDeploymentsService - expectedMessage string - }{ - { - title: "Missing password fails", - expectedMessage: `secrets "password-name" not found`, - }, - - { - title: "Wrong date format fails", - password: defaultTestPassword(), - dateOverride: "this-is-not-a-proper-date", - expectedMessage: `failed to parse "this-is-not-a-proper-date" to an ISO date: parsing time "this-is-not-a-proper-date"`, - }, - - { - title: "Expired user aborts", - password: defaultTestPassword(), - dateOverride: time.Now().Add(-time.Hour).Format("2006-01-02T15:04:05-07"), - expectedMessage: `The database user is expired and has been removed from Atlas`, - }, - - { - title: "Invalid user scope aborts", - password: defaultTestPassword(), - scopeOverrides: []akov2.ScopeSpec{{Type: akov2.DeploymentScopeType, Name: nonExistingCluster}}, - ds: fakeClusterExists(ctx, testProjectID, nonExistingCluster, false, nil), - expectedMessage: `"scopes" field references deployment named "non-existing-cluster" but such deployment doesn't exist in Atlas'`, - }, - - { - title: "User get fails", - password: defaultTestPassword(), - dus: fakeGetUser(ctx, nil, errRandom), - expectedMessage: errRandom.Error(), - }, - - { - title: "User not found is created successfully", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, nil, dbuser.ErrorNotFound) - return withFakeCreateUser(service, ctx, internalUser(user), nil) - }(), - expectedMessage: `Clusters are scheduled to handle database users updates`, - }, - - { - title: "User not found tries to create but fails", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, nil, dbuser.ErrorNotFound) - return withFakeCreateUser(service, ctx, internalUser(user), errRandom) - }(), - expectedMessage: errRandom.Error(), - }, - - { - title: "User found unchanged does nothing", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(user), nil) - return withFakeUpdateUser(service, ctx, internalUser(user), nil) - }(), - ds: func() deployment.AtlasDeploymentsService { - service := fakeListClusterNames(ctx, []string{testDeployment}, nil) - service = withFakeDeploymentIsReady(service, ctx) - return withFakeListDeploymentConnections(service, ctx, nil) - }(), - expectedMessage: "", - }, - - { - title: "User different from Atlas is updated successfully", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(differentUser), nil) - return withFakeUpdateUser(service, ctx, internalUser(user), nil) - }(), - expectedMessage: `Clusters are scheduled to handle database users updates`, - }, - - { - title: "User different from Atlas tries to update but fails", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(differentUser), nil) - return withFakeUpdateUser(service, ctx, internalUser(user), errRandom) - }(), - expectedMessage: errRandom.Error(), - }, - - { - title: "User found unchanged but fails to check clusters", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(user), nil) - return withFakeUpdateUser(service, ctx, internalUser(user), nil) - }(), - ds: fakeListClusterNames(ctx, []string{testDeployment}, errRandom), - expectedMessage: errRandom.Error(), - }, - - { - title: "User found unchanged but fails to check connections", - password: defaultTestPassword(), - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(user), nil) - return withFakeUpdateUser(service, ctx, internalUser(user), nil) - }(), - ds: func() deployment.AtlasDeploymentsService { - service := fakeListClusterNames(ctx, []string{testDeployment}, nil) - service = withFakeDeploymentIsReady(service, ctx) - return withFakeListDeploymentConnections(service, ctx, errRandom) - }(), - expectedMessage: errRandom.Error(), - }, - - { - title: "User found unchanged but changed name succeeds", - password: defaultTestPassword(), - nameOverride: "some-other-name", - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(user), nil) - service = withFakeUpdateUser(service, ctx, internalUser(user), nil) - return withFakeUserDeletion(service, ctx, testDatabase, testProjectID, "some-other-name", nil) - }(), - ds: func() deployment.AtlasDeploymentsService { - service := fakeListClusterNames(ctx, []string{testDeployment}, nil) - service = withFakeDeploymentIsReady(service, ctx) - return withFakeListDeploymentConnections(service, ctx, nil) - }(), - }, - - { - title: "User found unchanged but changed name but fix fails", - password: defaultTestPassword(), - nameOverride: "some-other-name", - dus: func() dbuser.AtlasUsersService { - service := fakeGetUser(ctx, internalUser(user), nil) - service = withFakeUpdateUser(service, ctx, internalUser(user), nil) - return withFakeUserDeletion(service, ctx, testDatabase, testProjectID, "some-other-name", errRandom) - }(), - ds: func() deployment.AtlasDeploymentsService { - service := fakeListClusterNames(ctx, []string{testDeployment}, nil) - service = withFakeDeploymentIsReady(service, ctx) - return withFakeListDeploymentConnections(service, ctx, nil) - }(), - expectedMessage: errRandom.Error(), - }, - } { - t.Run(tc.title, func(t *testing.T) { - if tc.password != nil { - require.NoError(t, fakeClient.Create(ctx, tc.password)) - defer fakeClient.Delete(ctx, tc.password) - } - user.Spec.DeleteAfterDate = tc.dateOverride - user.Spec.Scopes = tc.scopeOverrides - user.Status.UserName = tc.nameOverride - result := r.ensureDatabaseUser(workflowCtx, tc.dus, tc.ds, *project, *user) - if tc.expectedMessage == "" { - assert.Equal(t, true, result.IsOk()) - } else { - assert.Equal(t, false, result.IsOk()) - assert.Contains(t, result.GetMessage(), tc.expectedMessage) - } - }) - } -} - -func internalUser(user *akov2.AtlasDatabaseUser) *dbuser.User { - return &dbuser.User{ - AtlasDatabaseUserSpec: &user.Spec, - Password: "some-secret-here", - ProjectID: testProjectID, - } -} - -func defaultTestPassword() *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testUserPasswordName}, - Data: map[string][]byte{"password": []byte("some-secret-here")}, - } -} - -func fakeGetUser(ctx context.Context, usr *dbuser.User, err error) *mocked.AtlasUsersServiceMock { - service := mocked.AtlasUsersServiceMock{} - service.EXPECT().Get(ctx, testDatabase, testProjectID, testUsername).Return(usr, err) - return &service -} - -func withFakeCreateUser(service *mocked.AtlasUsersServiceMock, ctx context.Context, usr *dbuser.User, err error) *mocked.AtlasUsersServiceMock { - service.EXPECT().Create(ctx, usr).Return(err) - return service -} - -func withFakeUpdateUser(service *mocked.AtlasUsersServiceMock, ctx context.Context, usr *dbuser.User, err error) *mocked.AtlasUsersServiceMock { - service.EXPECT().Update(ctx, usr).Return(err) - return service -} - -func fakeClusterExists(ctx context.Context, projectID, clusterName string, exists bool, err error) *mocked.AtlasDeploymentsServiceMock { - service := mocked.AtlasDeploymentsServiceMock{} - service.EXPECT().ClusterExists(ctx, projectID, clusterName).Return(exists, err) - return &service -} - -func fakeListClusterNames(ctx context.Context, names []string, err error) *mocked.AtlasDeploymentsServiceMock { - service := mocked.AtlasDeploymentsServiceMock{} - service.EXPECT().ListClusterNames(ctx, testProjectID).Return(names, err) - return &service -} - -func withFakeDeploymentIsReady(service *mocked.AtlasDeploymentsServiceMock, ctx context.Context) *mocked.AtlasDeploymentsServiceMock { - service.EXPECT().DeploymentIsReady(ctx, testProjectID, testDeployment).Return(true, nil) - return service -} - -func withFakeListDeploymentConnections(service *mocked.AtlasDeploymentsServiceMock, ctx context.Context, err error) *mocked.AtlasDeploymentsServiceMock { - service.EXPECT().ListDeploymentConnections(ctx, testProjectID).Return(nil, err) - return service -} diff --git a/pkg/controller/atlasproject/alert_configurations.go b/pkg/controller/atlasproject/alert_configurations.go index ef2ea523b0..9dbb01c870 100644 --- a/pkg/controller/atlasproject/alert_configurations.go +++ b/pkg/controller/atlasproject/alert_configurations.go @@ -302,7 +302,7 @@ func isAlertConfigSpecEqualToAtlas(logger *zap.SugaredLogger, alertConfigSpec ak return false } - atlasMatchers := []akov2.Matcher{} + var atlasMatchers []akov2.Matcher err := compat.JSONCopy(atlasMatchers, atlasAlertConfig.GetMatchers()) if err != nil { logger.Errorf("unable to convert matchers to structured type: %s", err) diff --git a/pkg/controller/connectionsecret/connectionsecrets.go b/pkg/controller/connectionsecret/connectionsecrets.go index 29efccd68b..31dd141025 100644 --- a/pkg/controller/connectionsecret/connectionsecrets.go +++ b/pkg/controller/connectionsecret/connectionsecrets.go @@ -12,40 +12,75 @@ 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" 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, recorder record.EventRecorder, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser) workflow.Result { + advancedDeployments, _, err := ctx.Client.AdvancedClusters.List(ctx.Context, project.ID(), &mongodbatlas.ListOptions{}) if err != nil { return workflow.Terminate(workflow.DatabaseUserConnectionSecretsNotCreated, err.Error()) } + var deploymentSecrets []deploymentSecret + for _, c := range advancedDeployments.Results { + deploymentSecrets = append(deploymentSecrets, deploymentSecret{ + name: c.Name, + connectionStrings: c.ConnectionStrings, + }) + } + + serverlessDeployments, err := GetAllServerless(ctx, project.ID()) + if err != nil { + return workflow.Terminate(workflow.DatabaseUserConnectionSecretsNotCreated, err.Error()) + } + for _, c := range serverlessDeployments { + found := false + + for _, advancedDeployment := range advancedDeployments.Results { + if advancedDeployment.Name == c.Name { + found = true + break + } + } + + if !found { + deploymentSecrets = append(deploymentSecrets, deploymentSecret{ + name: c.Name, + connectionStrings: c.ConnectionStrings, + }) + } + } + // ensure secrets for both deployments and advanced deployment. - if result := createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx, k8sClient, recorder, project, dbUser, conns); !result.IsOk() { + if result := createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx, k8sClient, recorder, project, dbUser, deploymentSecrets); !result.IsOk() { return result } 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 { +// deploymentSecret holds the information required to ensure a secret for a user in a given deployment. +type deploymentSecret struct { + name string + connectionStrings *mongodbatlas.ConnectionStrings +} + +func createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx *workflow.Context, k8sClient client.Client, recorder record.EventRecorder, project akov2.AtlasProject, dbUser akov2.AtlasDatabaseUser, deploymentSecrets []deploymentSecret) workflow.Result { requeue := false secrets := make([]string, 0) - for _, di := range conns { + for _, ds := range deploymentSecrets { scopes := dbUser.GetScopes(akov2.DeploymentScopeType) - if len(scopes) != 0 && !stringutil.Contains(scopes, di.Name) { + if len(scopes) != 0 && !stringutil.Contains(scopes, ds.name) { continue } // Deployment may be not ready yet, so no connection urls - skipping // Note, that Atlas usually returns the not-nil connection strings with empty fields in it - if di.SrvConnURL == "" { - ctx.Log.Debugw("Deployment is not ready yet - not creating a connection Secret", "deployment", di.Name) + if ds.connectionStrings == nil || ds.connectionStrings.StandardSrv == "" { + ctx.Log.Debugw("Deployment is not ready yet - not creating a connection Secret", "deployment", ds.name) requeue = true continue } @@ -56,13 +91,13 @@ func createOrUpdateConnectionSecretsFromDeploymentSecrets(ctx *workflow.Context, data := ConnectionData{ DBUserName: dbUser.Spec.Username, Password: password, - ConnURL: di.ConnURL, - SrvConnURL: di.SrvConnURL, + ConnURL: ds.connectionStrings.Standard, + SrvConnURL: ds.connectionStrings.StandardSrv, } - FillPrivateConns(di, &data) + FillPrivateConnStrings(ds.connectionStrings, &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.Spec.Name, project.ID(), ds.name, data); err != nil { return workflow.Terminate(workflow.DatabaseUserConnectionSecretsNotCreated, err.Error()) } secrets = append(secrets, secretName) @@ -143,34 +178,6 @@ func RemoveStaleSecretsByUserName(ctx context.Context, k8sClient client.Client, return lastError } -func FillPrivateConns(conn deployment.Connection, data *ConnectionData) { - if conn.PrivateURL != "" { - data.PrivateConnURLs = append(data.PrivateConnURLs, PrivateLinkConnURLs{ - PvtConnURL: conn.PrivateURL, - PvtSrvConnURL: conn.SrvPrivateURL, - }) - } - - if conn.Serverless { - for _, pe := range conn.PrivateEndpoints { - data.PrivateConnURLs = append(data.PrivateConnURLs, PrivateLinkConnURLs{ - PvtSrvConnURL: pe.ServerURL, - }) - } - } else { - for _, pe := range conn.PrivateEndpoints { - data.PrivateConnURLs = append(data.PrivateConnURLs, PrivateLinkConnURLs{ - PvtConnURL: pe.URL, - PvtSrvConnURL: pe.ServerURL, - PvtShardConnURL: pe.ShardURL, - }) - } - } -} - -// FillPrivateConnStrings fills private conn urls from connection strings -// TODO: (CLOUDP-253951) remove once all usages move over to FillPrivateConns instead -// Right now only advanced deployment is using this one func FillPrivateConnStrings(connStrings *mongodbatlas.ConnectionStrings, data *ConnectionData) { if connStrings.Private != "" { data.PrivateConnURLs = append(data.PrivateConnURLs, PrivateLinkConnURLs{ diff --git a/scripts/int_local.sh b/scripts/int_local.sh index 626ca2187f..8df8a0d546 100755 --- a/scripts/int_local.sh +++ b/scripts/int_local.sh @@ -16,4 +16,4 @@ export MCLI_PUBLIC_API_KEY="${public_key}" export MCLI_PRIVATE_API_KEY="${private_key}" export MCLI_ORG_ID="${org_id}" -AKO_INT_TEST=1 ginkgo run --race --label-filter="${label}" --timeout 80m -v ./test/int ./test/int/clusterwide -coverprofile cover.out +AKO_INT_TEST=1 ginkgo --race --label-filter="${label}" --timeout 80m -v ./test/int -coverprofile cover.out \ No newline at end of file diff --git a/test/int/clusterwide/dbuser_test.go b/test/int/clusterwide/dbuser_test.go index ac755beaad..8a97c44850 100644 --- a/test/int/clusterwide/dbuser_test.go +++ b/test/int/clusterwide/dbuser_test.go @@ -59,6 +59,7 @@ var _ = Describe("clusterwide", Label("int", "clusterwide"), func() { // While developing tests we need to reuse the same project createdProject.Spec.Name = "dev-test atlas-project" } + Expect(k8sClient.Create(context.Background(), createdProject)).To(Succeed()) Eventually(func() bool { return resources.CheckCondition(k8sClient, createdProject, api.TrueCondition(api.ReadyType)) diff --git a/test/int/databaseuser_protected_test.go b/test/int/databaseuser_protected_test.go index daebdc9dc1..3204eaffca 100644 --- a/test/int/databaseuser_protected_test.go +++ b/test/int/databaseuser_protected_test.go @@ -84,7 +84,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) Describe("Operator is running with deletion protection enabled", func() { - It("Adds database users and protect them to be deleted when operator doesn't own resource", Label("unowned-protected"), func() { + It("Adds database users and protect them to be deleted when operator doesn't own resource", func() { By("First without setting atlas-resource-policy annotation", func() { passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) @@ -189,7 +189,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Adds database users and manage them when operator take ownership of existing resources", Label("owning-protected"), func() { + It("Adds database users and manage them when operator take ownership of existing resources", func() { By("First without setting atlas-resource-policy annotation", func() { passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) diff --git a/test/int/databaseuser_unprotected_test.go b/test/int/databaseuser_unprotected_test.go index 87c7a20ef8..d23334d52d 100644 --- a/test/int/databaseuser_unprotected_test.go +++ b/test/int/databaseuser_unprotected_test.go @@ -84,7 +84,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) Describe("Operator is running with deletion protection disabled", func() { - It("Adds database users and allow them to be deleted", Label("user-removable"), func() { + It("Adds database users and allow them to be deleted", func() { By("Creating a database user previously on Atlas", func() { dbUser := admin.NewCloudDatabaseUser("admin", testProject.ID(), dbUserName3) dbUser.SetPassword("mypass") @@ -214,7 +214,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Adds an user and manage roles", Label("user-manage-roles"), func() { + It("Adds an user and manage roles", func() { By("Creating an user with clusterMonitor role", func() { passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) @@ -276,7 +276,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Adds connection secret when new deployment is created", Label("user-add-secret"), func() { + It("Adds connection secret when new deployment is created", func() { secondDeployment := &akov2.AtlasDeployment{} By("Creating a database user", func() { @@ -333,7 +333,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Watches password secret", Label("user-watch-secret"), func() { + It("Watches password secret", func() { By("Creating a database user", func() { passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) @@ -384,7 +384,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Remove stale secrets", Label("user-gc-secrets"), func() { + It("Remove stale secrets", func() { secondTestDeployment := &akov2.AtlasDeployment{} By("Creating a second deployment", func() { @@ -474,7 +474,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Validates user date expiration", Label("user-date-expiration"), func() { + It("Validates user date expiration", func() { By("Creating expired user", func() { passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) @@ -533,7 +533,7 @@ var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "prote }) }) - It("Skips reconciliations.", Label("user-skip-reconciliation"), func() { + It("Skips reconciliations.", func() { By("Creating a database user", func() { passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed())