From b28dd4a3c4c2cb67bfca1f0196112225803b5340 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Fri, 11 Aug 2023 11:00:32 +0200 Subject: [PATCH 01/11] manage Maintenance Window, Auditing and settings --- .../atlasproject/atlasproject_controller.go | 6 +- pkg/controller/atlasproject/auditing.go | 61 ++- pkg/controller/atlasproject/auditing_test.go | 244 +++++++++++- .../atlasproject/maintenancewindow.go | 104 +++++- .../atlasproject/maintenancewindow_test.go | 222 +++++++++++ .../atlasproject/project_settings.go | 99 ++++- .../atlasproject/project_settings_test.go | 346 +++++++++++++++++- test/e2e/project_deletion_protection_test.go | 168 +++++++++ 8 files changed, 1215 insertions(+), 35 deletions(-) diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index 0a2a5a3cfc..bf4c00c0dd 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -309,7 +309,7 @@ func (r *AtlasProjectReconciler) ensureProjectResources(ctx context.Context, wor } results = append(results, result) - if result = ensureMaintenanceWindow(workflowCtx, project.ID(), project); result.IsOk() { + if result = ensureMaintenanceWindow(ctx, workflowCtx, project, r.SubObjectDeletionProtection); result.IsOk() { r.EventRecorder.Event(project, "Normal", string(status.MaintenanceWindowReadyType), "") } results = append(results, result) @@ -319,12 +319,12 @@ func (r *AtlasProjectReconciler) ensureProjectResources(ctx context.Context, wor } results = append(results, result) - if result = ensureAuditing(workflowCtx, project.ID(), project); result.IsOk() { + if result = ensureAuditing(ctx, workflowCtx, project, r.SubObjectDeletionProtection); result.IsOk() { r.EventRecorder.Event(project, "Normal", string(status.AuditingReadyType), "") } results = append(results, result) - if result = ensureProjectSettings(workflowCtx, project.ID(), project); result.IsOk() { + if result = ensureProjectSettings(ctx, workflowCtx, project, r.SubObjectDeletionProtection); result.IsOk() { r.EventRecorder.Event(project, "Normal", string(status.ProjectSettingsReadyType), "") } results = append(results, result) diff --git a/pkg/controller/atlasproject/auditing.go b/pkg/controller/atlasproject/auditing.go index f7ccebd95c..9fbeedf435 100644 --- a/pkg/controller/atlasproject/auditing.go +++ b/pkg/controller/atlasproject/auditing.go @@ -2,8 +2,14 @@ package atlasproject import ( "context" + "encoding/json" + "fmt" "reflect" + "github.com/google/go-cmp/cmp" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + "go.mongodb.org/atlas/mongodbatlas" v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" @@ -12,19 +18,37 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" ) -func ensureAuditing(ctx *workflow.Context, projectID string, project *v1.AtlasProject) workflow.Result { - result := createOrDeleteAuditing(ctx, projectID, project) +func ensureAuditing(ctx context.Context, workflowCtx *workflow.Context, project *v1.AtlasProject, protected bool) workflow.Result { + canReconcile, err := canAuditingReconcile(ctx, workflowCtx.Client, protected, project) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.AuditingReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.AuditingReadyType, result) + + return result + } + + result := createOrDeleteAuditing(workflowCtx, project.ID(), project) if !result.IsOk() { - ctx.SetConditionFromResult(status.AuditingReadyType, result) + workflowCtx.SetConditionFromResult(status.AuditingReadyType, result) return result } if isAuditingEmpty(project.Spec.Auditing) { - ctx.UnsetCondition(status.AuditingReadyType) + workflowCtx.UnsetCondition(status.AuditingReadyType) return workflow.OK() } - ctx.SetConditionTrue(status.AuditingReadyType) + workflowCtx.SetConditionTrue(status.AuditingReadyType) return workflow.OK() } @@ -65,6 +89,7 @@ func auditingInSync(atlas *mongodbatlas.Auditing, spec *v1.Auditing) bool { specAsAtlas := spec.ToAtlas() removeConfigurationType(atlas) + fmt.Println("AUDIT DIFF:", cmp.Diff(atlas, specAsAtlas)) return reflect.DeepEqual(atlas, specAsAtlas) } @@ -85,3 +110,29 @@ func patchAuditing(ctx *workflow.Context, projectID string, auditing *mongodbatl _, _, err := ctx.Client.Auditing.Configure(context.Background(), projectID, auditing) return err } + +func canAuditingReconcile(ctx context.Context, atlasClient mongodbatlas.Client, protected bool, akoProject *v1.AtlasProject) (bool, error) { + if !protected { + return true, nil + } + + latestConfig := &v1.AtlasProjectSpec{} + latestConfigString, ok := akoProject.Annotations[customresource.AnnotationLastAppliedConfiguration] + if ok { + if err := json.Unmarshal([]byte(latestConfigString), latestConfig); err != nil { + return false, err + } + } + + auditing, _, err := atlasClient.Auditing.Get(ctx, akoProject.ID()) + if err != nil { + return false, err + } + + if isAuditingEmpty(auditing) { + return true, nil + } + + return auditingInSync(auditing, latestConfig.Auditing) || + auditingInSync(auditing, akoProject.Spec.Auditing), nil +} diff --git a/pkg/controller/atlasproject/auditing_test.go b/pkg/controller/atlasproject/auditing_test.go index 5ecd87ffbd..eb4c5fd9e1 100644 --- a/pkg/controller/atlasproject/auditing_test.go +++ b/pkg/controller/atlasproject/auditing_test.go @@ -1,20 +1,244 @@ package atlasproject import ( + "context" + "errors" "testing" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" "github.com/stretchr/testify/assert" - "go.mongodb.org/atlas/mongodbatlas" - v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" + + "github.com/stretchr/testify/require" + + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + + "go.mongodb.org/atlas/mongodbatlas" ) -func Test_auditingInSync(t *testing.T) { +type auditingClient struct { + GetFunc func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) +} + +func (c *auditingClient) Get(_ context.Context, projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return c.GetFunc(projectID) +} +func (c *auditingClient) Configure(_ context.Context, _ string, _ *mongodbatlas.Auditing) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func TestCanAuditingReconcile(t *testing.T) { + t.Run("should return true when subResourceDeletionProtection is disabled", func(t *testing.T) { + result, err := canAuditingReconcile(context.TODO(), mongodbatlas.Client{}, false, &mdbv1.AtlasProject{}) + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return error when unable to deserialize last applied configuration", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{wrong}"}) + result, err := canAuditingReconcile(context.TODO(), mongodbatlas.Client{}, true, akoProject) + require.EqualError(t, err, "invalid character 'w' looking for beginning of object key string") + require.False(t, result) + }) + + t.Run("should return error when unable to fetch data from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canAuditingReconcile(context.TODO(), atlasClient, true, akoProject) + + require.EqualError(t, err, "failed to retrieve data") + require.False(t, result) + }) + + t.Run("should return true when configuration is empty in Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return nil, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canAuditingReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are no difference between current Atlas and previous applied configuration", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return &mongodbatlas.Auditing{ + Enabled: toptr.MakePtr(true), + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: toptr.MakePtr(false), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Auditing: &mdbv1.Auditing{ + Enabled: true, + AuditFilter: `{"atype":"authenticate","param":{"db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: false, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"auditing":{"auditFilter":"{\"atype\":\"authenticate\",\"param\":{\"user\":\"auditReadOnly\",\"db\":\"admin\",\"mechanism\":\"SCRAM-SHA-1\"}}","enabled":true,"auditAuthorizationSuccess":false}}`, + }, + ) + result, err := canAuditingReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are differences but new configuration synchronize operator", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return &mongodbatlas.Auditing{ + Enabled: toptr.MakePtr(true), + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: toptr.MakePtr(false), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Auditing: &mdbv1.Auditing{ + Enabled: true, + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: false, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"auditing":{"auditFilter":"{\"atype\":\"authenticate\",\"param\":{\"user\":\"auditReadOnly\",\"db\":\"admin\",\"mechanism\":\"SCRAM-SHA-1\"}}","enabled":true,"auditAuthorizationSuccess":true}}`, + }, + ) + result, err := canAuditingReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return false when unable to reconcile Auditing", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return &mongodbatlas.Auditing{ + Enabled: toptr.MakePtr(true), + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: toptr.MakePtr(false), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Auditing: &mdbv1.Auditing{ + Enabled: true, + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: true, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"auditing":{"auditFilter":"{\"atype\":\"authenticate\",\"param\":{\"db\":\"admin\",\"mechanism\":\"SCRAM-SHA-1\"}}","enabled":true,"auditAuthorizationSuccess":true}}`, + }, + ) + result, err := canAuditingReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.False(t, result) + }) +} + +func TestEnsureAuditing(t *testing.T) { + t.Run("should failed to reconcile when unable to decide resource ownership", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureAuditing(context.TODO(), workflowCtx, akoProject, true) + + require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) + }) + + t.Run("should failed to reconcile when unable to synchronize with Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Auditing: &auditingClient{ + GetFunc: func(projectID string) (*mongodbatlas.Auditing, *mongodbatlas.Response, error) { + return &mongodbatlas.Auditing{ + Enabled: toptr.MakePtr(true), + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: toptr.MakePtr(false), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Auditing: &mdbv1.Auditing{ + Enabled: true, + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + AuditAuthorizationSuccess: true, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"auditing":{"auditFilter":"{\"atype\":\"authenticate\",\"param\":{\"db\":\"admin\",\"mechanism\":\"SCRAM-SHA-1\"}}","enabled":true,"auditAuthorizationSuccess":true}}`, + }, + ) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureAuditing(context.TODO(), workflowCtx, akoProject, true) + + require.Equal( + t, + workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ), + result, + ) + }) +} + +func TestAuditingInSync(t *testing.T) { type args struct { atlas *mongodbatlas.Auditing - spec *v1.Auditing + spec *mdbv1.Auditing } tests := []struct { name string @@ -33,7 +257,7 @@ func Test_auditingInSync(t *testing.T) { name: "Atlas Auditing is empty and Operator doesn't", args: args{ atlas: nil, - spec: &v1.Auditing{Enabled: true}, + spec: &mdbv1.Auditing{Enabled: true}, }, want: false, }, @@ -54,7 +278,7 @@ func Test_auditingInSync(t *testing.T) { ConfigurationType: "ReadOnly", Enabled: toptr.MakePtr(true), }, - spec: &v1.Auditing{ + spec: &mdbv1.Auditing{ AuditAuthorizationSuccess: true, AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, Enabled: true, @@ -71,7 +295,7 @@ func Test_auditingInSync(t *testing.T) { ConfigurationType: "ReadOnly", Enabled: toptr.MakePtr(true), }, - spec: &v1.Auditing{ + spec: &mdbv1.Auditing{ AuditAuthorizationSuccess: false, AuditFilter: `{"atype":"authenticate","param":{"db":"admin","mechanism":"SCRAM-SHA-1"}}`, Enabled: true, @@ -88,7 +312,7 @@ func Test_auditingInSync(t *testing.T) { ConfigurationType: "ReadOnly", Enabled: toptr.MakePtr(true), }, - spec: &v1.Auditing{ + spec: &mdbv1.Auditing{ AuditAuthorizationSuccess: false, AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, Enabled: true, @@ -105,7 +329,7 @@ func Test_auditingInSync(t *testing.T) { ConfigurationType: "ReadOnly", Enabled: toptr.MakePtr(true), }, - spec: &v1.Auditing{ + spec: &mdbv1.Auditing{ AuditAuthorizationSuccess: false, AuditFilter: "{\"atype\":\"authenticate\",\"param\":{\"user\":\"auditReadOnly\",\"db\":\"admin\",\"mechanism\":\"SCRAM-SHA-1\"}}\n", Enabled: true, diff --git a/pkg/controller/atlasproject/maintenancewindow.go b/pkg/controller/atlasproject/maintenancewindow.go index 3c97f4e864..960ac5da23 100644 --- a/pkg/controller/atlasproject/maintenancewindow.go +++ b/pkg/controller/atlasproject/maintenancewindow.go @@ -2,10 +2,16 @@ package atlasproject import ( "context" + "encoding/json" "errors" + "fmt" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" "go.mongodb.org/atlas/mongodbatlas" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" @@ -15,26 +21,44 @@ import ( // ensureMaintenanceWindow ensures that the state of the Atlas Maintenance Window matches the // state of the Maintenance Window specified in the project CR. If a Maintenance Window exists // in Atlas but is not specified in the CR, it is deleted. -func ensureMaintenanceWindow(ctx *workflow.Context, projectID string, atlasProject *mdbv1.AtlasProject) workflow.Result { +func ensureMaintenanceWindow(ctx context.Context, workflowCtx *workflow.Context, atlasProject *mdbv1.AtlasProject, protected bool) workflow.Result { + canReconcile, err := canMaintenanceWindowReconcile(ctx, workflowCtx.Client, protected, atlasProject) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.IPAccessListReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.MaintenanceWindowReadyType, result) + + return result + } + if isEmptyWindow(atlasProject.Spec.MaintenanceWindow) { - if condition, found := ctx.GetCondition(status.MaintenanceWindowReadyType); found { - ctx.Log.Debugw("Window is empty, deleting in Atlas") - if result := deleteInAtlas(ctx.Client, projectID); !result.IsOk() { - ctx.SetConditionFromResult(condition.Type, result) + if condition, found := workflowCtx.GetCondition(status.MaintenanceWindowReadyType); found { + workflowCtx.Log.Debugw("Window is empty, deleting in Atlas") + if result := deleteInAtlas(workflowCtx.Client, atlasProject.ID()); !result.IsOk() { + workflowCtx.SetConditionFromResult(condition.Type, result) return result } - ctx.UnsetCondition(condition.Type) + workflowCtx.UnsetCondition(condition.Type) } return workflow.OK() } - if result := syncAtlasWithSpec(ctx, projectID, atlasProject.Spec.MaintenanceWindow); !result.IsOk() { - ctx.SetConditionFromResult(status.MaintenanceWindowReadyType, result) + if result := syncAtlasWithSpec(workflowCtx, atlasProject.ID(), atlasProject.Spec.MaintenanceWindow); !result.IsOk() { + workflowCtx.SetConditionFromResult(status.MaintenanceWindowReadyType, result) return result } - ctx.SetConditionTrue(status.MaintenanceWindowReadyType) + workflowCtx.SetConditionTrue(status.MaintenanceWindowReadyType) return workflow.OK() } @@ -169,3 +193,65 @@ func toggleAutoDeferInAtlas(client mongodbatlas.Client, projectID string) workfl } return workflow.OK() } + +func canMaintenanceWindowReconcile(ctx context.Context, atlasClient mongodbatlas.Client, protected bool, akoProject *mdbv1.AtlasProject) (bool, error) { + if !protected { + return true, nil + } + + latestConfig := &mdbv1.AtlasProjectSpec{} + latestConfigString, ok := akoProject.Annotations[customresource.AnnotationLastAppliedConfiguration] + if ok { + if err := json.Unmarshal([]byte(latestConfigString), latestConfig); err != nil { + return false, err + } + } + + mWindow, _, err := atlasClient.MaintenanceWindows.Get(ctx, akoProject.ID()) + if err != nil { + return false, err + } + + if isAtlasMaintenanceWindowEmpty(mWindow) { + return true, nil + } + + return isMaintenanceWindowConfigEqual(latestConfig.MaintenanceWindow, *mWindow) || + isMaintenanceWindowConfigEqual(akoProject.Spec.MaintenanceWindow, *mWindow), nil +} + +func isMaintenanceWindowConfigEqual(akoMWindow project.MaintenanceWindow, atlasMWindow mongodbatlas.MaintenanceWindow) bool { + if atlasMWindow.HourOfDay == nil { + atlasMWindow.HourOfDay = toptr.MakePtr(0) + } + + if atlasMWindow.StartASAP == nil { + atlasMWindow.StartASAP = toptr.MakePtr(false) + } + + if atlasMWindow.AutoDeferOnceEnabled == nil { + atlasMWindow.AutoDeferOnceEnabled = toptr.MakePtr(false) + } + + if akoMWindow.DayOfWeek != atlasMWindow.DayOfWeek { + return false + } + + if akoMWindow.HourOfDay != *atlasMWindow.HourOfDay { + return false + } + + if akoMWindow.StartASAP != *atlasMWindow.StartASAP { + return false + } + + if akoMWindow.AutoDefer != *atlasMWindow.AutoDeferOnceEnabled { + return false + } + + return true +} + +func isAtlasMaintenanceWindowEmpty(mWindow *mongodbatlas.MaintenanceWindow) bool { + return mWindow.DayOfWeek == 0 && mWindow.HourOfDay == nil && mWindow.StartASAP == nil && mWindow.AutoDeferOnceEnabled == nil +} diff --git a/pkg/controller/atlasproject/maintenancewindow_test.go b/pkg/controller/atlasproject/maintenancewindow_test.go index d57218d661..14bf88d4bc 100644 --- a/pkg/controller/atlasproject/maintenancewindow_test.go +++ b/pkg/controller/atlasproject/maintenancewindow_test.go @@ -1,13 +1,50 @@ package atlasproject import ( + "context" + "errors" "testing" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" + + "github.com/stretchr/testify/require" + + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + + "go.mongodb.org/atlas/mongodbatlas" + "github.com/stretchr/testify/assert" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" ) +type maintenanceWindowClient struct { + GetFunc func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) +} + +func (c *maintenanceWindowClient) Get(_ context.Context, projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return c.GetFunc(projectID) +} + +func (c *maintenanceWindowClient) Update(_ context.Context, _ string, _ *mongodbatlas.MaintenanceWindow) (*mongodbatlas.Response, error) { + return nil, nil +} + +func (c *maintenanceWindowClient) Defer(_ context.Context, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + +func (c *maintenanceWindowClient) AutoDefer(_ context.Context, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + +func (c *maintenanceWindowClient) Reset(_ context.Context, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + func TestValidateMaintenanceWindow(t *testing.T) { testCases := []struct { in project.MaintenanceWindow @@ -121,3 +158,188 @@ func TestValidateMaintenanceWindow(t *testing.T) { }) } } + +func TestCanMaintenanceWindowReconcile(t *testing.T) { + t.Run("should return true when subResourceDeletionProtection is disabled", func(t *testing.T) { + result, err := canMaintenanceWindowReconcile(context.TODO(), mongodbatlas.Client{}, false, &mdbv1.AtlasProject{}) + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return error when unable to deserialize last applied configuration", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{wrong}"}) + result, err := canMaintenanceWindowReconcile(context.TODO(), mongodbatlas.Client{}, true, akoProject) + require.EqualError(t, err, "invalid character 'w' looking for beginning of object key string") + require.False(t, result) + }) + + t.Run("should return error when unable to fetch data from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canMaintenanceWindowReconcile(context.TODO(), atlasClient, true, akoProject) + + require.EqualError(t, err, "failed to retrieve data") + require.False(t, result) + }) + + t.Run("should return true when configuration is empty in Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return &mongodbatlas.MaintenanceWindow{}, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canMaintenanceWindowReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are no difference between current Atlas and previous applied configuration", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return &mongodbatlas.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: toptr.MakePtr(1), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + MaintenanceWindow: project.MaintenanceWindow{ + DayOfWeek: 7, + HourOfDay: 20, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{\"maintenanceWindow\":{\"dayOfWeek\":1,\"hourOfDay\":1}}"}) + result, err := canMaintenanceWindowReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are differences but new configuration synchronize operator", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return &mongodbatlas.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: toptr.MakePtr(1), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + MaintenanceWindow: project.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: 1, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{\"maintenanceWindow\":{\"dayOfWeek\":7,\"hourOfDay\":20}}"}) + result, err := canMaintenanceWindowReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return false when unable to reconcile IP Access List", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return &mongodbatlas.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: toptr.MakePtr(1), + StartASAP: toptr.MakePtr(true), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + MaintenanceWindow: project.MaintenanceWindow{ + DayOfWeek: 7, + HourOfDay: 20, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{\"maintenanceWindow\":{\"dayOfWeek\":1,\"hourOfDay\":1}}"}) + result, err := canMaintenanceWindowReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.False(t, result) + }) +} + +func TestEnsureMaintenanceWindow(t *testing.T) { + t.Run("should failed to reconcile when unable to decide resource ownership", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureMaintenanceWindow(context.TODO(), workflowCtx, akoProject, true) + + require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) + }) + + t.Run("should failed to reconcile when unable to synchronize with Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + MaintenanceWindows: &maintenanceWindowClient{ + GetFunc: func(projectID string) (*mongodbatlas.MaintenanceWindow, *mongodbatlas.Response, error) { + return &mongodbatlas.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: toptr.MakePtr(1), + StartASAP: toptr.MakePtr(true), + AutoDeferOnceEnabled: toptr.MakePtr(true), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + MaintenanceWindow: project.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: 1, + StartASAP: true, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{\"maintenanceWindow\":{\"dayOfWeek\":1,\"hourOfDay\":20,\"startASAP\":true,\"autoDefer\":true}}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureMaintenanceWindow(context.TODO(), workflowCtx, akoProject, true) + + require.Equal( + t, + workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ), + result, + ) + }) +} diff --git a/pkg/controller/atlasproject/project_settings.go b/pkg/controller/atlasproject/project_settings.go index 055212ed71..0a7f2394a5 100644 --- a/pkg/controller/atlasproject/project_settings.go +++ b/pkg/controller/atlasproject/project_settings.go @@ -2,25 +2,51 @@ package atlasproject import ( "context" + "encoding/json" + "fmt" "reflect" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" + + "go.mongodb.org/atlas/mongodbatlas" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" ) -func ensureProjectSettings(ctx *workflow.Context, projectID string, project *v1.AtlasProject) (result workflow.Result) { - if result = syncProjectSettings(ctx, projectID, project); !result.IsOk() { - ctx.SetConditionFromResult(status.ProjectSettingsReadyType, result) +func ensureProjectSettings(ctx context.Context, workflowCtx *workflow.Context, project *v1.AtlasProject, protected bool) (result workflow.Result) { + canReconcile, err := canProjectSettingsReconcile(ctx, workflowCtx.Client, protected, project) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.ProjectSettingsReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.ProjectSettingsReadyType, result) + + return result + } + + if result = syncProjectSettings(workflowCtx, project.ID(), project); !result.IsOk() { + workflowCtx.SetConditionFromResult(status.ProjectSettingsReadyType, result) return result } if project.Spec.Settings == nil { - ctx.UnsetCondition(status.ProjectSettingsReadyType) + workflowCtx.UnsetCondition(status.ProjectSettingsReadyType) return workflow.OK() } - ctx.SetConditionTrue(status.ProjectSettingsReadyType) + workflowCtx.SetConditionTrue(status.ProjectSettingsReadyType) return workflow.OK() } @@ -93,3 +119,66 @@ func isOneContainedInOther(one, other *v1.ProjectSettings) bool { return true } + +func canProjectSettingsReconcile(ctx context.Context, atlasClient mongodbatlas.Client, protected bool, akoProject *v1.AtlasProject) (bool, error) { + if !protected { + return true, nil + } + + latestConfig := &v1.AtlasProjectSpec{} + latestConfigString, ok := akoProject.Annotations[customresource.AnnotationLastAppliedConfiguration] + if ok { + if err := json.Unmarshal([]byte(latestConfigString), latestConfig); err != nil { + return false, err + } + } + + settings, _, err := atlasClient.Projects.GetProjectSettings(ctx, akoProject.ID()) + if err != nil { + return false, err + } + + if settings == nil { + return true, nil + } + + return areSettingsEqual(latestConfig.Settings, settings) || + areSettingsEqual(akoProject.Spec.Settings, settings), nil +} + +func areSettingsEqual(operator *v1.ProjectSettings, atlas *mongodbatlas.ProjectSettings) bool { + if (operator == nil) != (atlas == nil) { + return false + } + + if operator.IsCollectDatabaseSpecificsStatisticsEnabled == nil { + operator.IsCollectDatabaseSpecificsStatisticsEnabled = toptr.MakePtr(false) + } + + if operator.IsDataExplorerEnabled == nil { + operator.IsDataExplorerEnabled = toptr.MakePtr(false) + } + + if operator.IsExtendedStorageSizesEnabled == nil { + operator.IsExtendedStorageSizesEnabled = toptr.MakePtr(false) + } + + if operator.IsPerformanceAdvisorEnabled == nil { + operator.IsPerformanceAdvisorEnabled = toptr.MakePtr(false) + } + + if operator.IsRealtimePerformancePanelEnabled == nil { + operator.IsRealtimePerformancePanelEnabled = toptr.MakePtr(false) + } + + if operator.IsSchemaAdvisorEnabled == nil { + operator.IsSchemaAdvisorEnabled = toptr.MakePtr(false) + } + + return *operator.IsCollectDatabaseSpecificsStatisticsEnabled == *atlas.IsCollectDatabaseSpecificsStatisticsEnabled && + *operator.IsDataExplorerEnabled == *atlas.IsDataExplorerEnabled && + *operator.IsExtendedStorageSizesEnabled == *atlas.IsExtendedStorageSizesEnabled && + *operator.IsPerformanceAdvisorEnabled == *atlas.IsPerformanceAdvisorEnabled && + *operator.IsRealtimePerformancePanelEnabled == *atlas.IsRealtimePerformancePanelEnabled && + *operator.IsSchemaAdvisorEnabled == *atlas.IsSchemaAdvisorEnabled +} diff --git a/pkg/controller/atlasproject/project_settings_test.go b/pkg/controller/atlasproject/project_settings_test.go index 7b39ae495e..a50087dbd9 100644 --- a/pkg/controller/atlasproject/project_settings_test.go +++ b/pkg/controller/atlasproject/project_settings_test.go @@ -1,23 +1,363 @@ package atlasproject import ( + "context" + "errors" "testing" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas/mongodbatlas" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" + "github.com/stretchr/testify/assert" - v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" ) +type projectClient struct { + GetProjectSettingsFunc func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) +} + +func (c *projectClient) GetAllProjects(_ context.Context, _ *mongodbatlas.ListOptions) (*mongodbatlas.Projects, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) GetOneProject(_ context.Context, _ string) (*mongodbatlas.Project, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) GetOneProjectByName(_ context.Context, _ string) (*mongodbatlas.Project, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) Create(_ context.Context, _ *mongodbatlas.Project, _ *mongodbatlas.CreateProjectOptions) (*mongodbatlas.Project, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) Update(_ context.Context, _ string, _ *mongodbatlas.ProjectUpdateRequest) (*mongodbatlas.Project, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) Delete(_ context.Context, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + +func (c *projectClient) GetProjectTeamsAssigned(_ context.Context, _ string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) AddTeamsToProject(_ context.Context, _ string, _ []*mongodbatlas.ProjectTeam) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) RemoveUserFromProject(_ context.Context, _ string, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + +func (c *projectClient) Invitations(_ context.Context, _ string, _ *mongodbatlas.InvitationOptions) ([]*mongodbatlas.Invitation, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) Invitation(_ context.Context, _ string, _ string) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) InviteUser(_ context.Context, _ string, _ *mongodbatlas.Invitation) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) UpdateInvitation(_ context.Context, _ string, _ *mongodbatlas.Invitation) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) UpdateInvitationByID(_ context.Context, _ string, _ string, _ *mongodbatlas.Invitation) (*mongodbatlas.Invitation, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *projectClient) DeleteInvitation(_ context.Context, _ string, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + +func (c *projectClient) GetProjectSettings(_ context.Context, projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return c.GetProjectSettingsFunc(projectID) +} + +func (c *projectClient) UpdateProjectSettings(_ context.Context, _ string, _ *mongodbatlas.ProjectSettings) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func TestProjectSettingsReconcile(t *testing.T) { + t.Run("should return true when subResourceDeletionProtection is disabled", func(t *testing.T) { + result, err := canProjectSettingsReconcile(context.TODO(), mongodbatlas.Client{}, false, &mdbv1.AtlasProject{}) + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return error when unable to deserialize last applied configuration", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{wrong}"}) + result, err := canProjectSettingsReconcile(context.TODO(), mongodbatlas.Client{}, true, akoProject) + require.EqualError(t, err, "invalid character 'w' looking for beginning of object key string") + require.False(t, result) + }) + + t.Run("should return error when unable to fetch data from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canProjectSettingsReconcile(context.TODO(), atlasClient, true, akoProject) + + require.EqualError(t, err, "failed to retrieve data") + require.False(t, result) + }) + + t.Run("should return true when configuration is empty in Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return nil, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canProjectSettingsReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are no difference between current Atlas and previous applied configuration", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return &mongodbatlas.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Settings: &mdbv1.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(true), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{ +"settings": { + "isCollectDatabaseSpecificsStatisticsEnabled": true, + "isDataExplorerEnabled": true, + "isPerformanceAdvisorEnabled": true, + "isRealtimePerformancePanelEnabled": true, + "isSchemaAdvisorEnabled": true +}}`, + }, + ) + result, err := canProjectSettingsReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are differences but new configuration synchronize operator", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return &mongodbatlas.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Settings: &mdbv1.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{ +"settings": { + "isCollectDatabaseSpecificsStatisticsEnabled": true, + "isDataExplorerEnabled": true, + "isExtendedStorageSizesEnabled": true, + "isPerformanceAdvisorEnabled": true, + "isRealtimePerformancePanelEnabled": true, + "isSchemaAdvisorEnabled": true +}}`, + }, + ) + result, err := canProjectSettingsReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return false when unable to reconcile Project Settings", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return &mongodbatlas.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Settings: &mdbv1.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(false), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{ +"settings": { + "isCollectDatabaseSpecificsStatisticsEnabled": true, + "isDataExplorerEnabled": true, + "isExtendedStorageSizesEnabled": true, + "isPerformanceAdvisorEnabled": true, + "isRealtimePerformancePanelEnabled": true, + "isSchemaAdvisorEnabled": true +}}`, + }, + ) + result, err := canProjectSettingsReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.False(t, result) + }) +} + +func TestEnsureProjectSettings(t *testing.T) { + t.Run("should failed to reconcile when unable to decide resource ownership", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureProjectSettings(context.TODO(), workflowCtx, akoProject, true) + + require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) + }) + + t.Run("should failed to reconcile when unable to synchronize with Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectSettingsFunc: func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) { + return &mongodbatlas.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Settings: &mdbv1.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(false), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{ +"settings": { + "isCollectDatabaseSpecificsStatisticsEnabled": true, + "isDataExplorerEnabled": true, + "isExtendedStorageSizesEnabled": true, + "isPerformanceAdvisorEnabled": true, + "isRealtimePerformancePanelEnabled": true, + "isSchemaAdvisorEnabled": true +}}`, + }, + ) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureProjectSettings(context.TODO(), workflowCtx, akoProject, true) + + require.Equal( + t, + workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ), + result, + ) + }) +} + func TestAreSettingsInSync(t *testing.T) { - atlas := &v1.ProjectSettings{ + atlas := &mdbv1.ProjectSettings{ IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), IsDataExplorerEnabled: toptr.MakePtr(true), IsPerformanceAdvisorEnabled: toptr.MakePtr(true), IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), IsSchemaAdvisorEnabled: toptr.MakePtr(true), } - spec := &v1.ProjectSettings{ + spec := &mdbv1.ProjectSettings{ IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), IsDataExplorerEnabled: toptr.MakePtr(true), } diff --git a/test/e2e/project_deletion_protection_test.go b/test/e2e/project_deletion_protection_test.go index 22b98075e5..510639a804 100644 --- a/test/e2e/project_deletion_protection_test.go +++ b/test/e2e/project_deletion_protection_test.go @@ -194,6 +194,34 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote Expect(err).ToNot(HaveOccurred()) }) + By("Adding Maintenance Window to the project", func() { + _, err := atlasClient.Client.MaintenanceWindows.Update(ctx, projectID, &mongodbatlas.MaintenanceWindow{ + DayOfWeek: 7, + HourOfDay: toptr.MakePtr(20), + }) + Expect(err).ToNot(HaveOccurred()) + }) + + By("Adding Auditing to the project", func() { + _, _, err := atlasClient.Client.Auditing.Configure(ctx, projectID, &mongodbatlas.Auditing{ + AuditFilter: `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}`, + Enabled: toptr.MakePtr(true), + }) + Expect(err).ToNot(HaveOccurred()) + }) + + By("Adding Settings to the Project", func() { + _, _, err := atlasClient.Client.Projects.UpdateProjectSettings(ctx, projectID, &mongodbatlas.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(true), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }) + Expect(err).ToNot(HaveOccurred()) + }) + By("Creating a project to be managed by the operator", func() { akoProject := &mdbv1.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ @@ -232,6 +260,23 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote }, }, }, + MaintenanceWindow: project.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: 20, + }, + Auditing: &mdbv1.Auditing{ + AuditAuthorizationSuccess: false, + AuditFilter: `{"$or":[{"users":[]},{"$and":[{"users":{"$elemMatch":{"$or":[{"db":"admin"}]}}},{"atype":{"$in":["authenticate","dropDatabase","createUser","dropUser","dropAllUsersFromDatabase","dropAllRolesFromDatabase","shutdown"]}}]}]}`, + Enabled: true, + }, + Settings: &mdbv1.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(false), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, }, } testData.Project = akoProject @@ -258,6 +303,15 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.IntegrationReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Integrations due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.MaintenanceWindowReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.AuditingReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -285,6 +339,15 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.IntegrationReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Integrations due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.MaintenanceWindowReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.AuditingReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -325,6 +388,15 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.IntegrationReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Integrations due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.MaintenanceWindowReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.AuditingReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -355,6 +427,15 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.IntegrationReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Integrations due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.MaintenanceWindowReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.AuditingReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -379,6 +460,90 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote testData.Project.Spec.Integrations[0].Type = "PAGER_DUTY" Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.FalseCondition(status.MaintenanceWindowReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Maintenance Window due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.AuditingReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + ) + + g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Maintenance Window is ready after configured properly", func() { + Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.MaintenanceWindow.DayOfWeek = 7 + Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.FalseCondition(status.AuditingReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Auditing due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + ) + + g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Auditing is ready after configured properly", func() { + Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.Auditing.AuditFilter = `{"atype":"authenticate","param":{"user":"auditReadOnly","db":"admin","mechanism":"SCRAM-SHA-1"}}` + Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.FalseCondition(status.ProjectSettingsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + ) + + g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Maintenance Window is ready after configured properly", func() { + Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.Settings.IsDataExplorerEnabled = toptr.MakePtr(true) + Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { expectedConditions := testutil.MatchConditions( status.TrueCondition(status.ValidationSucceeded), @@ -388,6 +553,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.TrueCondition(status.CloudProviderAccessReadyType), status.TrueCondition(status.NetworkPeerReadyType), status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) From 4787fcb1a3c028504292e82ff844ceb2968e4cf8 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Tue, 15 Aug 2023 20:27:10 +0200 Subject: [PATCH 02/11] manage encryption at rest --- .../atlasproject/atlasproject_controller.go | 2 +- pkg/controller/atlasproject/auditing.go | 4 +- .../atlasproject/encryption_at_rest.go | 102 +++- .../atlasproject/encryption_at_rest_test.go | 565 ++++++++++++++++++ test/e2e/project_deletion_protection_test.go | 82 ++- 5 files changed, 742 insertions(+), 13 deletions(-) diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index bf4c00c0dd..37364278cc 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -314,7 +314,7 @@ func (r *AtlasProjectReconciler) ensureProjectResources(ctx context.Context, wor } results = append(results, result) - if result = r.ensureEncryptionAtRest(workflowCtx, project.ID(), project); result.IsOk() { + if result = r.ensureEncryptionAtRest(ctx, workflowCtx, project, r.SubObjectDeletionProtection); result.IsOk() { r.EventRecorder.Event(project, "Normal", string(status.EncryptionAtRestReadyType), "") } results = append(results, result) diff --git a/pkg/controller/atlasproject/auditing.go b/pkg/controller/atlasproject/auditing.go index 9fbeedf435..dc35c7d589 100644 --- a/pkg/controller/atlasproject/auditing.go +++ b/pkg/controller/atlasproject/auditing.go @@ -6,8 +6,6 @@ import ( "fmt" "reflect" - "github.com/google/go-cmp/cmp" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" "go.mongodb.org/atlas/mongodbatlas" @@ -89,7 +87,7 @@ func auditingInSync(atlas *mongodbatlas.Auditing, spec *v1.Auditing) bool { specAsAtlas := spec.ToAtlas() removeConfigurationType(atlas) - fmt.Println("AUDIT DIFF:", cmp.Diff(atlas, specAsAtlas)) + return reflect.DeepEqual(atlas, specAsAtlas) } diff --git a/pkg/controller/atlasproject/encryption_at_rest.go b/pkg/controller/atlasproject/encryption_at_rest.go index 646ba28186..1030b2cf15 100644 --- a/pkg/controller/atlasproject/encryption_at_rest.go +++ b/pkg/controller/atlasproject/encryption_at_rest.go @@ -2,11 +2,14 @@ package atlasproject import ( "context" + "encoding/json" "fmt" "reflect" "regexp" "strings" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + v1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -24,24 +27,42 @@ const ( ObjectIDRegex = "^([a-f0-9]{24})$" ) -func (r *AtlasProjectReconciler) ensureEncryptionAtRest(ctx *workflow.Context, projectID string, project *mdbv1.AtlasProject) workflow.Result { - if err := readEncryptionAtRestSecrets(r.Client, ctx, project.Spec.EncryptionAtRest, project.Namespace); err != nil { - ctx.UnsetCondition(status.EncryptionAtRestReadyType) +func (r *AtlasProjectReconciler) ensureEncryptionAtRest(ctx context.Context, workflowCtx *workflow.Context, project *mdbv1.AtlasProject, protected bool) workflow.Result { + if err := readEncryptionAtRestSecrets(r.Client, workflowCtx, project.Spec.EncryptionAtRest, project.Namespace); err != nil { + workflowCtx.UnsetCondition(status.EncryptionAtRestReadyType) return workflow.Terminate(workflow.ProjectEncryptionAtRestReady, err.Error()) } - result := createOrDeleteEncryptionAtRests(ctx, projectID, project) + canReconcile, err := canEncryptionAtRestReconcile(ctx, workflowCtx.Client, protected, project) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.EncryptionAtRestReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.EncryptionAtRestReadyType, result) + + return result + } + + result := createOrDeleteEncryptionAtRests(workflowCtx, project.ID(), project) if !result.IsOk() { - ctx.SetConditionFromResult(status.EncryptionAtRestReadyType, result) + workflowCtx.SetConditionFromResult(status.EncryptionAtRestReadyType, result) return result } if IsEncryptionSpecEmpty(project.Spec.EncryptionAtRest) { - ctx.UnsetCondition(status.EncryptionAtRestReadyType) + workflowCtx.UnsetCondition(status.EncryptionAtRestReadyType) return workflow.OK() } - ctx.SetConditionTrue(status.EncryptionAtRestReadyType) + workflowCtx.SetConditionTrue(status.EncryptionAtRestReadyType) return workflow.OK() } @@ -376,3 +397,70 @@ func selectRole(accessRoles []status.CloudProviderAccessRole, providerName strin return } + +func canEncryptionAtRestReconcile(ctx context.Context, atlasClient mongodbatlas.Client, protected bool, akoProject *mdbv1.AtlasProject) (bool, error) { + if !protected { + return true, nil + } + + latestConfig := &mdbv1.AtlasProjectSpec{} + latestConfigString, ok := akoProject.Annotations[customresource.AnnotationLastAppliedConfiguration] + if ok { + if err := json.Unmarshal([]byte(latestConfigString), latestConfig); err != nil { + return false, err + } + } + + ear, _, err := atlasClient.EncryptionsAtRest.Get(ctx, akoProject.ID()) + if err != nil { + return false, err + } + + if IsEncryptionAtlasEmpty(ear) { + return true, nil + } + + return areEaRConfigEqual(*latestConfig.EncryptionAtRest, ear) || + areEaRConfigEqual(*akoProject.Spec.EncryptionAtRest, ear), nil +} + +func areEaRConfigEqual(operator mdbv1.EncryptionAtRest, atlas *mongodbatlas.EncryptionAtRest) bool { + return areAWSConfigEqual(operator.AwsKms, atlas.AwsKms) && + areGCPConfigEqual(operator.GoogleCloudKms, atlas.GoogleCloudKms) && + areAzureConfigEqual(operator.AzureKeyVault, atlas.AzureKeyVault) +} + +func areAWSConfigEqual(operator mdbv1.AwsKms, atlas mongodbatlas.AwsKms) bool { + if operator.Enabled == nil { + operator.Enabled = toptr.MakePtr(false) + } + + return *operator.Enabled == *atlas.Enabled && + operator.Region == atlas.Region && + operator.CustomerMasterKeyID == atlas.CustomerMasterKeyID && + operator.AccessKeyID == atlas.AccessKeyID +} + +func areGCPConfigEqual(operator mdbv1.GoogleCloudKms, atlas mongodbatlas.GoogleCloudKms) bool { + if operator.Enabled == nil { + operator.Enabled = toptr.MakePtr(false) + } + + return *operator.Enabled == *atlas.Enabled && + operator.KeyVersionResourceID == atlas.KeyVersionResourceID +} + +func areAzureConfigEqual(operator mdbv1.AzureKeyVault, atlas mongodbatlas.AzureKeyVault) bool { + if operator.Enabled == nil { + operator.Enabled = toptr.MakePtr(false) + } + + return *operator.Enabled == *atlas.Enabled && + operator.AzureEnvironment == atlas.AzureEnvironment && + operator.ClientID == atlas.ClientID && + operator.KeyIdentifier == atlas.KeyIdentifier && + operator.KeyVaultName == atlas.KeyVaultName && + operator.ResourceGroupName == atlas.ResourceGroupName && + operator.SubscriptionID == atlas.SubscriptionID && + operator.TenantID == atlas.TenantID +} diff --git a/pkg/controller/atlasproject/encryption_at_rest_test.go b/pkg/controller/atlasproject/encryption_at_rest_test.go index b04b694900..16219f3b53 100644 --- a/pkg/controller/atlasproject/encryption_at_rest_test.go +++ b/pkg/controller/atlasproject/encryption_at_rest_test.go @@ -1,8 +1,14 @@ package atlasproject import ( + "context" + "errors" "testing" + "github.com/stretchr/testify/require" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + "github.com/stretchr/testify/assert" "go.mongodb.org/atlas/mongodbatlas" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,6 +23,298 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" ) +type encryptionAtRestClient struct { + GetFunc func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) +} + +func (c *encryptionAtRestClient) Create(_ context.Context, _ *mongodbatlas.EncryptionAtRest) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *encryptionAtRestClient) Get(_ context.Context, projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return c.GetFunc(projectID) +} +func (c *encryptionAtRestClient) Delete(_ context.Context, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + +func TestCanEncryptionAtRestReconcile(t *testing.T) { + t.Run("should return true when subResourceDeletionProtection is disabled", func(t *testing.T) { + result, err := canEncryptionAtRestReconcile(context.TODO(), mongodbatlas.Client{}, false, &mdbv1.AtlasProject{}) + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return error when unable to deserialize last applied configuration", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{wrong}"}) + result, err := canEncryptionAtRestReconcile(context.TODO(), mongodbatlas.Client{}, true, akoProject) + require.EqualError(t, err, "invalid character 'w' looking for beginning of object key string") + require.False(t, result) + }) + + t.Run("should return error when unable to fetch data from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canEncryptionAtRestReconcile(context.TODO(), atlasClient, true, akoProject) + + require.EqualError(t, err, "failed to retrieve data") + require.False(t, result) + }) + + t.Run("should return true when all providers are disabled in Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return &mongodbatlas.EncryptionAtRest{ + AwsKms: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(false), + }, + AzureKeyVault: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + }, + GoogleCloudKms: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canEncryptionAtRestReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are no difference between current Atlas and previous applied configuration", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return &mongodbatlas.EncryptionAtRest{ + AwsKms: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-west-1", + Valid: toptr.MakePtr(true), + }, + AzureKeyVault: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + }, + GoogleCloudKms: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-west-2", + RoleID: "aws:id:arn/my-role", + }, + AzureKeyVault: mdbv1.AzureKeyVault{}, + GoogleCloudKms: mdbv1.GoogleCloudKms{}, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"encryptionAtRest":{"awsKms":{"enabled":true,"customerMasterKeyID":"aws-kms-master-key","region":"eu-west-1","roleId":"aws:id:arn/my-role"},"azureKeyVault":{},"googleCloudKms":{}}}`, + }, + ) + result, err := canEncryptionAtRestReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return true when there are differences but new configuration synchronize operator", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return &mongodbatlas.EncryptionAtRest{ + AwsKms: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-west-1", + Valid: toptr.MakePtr(true), + }, + AzureKeyVault: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + }, + GoogleCloudKms: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-west-1", + RoleID: "aws:id:arn/my-role", + }, + AzureKeyVault: mdbv1.AzureKeyVault{}, + GoogleCloudKms: mdbv1.GoogleCloudKms{}, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"encryptionAtRest":{"awsKms":{"enabled":true,"customerMasterKeyID":"aws-kms-master-key","region":"eu-west-2","roleId":"aws:id:arn/my-role"},"azureKeyVault":{},"googleCloudKms":{}}}`, + }, + ) + result, err := canEncryptionAtRestReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.True(t, result) + }) + + t.Run("should return false when unable to reconcile Encryption at Rest", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return &mongodbatlas.EncryptionAtRest{ + AwsKms: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-west-1", + Valid: toptr.MakePtr(true), + }, + AzureKeyVault: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + }, + GoogleCloudKms: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-central-1", + RoleID: "aws:id:arn/my-role", + }, + AzureKeyVault: mdbv1.AzureKeyVault{}, + GoogleCloudKms: mdbv1.GoogleCloudKms{}, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"encryptionAtRest":{"awsKms":{"enabled":true,"customerMasterKeyID":"aws-kms-master-key","region":"eu-west-2","roleId":"aws:id:arn/my-role"},"azureKeyVault":{},"googleCloudKms":{}}}`, + }, + ) + result, err := canEncryptionAtRestReconcile(context.TODO(), atlasClient, true, akoProject) + + require.NoError(t, err) + require.False(t, result) + }) +} + +func TestEnsureEncryptionAtRest(t *testing.T) { + t.Run("should failed to reconcile when unable to decide resource ownership", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + reconciler := &AtlasProjectReconciler{ + SubObjectDeletionProtection: true, + } + result := reconciler.ensureEncryptionAtRest(context.TODO(), workflowCtx, akoProject, true) + + require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) + }) + + t.Run("should failed to reconcile when unable to synchronize with Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + EncryptionsAtRest: &encryptionAtRestClient{ + GetFunc: func(projectID string) (*mongodbatlas.EncryptionAtRest, *mongodbatlas.Response, error) { + return &mongodbatlas.EncryptionAtRest{ + AwsKms: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-west-1", + Valid: toptr.MakePtr(true), + }, + AzureKeyVault: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + }, + GoogleCloudKms: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "aws-kms-master-key", + Region: "eu-central-1", + RoleID: "aws:id:arn/my-role", + }, + AzureKeyVault: mdbv1.AzureKeyVault{}, + GoogleCloudKms: mdbv1.GoogleCloudKms{}, + }, + }, + } + akoProject.WithAnnotations( + map[string]string{ + customresource.AnnotationLastAppliedConfiguration: `{"encryptionAtRest":{"awsKms":{"enabled":true,"customerMasterKeyID":"aws-kms-master-key","region":"eu-west-2","roleId":"aws:id:arn/my-role"},"azureKeyVault":{},"googleCloudKms":{}}}`, + }, + ) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + reconciler := &AtlasProjectReconciler{ + SubObjectDeletionProtection: true, + } + result := reconciler.ensureEncryptionAtRest(context.TODO(), workflowCtx, akoProject, true) + + require.Equal( + t, + workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ), + result, + ) + }) +} + func TestReadEncryptionAtRestSecrets(t *testing.T) { t.Run("AWS with correct secret data", func(t *testing.T) { secretData := map[string][]byte{ @@ -395,3 +693,270 @@ func TestAtlasInSync(t *testing.T) { assert.NoError(t, err) assert.True(t, areInSync, "Realistic exampel. should be equal") } + +func TestAreAzureConfigEqual(t *testing.T) { + type args struct { + operator mdbv1.AzureKeyVault + atlas mongodbatlas.AzureKeyVault + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Azure configuration are equal", + args: args{ + operator: mdbv1.AzureKeyVault{ + Enabled: toptr.MakePtr(true), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + atlas: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(true), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + }, + want: true, + }, + { + name: "Azure configuration are equal when disabled and nullable", + args: args{ + operator: mdbv1.AzureKeyVault{ + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + atlas: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + }, + want: true, + }, + { + name: "Azure configuration differ by enabled field", + args: args{ + operator: mdbv1.AzureKeyVault{ + Enabled: toptr.MakePtr(false), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + atlas: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(true), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + }, + want: false, + }, + { + name: "Azure configuration differ by other field", + args: args{ + operator: mdbv1.AzureKeyVault{ + Enabled: toptr.MakePtr(true), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + atlas: mongodbatlas.AzureKeyVault{ + Enabled: toptr.MakePtr(true), + ClientID: "client id", + AzureEnvironment: "azure env", + SubscriptionID: "sub id", + ResourceGroupName: "resource group name", + KeyVaultName: "vault name", + KeyIdentifier: "key id", + TenantID: "tenant id", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, areAzureConfigEqual(tt.args.operator, tt.args.atlas), "areAzureConfigEqual(%v, %v)", tt.args.operator, tt.args.atlas) + }) + } +} + +func TestAreGCPConfigEqual(t *testing.T) { + type args struct { + operator mdbv1.GoogleCloudKms + atlas mongodbatlas.GoogleCloudKms + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "GCP configuration are equal", + args: args{ + operator: mdbv1.GoogleCloudKms{ + Enabled: toptr.MakePtr(true), + KeyVersionResourceID: "key version id", + }, + atlas: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(true), + KeyVersionResourceID: "key version id", + }, + }, + want: true, + }, + { + name: "GCP configuration are equal when disabled and nullable", + args: args{ + operator: mdbv1.GoogleCloudKms{ + KeyVersionResourceID: "key version id", + }, + atlas: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + KeyVersionResourceID: "key version id", + }, + }, + want: true, + }, + { + name: "GCP configuration are different by enable field", + args: args{ + operator: mdbv1.GoogleCloudKms{ + Enabled: toptr.MakePtr(true), + KeyVersionResourceID: "key version id", + }, + atlas: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(false), + KeyVersionResourceID: "key version id", + }, + }, + want: false, + }, + { + name: "GCP configuration are different by another field", + args: args{ + operator: mdbv1.GoogleCloudKms{ + Enabled: toptr.MakePtr(true), + KeyVersionResourceID: "key version resource id", + }, + atlas: mongodbatlas.GoogleCloudKms{ + Enabled: toptr.MakePtr(true), + KeyVersionResourceID: "key version id", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, areGCPConfigEqual(tt.args.operator, tt.args.atlas), "areGCPConfigEqual(%v, %v)", tt.args.operator, tt.args.atlas) + }) + } +} + +func TestAreAWSConfigEqual(t *testing.T) { + type args struct { + operator mdbv1.AwsKms + atlas mongodbatlas.AwsKms + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "AWS configuration are equal", + args: args{ + operator: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "customer master key", + }, + atlas: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "customer master key", + }, + }, + want: true, + }, + { + name: "AWS configuration are equal when disabled and nullable", + args: args{ + operator: mdbv1.AwsKms{ + CustomerMasterKeyID: "customer master key", + }, + atlas: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(false), + CustomerMasterKeyID: "customer master key", + }, + }, + want: true, + }, + { + name: "AWS configuration are different by enable field", + args: args{ + operator: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "customer master key", + }, + atlas: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(false), + CustomerMasterKeyID: "customer master key", + }, + }, + want: false, + }, + { + name: "AWS configuration are different by another field", + args: args{ + operator: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "customer master key", + }, + atlas: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: "customer master key id", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, areAWSConfigEqual(tt.args.operator, tt.args.atlas), "areGCPConfigEqual(%v, %v)", tt.args.operator, tt.args.atlas) + }) + } +} diff --git a/test/e2e/project_deletion_protection_test.go b/test/e2e/project_deletion_protection_test.go index 510639a804..6c28316d9c 100644 --- a/test/e2e/project_deletion_protection_test.go +++ b/test/e2e/project_deletion_protection_test.go @@ -33,10 +33,10 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model" ) -var _ = Describe("Project Deletion Protection", Label("project", "deletion-protection"), func() { +var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-protection"), func() { var testData *model.TestDataProvider var managerStop context.CancelFunc - var projectID, networkPeerID, awsRoleARN, awsAccountID, AwsVpcID string + var projectID, networkPeerID, awsRoleARN, awsAccountID, AwsVpcID, customerMasterKeyID, atlasAccountARN, atlasRoleID string ctx := context.Background() BeforeEach(func() { @@ -130,6 +130,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote ) g.Expect(err).ToNot(HaveOccurred()) }).WithTimeout(time.Minute).WithPolling(time.Second * 15).Should(Succeed()) + + atlasRoleID = cloudProvider.RoleID + atlasAccountARN = cloudProvider.AtlasAWSAccountARN }) By("Adding Network peering to the project", func() { @@ -222,6 +225,24 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote Expect(err).ToNot(HaveOccurred()) }) + By("Adding AWS Encryption At Rest", func() { + awsAction, err := cloud.NewAWSAction(GinkgoT()) + Expect(err).ToNot(HaveOccurred()) + customerMasterKeyID, err = awsAction.CreateKMS(config.AWSRegionUS, atlasAccountARN, awsRoleARN) + Expect(err).ToNot(HaveOccurred()) + + _, _, err = atlasClient.Client.EncryptionsAtRest.Create(ctx, &mongodbatlas.EncryptionAtRest{ + GroupID: projectID, + AwsKms: mongodbatlas.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: customerMasterKeyID, + Region: "US_EAST_1", + RoleID: atlasRoleID, + }, + }) + Expect(err).ToNot(HaveOccurred()) + }) + By("Creating a project to be managed by the operator", func() { akoProject := &mdbv1.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ @@ -277,6 +298,14 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), IsSchemaAdvisorEnabled: toptr.MakePtr(true), }, + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: customerMasterKeyID, + Region: "EU_WEST_1", + RoleID: atlasRoleID, + }, + }, }, } testData.Project = akoProject @@ -312,6 +341,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -348,6 +380,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -397,6 +432,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -436,6 +474,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -478,6 +519,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -506,6 +550,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -532,6 +579,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectSettingsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Project Settings due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -544,6 +594,33 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote testData.Project.Spec.Settings.IsDataExplorerEnabled = toptr.MakePtr(true) Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.FalseCondition(status.EncryptionAtRestReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + ) + + g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Encryption At Rest is ready after configured properly", func() { + Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.EncryptionAtRest.AwsKms.Region = "US_EAST_1" + Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { expectedConditions := testutil.MatchConditions( status.TrueCondition(status.ValidationSucceeded), @@ -556,6 +633,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.TrueCondition(status.MaintenanceWindowReadyType), status.TrueCondition(status.AuditingReadyType), status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.EncryptionAtRestReadyType), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) From 3e9fa87252b046dc781eb67e5d1778d79b8cc438 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Thu, 17 Aug 2023 16:17:07 +0200 Subject: [PATCH 03/11] manage custom roles --- .../atlasproject/atlasproject_controller.go | 2 +- pkg/controller/atlasproject/custom_roles.go | 82 +++- .../atlasproject/custom_roles_test.go | 367 +++++++++++++++++- test/e2e/project_deletion_protection_test.go | 101 ++++- 4 files changed, 524 insertions(+), 28 deletions(-) diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index 37364278cc..73f3e5c856 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -329,7 +329,7 @@ func (r *AtlasProjectReconciler) ensureProjectResources(ctx context.Context, wor } results = append(results, result) - if result = ensureCustomRoles(workflowCtx, project.ID(), project); result.IsOk() { + if result = ensureCustomRoles(ctx, workflowCtx, project, r.SubObjectDeletionProtection); result.IsOk() { r.EventRecorder.Event(project, "Normal", string(status.ProjectCustomRolesReadyType), "") } results = append(results, result) diff --git a/pkg/controller/atlasproject/custom_roles.go b/pkg/controller/atlasproject/custom_roles.go index 75dfc9e960..04b9cc6c3c 100644 --- a/pkg/controller/atlasproject/custom_roles.go +++ b/pkg/controller/atlasproject/custom_roles.go @@ -2,9 +2,14 @@ package atlasproject import ( "context" + "encoding/json" "errors" "fmt" + "go.mongodb.org/atlas/mongodbatlas" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" "github.com/google/go-cmp/cmp" @@ -14,30 +19,48 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" ) -func ensureCustomRoles(ctx *workflow.Context, projectID string, project *v1.AtlasProject) workflow.Result { - currentCustomRoles, err := fetchCustomRoles(ctx, projectID) +func ensureCustomRoles(ctx context.Context, workflowCtx *workflow.Context, project *v1.AtlasProject, protected bool) workflow.Result { + canReconcile, err := canCustomRolesReconcile(ctx, workflowCtx.Client, protected, project) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.ProjectCustomRolesReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.ProjectCustomRolesReadyType, result) + + return result + } + + currentCustomRoles, err := fetchCustomRoles(workflowCtx, project.ID()) if err != nil { return workflow.Terminate(workflow.ProjectCustomRolesReady, err.Error()) } ops := calculateChanges(currentCustomRoles, project.Spec.CustomRoles) - deleteStatus := deleteCustomRoles(ctx, projectID, ops.Delete) - updateStatus := updateCustomRoles(ctx, projectID, ops.Update) - createStatus := createCustomRoles(ctx, projectID, ops.Create) + deleteStatus := deleteCustomRoles(workflowCtx, project.ID(), ops.Delete) + updateStatus := updateCustomRoles(workflowCtx, project.ID(), ops.Update) + createStatus := createCustomRoles(workflowCtx, project.ID(), ops.Create) - result := syncCustomRolesStatus(ctx, project.Spec.CustomRoles, createStatus, updateStatus, deleteStatus) + result := syncCustomRolesStatus(workflowCtx, project.Spec.CustomRoles, createStatus, updateStatus, deleteStatus) if !result.IsOk() { - ctx.SetConditionFromResult(status.ProjectCustomRolesReadyType, result) + workflowCtx.SetConditionFromResult(status.ProjectCustomRolesReadyType, result) return result } - ctx.SetConditionTrue(status.ProjectCustomRolesReadyType) + workflowCtx.SetConditionTrue(status.ProjectCustomRolesReadyType) if len(project.Spec.CustomRoles) == 0 { - ctx.UnsetCondition(status.ProjectCustomRolesReadyType) + workflowCtx.UnsetCondition(status.ProjectCustomRolesReadyType) } return result @@ -55,9 +78,13 @@ func fetchCustomRoles(ctx *workflow.Context, projectID string) ([]v1.CustomRole, ctx.Log.Debugw("Got Custom Roles", "NumItems", len(*data)) - customRoles := make([]v1.CustomRole, 0, len(*data)) + return mapToOperator(data), nil +} + +func mapToOperator(atlasCustomRoles *[]mongodbatlas.CustomDBRole) []v1.CustomRole { + customRoles := make([]v1.CustomRole, 0, len(*atlasCustomRoles)) - for _, atlasCustomRole := range *data { + for _, atlasCustomRole := range *atlasCustomRoles { inheritedRoles := make([]v1.Role, 0, len(atlasCustomRole.InheritedRoles)) for _, atlasInheritedRole := range atlasCustomRole.InheritedRoles { @@ -96,7 +123,7 @@ func fetchCustomRoles(ctx *workflow.Context, projectID string) ([]v1.CustomRole, }) } - return customRoles, nil + return customRoles } func deleteCustomRoles(ctx *workflow.Context, projectID string, toDelete map[string]v1.CustomRole) map[string]status.CustomRole { @@ -282,3 +309,34 @@ func syncCustomRolesStatus(ctx *workflow.Context, desiredCustomRoles []v1.Custom return workflow.OK() } + +func canCustomRolesReconcile(ctx context.Context, atlasClient mongodbatlas.Client, protected bool, akoProject *v1.AtlasProject) (bool, error) { + if !protected { + return true, nil + } + + latestConfig := &v1.AtlasProjectSpec{} + latestConfigString, ok := akoProject.Annotations[customresource.AnnotationLastAppliedConfiguration] + if ok { + if err := json.Unmarshal([]byte(latestConfigString), latestConfig); err != nil { + return false, err + } + } + + atlasData, _, err := atlasClient.CustomDBRoles.List(ctx, akoProject.ID(), nil) + if err != nil { + return false, err + } + + if atlasData == nil || len(*atlasData) == 0 { + return true, nil + } + + atlasCustomRoles := mapToOperator(atlasData) + + if cmp.Diff(latestConfig.CustomRoles, atlasCustomRoles, cmpopts.EquateEmpty()) == "" { + return true, nil + } + + return cmp.Diff(akoProject.Spec.CustomRoles, atlasCustomRoles, cmpopts.EquateEmpty()) == "", nil +} diff --git a/pkg/controller/atlasproject/custom_roles_test.go b/pkg/controller/atlasproject/custom_roles_test.go index d987d710aa..eec8246bb2 100644 --- a/pkg/controller/atlasproject/custom_roles_test.go +++ b/pkg/controller/atlasproject/custom_roles_test.go @@ -1,25 +1,59 @@ package atlasproject import ( + "context" + "errors" "testing" + "github.com/stretchr/testify/require" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + + "go.mongodb.org/atlas/mongodbatlas" + "github.com/stretchr/testify/assert" "go.uber.org/zap" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" - v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" ) +type customRolesClient struct { + ListFunc func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) +} + +func (c *customRolesClient) List(_ context.Context, projectID string, _ *mongodbatlas.ListOptions) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return c.ListFunc(projectID) +} + +func (c *customRolesClient) Get(_ context.Context, _ string, _ string) (*mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *customRolesClient) Create(_ context.Context, _ string, _ *mongodbatlas.CustomDBRole) (*mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *customRolesClient) Update(_ context.Context, _ string, _ string, _ *mongodbatlas.CustomDBRole) (*mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return nil, nil, nil +} + +func (c *customRolesClient) Delete(_ context.Context, _ string, _ string) (*mongodbatlas.Response, error) { + return nil, nil +} + func TestCalculateChanges(t *testing.T) { - desired := []v1.CustomRole{ + desired := []mdbv1.CustomRole{ { Name: "cr-1", }, { Name: "cr-3", - InheritedRoles: []v1.Role{ + InheritedRoles: []mdbv1.Role{ { Name: "admin", Database: "test", @@ -30,7 +64,7 @@ func TestCalculateChanges(t *testing.T) { Name: "cr-4", }, } - current := []v1.CustomRole{ + current := []mdbv1.CustomRole{ { Name: "cr-1", }, @@ -45,15 +79,15 @@ func TestCalculateChanges(t *testing.T) { assert.Equal( t, CustomRolesOperations{ - Create: map[string]v1.CustomRole{ + Create: map[string]mdbv1.CustomRole{ "cr-4": { Name: "cr-4", }, }, - Update: map[string]v1.CustomRole{ + Update: map[string]mdbv1.CustomRole{ "cr-3": { Name: "cr-3", - InheritedRoles: []v1.Role{ + InheritedRoles: []mdbv1.Role{ { Name: "admin", Database: "test", @@ -61,7 +95,7 @@ func TestCalculateChanges(t *testing.T) { }, }, }, - Delete: map[string]v1.CustomRole{ + Delete: map[string]mdbv1.CustomRole{ "cr-2": { Name: "cr-2", }, @@ -73,13 +107,13 @@ func TestCalculateChanges(t *testing.T) { func TestSyncCustomRolesStatus(t *testing.T) { t.Run("sync status when all operations were done successfully", func(t *testing.T) { - desired := []v1.CustomRole{ + desired := []mdbv1.CustomRole{ { Name: "cr-1", }, { Name: "cr-3", - InheritedRoles: []v1.Role{ + InheritedRoles: []mdbv1.Role{ { Name: "admin", Database: "test", @@ -140,13 +174,13 @@ func TestSyncCustomRolesStatus(t *testing.T) { }) t.Run("sync status when a operation fails", func(t *testing.T) { - desired := []v1.CustomRole{ + desired := []mdbv1.CustomRole{ { Name: "cr-1", }, { Name: "cr-3", - InheritedRoles: []v1.Role{ + InheritedRoles: []mdbv1.Role{ { Name: "admin", Database: "test", @@ -208,3 +242,312 @@ func TestSyncCustomRolesStatus(t *testing.T) { ) }) } + +func TestCanCustomRolesReconcile(t *testing.T) { + t.Run("should return true when subResourceDeletionProtection is disabled", func(t *testing.T) { + result, err := canCustomRolesReconcile(context.TODO(), mongodbatlas.Client{}, false, &mdbv1.AtlasProject{}) + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return error when unable to deserialize last applied configuration", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{wrong}"}) + result, err := canCustomRolesReconcile(context.TODO(), mongodbatlas.Client{}, true, akoProject) + assert.EqualError(t, err, "invalid character 'w' looking for beginning of object key string") + assert.False(t, result) + }) + + t.Run("should return error when unable to fetch data from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) + + assert.EqualError(t, err, "failed to retrieve data") + assert.False(t, result) + }) + + t.Run("should return true when return nil from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return nil, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return true when return empty list from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return &[]mongodbatlas.CustomDBRole{}, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return true when there are no difference between current Atlas and previous applied configuration", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return &[]mongodbatlas.CustomDBRole{ + { + RoleName: "testRole1", + InheritedRoles: nil, + Actions: []mongodbatlas.Action{ + { + Action: "INSERT", + Resources: []mongodbatlas.Resource{ + { + Cluster: toptr.MakePtr(false), + DB: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + CustomRoles: []mdbv1.CustomRole{ + { + Name: "testRole", + InheritedRoles: nil, + Actions: []mdbv1.Action{ + { + Name: "INSERT", + Resources: []mdbv1.Resource{ + { + Cluster: toptr.MakePtr(false), + Database: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"cluster":false,"database":"testDB","collection":"testCollection"}]}]}]}`}) + result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return true when there are differences but new configuration synchronize operator", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return &[]mongodbatlas.CustomDBRole{ + { + RoleName: "testRole", + InheritedRoles: nil, + Actions: []mongodbatlas.Action{ + { + Action: "INSERT", + Resources: []mongodbatlas.Resource{ + { + Cluster: toptr.MakePtr(false), + DB: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + CustomRoles: []mdbv1.CustomRole{ + { + Name: "testRole", + InheritedRoles: nil, + Actions: []mdbv1.Action{ + { + Name: "INSERT", + Resources: []mdbv1.Resource{ + { + Cluster: toptr.MakePtr(false), + Database: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"cluster":false,"database":"testDB","collection":"testCollection"}]}]}]}`}) + result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return false when unable to reconcile custom roles", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return &[]mongodbatlas.CustomDBRole{ + { + RoleName: "testRole", + InheritedRoles: nil, + Actions: []mongodbatlas.Action{ + { + Action: "INSERT", + Resources: []mongodbatlas.Resource{ + { + Cluster: toptr.MakePtr(false), + DB: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + CustomRoles: []mdbv1.CustomRole{ + { + Name: "testRole2", + InheritedRoles: nil, + Actions: []mdbv1.Action{ + { + Name: "INSERT", + Resources: []mdbv1.Resource{ + { + Cluster: toptr.MakePtr(false), + Database: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"cluster":false,"database":"testDB","collection":"testCollection"}]}]}]}`}) + result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) + + assert.NoError(t, err) + assert.False(t, result) + }) +} + +func TestEnsureCustomRoles(t *testing.T) { + t.Run("should failed to reconcile when unable to decide resource ownership", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureCustomRoles(context.TODO(), workflowCtx, akoProject, true) + + require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) + }) + + t.Run("should failed to reconcile when unable to synchronize with Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + CustomDBRoles: &customRolesClient{ + ListFunc: func(projectID string) (*[]mongodbatlas.CustomDBRole, *mongodbatlas.Response, error) { + return &[]mongodbatlas.CustomDBRole{ + { + RoleName: "testRole", + InheritedRoles: nil, + Actions: []mongodbatlas.Action{ + { + Action: "INSERT", + Resources: []mongodbatlas.Resource{ + { + Cluster: toptr.MakePtr(false), + DB: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + CustomRoles: []mdbv1.CustomRole{ + { + Name: "testRole2", + InheritedRoles: nil, + Actions: []mdbv1.Action{ + { + Name: "INSERT", + Resources: []mdbv1.Resource{ + { + Cluster: toptr.MakePtr(false), + Database: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"cluster":false,"database":"testDB","collection":"testCollection"}]}]}]}`}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + result := ensureCustomRoles(context.TODO(), workflowCtx, akoProject, true) + + require.Equal( + t, + workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ), + result, + ) + }) +} diff --git a/test/e2e/project_deletion_protection_test.go b/test/e2e/project_deletion_protection_test.go index 6c28316d9c..a1db3e1707 100644 --- a/test/e2e/project_deletion_protection_test.go +++ b/test/e2e/project_deletion_protection_test.go @@ -33,7 +33,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model" ) -var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-protection"), func() { +var _ = Describe("Project Deletion Protection", Label("project", "deletion-protection"), func() { var testData *model.TestDataProvider var managerStop context.CancelFunc var projectID, networkPeerID, awsRoleARN, awsAccountID, AwsVpcID, customerMasterKeyID, atlasAccountARN, atlasRoleID string @@ -213,7 +213,7 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot Expect(err).ToNot(HaveOccurred()) }) - By("Adding Settings to the Project", func() { + By("Adding Settings to the project", func() { _, _, err := atlasClient.Client.Projects.UpdateProjectSettings(ctx, projectID, &mongodbatlas.ProjectSettings{ IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), IsDataExplorerEnabled: toptr.MakePtr(true), @@ -225,7 +225,7 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot Expect(err).ToNot(HaveOccurred()) }) - By("Adding AWS Encryption At Rest", func() { + By("Adding AWS Encryption At Rest to the project", func() { awsAction, err := cloud.NewAWSAction(GinkgoT()) Expect(err).ToNot(HaveOccurred()) customerMasterKeyID, err = awsAction.CreateKMS(config.AWSRegionUS, atlasAccountARN, awsRoleARN) @@ -243,6 +243,30 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot Expect(err).ToNot(HaveOccurred()) }) + By("Adding Custom Roles to the project", func() { + _, _, err := atlasClient.Client.CustomDBRoles.Create( + ctx, + projectID, + &mongodbatlas.CustomDBRole{ + RoleName: "testRole", + InheritedRoles: nil, + Actions: []mongodbatlas.Action{ + { + Action: "INSERT", + Resources: []mongodbatlas.Resource{ + { + Cluster: toptr.MakePtr(false), + DB: toptr.MakePtr("testDB"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + ) + Expect(err).ToNot(HaveOccurred()) + }) + By("Creating a project to be managed by the operator", func() { akoProject := &mdbv1.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ @@ -306,6 +330,24 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot RoleID: atlasRoleID, }, }, + CustomRoles: []mdbv1.CustomRole{ + { + Name: "testRole", + InheritedRoles: nil, + Actions: []mdbv1.Action{ + { + Name: "INSERT", + Resources: []mdbv1.Resource{ + { + Cluster: toptr.MakePtr(false), + Database: toptr.MakePtr("testD"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, }, } testData.Project = akoProject @@ -344,6 +386,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -383,6 +428,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -435,6 +483,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -477,6 +528,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -522,6 +576,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -553,6 +610,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -582,6 +642,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -609,6 +672,9 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.FalseCondition(status.EncryptionAtRestReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Encryption At Rest due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -621,6 +687,34 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot testData.Project.Spec.EncryptionAtRest.AwsKms.Region = "US_EAST_1" Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.EncryptionAtRestReadyType), + status.FalseCondition(status.ProjectCustomRolesReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + ) + + g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Custom Roles is ready after configured properly", func() { + Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.CustomRoles[0].Actions[0].Resources[0].Database = toptr.MakePtr("testDB") + Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { expectedConditions := testutil.MatchConditions( status.TrueCondition(status.ValidationSucceeded), @@ -634,6 +728,7 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot status.TrueCondition(status.AuditingReadyType), status.TrueCondition(status.ProjectSettingsReadyType), status.TrueCondition(status.EncryptionAtRestReadyType), + status.TrueCondition(status.ProjectCustomRolesReadyType), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) From 1c734f7e2d9237b58d4c64d53733f64192e26752 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Thu, 17 Aug 2023 18:29:16 +0200 Subject: [PATCH 04/11] manage assigned teams --- .../atlasproject/atlasproject_controller.go | 2 +- .../atlasproject/project_settings_test.go | 7 +- pkg/controller/atlasproject/teams.go | 130 ++++++- pkg/controller/atlasproject/teams_test.go | 319 ++++++++++++++++++ test/e2e/project_deletion_protection_test.go | 129 ++++++- 5 files changed, 572 insertions(+), 15 deletions(-) create mode 100644 pkg/controller/atlasproject/teams_test.go diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index 73f3e5c856..f71452e9a4 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -334,7 +334,7 @@ func (r *AtlasProjectReconciler) ensureProjectResources(ctx context.Context, wor } results = append(results, result) - if result = r.ensureAssignedTeams(workflowCtx, project.ID(), project); result.IsOk() { + if result = r.ensureAssignedTeams(ctx, workflowCtx, project, r.SubObjectDeletionProtection); result.IsOk() { r.EventRecorder.Event(project, "Normal", string(status.ProjectTeamsReadyType), "") } results = append(results, result) diff --git a/pkg/controller/atlasproject/project_settings_test.go b/pkg/controller/atlasproject/project_settings_test.go index a50087dbd9..4bcfecd4fa 100644 --- a/pkg/controller/atlasproject/project_settings_test.go +++ b/pkg/controller/atlasproject/project_settings_test.go @@ -18,7 +18,8 @@ import ( ) type projectClient struct { - GetProjectSettingsFunc func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) + GetProjectSettingsFunc func(projectID string) (*mongodbatlas.ProjectSettings, *mongodbatlas.Response, error) + GetProjectTeamsAssignedFunc func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) } func (c *projectClient) GetAllProjects(_ context.Context, _ *mongodbatlas.ListOptions) (*mongodbatlas.Projects, *mongodbatlas.Response, error) { @@ -45,8 +46,8 @@ func (c *projectClient) Delete(_ context.Context, _ string) (*mongodbatlas.Respo return nil, nil } -func (c *projectClient) GetProjectTeamsAssigned(_ context.Context, _ string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { - return nil, nil, nil +func (c *projectClient) GetProjectTeamsAssigned(_ context.Context, projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return c.GetProjectTeamsAssignedFunc(projectID) } func (c *projectClient) AddTeamsToProject(_ context.Context, _ string, _ []*mongodbatlas.ProjectTeam) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { diff --git a/pkg/controller/atlasproject/teams.go b/pkg/controller/atlasproject/teams.go index 3978f6e9ee..0df2bac437 100644 --- a/pkg/controller/atlasproject/teams.go +++ b/pkg/controller/atlasproject/teams.go @@ -2,6 +2,16 @@ package atlasproject import ( "context" + "encoding/json" + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + apiErrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" @@ -25,10 +35,28 @@ type TeamDataContainer struct { Context *workflow.Context } -func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx *workflow.Context, projectID string, project *v1.AtlasProject) workflow.Result { +func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx context.Context, workflowCtx *workflow.Context, project *v1.AtlasProject, protected bool) workflow.Result { + canReconcile, err := canAssignedTeamsReconcile(ctx, workflowCtx.Client, r.Client, protected, project) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.ProjectTeamsReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.ProjectTeamsReadyType, result) + + return result + } + resourcesToWatch := make([]watch.WatchedObject, 0, len(project.Spec.Teams)) defer func() { - ctx.AddResourcesToWatch(resourcesToWatch...) + workflowCtx.AddResourcesToWatch(resourcesToWatch...) r.Log.Debugf("watching team resources: %v\r\n", r.WatchedResources) }() @@ -37,7 +65,7 @@ func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx *workflow.Context, proj assignedTeam := entry if assignedTeam.TeamRef.Name == "" { - ctx.Log.Warnf("missing team name. skiping assignement for entry %v", assignedTeam) + workflowCtx.Log.Warnf("missing team name. skiping assignement for entry %v", assignedTeam) continue } @@ -47,13 +75,13 @@ func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx *workflow.Context, proj } team := &v1.AtlasTeam{} - teamReconciler := r.teamReconcile(team, ctx.Connection) + teamReconciler := r.teamReconcile(team, workflowCtx.Connection) _, err := teamReconciler( context.Background(), controllerruntime.Request{NamespacedName: types.NamespacedName{Name: assignedTeam.TeamRef.Name, Namespace: assignedTeam.TeamRef.Namespace}}, ) if err != nil { - ctx.Log.Warnf("unable to reconcile team %s. skipping assignment. %s", assignedTeam.TeamRef.GetObject(""), err.Error()) + workflowCtx.Log.Warnf("unable to reconcile team %s. skipping assignment. %s", assignedTeam.TeamRef.GetObject(""), err.Error()) continue } @@ -65,17 +93,17 @@ func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx *workflow.Context, proj teamsToAssign[team.Status.ID] = &assignedTeam } - err := r.syncAssignedTeams(ctx, projectID, project, teamsToAssign) + err = r.syncAssignedTeams(workflowCtx, project.ID(), project, teamsToAssign) if err != nil { - ctx.SetConditionFalse(status.ProjectTeamsReadyType) + workflowCtx.SetConditionFalse(status.ProjectTeamsReadyType) return workflow.Terminate(workflow.ProjectTeamUnavailable, err.Error()) } - ctx.SetConditionTrue(status.ProjectTeamsReadyType) + workflowCtx.SetConditionTrue(status.ProjectTeamsReadyType) if len(project.Spec.Teams) == 0 { - ctx.EnsureStatusOption(status.AtlasProjectSetTeamsOption(nil)) - ctx.UnsetCondition(status.ProjectTeamsReadyType) + workflowCtx.EnsureStatusOption(status.AtlasProjectSetTeamsOption(nil)) + workflowCtx.UnsetCondition(status.ProjectTeamsReadyType) } return workflow.OK() @@ -243,3 +271,85 @@ func hasTeamRolesChanged(current []string, desired []v1.TeamRole) bool { return len(desiredMap) != 0 } + +type assignedTeamInfo struct { + ID string + Roles []string +} + +func canAssignedTeamsReconcile(ctx context.Context, atlasClient mongodbatlas.Client, k8sClient client.Client, protected bool, akoProject *v1.AtlasProject) (bool, error) { + if !protected { + return true, nil + } + + latestConfig := &v1.AtlasProjectSpec{} + latestConfigString, ok := akoProject.Annotations[customresource.AnnotationLastAppliedConfiguration] + if ok { + if err := json.Unmarshal([]byte(latestConfigString), latestConfig); err != nil { + return false, err + } + } + + atlasAssignedTeams, _, err := atlasClient.Projects.GetProjectTeamsAssigned(ctx, akoProject.ID()) + if err != nil { + return false, err + } + + if atlasAssignedTeams == nil || atlasAssignedTeams.TotalCount == 0 { + return true, nil + } + + atlasAssignedTeamsInfo := make([]assignedTeamInfo, 0, atlasAssignedTeams.TotalCount) + for _, atlasAssignedTeam := range atlasAssignedTeams.Results { + if atlasAssignedTeam != nil { + atlasAssignedTeamsInfo = append( + atlasAssignedTeamsInfo, + assignedTeamInfo{ + ID: atlasAssignedTeam.TeamID, + Roles: atlasAssignedTeam.RoleNames, + }, + ) + } + } + + lastAssignedTeamsInfo, err := collectTeams(ctx, k8sClient, latestConfig, akoProject.Namespace) + if err != nil { + return false, err + } + + if cmp.Diff(atlasAssignedTeamsInfo, lastAssignedTeamsInfo, cmpopts.EquateEmpty()) == "" { + return true, nil + } + + currentAssignedTeamsInfo, err := collectTeams(ctx, k8sClient, &akoProject.Spec, akoProject.Namespace) + if err != nil { + return false, err + } + + return cmp.Diff(atlasAssignedTeamsInfo, currentAssignedTeamsInfo, cmpopts.EquateEmpty()) == "", nil +} + +func collectTeams(ctx context.Context, k8sClient client.Client, projectSpec *v1.AtlasProjectSpec, projectNamespace string) ([]assignedTeamInfo, error) { + teams := make([]assignedTeamInfo, 0, len(projectSpec.Teams)) + + for _, assignedTeam := range projectSpec.Teams { + team := &v1.AtlasTeam{} + err := k8sClient.Get(ctx, *assignedTeam.TeamRef.GetObject(projectNamespace), team) + if err != nil { + if !apiErrors.IsNotFound(err) { + return nil, err + } + } + + info := assignedTeamInfo{ + ID: team.Status.ID, + } + for _, role := range assignedTeam.Roles { + info.Roles = append(info.Roles, string(role)) + } + + teams = append(teams, info) + } + + return teams, nil +} diff --git a/pkg/controller/atlasproject/teams_test.go b/pkg/controller/atlasproject/teams_test.go new file mode 100644 index 0000000000..a8d9ca9d61 --- /dev/null +++ b/pkg/controller/atlasproject/teams_test.go @@ -0,0 +1,319 @@ +package atlasproject + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + + "github.com/stretchr/testify/assert" + "go.mongodb.org/atlas/mongodbatlas" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" +) + +func TestCanAssignedTeamsReconcile(t *testing.T) { + team1 := &mdbv1.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "team1", + Namespace: "default", + }, + Status: status.TeamStatus{ + ID: "team1", + }, + } + team2 := &mdbv1.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "team2", + Namespace: "default", + }, + Status: status.TeamStatus{ + ID: "team2", + }, + } + + testScheme := runtime.NewScheme() + testScheme.AddKnownTypes(mdbv1.GroupVersion, &mdbv1.AtlasProject{}) + testScheme.AddKnownTypes(mdbv1.GroupVersion, &mdbv1.AtlasTeam{}) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(team1, team2). + Build() + + t.Run("should return true when subResourceDeletionProtection is disabled", func(t *testing.T) { + result, err := canAssignedTeamsReconcile(context.TODO(), mongodbatlas.Client{}, k8sClient, false, &mdbv1.AtlasProject{}) + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return error when unable to deserialize last applied configuration", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{wrong}"}) + result, err := canAssignedTeamsReconcile(context.TODO(), mongodbatlas.Client{}, k8sClient, true, akoProject) + assert.EqualError(t, err, "invalid character 'w' looking for beginning of object key string") + assert.False(t, result) + }) + + t.Run("should return error when unable to fetch data from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canAssignedTeamsReconcile(context.TODO(), atlasClient, k8sClient, true, akoProject) + + assert.EqualError(t, err, "failed to retrieve data") + assert.False(t, result) + }) + + t.Run("should return true when return nil from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return nil, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canAssignedTeamsReconcile(context.TODO(), atlasClient, k8sClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return true when return empty list from Atlas", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{TotalCount: 0}, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + result, err := canAssignedTeamsReconcile(context.TODO(), atlasClient, k8sClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return true when there are no difference between current Atlas and previous applied configuration", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{ + Results: []*mongodbatlas.Result{ + { + TeamID: "team1", + RoleNames: []string{"GROUP_OWNER"}, + }, + }, + TotalCount: 1, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Teams: []mdbv1.Team{ + { + TeamRef: common.ResourceRefNamespaced{ + Name: "team2", + Namespace: "default", + }, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"teams":[{"teamRef":{"name":"team1","namespace":"default"},"roles":["GROUP_OWNER"]}]}`}) + result, err := canAssignedTeamsReconcile(context.TODO(), atlasClient, k8sClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return true when there are differences but new configuration synchronize operator", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{ + Results: []*mongodbatlas.Result{ + { + TeamID: "team2", + RoleNames: []string{"GROUP_READ_ONLY"}, + }, + }, + TotalCount: 1, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Teams: []mdbv1.Team{ + { + TeamRef: common.ResourceRefNamespaced{ + Name: "team2", + Namespace: "default", + }, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"teams":[{"teamRef":{"name":"team1","namespace":"default"},"roles":["GROUP_OWNER"]}]}`}) + result, err := canAssignedTeamsReconcile(context.TODO(), atlasClient, k8sClient, true, akoProject) + + assert.NoError(t, err) + assert.True(t, result) + }) + + t.Run("should return false when unable to reconcile assigned teams", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{ + Results: []*mongodbatlas.Result{ + { + TeamID: "team2", + RoleNames: []string{"GROUP_READ_ONLY"}, + }, + }, + TotalCount: 1, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Teams: []mdbv1.Team{ + { + TeamRef: common.ResourceRefNamespaced{ + Name: "team3", + Namespace: "default", + }, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"teams":[{"teamRef":{"name":"team1","namespace":"default"},"roles":["GROUP_OWNER"]}]}`}) + result, err := canAssignedTeamsReconcile(context.TODO(), atlasClient, k8sClient, true, akoProject) + + assert.NoError(t, err) + assert.False(t, result) + }) +} + +func TestEnsureAssignedTeams(t *testing.T) { + t.Run("should failed to reconcile when unable to decide resource ownership", func(t *testing.T) { + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return nil, nil, errors.New("failed to retrieve data") + }, + }, + } + akoProject := &mdbv1.AtlasProject{} + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + reconciler := &AtlasProjectReconciler{} + result := reconciler.ensureAssignedTeams(context.TODO(), workflowCtx, akoProject, true) + + require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) + }) + + t.Run("should failed to reconcile when unable to synchronize with Atlas", func(t *testing.T) { + team1 := &mdbv1.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "team1", + Namespace: "default", + }, + Status: status.TeamStatus{ + ID: "team1", + }, + } + team2 := &mdbv1.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: "team2", + Namespace: "default", + }, + Status: status.TeamStatus{ + ID: "team2", + }, + } + + testScheme := runtime.NewScheme() + testScheme.AddKnownTypes(mdbv1.GroupVersion, &mdbv1.AtlasProject{}) + testScheme.AddKnownTypes(mdbv1.GroupVersion, &mdbv1.AtlasTeam{}) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(team1, team2). + Build() + + atlasClient := mongodbatlas.Client{ + Projects: &projectClient{ + GetProjectTeamsAssignedFunc: func(projectID string) (*mongodbatlas.TeamsAssigned, *mongodbatlas.Response, error) { + return &mongodbatlas.TeamsAssigned{ + Results: []*mongodbatlas.Result{ + { + TeamID: "team2", + RoleNames: []string{"GROUP_READ_ONLY"}, + }, + }, + TotalCount: 1, + }, nil, nil + }, + }, + } + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + Teams: []mdbv1.Team{ + { + TeamRef: common.ResourceRefNamespaced{ + Name: "team3", + Namespace: "default", + }, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + }, + }, + }, + } + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"teams":[{"teamRef":{"name":"team1","namespace":"default"},"roles":["GROUP_OWNER"]}]}`}) + workflowCtx := &workflow.Context{ + Client: atlasClient, + } + reconciler := &AtlasProjectReconciler{ + Client: k8sClient, + } + result := reconciler.ensureAssignedTeams(context.TODO(), workflowCtx, akoProject, true) + + require.Equal( + t, + workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ), + result, + ) + }) +} diff --git a/test/e2e/project_deletion_protection_test.go b/test/e2e/project_deletion_protection_test.go index a1db3e1707..801512c41c 100644 --- a/test/e2e/project_deletion_protection_test.go +++ b/test/e2e/project_deletion_protection_test.go @@ -36,7 +36,9 @@ import ( var _ = Describe("Project Deletion Protection", Label("project", "deletion-protection"), func() { var testData *model.TestDataProvider var managerStop context.CancelFunc - var projectID, networkPeerID, awsRoleARN, awsAccountID, AwsVpcID, customerMasterKeyID, atlasAccountARN, atlasRoleID string + var projectID, networkPeerID, atlasAccountARN, atlasRoleID, teamID string + var awsRoleARN, awsAccountID, AwsVpcID, customerMasterKeyID string + var usernames []string ctx := context.Background() BeforeEach(func() { @@ -267,7 +269,55 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote Expect(err).ToNot(HaveOccurred()) }) + By("Adding Assign team to the project", func() { + users, _, err := atlasClient.Client.AtlasUsers.List(ctx, os.Getenv("MCLI_ORG_ID"), &mongodbatlas.ListOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(users).ToNot(BeEmpty()) + + usernames = make([]string, 0, len(users)) + for _, user := range users { + usernames = append(usernames, user.Username) + } + + team := &mongodbatlas.Team{ + Name: fmt.Sprintf("%s-team", projectName), + Usernames: usernames, + } + + team, _, err = atlasClient.Client.Teams.Create(ctx, os.Getenv("MCLI_ORG_ID"), team) + Expect(err).ToNot(HaveOccurred()) + teamID = team.ID + + _, _, err = atlasClient.Client.Projects.AddTeamsToProject( + ctx, + projectID, + []*mongodbatlas.ProjectTeam{ + { + TeamID: team.ID, + RoleNames: []string{"GROUP_OWNER"}, + }, + }, + ) + Expect(err).ToNot(HaveOccurred()) + }) + By("Creating a project to be managed by the operator", func() { + akoTeam := &mdbv1.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-team", projectName), + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.TeamSpec{ + Name: fmt.Sprintf("%s-team", projectName), + Usernames: make([]mdbv1.TeamUser, 0, len(usernames)), + }, + } + for _, username := range usernames { + akoTeam.Spec.Usernames = append(akoTeam.Spec.Usernames, mdbv1.TeamUser(username)) + } + testData.Teams = []*mdbv1.AtlasTeam{akoTeam} + Expect(testData.K8SClient.Create(ctx, testData.Teams[0])) + akoProject := &mdbv1.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: projectName, @@ -348,6 +398,12 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote }, }, }, + Teams: []mdbv1.Team{ + { + TeamRef: common.ResourceRefNamespaced{}, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + }, + }, }, } testData.Project = akoProject @@ -389,6 +445,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -431,6 +490,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -486,6 +548,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -531,6 +596,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -579,6 +647,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -613,6 +684,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -645,6 +719,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -675,6 +752,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -703,6 +783,9 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.FalseCondition(status.ProjectCustomRolesReadyType). WithReason(string(workflow.AtlasDeletionProtection)). WithMessageRegexp("unable to reconcile Custom Roles due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -715,6 +798,35 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote testData.Project.Spec.CustomRoles[0].Actions[0].Resources[0].Database = toptr.MakePtr("testDB") Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.EncryptionAtRestReadyType), + status.TrueCondition(status.ProjectCustomRolesReadyType), + status.FalseCondition(status.ProjectTeamsReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information"), + ) + + g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Assigned Teams is ready after configured properly", func() { + Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.Teams[0].Roles[0] = "GROUP_OWNER" + Expect(testData.K8SClient.Update(context.TODO(), testData.Project)).To(Succeed()) + Eventually(func(g Gomega) { expectedConditions := testutil.MatchConditions( status.TrueCondition(status.ValidationSucceeded), @@ -729,6 +841,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.TrueCondition(status.ProjectSettingsReadyType), status.TrueCondition(status.EncryptionAtRestReadyType), status.TrueCondition(status.ProjectCustomRolesReadyType), + status.FalseCondition(status.ProjectTeamsReadyType), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) @@ -739,13 +852,27 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote AfterEach(func() { By("Deleting project from the operator", func() { + Expect(testData.K8SClient.Delete(ctx, testData.Teams[0])).To(Succeed()) Expect(testData.K8SClient.Delete(ctx, testData.Project)).To(Succeed()) + time.Sleep(time.Second * 30) }) By("Stopping the operator", func() { managerStop() }) + By("Deleting Team", func() { + if teamID != "" { + _, err := atlasClient.Client.Teams.RemoveTeamFromOrganization(ctx, os.Getenv("MCLI_ORG_ID"), teamID) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func(g Gomega) { + _, _, err := atlasClient.Client.Teams.Get(ctx, os.Getenv("MCLI_ORG_ID"), teamID) + g.Expect(err).To(HaveOccurred()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + } + }) + By("Deleting Network Peering", func() { if networkPeerID != "" { _, err := atlasClient.Client.Peers.Delete(ctx, projectID, networkPeerID) From 44c95b2841a45f98c036764fd36cafa608fe82ee Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Mon, 21 Aug 2023 15:17:06 +0200 Subject: [PATCH 05/11] fixes --- pkg/controller/atlasproject/auditing.go | 10 +++++++--- .../atlasproject/project_settings.go | 18 +++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pkg/controller/atlasproject/auditing.go b/pkg/controller/atlasproject/auditing.go index dc35c7d589..a2a32018ea 100644 --- a/pkg/controller/atlasproject/auditing.go +++ b/pkg/controller/atlasproject/auditing.go @@ -81,11 +81,15 @@ func auditingInSync(atlas *mongodbatlas.Auditing, spec *v1.Auditing) bool { return true } - if isAuditingEmpty(atlas) || isAuditingEmpty(spec) { - return false + specAsAtlas := &mongodbatlas.Auditing{ + AuditAuthorizationSuccess: toptr.MakePtr(false), + Enabled: toptr.MakePtr(false), + } + + if !isAuditingEmpty(spec) { + specAsAtlas = spec.ToAtlas() } - specAsAtlas := spec.ToAtlas() removeConfigurationType(atlas) return reflect.DeepEqual(atlas, specAsAtlas) diff --git a/pkg/controller/atlasproject/project_settings.go b/pkg/controller/atlasproject/project_settings.go index 0a7f2394a5..7844efbf74 100644 --- a/pkg/controller/atlasproject/project_settings.go +++ b/pkg/controller/atlasproject/project_settings.go @@ -147,16 +147,20 @@ func canProjectSettingsReconcile(ctx context.Context, atlasClient mongodbatlas.C } func areSettingsEqual(operator *v1.ProjectSettings, atlas *mongodbatlas.ProjectSettings) bool { - if (operator == nil) != (atlas == nil) { - return false + if operator == nil && atlas == nil { + return true + } + + if operator == nil { + operator = &v1.ProjectSettings{} } if operator.IsCollectDatabaseSpecificsStatisticsEnabled == nil { - operator.IsCollectDatabaseSpecificsStatisticsEnabled = toptr.MakePtr(false) + operator.IsCollectDatabaseSpecificsStatisticsEnabled = toptr.MakePtr(true) } if operator.IsDataExplorerEnabled == nil { - operator.IsDataExplorerEnabled = toptr.MakePtr(false) + operator.IsDataExplorerEnabled = toptr.MakePtr(true) } if operator.IsExtendedStorageSizesEnabled == nil { @@ -164,15 +168,15 @@ func areSettingsEqual(operator *v1.ProjectSettings, atlas *mongodbatlas.ProjectS } if operator.IsPerformanceAdvisorEnabled == nil { - operator.IsPerformanceAdvisorEnabled = toptr.MakePtr(false) + operator.IsPerformanceAdvisorEnabled = toptr.MakePtr(true) } if operator.IsRealtimePerformancePanelEnabled == nil { - operator.IsRealtimePerformancePanelEnabled = toptr.MakePtr(false) + operator.IsRealtimePerformancePanelEnabled = toptr.MakePtr(true) } if operator.IsSchemaAdvisorEnabled == nil { - operator.IsSchemaAdvisorEnabled = toptr.MakePtr(false) + operator.IsSchemaAdvisorEnabled = toptr.MakePtr(true) } return *operator.IsCollectDatabaseSpecificsStatisticsEnabled == *atlas.IsCollectDatabaseSpecificsStatisticsEnabled && From 53ee5362bb5a328f4c6df356a6e6d7c906456199 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Mon, 21 Aug 2023 17:52:11 +0200 Subject: [PATCH 06/11] fix auditing --- pkg/controller/atlasproject/auditing.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/controller/atlasproject/auditing.go b/pkg/controller/atlasproject/auditing.go index a2a32018ea..025b718866 100644 --- a/pkg/controller/atlasproject/auditing.go +++ b/pkg/controller/atlasproject/auditing.go @@ -90,6 +90,13 @@ func auditingInSync(atlas *mongodbatlas.Auditing, spec *v1.Auditing) bool { specAsAtlas = spec.ToAtlas() } + if isAuditingEmpty(atlas) { + atlas = &mongodbatlas.Auditing{ + AuditAuthorizationSuccess: toptr.MakePtr(false), + Enabled: toptr.MakePtr(false), + } + } + removeConfigurationType(atlas) return reflect.DeepEqual(atlas, specAsAtlas) From 6421364357ce08fe1f4e8ea70f6a7bc604c15850 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Mon, 21 Aug 2023 17:52:22 +0200 Subject: [PATCH 07/11] fmt --- pkg/controller/atlasproject/custom_roles.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/controller/atlasproject/custom_roles.go b/pkg/controller/atlasproject/custom_roles.go index 04b9cc6c3c..b00ee37079 100644 --- a/pkg/controller/atlasproject/custom_roles.go +++ b/pkg/controller/atlasproject/custom_roles.go @@ -6,16 +6,14 @@ import ( "errors" "fmt" - "go.mongodb.org/atlas/mongodbatlas" - - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" - - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" + v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "go.mongodb.org/atlas/mongodbatlas" - v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" ) From 83c6387ee51abcced35602b4ed768b9bca96bea2 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Mon, 21 Aug 2023 17:52:36 +0200 Subject: [PATCH 08/11] e2e test complete --- test/e2e/project_deletion_protection_test.go | 62 +++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/test/e2e/project_deletion_protection_test.go b/test/e2e/project_deletion_protection_test.go index 801512c41c..e689c27a18 100644 --- a/test/e2e/project_deletion_protection_test.go +++ b/test/e2e/project_deletion_protection_test.go @@ -6,10 +6,15 @@ import ( "os" "time" + corev1 "k8s.io/api/core/v1" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/cloud" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" @@ -19,21 +24,18 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/testutil" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions" - "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/cloud" "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/cloudaccess" "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/config" "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/k8s" "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model" ) -var _ = Describe("Project Deletion Protection", Label("project", "deletion-protection"), func() { +var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-protection"), func() { var testData *model.TestDataProvider var managerStop context.CancelFunc var projectID, networkPeerID, atlasAccountARN, atlasRoleID, teamID string @@ -257,7 +259,6 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote Action: "INSERT", Resources: []mongodbatlas.Resource{ { - Cluster: toptr.MakePtr(false), DB: toptr.MakePtr("testDB"), Collection: toptr.MakePtr("testCollection"), }, @@ -270,7 +271,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote }) By("Adding Assign team to the project", func() { - users, _, err := atlasClient.Client.AtlasUsers.List(ctx, os.Getenv("MCLI_ORG_ID"), &mongodbatlas.ListOptions{}) + users, _, err := atlasClient.Client.AtlasUsers.List(ctx, projectID, &mongodbatlas.ListOptions{}) Expect(err).ToNot(HaveOccurred()) Expect(users).ToNot(BeEmpty()) @@ -389,7 +390,6 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote Name: "INSERT", Resources: []mdbv1.Resource{ { - Cluster: toptr.MakePtr(false), Database: toptr.MakePtr("testD"), Collection: toptr.MakePtr("testCollection"), }, @@ -400,8 +400,11 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote }, Teams: []mdbv1.Team{ { - TeamRef: common.ResourceRefNamespaced{}, - Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + TeamRef: common.ResourceRefNamespaced{ + Name: fmt.Sprintf("%s-team", projectName), + Namespace: testData.Resources.Namespace, + }, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, }, }, }, @@ -654,7 +657,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) By("Maintenance Window is ready after configured properly", func() { @@ -691,7 +694,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) By("Auditing is ready after configured properly", func() { @@ -726,7 +729,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) By("Maintenance Window is ready after configured properly", func() { @@ -759,7 +762,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) By("Encryption At Rest is ready after configured properly", func() { @@ -790,7 +793,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) By("Custom Roles is ready after configured properly", func() { @@ -802,15 +805,15 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote expectedConditions := testutil.MatchConditions( status.TrueCondition(status.ValidationSucceeded), status.TrueCondition(status.ProjectReadyType), - status.FalseCondition(status.ReadyType), - status.TrueCondition(status.IPAccessListReadyType), - status.TrueCondition(status.CloudProviderAccessReadyType), - status.TrueCondition(status.NetworkPeerReadyType), - status.TrueCondition(status.IntegrationReadyType), - status.TrueCondition(status.MaintenanceWindowReadyType), - status.TrueCondition(status.AuditingReadyType), - status.TrueCondition(status.ProjectSettingsReadyType), - status.TrueCondition(status.EncryptionAtRestReadyType), + status.FalseCondition(status.ReadyType), /* + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.EncryptionAtRestReadyType),*/ status.TrueCondition(status.ProjectCustomRolesReadyType), status.FalseCondition(status.ProjectTeamsReadyType). WithReason(string(workflow.AtlasDeletionProtection)). @@ -819,7 +822,7 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) By("Assigned Teams is ready after configured properly", func() { @@ -841,12 +844,12 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote status.TrueCondition(status.ProjectSettingsReadyType), status.TrueCondition(status.EncryptionAtRestReadyType), status.TrueCondition(status.ProjectCustomRolesReadyType), - status.FalseCondition(status.ProjectTeamsReadyType), + status.TrueCondition(status.ProjectTeamsReadyType), ) g.Expect(testData.K8SClient.Get(context.TODO(), client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) - }).WithTimeout(time.Minute * 1).WithPolling(time.Second * 20).Should(Succeed()) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) }) }) @@ -863,7 +866,10 @@ var _ = Describe("Project Deletion Protection", Label("project", "deletion-prote By("Deleting Team", func() { if teamID != "" { - _, err := atlasClient.Client.Teams.RemoveTeamFromOrganization(ctx, os.Getenv("MCLI_ORG_ID"), teamID) + _, err := atlasClient.Client.Teams.RemoveTeamFromProject(ctx, projectID, teamID) + Expect(err).ToNot(HaveOccurred()) + + _, err = atlasClient.Client.Teams.RemoveTeamFromOrganization(ctx, os.Getenv("MCLI_ORG_ID"), teamID) Expect(err).ToNot(HaveOccurred()) Eventually(func(g Gomega) { From 75dd43c28a4382f9973a583678ddd4c606c07063 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Mon, 21 Aug 2023 18:05:59 +0200 Subject: [PATCH 09/11] fix teams --- pkg/controller/atlasproject/teams.go | 54 +++++++++++------------ pkg/controller/atlasproject/teams_test.go | 11 ++++- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/pkg/controller/atlasproject/teams.go b/pkg/controller/atlasproject/teams.go index 0df2bac437..3934ddcae1 100644 --- a/pkg/controller/atlasproject/teams.go +++ b/pkg/controller/atlasproject/teams.go @@ -5,28 +5,24 @@ import ( "encoding/json" "fmt" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - apiErrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" - - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" + controllerruntime "sigs.k8s.io/controller-runtime" v1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" - controllerruntime "sigs.k8s.io/controller-runtime" - + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "go.mongodb.org/atlas/mongodbatlas" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/statushandler" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/watch" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" ) type TeamDataContainer struct { @@ -36,24 +32,6 @@ type TeamDataContainer struct { } func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx context.Context, workflowCtx *workflow.Context, project *v1.AtlasProject, protected bool) workflow.Result { - canReconcile, err := canAssignedTeamsReconcile(ctx, workflowCtx.Client, r.Client, protected, project) - if err != nil { - result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) - workflowCtx.SetConditionFromResult(status.ProjectTeamsReadyType, result) - - return result - } - - if !canReconcile { - result := workflow.Terminate( - workflow.AtlasDeletionProtection, - "unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", - ) - workflowCtx.SetConditionFromResult(status.ProjectTeamsReadyType, result) - - return result - } - resourcesToWatch := make([]watch.WatchedObject, 0, len(project.Spec.Teams)) defer func() { workflowCtx.AddResourcesToWatch(resourcesToWatch...) @@ -93,6 +71,24 @@ func (r *AtlasProjectReconciler) ensureAssignedTeams(ctx context.Context, workfl teamsToAssign[team.Status.ID] = &assignedTeam } + canReconcile, err := canAssignedTeamsReconcile(ctx, workflowCtx.Client, r.Client, protected, project) + if err != nil { + result := workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.ProjectTeamsReadyType, result) + + return result + } + + if !canReconcile { + result := workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile Assigned Teams due to deletion protection being enabled. see https://dochub.mongodb.org/core/ako-deletion-protection for further information", + ) + workflowCtx.SetConditionFromResult(status.ProjectTeamsReadyType, result) + + return result + } + err = r.syncAssignedTeams(workflowCtx, project.ID(), project, teamsToAssign) if err != nil { workflowCtx.SetConditionFalse(status.ProjectTeamsReadyType) diff --git a/pkg/controller/atlasproject/teams_test.go b/pkg/controller/atlasproject/teams_test.go index a8d9ca9d61..76ef32f599 100644 --- a/pkg/controller/atlasproject/teams_test.go +++ b/pkg/controller/atlasproject/teams_test.go @@ -5,6 +5,8 @@ import ( "errors" "testing" + "go.uber.org/zap/zaptest" + "github.com/stretchr/testify/require" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" @@ -233,10 +235,14 @@ func TestEnsureAssignedTeams(t *testing.T) { } akoProject := &mdbv1.AtlasProject{} akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: "{}"}) + logger := zaptest.NewLogger(t).Sugar() workflowCtx := &workflow.Context{ Client: atlasClient, + Log: logger, + } + reconciler := &AtlasProjectReconciler{ + Log: logger, } - reconciler := &AtlasProjectReconciler{} result := reconciler.ensureAssignedTeams(context.TODO(), workflowCtx, akoProject, true) require.Equal(t, workflow.Terminate(workflow.Internal, "unable to resolve ownership for deletion protection: failed to retrieve data"), result) @@ -299,11 +305,14 @@ func TestEnsureAssignedTeams(t *testing.T) { }, } akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"teams":[{"teamRef":{"name":"team1","namespace":"default"},"roles":["GROUP_OWNER"]}]}`}) + logger := zaptest.NewLogger(t).Sugar() workflowCtx := &workflow.Context{ Client: atlasClient, + Log: logger, } reconciler := &AtlasProjectReconciler{ Client: k8sClient, + Log: logger, } result := reconciler.ensureAssignedTeams(context.TODO(), workflowCtx, akoProject, true) From a0155fdc9197a48f5bbec55827cee5a0a34e1efc Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Tue, 22 Aug 2023 16:39:57 +0200 Subject: [PATCH 10/11] fix tests --- pkg/controller/atlasproject/custom_roles.go | 3 --- pkg/controller/atlasproject/custom_roles_test.go | 8 ++------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pkg/controller/atlasproject/custom_roles.go b/pkg/controller/atlasproject/custom_roles.go index b00ee37079..f3802fe574 100644 --- a/pkg/controller/atlasproject/custom_roles.go +++ b/pkg/controller/atlasproject/custom_roles.go @@ -98,9 +98,6 @@ func mapToOperator(atlasCustomRoles *[]mongodbatlas.CustomDBRole) []v1.CustomRol resources := make([]v1.Resource, 0, len(atlasAction.Resources)) for _, atlasResource := range atlasAction.Resources { - if atlasResource.Cluster != nil && !*atlasResource.Cluster { - atlasResource.Cluster = nil - } resources = append(resources, v1.Resource{ Cluster: atlasResource.Cluster, Database: atlasResource.DB, diff --git a/pkg/controller/atlasproject/custom_roles_test.go b/pkg/controller/atlasproject/custom_roles_test.go index eec8246bb2..dcc524331b 100644 --- a/pkg/controller/atlasproject/custom_roles_test.go +++ b/pkg/controller/atlasproject/custom_roles_test.go @@ -319,7 +319,6 @@ func TestCanCustomRolesReconcile(t *testing.T) { Action: "INSERT", Resources: []mongodbatlas.Resource{ { - Cluster: toptr.MakePtr(false), DB: toptr.MakePtr("testDB"), Collection: toptr.MakePtr("testCollection"), }, @@ -342,7 +341,6 @@ func TestCanCustomRolesReconcile(t *testing.T) { Name: "INSERT", Resources: []mdbv1.Resource{ { - Cluster: toptr.MakePtr(false), Database: toptr.MakePtr("testDB"), Collection: toptr.MakePtr("testCollection"), }, @@ -353,7 +351,7 @@ func TestCanCustomRolesReconcile(t *testing.T) { }, }, } - akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"cluster":false,"database":"testDB","collection":"testCollection"}]}]}]}`}) + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"database":"testDB","collection":"testCollection"}]}]}]}`}) result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) assert.NoError(t, err) @@ -373,7 +371,6 @@ func TestCanCustomRolesReconcile(t *testing.T) { Action: "INSERT", Resources: []mongodbatlas.Resource{ { - Cluster: toptr.MakePtr(false), DB: toptr.MakePtr("testDB"), Collection: toptr.MakePtr("testCollection"), }, @@ -396,7 +393,6 @@ func TestCanCustomRolesReconcile(t *testing.T) { Name: "INSERT", Resources: []mdbv1.Resource{ { - Cluster: toptr.MakePtr(false), Database: toptr.MakePtr("testDB"), Collection: toptr.MakePtr("testCollection"), }, @@ -407,7 +403,7 @@ func TestCanCustomRolesReconcile(t *testing.T) { }, }, } - akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"cluster":false,"database":"testDB","collection":"testCollection"}]}]}]}`}) + akoProject.WithAnnotations(map[string]string{customresource.AnnotationLastAppliedConfiguration: `{"customRoles":[{"name":"testRole1","actions":[{"name":"INSERT","resources":[{"database":"testDB","collection":"testCollection"}]}]}]}`}) result, err := canCustomRolesReconcile(context.TODO(), atlasClient, true, akoProject) assert.NoError(t, err) From e82713d40e23cb9005f6aca6a8fa3da03c801c40 Mon Sep 17 00:00:00 2001 From: Helder Santana Date: Tue, 22 Aug 2023 16:47:54 +0200 Subject: [PATCH 11/11] remove focus --- test/e2e/project_deletion_protection_test.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/e2e/project_deletion_protection_test.go b/test/e2e/project_deletion_protection_test.go index e689c27a18..62b5d199e0 100644 --- a/test/e2e/project_deletion_protection_test.go +++ b/test/e2e/project_deletion_protection_test.go @@ -35,7 +35,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model" ) -var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-protection"), func() { +var _ = Describe("Project Deletion Protection", Label("project", "deletion-protection"), func() { var testData *model.TestDataProvider var managerStop context.CancelFunc var projectID, networkPeerID, atlasAccountARN, atlasRoleID, teamID string @@ -805,15 +805,15 @@ var _ = FDescribe("Project Deletion Protection", Label("project", "deletion-prot expectedConditions := testutil.MatchConditions( status.TrueCondition(status.ValidationSucceeded), status.TrueCondition(status.ProjectReadyType), - status.FalseCondition(status.ReadyType), /* - status.TrueCondition(status.IPAccessListReadyType), - status.TrueCondition(status.CloudProviderAccessReadyType), - status.TrueCondition(status.NetworkPeerReadyType), - status.TrueCondition(status.IntegrationReadyType), - status.TrueCondition(status.MaintenanceWindowReadyType), - status.TrueCondition(status.AuditingReadyType), - status.TrueCondition(status.ProjectSettingsReadyType), - status.TrueCondition(status.EncryptionAtRestReadyType),*/ + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.EncryptionAtRestReadyType), status.TrueCondition(status.ProjectCustomRolesReadyType), status.FalseCondition(status.ProjectTeamsReadyType). WithReason(string(workflow.AtlasDeletionProtection)).