diff --git a/Dockerfile b/Dockerfile index f10838afbb..0a73a25660 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,9 +27,9 @@ ENV TARGET_OS=${TARGETOS} RUN make manager -FROM registry.access.redhat.com/ubi8/ubi-minimal:8.8 +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.2 -RUN microdnf install yum &&\ +RUN microdnf install -y yum &&\ yum -y update &&\ yum -y upgrade &&\ yum clean all &&\ diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9bf92c4e9b..940b3eb671 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -25,8 +25,6 @@ import ( "strings" "time" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/version" - "go.uber.org/zap/zapcore" ctrzap "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -52,7 +50,14 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/watch" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" - // +kubebuilder:scaffold:imports + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/version" +) + +const ( + objectDeletionProtectionFlag = "object-deletion-protection" + subobjectDeletionProtectionFlag = "subobject-deletion-protection" + objectDeletionProtectionDefault = true + subobjectDeletionProtectionDefault = true ) var ( @@ -62,9 +67,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(mdbv1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme } func main() { @@ -158,14 +161,16 @@ func main() { } if err = (&atlasdatabaseuser.AtlasDatabaseUserReconciler{ - Client: mgr.GetClient(), - Log: logger.Named("controllers").Named("AtlasDatabaseUser").Sugar(), - Scheme: mgr.GetScheme(), - AtlasDomain: config.AtlasDomain, - ResourceWatcher: watch.NewResourceWatcher(), - GlobalAPISecret: config.GlobalAPISecret, - GlobalPredicates: globalPredicates, - EventRecorder: mgr.GetEventRecorderFor("AtlasDatabaseUser"), + ResourceWatcher: watch.NewResourceWatcher(), + Client: mgr.GetClient(), + Log: logger.Named("controllers").Named("AtlasDatabaseUser").Sugar(), + Scheme: mgr.GetScheme(), + AtlasDomain: config.AtlasDomain, + GlobalAPISecret: config.GlobalAPISecret, + EventRecorder: mgr.GetEventRecorderFor("AtlasDatabaseUser"), + GlobalPredicates: globalPredicates, + ObjectDeletionProtection: config.ObjectDeletionProtection, + SubObjectDeletionProtection: config.SubObjectDeletionProtection, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AtlasDatabaseUser") os.Exit(1) @@ -203,15 +208,17 @@ func main() { } type Config struct { - AtlasDomain string - EnableLeaderElection bool - MetricsAddr string - Namespace string - WatchedNamespaces map[string]bool - ProbeAddr string - GlobalAPISecret client.ObjectKey - LogLevel string - LogEncoder string + AtlasDomain string + EnableLeaderElection bool + MetricsAddr string + Namespace string + WatchedNamespaces map[string]bool + ProbeAddr string + GlobalAPISecret client.ObjectKey + LogLevel string + LogEncoder string + ObjectDeletionProtection bool + SubObjectDeletionProtection bool } // ParseConfiguration fills the 'OperatorConfig' from the flags passed to the program @@ -228,6 +235,10 @@ func parseConfiguration() Config { "Enabling this will ensure there is only one active controller manager.") flag.StringVar(&config.LogLevel, "log-level", "info", "Log level. Available values: debug | info | warn | error | dpanic | panic | fatal") flag.StringVar(&config.LogEncoder, "log-encoder", "json", "Log encoder. Available values: json | console") + flag.BoolVar(&config.ObjectDeletionProtection, objectDeletionProtectionFlag, true, "Defines the operator will not delete Atlas resource "+ + "when a Custom Resource is deleted") + flag.BoolVar(&config.SubObjectDeletionProtection, subobjectDeletionProtectionFlag, true, "Defines that the operator will not overwrite "+ + "(and consequently delete) subresources that were not previously created by the operator") appVersion := flag.Bool("v", false, "prints application version") flag.Parse() @@ -251,6 +262,8 @@ func parseConfiguration() Config { config.Namespace = watchedNamespace } + configureDeletionProtectionFlags(&config) + return config } @@ -303,3 +316,46 @@ func initCustomZapLogger(level, encoding string) (*zap.Logger, error) { } return cfg.Build() } + +func configureDeletionProtectionFlags(config *Config) { + if config == nil { + return + } + + objectDeletionSet := false + subObjectDeletionSet := false + + flag.Visit(func(f *flag.Flag) { + if f.Name == objectDeletionProtectionFlag { + objectDeletionSet = true + } + + if f.Name == subobjectDeletionProtectionFlag { + subObjectDeletionSet = true + } + }) + + if !objectDeletionSet { + objDeletion := strings.ToLower(os.Getenv("OBJECT_DELETION_PROTECTION")) + switch objDeletion { + case "true": + config.ObjectDeletionProtection = true + case "false": + config.ObjectDeletionProtection = false + default: + config.ObjectDeletionProtection = objectDeletionProtectionDefault + } + } + + if !subObjectDeletionSet { + objDeletion := strings.ToLower(os.Getenv("SUBOBJECT_DELETION_PROTECTION")) + switch objDeletion { + case "true": + config.SubObjectDeletionProtection = true + case "false": + config.SubObjectDeletionProtection = false + default: + config.SubObjectDeletionProtection = subobjectDeletionProtectionDefault + } + } +} diff --git a/pkg/api/v1/atlascustomresource.go b/pkg/api/v1/atlascustomresource.go index 83f24cddc4..c4ae4bb7bd 100644 --- a/pkg/api/v1/atlascustomresource.go +++ b/pkg/api/v1/atlascustomresource.go @@ -18,7 +18,6 @@ type AtlasCustomResource interface { } var _ AtlasCustomResource = &AtlasProject{} - var _ AtlasCustomResource = &AtlasDeployment{} - +var _ AtlasCustomResource = &AtlasDatabaseUser{} var _ AtlasCustomResource = &AtlasDataFederation{} diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index e9496821a6..70057acb98 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -14,9 +14,10 @@ a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package v1 import ( + "k8s.io/apimachinery/pkg/runtime" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" - "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go index 6700106e13..fc4c7f0c46 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go @@ -20,7 +20,6 @@ import ( "context" "errors" "fmt" - "time" "go.mongodb.org/atlas/mongodbatlas" "go.uber.org/zap" @@ -28,9 +27,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" @@ -43,19 +41,20 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/validate" "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" ) // AtlasDatabaseUserReconciler reconciles an AtlasDatabaseUser object type AtlasDatabaseUserReconciler struct { watch.ResourceWatcher - Client client.Client - Log *zap.SugaredLogger - Scheme *runtime.Scheme - AtlasDomain string - GlobalAPISecret client.ObjectKey - EventRecorder record.EventRecorder - GlobalPredicates []predicate.Predicate + Client client.Client + Log *zap.SugaredLogger + Scheme *runtime.Scheme + AtlasDomain string + GlobalAPISecret client.ObjectKey + EventRecorder record.EventRecorder + GlobalPredicates []predicate.Predicate + ObjectDeletionProtection bool + SubObjectDeletionProtection bool } // +kubebuilder:rbac:groups=atlas.mongodb.com,resources=atlasdatabaseusers,verbs=get;list;watch;create;update;patch;delete @@ -68,8 +67,7 @@ type AtlasDatabaseUserReconciler struct { // +kubebuilder:rbac:groups="",namespace=default,resources=secrets,verbs=create;update;patch;delete // +kubebuilder:rbac:groups="",namespace=default,resources=events,verbs=create;patch -func (r *AtlasDatabaseUserReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = context +func (r *AtlasDatabaseUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.With("atlasdatabaseuser", req.NamespacedName) databaseUser := &mdbv1.AtlasDatabaseUser{} @@ -78,7 +76,7 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(context context.Context, req ctr return result.ReconcileResult(), nil } - if shouldSkip := customresource.ReconciliationShouldBeSkipped(databaseUser); shouldSkip { + if customresource.ReconciliationShouldBeSkipped(databaseUser) { log.Infow(fmt.Sprintf("-> Skipping AtlasDatabaseUser reconciliation as annotation %s=%s", customresource.ReconciliationPolicyAnnotation, customresource.ReconciliationPolicySkip), "spec", databaseUser.Spec) return workflow.OK().ReconcileResult(), nil } @@ -86,147 +84,204 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(context context.Context, req ctr if databaseUser.Spec.PasswordSecret != nil { r.EnsureResourcesAreWatched(req.NamespacedName, "Secret", log, *databaseUser.PasswordSecretObjectKey()) } - ctx := customresource.MarkReconciliationStarted(r.Client, databaseUser, log) + workflowCtx := customresource.MarkReconciliationStarted(r.Client, databaseUser, log) log.Infow("-> Starting AtlasDatabaseUser reconciliation", "spec", databaseUser.Spec, "status", databaseUser.Status) - defer statushandler.Update(ctx, r.Client, r.EventRecorder, databaseUser) + defer statushandler.Update(workflowCtx, r.Client, r.EventRecorder, databaseUser) - resourceVersionIsValid := customresource.ValidateResourceVersion(ctx, databaseUser, r.Log) + resourceVersionIsValid := customresource.ValidateResourceVersion(workflowCtx, databaseUser, r.Log) if !resourceVersionIsValid.IsOk() { r.Log.Debugf("databaseuser validation result: %v", resourceVersionIsValid) + return resourceVersionIsValid.ReconcileResult(), nil } if err := validate.DatabaseUser(databaseUser); err != nil { - result := workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(status.ValidationSucceeded, result) + result = workflow.Terminate(workflow.Internal, err.Error()) + workflowCtx.SetConditionFromResult(status.ValidationSucceeded, result) + return result.ReconcileResult(), nil } - ctx.SetConditionTrue(status.ValidationSucceeded) + workflowCtx.SetConditionTrue(status.ValidationSucceeded) project := &mdbv1.AtlasProject{} - if result := r.readProjectResource(databaseUser, project); !result.IsOk() { - ctx.SetConditionFromResult(status.DatabaseUserReadyType, result) + if result = r.readProjectResource(databaseUser, project); !result.IsOk() { + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + return result.ReconcileResult(), nil } connection, err := atlas.ReadConnection(log, r.Client, r.GlobalAPISecret, project.ConnectionSecretObjectKey()) if err != nil { - result := workflow.Terminate(workflow.AtlasCredentialsNotProvided, err.Error()) - ctx.SetConditionFromResult(status.DatabaseUserReadyType, result) + result = workflow.Terminate(workflow.AtlasCredentialsNotProvided, err.Error()) + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + return result.ReconcileResult(), nil } - ctx.Connection = connection + workflowCtx.Connection = connection atlasClient, err := atlas.Client(r.AtlasDomain, connection, log) if err != nil { - result := workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(status.DeploymentReadyType, result) + result = workflow.Terminate(workflow.Internal, err.Error()) + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + return result.ReconcileResult(), nil } - ctx.Client = atlasClient + workflowCtx.Client = atlasClient + + owner, err := customresource.IsOwner(databaseUser, r.ObjectDeletionProtection, managedByOperator(), managedByAtlas(ctx, atlasClient, project.ID(), log)) + if err != nil { + result = workflow.Terminate(workflow.Internal, fmt.Sprintf("enable to resolve ownership for deletion protection: %s", err)) + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + log.Error(result.GetMessage()) - result = r.ensureDatabaseUser(ctx, *project, *databaseUser) - if !result.IsOk() { - ctx.SetConditionFromResult(status.DatabaseUserReadyType, result) return result.ReconcileResult(), nil } - ctx.SetConditionTrue(status.DatabaseUserReadyType) - ctx.SetConditionTrue(status.ReadyType) - return result.ReconcileResult(), nil -} + if !owner { + result = workflow.Terminate( + workflow.AtlasDeletionProtection, + "unable to reconcile database user: it already exists in Atlas, it was not previously managed by the operator, and the deletion protection is enabled.", + ) + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + log.Error(result.GetMessage()) -func (r *AtlasDatabaseUserReconciler) readProjectResource(user *mdbv1.AtlasDatabaseUser, project *mdbv1.AtlasProject) workflow.Result { - if err := r.Client.Get(context.Background(), user.AtlasProjectObjectKey(), project); err != nil { - return workflow.Terminate(workflow.Internal, err.Error()) + return result.ReconcileResult(), nil } - return workflow.OK() -} -func (r *AtlasDatabaseUserReconciler) SetupWithManager(mgr ctrl.Manager) error { - c, err := controller.New("AtlasDatabaseUser", mgr, controller.Options{Reconciler: r}) - if err != nil { - return err + deletionRequest, result := r.handleDeletion(ctx, databaseUser, project, atlasClient, log) + if deletionRequest { + return result.ReconcileResult(), nil } - // Watch for changes to primary resource AtlasDatabaseUser & handle delete separately - err = c.Watch(&source.Kind{Type: &mdbv1.AtlasDatabaseUser{}}, &watch.EventHandlerWithDelete{Controller: r}, r.GlobalPredicates...) + err = customresource.ApplyLastConfigApplied(ctx, databaseUser, r.Client) if err != nil { - return err + result = workflow.Terminate(workflow.Internal, err.Error()) + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + log.Error(result.GetMessage()) + + return result.ReconcileResult(), nil } - // Watch for DatabaseUser password Secrets - err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, watch.NewSecretHandler(r.WatchedResources)) - if err != nil { - return err + result = r.ensureDatabaseUser(workflowCtx, *project, *databaseUser) + if !result.IsOk() { + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + + return result.ReconcileResult(), nil } - return nil -} + err = customresource.ManageFinalizer(ctx, r.Client, databaseUser, customresource.SetFinalizer) + if err != nil { + result = workflow.Terminate(workflow.AtlasFinalizerNotSet, err.Error()) + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + log.Error(result.GetMessage()) -func (r AtlasDatabaseUserReconciler) Delete(e event.DeleteEvent) error { - dbUser, ok := e.Object.(*mdbv1.AtlasDatabaseUser) - if !ok { - r.Log.Errorf("Ignoring malformed Delete() call (expected type %T, got %T)", &mdbv1.AtlasDatabaseUser{}, e.Object) - return nil + return result.ReconcileResult(), nil } - log := r.Log.With("atlasdatabaseuser", kube.ObjectKeyFromObject(dbUser)) + workflowCtx.SetConditionTrue(status.DatabaseUserReadyType) + workflowCtx.SetConditionTrue(status.ReadyType) - log.Infow("-> Starting AtlasDatabaseUser deletion", "spec", dbUser.Spec) + return result.ReconcileResult(), nil +} - project := &mdbv1.AtlasProject{} - if result := r.readProjectResource(dbUser, project); !result.IsOk() { - return errors.New("cannot read project resource") +func (r *AtlasDatabaseUserReconciler) readProjectResource(user *mdbv1.AtlasDatabaseUser, project *mdbv1.AtlasProject) workflow.Result { + if err := r.Client.Get(context.Background(), user.AtlasProjectObjectKey(), project); err != nil { + return workflow.Terminate(workflow.Internal, err.Error()) } + return workflow.OK() +} - if customresource.ResourceShouldBeLeftInAtlas(dbUser) { - log.Infof("Not removing Atlas database user from Atlas as the '%s' annotation is set", customresource.ResourcePolicyAnnotation) - } else if err := r.deleteUserFromAtlas(dbUser, project, log); err != nil { - log.Error("Failed to remove database user from Atlas: %s", err) +func (r *AtlasDatabaseUserReconciler) handleDeletion( + ctx context.Context, + dbUser *mdbv1.AtlasDatabaseUser, + project *mdbv1.AtlasProject, + atlasClient mongodbatlas.Client, + log *zap.SugaredLogger, +) (bool, workflow.Result) { + if dbUser.GetDeletionTimestamp().IsZero() { + return false, workflow.OK() } - // We ignore the error as it will be printed by the function - _ = connectionsecret.RemoveStaleSecretsByUserName(r.Client, project.ID(), dbUser.Spec.Username, *dbUser, log) + if customresource.HaveFinalizer(dbUser, customresource.FinalizerLabel) { + err := connectionsecret.RemoveStaleSecretsByUserName(r.Client, project.ID(), dbUser.Spec.Username, *dbUser, log) + if err != nil { + return true, workflow.Terminate(workflow.DatabaseUserConnectionSecretsNotDeleted, err.Error()) + } + } - return nil -} + if customresource.IsResourceProtected(dbUser, r.ObjectDeletionProtection) { + log.Info("Not removing Atlas database user from Atlas as per configuration") -func (r AtlasDatabaseUserReconciler) deleteUserFromAtlas(dbUser *mdbv1.AtlasDatabaseUser, project *mdbv1.AtlasProject, log *zap.SugaredLogger) error { - connection, err := atlas.ReadConnection(log, r.Client, r.GlobalAPISecret, project.ConnectionSecretObjectKey()) + err := customresource.ManageFinalizer(ctx, r.Client, dbUser, customresource.UnsetFinalizer) + if err != nil { + return true, workflow.Terminate(workflow.AtlasFinalizerNotRemoved, err.Error()) + } + + return true, workflow.OK() + } + + _, err := atlasClient.DatabaseUsers.Delete(context.Background(), dbUser.Spec.DatabaseName, project.ID(), dbUser.Spec.Username) if err != nil { - return err + var apiError *mongodbatlas.ErrorResponse + if errors.As(err, &apiError) && apiError.ErrorCode != atlas.UsernameNotFound { + return true, workflow.Terminate(workflow.DatabaseUserNotDeletedInAtlas, err.Error()) + } + + log.Info("Database user doesn't exist or is already deleted") } - atlasClient, err := atlas.Client(r.AtlasDomain, connection, log) + err = customresource.ManageFinalizer(ctx, r.Client, dbUser, customresource.UnsetFinalizer) if err != nil { - return fmt.Errorf("cannot build Atlas client: %w", err) + return true, workflow.Terminate(workflow.AtlasFinalizerNotRemoved, err.Error()) } - userName := dbUser.Spec.Username + return true, workflow.OK() +} + +func (r *AtlasDatabaseUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("AtlasDatabaseUser"). + For(&mdbv1.AtlasDatabaseUser{}, builder.WithPredicates(r.GlobalPredicates...)). + Watches(&source.Kind{Type: &corev1.Secret{}}, watch.NewSecretHandler(r.WatchedResources)). + Complete(r) +} - go func() { - timeout := time.Now().Add(workflow.DefaultTimeout) +func managedByAtlas(ctx context.Context, atlasClient mongodbatlas.Client, projectID string, log *zap.SugaredLogger) customresource.AtlasChecker { + return func(resource mdbv1.AtlasCustomResource) (bool, error) { + dbUser, ok := resource.(*mdbv1.AtlasDatabaseUser) + if !ok { + return false, errors.New("failed to match resource type as AtlasDatabaseUser") + } - for time.Now().Before(timeout) { - _, err = atlasClient.DatabaseUsers.Delete(context.Background(), dbUser.Spec.DatabaseName, project.ID(), userName) + atlasDBUser, _, err := atlasClient.DatabaseUsers.Get(ctx, dbUser.Spec.DatabaseName, projectID, dbUser.Spec.Username) + if err != nil { var apiError *mongodbatlas.ErrorResponse if errors.As(err, &apiError) && apiError.ErrorCode == atlas.UsernameNotFound { - log.Info("Database user doesn't exist or is already deleted") - return + return false, nil } - if err != nil { - log.Errorw("Cannot delete Atlas database user", "error", err) - time.Sleep(workflow.DefaultRetry) - continue - } + return false, err + } - log.Infow("Started DatabaseUser deletion process in Atlas", "projectID", project.ID(), "userName", userName) - return + isSame, err := userMatchesSpec(log, atlasDBUser, dbUser.Spec) + if err != nil { + return true, err } - }() - return nil + return !isSame, nil + } +} + +func managedByOperator() customresource.OperatorChecker { + return func(resource mdbv1.AtlasCustomResource) (bool, error) { + annotations := resource.GetAnnotations() + if annotations == nil { + return false, nil + } + + _, ok := annotations[customresource.AnnotationLastAppliedConfiguration] + + return ok, nil + } } diff --git a/pkg/controller/customresource/customresource.go b/pkg/controller/customresource/customresource.go index 9463ffc8d4..c23ff42acd 100644 --- a/pkg/controller/customresource/customresource.go +++ b/pkg/controller/customresource/customresource.go @@ -26,6 +26,7 @@ const ( ResourceVersionOverride = "mongodb.com/atlas-resource-version-policy" ResourcePolicyKeep = "keep" + ResourcePolicyDelete = "delete" ReconciliationPolicySkip = "skip" ResourceVersionAllow = "allow" ) diff --git a/pkg/controller/customresource/finalizer.go b/pkg/controller/customresource/finalizer.go index 569fe8df42..1da0058410 100644 --- a/pkg/controller/customresource/finalizer.go +++ b/pkg/controller/customresource/finalizer.go @@ -1,11 +1,19 @@ package customresource import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" ) const FinalizerLabel = "mongodbatlas/finalizer" +type FinalizerOperator func(resource mdbv1.AtlasCustomResource, finalizer string) + func HaveFinalizer(resource mdbv1.AtlasCustomResource, finalizer string) bool { for _, f := range resource.GetFinalizers() { if f == finalizer { @@ -35,3 +43,24 @@ func UnsetFinalizer(resource mdbv1.AtlasCustomResource, finalizer string) { resource.SetFinalizers(finalizers) } + +func ManageFinalizer( + ctx context.Context, + client client.Client, + resource mdbv1.AtlasCustomResource, + op FinalizerOperator, +) error { + err := client.Get(ctx, kube.ObjectKeyFromObject(resource), resource) + if err != nil { + return fmt.Errorf("failed to get %t before removing deletion finalizer: %w", resource, err) + } + + op(resource, FinalizerLabel) + + err = client.Update(ctx, resource) + if err != nil { + return fmt.Errorf("failed to remove deletion finalizer from %s: %w", resource.GetName(), err) + } + + return nil +} diff --git a/pkg/controller/customresource/protection.go b/pkg/controller/customresource/protection.go new file mode 100644 index 0000000000..d4b861a51c --- /dev/null +++ b/pkg/controller/customresource/protection.go @@ -0,0 +1,70 @@ +package customresource + +import ( + "context" + "encoding/json" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" +) + +const ( + AnnotationLastAppliedConfiguration = "mongodb.com/last-applied-configuration" +) + +type OperatorChecker func(resource mdbv1.AtlasCustomResource) (bool, error) +type AtlasChecker func(resource mdbv1.AtlasCustomResource) (bool, error) + +func IsOwner(resource mdbv1.AtlasCustomResource, protectionFlag bool, operatorChecker OperatorChecker, atlasChecker AtlasChecker) (bool, error) { + if !protectionFlag { + return true, nil + } + + wasManaged, err := operatorChecker(resource) + if err != nil { + return false, err + } + + if wasManaged { + return true, nil + } + + existInAtlas, err := atlasChecker(resource) + if err != nil { + return false, err + } + + return !existInAtlas, nil +} + +func IsResourceProtected(resource mdbv1.AtlasCustomResource, protectionFlag bool) bool { + if policy, ok := resource.GetAnnotations()[ResourcePolicyAnnotation]; ok { + return policy == ResourcePolicyKeep + } + + return protectionFlag +} + +func ApplyLastConfigApplied(ctx context.Context, resource mdbv1.AtlasCustomResource, k8sClient client.Client) error { + uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource) + if err != nil { + return err + } + + js, err := json.Marshal(uObj["spec"]) + if err != nil { + return err + } + + annotations := resource.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + + annotations[AnnotationLastAppliedConfiguration] = string(js) + resource.SetAnnotations(annotations) + + return k8sClient.Update(ctx, resource, &client.UpdateOptions{}) +} diff --git a/pkg/controller/workflow/reason.go b/pkg/controller/workflow/reason.go index 0df7a8435e..36095e4648 100644 --- a/pkg/controller/workflow/reason.go +++ b/pkg/controller/workflow/reason.go @@ -10,6 +10,9 @@ const ( Internal ConditionReason = "InternalError" AtlasResourceVersionMismatch ConditionReason = "AtlasResourceVersionMismatch" AtlasResourceVersionIsInvalid ConditionReason = "AtlasResourceVersionIsInvalid" + AtlasFinalizerNotSet ConditionReason = "AtlasFinalizerNotSet" + AtlasFinalizerNotRemoved ConditionReason = "AtlasFinalizerNotRemoved" + AtlasDeletionProtection ConditionReason = "AtlasDeletionProtection" ) // Atlas Project reasons @@ -57,7 +60,9 @@ const ( const ( DatabaseUserNotCreatedInAtlas ConditionReason = "DatabaseUserNotCreatedInAtlas" DatabaseUserNotUpdatedInAtlas ConditionReason = "DatabaseUserNotUpdatedInAtlas" + DatabaseUserNotDeletedInAtlas ConditionReason = "DatabaseUserNotDeletedInAtlas" DatabaseUserConnectionSecretsNotCreated ConditionReason = "DatabaseUserConnectionSecretsNotCreated" + DatabaseUserConnectionSecretsNotDeleted ConditionReason = "DatabaseUserConnectionSecretsNotDeleted" DatabaseUserStaleConnectionSecrets ConditionReason = "DatabaseUserStaleConnectionSecrets" DatabaseUserDeploymentAppliedChanges ConditionReason = "DeploymentAppliedDatabaseUsersChanges" DatabaseUserInvalidSpec ConditionReason = "DatabaseUserInvalidSpec" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index a0a6a23a90..1a48e3bbea 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -9,7 +9,7 @@ if [[ -z "${REGISTRY:-}" ]]; then fi image="${REGISTRY}/mongodb-atlas-kubernetes-operator" -docker build -t "${image}" . +docker build --rm -t "${image}" . docker push "${image}" #Prepare CRDs diff --git a/test/int/databaseuser_protected_test.go b/test/int/databaseuser_protected_test.go new file mode 100644 index 0000000000..d910da7f14 --- /dev/null +++ b/test/int/databaseuser_protected_test.go @@ -0,0 +1,344 @@ +package int + +import ( + "context" + "fmt" + "net/http" + "time" + + corev1 "k8s.io/api/core/v1" + + "go.mongodb.org/atlas/mongodbatlas" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" + + 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" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/testutil" +) + +var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "protection-enabled"), func() { + var testNamespace *corev1.Namespace + var stopManager context.CancelFunc + var projectName string + projectNamePrefix := "database-user-protected" + dbUserName1 := "db-user1" + dbUserName2 := "db-user2" + dbUserName3 := "db-user3" + testProject := &mdbv1.AtlasProject{} + testDeployment := &mdbv1.AtlasDeployment{} + testDBUser1 := &mdbv1.AtlasDatabaseUser{} + testDBUser2 := &mdbv1.AtlasDatabaseUser{} + testDBUser3 := &mdbv1.AtlasDatabaseUser{} + + BeforeEach(func() { + testNamespace, stopManager = prepareControllers(true) + projectName = fmt.Sprintf("%s-%s", projectNamePrefix, testNamespace.Name) + + By("Creating a project", func() { + connSecret := buildConnectionSecret("my-atlas-key") + Expect(k8sClient.Create(context.TODO(), &connSecret)).To(Succeed()) + + testProject = mdbv1.NewProject(testNamespace.Name, projectName, projectName). + WithConnectionSecret(connSecret.Name). + WithIPAccessList(project.NewIPAccessList().WithCIDR("0.0.0.0/0")) + Expect(k8sClient.Create(context.TODO(), testProject, &client.CreateOptions{})).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testProject, status.TrueCondition(status.ReadyType)) + }).WithTimeout(15 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Creating a deployment", func() { + testDeployment = mdbv1.DefaultAWSDeployment(testNamespace.Name, projectName).Lightweight() + Expect(k8sClient.Create(context.TODO(), testDeployment)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDeployment, status.TrueCondition(status.ReadyType)) + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Creating database user", func() { + dbUser := &mongodbatlas.DatabaseUser{ + Username: dbUserName3, + Password: "mypass", + DatabaseName: "admin", + Roles: []mongodbatlas.Role{ + { + RoleName: "readAnyDatabase", + DatabaseName: "admin", + }, + }, + Scopes: []mongodbatlas.Scope{}, + } + _, _, err := atlasClient.DatabaseUsers.Create(context.TODO(), testProject.ID(), dbUser) + Expect(err).To(BeNil()) + }) + }) + + Describe("Operator is running with deletion protection enabled", func() { + It("Adds database users and protect them to be deleted when operator doesn't own resource", func() { + By("First without setting atlas-resource-policy annotation", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + // nolint:dupl + By("Second setting atlas-resource-policy annotation to delete", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret2, DBUserPassword2) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser2 = mdbv1.NewDBUser(testNamespace.Name, dbUserName2, dbUserName2, projectName). + WithPasswordSecret(UserPasswordSecret2). + WithRole("readWriteAnyDatabase", "admin", "") + testDBUser2.SetAnnotations(map[string]string{customresource.ResourcePolicyAnnotation: customresource.ResourcePolicyDelete}) + Expect(k8sClient.Create(context.TODO(), testDBUser2)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser2, status.TrueCondition(status.ReadyType)) + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser2) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser2)).Should(Succeed()) + }) + + By("Third previously added in Atlas", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, "third-pass-secret", "mypass") + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser3 = mdbv1.NewDBUser(testNamespace.Name, dbUserName3, dbUserName3, projectName). + WithPasswordSecret("third-pass-secret"). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser3)).To(Succeed()) + + Eventually(func(g Gomega) bool { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.FalseCondition(status.ReadyType), + status.FalseCondition(status.DatabaseUserReadyType). + WithReason(string(workflow.AtlasDeletionProtection)). + WithMessageRegexp("unable to reconcile database user: it already exists in Atlas, it was not previously managed by the operator, and the deletion protection is enabled."), + ) + + g.Expect(k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(testDBUser3), testDBUser3, &client.GetOptions{})) + g.Expect(testDBUser3.Status.Conditions).To(ContainElements(expectedConditions)) + + return true + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Deleting AtlasDatabaseUser custom resource", func() { + By("Keeping database user 1 in Atlas", func() { + Expect(k8sClient.Delete(context.TODO(), testDBUser1)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName1), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser1)). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeFalse()) + }) + + By("Deleting database user 2 in Atlas", func() { + Expect(k8sClient.Delete(context.TODO(), testDBUser2)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName2), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser2)). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Keeping database user 3 in Atlas", func() { + Expect(k8sClient.Delete(context.TODO(), testDBUser3)).To(Succeed()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser3)). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeFalse()) + }) + }) + }) + + It("Adds database users and manage them when operator take ownership of existing resources", func() { + By("First without setting atlas-resource-policy annotation", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + // nolint:dupl + By("Second setting atlas-resource-policy annotation to delete", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret2, DBUserPassword2) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser2 = mdbv1.NewDBUser(testNamespace.Name, dbUserName2, dbUserName2, projectName). + WithPasswordSecret(UserPasswordSecret2). + WithRole("readWriteAnyDatabase", "admin", "") + testDBUser2.SetAnnotations(map[string]string{customresource.ResourcePolicyAnnotation: customresource.ResourcePolicyDelete}) + Expect(k8sClient.Create(context.TODO(), testDBUser2)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser2, status.TrueCondition(status.ReadyType)) + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser2) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser2)).Should(Succeed()) + }) + + By("Third previously added in Atlas", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, "third-pass-secret", "mypass") + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser3 = mdbv1.NewDBUser(testNamespace.Name, dbUserName3, dbUserName3, projectName). + WithPasswordSecret("third-pass-secret"). + WithRole("readAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser3)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser3, status.TrueCondition(status.ReadyType)) + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser3) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser3)).Should(Succeed()) + }) + + By("Deleting AtlasDatabaseUser custom resource", func() { + By("Keeping database user 1 in Atlas", func() { + Expect(k8sClient.Delete(context.TODO(), testDBUser1)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName1), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser1)). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeFalse()) + }) + + By("Deleting database user 2 in Atlas", func() { + Expect(k8sClient.Delete(context.TODO(), testDBUser2)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName2), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser2)). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Keeping database user 3 in Atlas", func() { + Expect(k8sClient.Delete(context.TODO(), testDBUser3)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName3), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser3)). + WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeFalse()) + }) + }) + }) + }) + + // nolint:dupl + AfterEach(func() { + By("Deleting deployment", func() { + deploymentName := testDeployment.GetDeploymentName() + Expect(k8sClient.Delete(context.TODO(), testDeployment)).To(Succeed()) + + Eventually(func() bool { + _, r, err := atlasClient.AdvancedClusters.Get(context.TODO(), testProject.ID(), deploymentName) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Deleting project", func() { + projectID := testProject.ID() + Expect(k8sClient.Delete(context.TODO(), testProject)).To(Succeed()) + + Eventually(func() bool { + _, r, err := atlasClient.Projects.GetOneProject(context.TODO(), projectID) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + }).WithTimeout(15 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Stopping the operator", func() { + stopManager() + + By("Removing the namespace " + testNamespace.Name) + err := k8sClient.Delete(context.Background(), testNamespace) + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) diff --git a/test/int/databaseuser_unprotected_test.go b/test/int/databaseuser_unprotected_test.go new file mode 100644 index 0000000000..adcde7b88f --- /dev/null +++ b/test/int/databaseuser_unprotected_test.go @@ -0,0 +1,831 @@ +package int + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/timeutil" + + corev1 "k8s.io/api/core/v1" + + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" + + "go.mongodb.org/atlas/mongodbatlas" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" + + 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" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/testutil" +) + +const ( + databaseUserTimeout = 10 * time.Minute + + UserPasswordSecret = "user-password-secret" + DBUserPassword = "Passw0rd!" + UserPasswordSecret2 = "second-user-password-secret" + DBUserPassword2 = "H@lla#!" +) + +var _ = Describe("Atlas Database User", Label("int", "AtlasDatabaseUser", "protection-disabled"), func() { + var testNamespace *corev1.Namespace + var stopManager context.CancelFunc + var projectName string + projectNamePrefix := "database-user-unprotected" + dbUserName1 := "db-user1" + dbUserName2 := "db-user2" + dbUserName3 := "db-user3" + testProject := &mdbv1.AtlasProject{} + testDeployment := &mdbv1.AtlasDeployment{} + testDBUser1 := &mdbv1.AtlasDatabaseUser{} + testDBUser2 := &mdbv1.AtlasDatabaseUser{} + testDBUser3 := &mdbv1.AtlasDatabaseUser{} + + BeforeEach(func() { + testNamespace, stopManager = prepareControllers(false) + projectName = fmt.Sprintf("%s-%s", projectNamePrefix, testNamespace.Name) + + By("Creating a project", func() { + connSecret := buildConnectionSecret("my-atlas-key") + Expect(k8sClient.Create(context.TODO(), &connSecret)).To(Succeed()) + + testProject = mdbv1.NewProject(testNamespace.Name, projectName, projectName). + WithConnectionSecret(connSecret.Name). + WithIPAccessList(project.NewIPAccessList().WithCIDR("0.0.0.0/0")) + Expect(k8sClient.Create(context.TODO(), testProject, &client.CreateOptions{})).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testProject, status.TrueCondition(status.ReadyType)) + }).WithTimeout(15 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Creating a deployment", func() { + testDeployment = mdbv1.DefaultAWSDeployment(testNamespace.Name, projectName).Lightweight() + Expect(k8sClient.Create(context.TODO(), testDeployment)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDeployment, status.TrueCondition(status.ReadyType)) + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + }) + + Describe("Operator is running with deletion protection disabled", func() { + It("Adds database users and allow them to be deleted", func() { + By("Creating a database user previously on Atlas", func() { + dbUser := &mongodbatlas.DatabaseUser{ + Username: dbUserName3, + Password: "mypass", + DatabaseName: "admin", + Roles: []mongodbatlas.Role{ + { + RoleName: "readWriteAnyDatabase", + DatabaseName: "admin", + }, + }, + Scopes: []mongodbatlas.Scope{}, + } + _, _, err := atlasClient.DatabaseUsers.Create(context.TODO(), testProject.ID(), dbUser) + Expect(err).To(BeNil()) + }) + + By("First without setting atlas-resource-policy annotation", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Second setting atlas-resource-policy annotation to keep", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret2, DBUserPassword2) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser2 = mdbv1.NewDBUser(testNamespace.Name, dbUserName2, dbUserName2, projectName). + WithPasswordSecret(UserPasswordSecret2). + WithRole("readWriteAnyDatabase", "admin", "") + testDBUser2.SetAnnotations(map[string]string{customresource.ResourcePolicyAnnotation: customresource.ResourcePolicyKeep}) + Expect(k8sClient.Create(context.TODO(), testDBUser2)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser2, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser2) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser2)).Should(Succeed()) + }) + + By("Third previously added in Atlas", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, "third-pass-secret", "mypass") + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser3 = mdbv1.NewDBUser(testNamespace.Name, dbUserName3, dbUserName3, projectName). + WithPasswordSecret("third-pass-secret"). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser3)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser3, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser3) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser3)).Should(Succeed()) + }) + + By("Deleting AtlasDatabaseUser custom resource", func() { + By("Deleting database user 1 in Atlas", func() { + deleteSecret(testDBUser1) + Expect(k8sClient.Delete(context.TODO(), testDBUser1)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName1), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser1)). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Keeping database user 2 in Atlas", func() { + deleteSecret(testDBUser2) + Expect(k8sClient.Delete(context.TODO(), testDBUser2)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName2), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser2)). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeFalse()) + + _, err := atlasClient.DatabaseUsers.Delete(context.TODO(), "admin", testProject.ID(), dbUserName2) + Expect(err).To(BeNil()) + }) + + By("Deleting database user 3 in Atlas", func() { + deleteSecret(testDBUser3) + Expect(k8sClient.Delete(context.TODO(), testDBUser3)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName3), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser3)). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + }) + }) + }) + + It("Adds an user and manage roles", func() { + By("Creating an user with clusterMonitor role", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("clusterMonitor", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Validating credentials and cluster access", func() { + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + + err := tryWrite(testProject.ID(), *testDeployment, *testDBUser1, "test", "operatortest") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(MatchRegexp("user is not allowed")) + }) + + By("Giving user readWrite permissions", func() { + // Adding the role allowing read/write + testDBUser1 = testDBUser1.WithRole("readWriteAnyDatabase", "admin", "") + + Expect(k8sClient.Update(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Validating user has permission to write", func() { + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + + Expect(tryWrite(testProject.ID(), *testDeployment, *testDBUser1, "test", "operatortest")).To(Succeed()) + }) + + By("Deleting database user", func() { + deleteSecret(testDBUser1) + Expect(k8sClient.Delete(context.TODO(), testDBUser1)).To(Succeed()) + + secretName := fmt.Sprintf( + "%s-%s-%s", + kube.NormalizeIdentifier(projectName), + kube.NormalizeIdentifier(testDeployment.GetDeploymentName()), + kube.NormalizeIdentifier(dbUserName1), + ) + Eventually(checkSecretsDontExist(testProject.ID(), []string{secretName})). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + Eventually(checkAtlasDatabaseUserRemoved(testProject.ID(), *testDBUser1)). + WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + }) + }) + + It("Adds connection secret when new deployment is created", func() { + secondDeployment := &mdbv1.AtlasDeployment{} + + By("Creating a database user", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Creating a second deployment", func() { + secondDeployment = mdbv1.DefaultAzureDeployment(testNamespace.Name, projectName).Lightweight() + Expect(k8sClient.Create(context.TODO(), secondDeployment)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, secondDeployment, status.TrueCondition(status.ReadyType)) + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Validating connection secrets were created", func() { + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + validateSecret(k8sClient, *testProject, *secondDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + Expect(tryConnect(testProject.ID(), *secondDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Deleting the second deployment", func() { + deploymentName := secondDeployment.GetDeploymentName() + Expect(k8sClient.Delete(context.TODO(), secondDeployment)).To(Succeed()) + + Eventually(func() bool { + _, r, err := atlasClient.AdvancedClusters.Get(context.TODO(), testProject.ID(), deploymentName) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + }) + + It("Watches password secret", func() { + By("Creating a database user", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Breaking the password secret", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, "") + Expect(k8sClient.Update(context.TODO(), &passwordSecret)).To(Succeed()) + + expectedCondition := status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.Internal)).WithMessageRegexp("the 'password' field is empty") + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, expectedCondition) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + testutil.EventExists(k8sClient, testDBUser1, "Warning", string(workflow.Internal), "the 'password' field is empty") + }) + + By("Fixing the password secret", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, "someNewPassw00rd") + Expect(k8sClient.Update(context.TODO(), &passwordSecret)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + // We need to make sure that the new connection secret is different from the initial one + connSecretUpdated := validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + Expect(string(connSecretUpdated.Data["password"])).To(Equal("someNewPassw00rd")) + + var updatedPwdSecret corev1.Secret + Expect(k8sClient.Get(context.TODO(), kube.ObjectKey(testNamespace.Name, UserPasswordSecret), &updatedPwdSecret)).To(Succeed()) + Expect(testDBUser1.Status.PasswordVersion).To(Equal(updatedPwdSecret.ResourceVersion)) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + }) + + It("Remove stale secrets", func() { + secondTestDeployment := &mdbv1.AtlasDeployment{} + + By("Creating a second deployment", func() { + secondTestDeployment = mdbv1.DefaultAzureDeployment(testNamespace.Name, projectName).Lightweight() + Expect(k8sClient.Create(context.TODO(), secondTestDeployment)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, secondTestDeployment, status.TrueCondition(status.ReadyType)) + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Creating a database user", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + validateSecret(k8sClient, *testProject, *secondTestDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + Expect(tryConnect(testProject.ID(), *secondTestDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Renaming username, new user is added and stale secrets are removed", func() { + oldName := testDBUser1.Spec.Username + testDBUser1 = testDBUser1.WithAtlasUserName("new-user") + Expect(k8sClient.Update(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + _, _, err := atlasClient.DatabaseUsers.Get(context.TODO(), testDBUser1.Spec.DatabaseName, testProject.ID(), oldName) + Expect(err).To(HaveOccurred()) + + checkNumberOfConnectionSecrets(k8sClient, *testProject, testNamespace.Name, 2) + secret := validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + Expect(secret.Name).To(Equal(fmt.Sprintf("%s-test-deployment-aws-new-user", kube.NormalizeIdentifier(testProject.Spec.Name)))) + secret = validateSecret(k8sClient, *testProject, *secondTestDeployment, *testDBUser1) + Expect(secret.Name).To(Equal(fmt.Sprintf("%s-test-deployment-azure-new-user", kube.NormalizeIdentifier(testProject.Spec.Name)))) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + Expect(tryConnect(testProject.ID(), *secondTestDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Scoping user to one cluster, a stale secret is removed", func() { + testDBUser1 = testDBUser1.ClearScopes().WithScope(mdbv1.DeploymentScopeType, testDeployment.GetDeploymentName()) + Expect(k8sClient.Update(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + checkNumberOfConnectionSecrets(k8sClient, *testProject, testNamespace.Name, 1) + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + Expect(tryConnect(testProject.ID(), *secondTestDeployment, *testDBUser1)).ShouldNot(Succeed()) + }) + + By("Deleting second deployment", func() { + deploymentName := secondTestDeployment.GetDeploymentName() + Expect(k8sClient.Delete(context.TODO(), secondTestDeployment)).To(Succeed()) + + Eventually(func() bool { + _, r, err := atlasClient.AdvancedClusters.Get(context.TODO(), testProject.ID(), deploymentName) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + }) + + It("Validates user date expiration", func() { + By("Creating expired user", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + before := time.Now().UTC().Add(time.Minute * -10).Format("2006-01-02T15:04:05") + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", ""). + WithDeleteAfterDate(before) + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserExpired))) + }).WithTimeout(10 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + + checkNumberOfConnectionSecrets(k8sClient, *testProject, testNamespace.Name, 0) + + _, _, err := atlasClient.DatabaseUsers.Get(context.TODO(), testDBUser1.Spec.DatabaseName, testProject.ID(), testDBUser1.Spec.Username) + Expect(err).To(HaveOccurred()) + }) + + By("Fixing the user date expiration", func() { + after := time.Now().UTC().Add(time.Hour * 10).Format("2006-01-02T15:04:05") + testDBUser1 = testDBUser1.WithDeleteAfterDate(after) + + Expect(k8sClient.Update(context.TODO(), testDBUser1)).To(Succeed()) + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + checkNumberOfConnectionSecrets(k8sClient, *testProject, testNamespace.Name, 1) + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Expiring the User", func() { + before := time.Now().UTC().Add(time.Minute * -5).Format("2006-01-02T15:04:05") + testDBUser1 = testDBUser1.WithDeleteAfterDate(before) + + Expect(k8sClient.Update(context.TODO(), testDBUser1)).To(Succeed()) + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserExpired))) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + expectedConditionsMatchers := testutil.MatchConditions( + status.FalseCondition(status.DatabaseUserReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ResourceVersionStatus), + ) + Expect(testDBUser1.Status.Conditions).To(ConsistOf(expectedConditionsMatchers)) + + checkNumberOfConnectionSecrets(k8sClient, *testProject, testNamespace.Name, 0) + }) + }) + + It("Skips reconciliations.", func() { + By("Creating a database user", func() { + passwordSecret := buildPasswordSecret(testNamespace.Name, UserPasswordSecret, DBUserPassword) + Expect(k8sClient.Create(context.TODO(), &passwordSecret)).To(Succeed()) + + testDBUser1 = mdbv1.NewDBUser(testNamespace.Name, dbUserName1, dbUserName1, projectName). + WithPasswordSecret(UserPasswordSecret). + WithRole("readWriteAnyDatabase", "admin", "") + Expect(k8sClient.Create(context.TODO(), testDBUser1)).To(Succeed()) + + Eventually(func() bool { + return testutil.CheckCondition(k8sClient, testDBUser1, status.TrueCondition(status.ReadyType)) + }).WithTimeout(databaseUserTimeout).WithPolling(PollingInterval).Should(BeTrue()) + + validateSecret(k8sClient, *testProject, *testDeployment, *testDBUser1) + + Expect(tryConnect(testProject.ID(), *testDeployment, *testDBUser1)).Should(Succeed()) + }) + + By("Skipping reconciliation", func() { + testDBUser1.ObjectMeta.Annotations = map[string]string{customresource.ReconciliationPolicyAnnotation: customresource.ReconciliationPolicySkip} + testDBUser1.Spec.Roles = append(testDBUser1.Spec.Roles, mdbv1.RoleSpec{ + RoleName: "new-role", + DatabaseName: "new-database", + CollectionName: "new-collection", + }) + + Expect(k8sClient.Update(context.TODO(), testDBUser1)).To(Succeed()) + + ctx, cancel := context.WithTimeout(context.TODO(), time.Minute*2) + defer cancel() + containsDatabaseUser := func(dbUser *mongodbatlas.DatabaseUser) bool { + for _, role := range dbUser.Roles { + if role.RoleName == "new-role" && role.DatabaseName == "new-database" && role.CollectionName == "new-collection" { + return true + } + } + return false + } + + Eventually(testutil.WaitForAtlasDatabaseUserStateToNotBeReached(ctx, atlasClient, "admin", testProject.Name, testDeployment.GetDeploymentName(), containsDatabaseUser)) + }) + }) + }) + + // nolint:dupl + AfterEach(func() { + By("Deleting the deployment", func() { + deploymentName := testDeployment.GetDeploymentName() + Expect(k8sClient.Delete(context.TODO(), testDeployment)).To(Succeed()) + + Eventually(func() bool { + _, r, err := atlasClient.AdvancedClusters.Get(context.TODO(), testProject.ID(), deploymentName) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + }).WithTimeout(20 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Deleting the project", func() { + projectID := testProject.ID() + Expect(k8sClient.Delete(context.TODO(), testProject)).To(Succeed()) + + Eventually(func() bool { + _, r, err := atlasClient.Projects.GetOneProject(context.TODO(), projectID) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + }).WithTimeout(15 * time.Minute).WithPolling(PollingInterval).Should(BeTrue()) + }) + + By("Stopping the operator", func() { + stopManager() + + By("Removing the namespace " + testNamespace.Name) + err := k8sClient.Delete(context.TODO(), testNamespace) + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) + +func buildPasswordSecret(namespace, name, password string) corev1.Secret { + return corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + connectionsecret.TypeLabelKey: connectionsecret.CredLabelVal, + }, + }, + StringData: map[string]string{"password": password}, + } +} + +func validateSecret(k8sClient client.Client, project mdbv1.AtlasProject, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser) corev1.Secret { + secret := corev1.Secret{} + username := user.Spec.Username + secretName := fmt.Sprintf("%s-%s-%s", kube.NormalizeIdentifier(project.Spec.Name), kube.NormalizeIdentifier(deployment.GetDeploymentName()), kube.NormalizeIdentifier(username)) + Expect(k8sClient.Get(context.TODO(), kube.ObjectKey(project.Namespace, secretName), &secret)).To(Succeed()) + + password, err := user.ReadPassword(k8sClient) + Expect(err).NotTo(HaveOccurred()) + + c, _, err := atlasClient.AdvancedClusters.Get(context.TODO(), project.ID(), deployment.GetDeploymentName()) + Expect(err).NotTo(HaveOccurred()) + + expectedData := map[string][]byte{ + "connectionStringStandard": []byte(buildConnectionURL(c.ConnectionStrings.Standard, username, password)), + "connectionStringStandardSrv": []byte(buildConnectionURL(c.ConnectionStrings.StandardSrv, username, password)), + "connectionStringPrivate": []byte(buildConnectionURL(c.ConnectionStrings.Private, username, password)), + "connectionStringPrivateSrv": []byte(buildConnectionURL(c.ConnectionStrings.PrivateSrv, username, password)), + "username": []byte(username), + "password": []byte(password), + } + expectedLabels := map[string]string{ + "atlas.mongodb.com/project-id": project.ID(), + "atlas.mongodb.com/cluster-name": deployment.GetDeploymentName(), + connectionsecret.TypeLabelKey: connectionsecret.CredLabelVal, + } + Expect(secret.Data).To(Equal(expectedData)) + Expect(secret.Labels).To(Equal(expectedLabels)) + + return secret +} + +func checkNumberOfConnectionSecrets(k8sClient client.Client, project mdbv1.AtlasProject, namespace string, length int) { + secretList := corev1.SecretList{} + Expect(k8sClient.List(context.TODO(), &secretList, client.InNamespace(namespace))).To(Succeed()) + + names := make([]string, 0) + for _, item := range secretList.Items { + if strings.HasPrefix(item.Name, kube.NormalizeIdentifier(project.Spec.Name)) { + names = append(names, item.Name) + } + } + Expect(names).To(HaveLen(length), fmt.Sprintf("Expected %d items, but found %d (%v)", length, len(names), names)) +} + +func buildConnectionURL(connURL, userName, password string) string { + if connURL == "" { + return "" + } + + u, err := connectionsecret.AddCredentialsToConnectionURL(connURL, userName, password) + Expect(err).NotTo(HaveOccurred()) + return u +} + +func mongoClient(projectID string, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser) (*mongo.Client, error) { + ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) + defer cancel() + c, _, err := atlasClient.AdvancedClusters.Get(context.TODO(), projectID, deployment.GetDeploymentName()) + Expect(err).NotTo(HaveOccurred()) + + if c.ConnectionStrings == nil { + return nil, errors.New("connection strings are not provided") + } + + cs, err := url.Parse(c.ConnectionStrings.StandardSrv) + Expect(err).NotTo(HaveOccurred()) + + password, err := user.ReadPassword(k8sClient) + Expect(err).NotTo(HaveOccurred()) + cs.User = url.UserPassword(user.Spec.Username, password) + + dbClient, err := mongo.Connect(ctx, options.Client().ApplyURI(cs.String())) + if err != nil { + return nil, err + } + err = dbClient.Ping(context.TODO(), nil) + + return dbClient, err +} + +func tryConnect(projectID string, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser) error { + _, err := mongoClient(projectID, deployment, user) + return err +} + +func tryWrite(projectID string, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser, dbName, collectionName string) error { + dbClient, err := mongoClient(projectID, deployment, user) + Expect(err).NotTo(HaveOccurred()) + defer func() { + if err = dbClient.Disconnect(context.Background()); err != nil { + panic(err) + } + }() + + collection := dbClient.Database(dbName).Collection(collectionName) + + type Person struct { + Name string `json:"name"` + Age int `json:"age"` + } + + p := Person{ + Name: "Patrick", + Age: 32, + } + + _, err = collection.InsertOne(context.TODO(), p) + if err != nil { + return err + } + filter := bson.D{{Key: "name", Value: "Patrick"}} + + var s Person + + err = collection.FindOne(context.TODO(), filter).Decode(&s) + Expect(err).NotTo(HaveOccurred()) + // Shouldn't return the error - by this step the roles should be propagated + Expect(s).To(Equal(p)) + return nil +} + +func checkAtlasDatabaseUserRemoved(projectID string, user mdbv1.AtlasDatabaseUser) func() bool { + return func() bool { + _, r, err := atlasClient.DatabaseUsers.Get(context.TODO(), user.Spec.DatabaseName, projectID, user.Spec.Username) + if err != nil { + if r != nil && r.StatusCode == http.StatusNotFound { + return true + } + } + + return false + } +} + +func checkSecretsDontExist(namespace string, secretNames []string) func() bool { + return func() bool { + nonExisting := 0 + for _, name := range secretNames { + s := corev1.Secret{} + err := k8sClient.Get(context.TODO(), kube.ObjectKey(namespace, name), &s) + if err != nil && apiErrors.IsNotFound(err) { + nonExisting++ + } + } + return nonExisting == len(secretNames) + } +} + +func checkUserInAtlas(projectID string, user mdbv1.AtlasDatabaseUser) { + By("Verifying Database User state in Atlas", func() { + atlasDBUser, _, err := atlasClient.DatabaseUsers.Get(context.TODO(), user.Spec.DatabaseName, projectID, user.Spec.Username) + Expect(err).ToNot(HaveOccurred()) + operatorDBUser, err := user.ToAtlas(k8sClient) + Expect(err).ToNot(HaveOccurred()) + + Expect(*atlasDBUser).To(Equal(normalize(*operatorDBUser, projectID))) + }) +} + +// normalize brings the operator 'user' to the user returned by Atlas that allows to perform comparison for equality +func normalize(user mongodbatlas.DatabaseUser, projectID string) mongodbatlas.DatabaseUser { + if user.Scopes == nil { + user.Scopes = []mongodbatlas.Scope{} + } + if user.Labels == nil { + user.Labels = []mongodbatlas.Label{} + } + if user.LDAPAuthType == "" { + user.LDAPAuthType = "NONE" + } + if user.AWSIAMType == "" { + user.AWSIAMType = "NONE" + } + if user.X509Type == "" { + user.X509Type = "NONE" + } + if user.DeleteAfterDate != "" { + user.DeleteAfterDate = timeutil.FormatISO8601(timeutil.MustParseISO8601(user.DeleteAfterDate)) + } + user.GroupID = projectID + user.Password = "" + return user +} + +func deleteSecret(user *mdbv1.AtlasDatabaseUser) { + secret := &corev1.Secret{} + Expect( + k8sClient.Get( + context.TODO(), + client.ObjectKey{Namespace: user.Namespace, Name: user.Spec.PasswordSecret.Name}, + secret, + &client.GetOptions{}, + ), + ).To(Succeed()) + + Expect(k8sClient.Delete(context.TODO(), secret)).To(Succeed()) +} diff --git a/test/int/datafederation_test.go b/test/int/datafederation_test.go index 99f4edaa8d..35929de4d4 100644 --- a/test/int/datafederation_test.go +++ b/test/int/datafederation_test.go @@ -38,7 +38,7 @@ var _ = Describe("AtlasDataFederation", Label("AtlasDataFederation"), func() { ) BeforeEach(func() { - prepareControllers() + prepareControllers(false) manualDeletion = false diff --git a/test/int/dbuser_test.go b/test/int/dbuser_test.go deleted file mode 100644 index e94c20bb4c..0000000000 --- a/test/int/dbuser_test.go +++ /dev/null @@ -1,851 +0,0 @@ -package int - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "go.mongodb.org/atlas/mongodbatlas" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - corev1 "k8s.io/api/core/v1" - apiErrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - 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" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/customresource" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/workflow" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/kube" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/testutil" - "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/timeutil" -) - -const ( - DevMode = false - UserPasswordSecret = "user-password-secret" - DBUserPassword = "Passw0rd!" - UserPasswordSecret2 = "second-user-password-secret" - DBUserPassword2 = "H@lla#!" - // M2 deployments take longer time to apply changes - DBUserUpdateTimeout = time.Minute * 4 -) - -var _ = Describe("AtlasDatabaseUser", Label("int", "AtlasDatabaseUser"), func() { - const ( - interval = PollingInterval - intervalShort = time.Second * 2 - ) - - var ( - connectionSecret corev1.Secret - createdProject *mdbv1.AtlasProject - createdDeploymentAWS *mdbv1.AtlasDeployment - createdDeploymentGCP *mdbv1.AtlasDeployment - createdDeploymentAzure *mdbv1.AtlasDeployment - createdDBUser *mdbv1.AtlasDatabaseUser - secondDBUser *mdbv1.AtlasDatabaseUser - ) - - BeforeEach(func() { - prepareControllers() - createdDBUser = &mdbv1.AtlasDatabaseUser{} - - connectionSecret = buildConnectionSecret("my-atlas-key") - Expect(k8sClient.Create(context.Background(), &connectionSecret)).To(Succeed()) - - By(fmt.Sprintf("Creating password Secret %s", UserPasswordSecret)) - passwordSecret := buildPasswordSecret(UserPasswordSecret, DBUserPassword) - Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) - - By(fmt.Sprintf("Creating password Secret %s", UserPasswordSecret2)) - passwordSecret2 := buildPasswordSecret(UserPasswordSecret2, DBUserPassword2) - Expect(k8sClient.Create(context.Background(), &passwordSecret2)).To(Succeed()) - - By("Creating the project", func() { - // adding whitespace to the name to check normalization for connection secrets names - createdProject = mdbv1.DefaultProject(namespace.Name, connectionSecret.Name). - WithAtlasName(namespace.Name + " some"). - WithIPAccessList(project.NewIPAccessList().WithCIDR("0.0.0.0/0")) - if DevMode { - // While developing tests we need to reuse the same project - createdProject.Spec.Name = "dev-test atlas-project" - } - - Expect(k8sClient.Create(context.Background(), createdProject)).To(Succeed()) - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdProject, status.TrueCondition(status.ReadyType)) - }).WithTimeout(ProjectCreationTimeout).WithPolling(interval).Should(BeTrue()) - }) - }) - - AfterEach(func() { - if DevMode { - // No tearDown in dev mode - projects and both deployments will stay in Atlas so it's easier to develop - // tests. Just rerun the test and the project + deployments in Atlas will be reused. - // We only need to wipe data in the databases. - if createdDeploymentAWS != nil { - dbClient, err := mongoClient(createdProject.ID(), *createdDeploymentAWS, *createdDBUser) - if err == nil { - _ = dbClient.Database("test").Collection("operatortest").Drop(context.Background()) - } - } - if createdDeploymentGCP != nil { - dbClient, err := mongoClient(createdProject.ID(), *createdDeploymentGCP, *createdDBUser) - if err == nil { - _ = dbClient.Database("test").Collection("operatortest").Drop(context.Background()) - } - } - if createdDeploymentAzure != nil { - dbClient, err := mongoClient(createdProject.ID(), *createdDeploymentAzure, *createdDBUser) - if err == nil { - _ = dbClient.Database("test").Collection("operatortest").Drop(context.Background()) - } - } - - Expect(k8sClient.Delete(context.Background(), createdDBUser)).To(Succeed()) - Eventually(checkAtlasDatabaseUserRemoved(createdProject.ID(), *createdDBUser), 20, interval).Should(BeTrue()) - if secondDBUser != nil { - Expect(k8sClient.Delete(context.Background(), secondDBUser)).To(Succeed()) - Eventually(checkAtlasDatabaseUserRemoved(createdProject.ID(), *secondDBUser), 20, interval).Should(BeTrue()) - } - return - } - - if createdProject != nil && createdProject.ID() != "" { - list := mdbv1.AtlasDeploymentList{} - Expect(k8sClient.List(context.Background(), &list, client.InNamespace(namespace.Name))).To(Succeed()) - - for i := range list.Items { - By("Removing Atlas Deployment " + list.Items[i].Name) - Expect(k8sClient.Delete(context.Background(), &list.Items[i])).To(Succeed()) - } - for i := range list.Items { - Eventually(checkAtlasDeploymentRemoved(createdProject.ID(), list.Items[i].GetDeploymentName()), 600, interval).Should(BeTrue()) - } - - By("Removing Atlas Project " + createdProject.Status.ID) - Expect(k8sClient.Delete(context.Background(), createdProject)).To(Succeed()) - Eventually(checkAtlasProjectRemoved(createdProject.Status.ID), 60, interval).Should(BeTrue()) - } - removeControllersAndNamespace() - }) - - connSecretname := func(suffix string) string { - return kube.NormalizeIdentifier(createdProject.Spec.Name) + suffix - } - - byCreatingDefaultAWSandAzureDeployments := func() { - By("Creating deployments", func() { - createdDeploymentAWS = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAWS)).ToNot(HaveOccurred()) - - createdDeploymentAzure = mdbv1.DefaultAzureDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAzure)).ToNot(HaveOccurred()) - - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAWS, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(interval).Should(BeTrue()) - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAzure, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - }) - } - - Describe("Create/Update two users, two deployments", func() { - It("They should be created successfully", func() { - byCreatingDefaultAWSandAzureDeployments() - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) - - By(fmt.Sprintf("Creating the Database User %s", kube.ObjectKeyFromObject(createdDBUser)), func() { - Expect(k8sClient.Create(context.Background(), createdDBUser)).ToNot(HaveOccurred()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - - Expect(tryConnect(createdProject.ID(), *createdDeploymentAzure, *createdDBUser)).Should(Succeed()) - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).Should(Succeed()) - By("Checking connection Secrets", func() { - validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *createdDBUser) - validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 2) - }) - By("Checking connectivity to Deployments", func() { - // The user created lacks read/write roles - err := tryWrite(createdProject.ID(), *createdDeploymentAzure, *createdDBUser, "test", "operatortest") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(MatchRegexp("user is not allowed")) - - err = tryWrite(createdProject.ID(), *createdDeploymentAWS, *createdDBUser, "test", "operatortest") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(MatchRegexp("user is not allowed")) - }) - }) - By("Update database user - give readWrite permissions", func() { - // Adding the role allowing read/write - createdDBUser = createdDBUser.WithRole("readWriteAnyDatabase", "admin", "") - - Expect(k8sClient.Update(context.Background(), createdDBUser)).ToNot(HaveOccurred()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - - By("Checking connection Secrets", func() { - validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *createdDBUser) - validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 2) - }) - - By("Checking write permissions for Deployments", func() { - Expect(tryWrite(createdProject.ID(), *createdDeploymentAzure, *createdDBUser, "test", "operatortest")).Should(Succeed()) - Expect(tryWrite(createdProject.ID(), *createdDeploymentAWS, *createdDBUser, "test", "operatortest")).Should(Succeed()) - }) - }) - By("Adding second user for Azure deployment only (fails, wrong scope)", func() { - secondDBUser = mdbv1.DefaultDBUser(namespace.Name, "second-db-user", createdProject.Name). - WithPasswordSecret(UserPasswordSecret2). - WithRole("readWrite", "someDB", "thisIsTheOnlyAllowedCollection"). - // Deployment doesn't exist - WithScope(mdbv1.DeploymentScopeType, createdDeploymentAzure.GetDeploymentName()+"-foo") - - Expect(k8sClient.Create(context.Background(), secondDBUser)).ToNot(HaveOccurred()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, secondDBUser, - status. - FalseCondition(status.DatabaseUserReadyType). - WithReason(string(workflow.DatabaseUserInvalidSpec)). - WithMessageRegexp("such deployment doesn't exist in Atlas")) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(intervalShort).Should(BeTrue()) - }) - By("Fixing second user", func() { - secondDBUser = secondDBUser.ClearScopes().WithScope(mdbv1.DeploymentScopeType, createdDeploymentAzure.GetDeploymentName()) - - Expect(k8sClient.Update(context.Background(), secondDBUser)).ToNot(HaveOccurred()) - - // First we need to wait for "such deployment doesn't exist in Atlas" error to be gone - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, secondDBUser, - status.FalseCondition(status.DatabaseUserReadyType). - WithReason(string(workflow.DatabaseUserDeploymentAppliedChanges))) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(intervalShort).Should(BeTrue()) - - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, secondDBUser, - status.TrueCondition(status.ReadyType), validateDatabaseUserUpdatingFunc(g)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *secondDBUser) - - By("Checking connection Secrets", func() { - validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *createdDBUser) - validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *secondDBUser) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 3) - }) - - By("Checking write permissions for Deployments", func() { - // We still can write by the first user - Expect(tryWrite(createdProject.ID(), *createdDeploymentAzure, *createdDBUser, "test", "testCollection")).Should(Succeed()) - Expect(tryWrite(createdProject.ID(), *createdDeploymentAWS, *createdDBUser, "test", "testCollection")).Should(Succeed()) - - // The second user can eventually write to one collection only - Expect(tryConnect(createdProject.ID(), *createdDeploymentAzure, *secondDBUser)).Should(Succeed()) - Expect(tryWrite(createdProject.ID(), *createdDeploymentAzure, *secondDBUser, "someDB", "thisIsTheOnlyAllowedCollection")).Should(Succeed()) - - err := tryWrite(createdProject.ID(), *createdDeploymentAzure, *secondDBUser, "test", "someNotAllowedCollection") - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(MatchRegexp("user is not allowed")) - }) - By("Removing Second user", func() { - Expect(k8sClient.Delete(context.Background(), secondDBUser)).To(Succeed()) - Eventually(checkAtlasDatabaseUserRemoved(createdProject.Status.ID, *secondDBUser), 50, interval).Should(BeTrue()) - - secretNames := []string{connSecretname("-test-deployment-azure-second-db-user")} - Eventually(checkSecretsDontExist(namespace.Name, secretNames), 50, interval).Should(BeTrue()) - }) - }) - By("Removing First user", func() { - Expect(k8sClient.Delete(context.Background(), createdDBUser)).To(Succeed()) - Eventually(checkAtlasDatabaseUserRemoved(createdProject.Status.ID, *createdDBUser), 50, interval).Should(BeTrue()) - - secretNames := []string{connSecretname("-test-deployment-aws-test-db-user"), connSecretname("-test-deployment-azure-test-db-user")} - Eventually(checkSecretsDontExist(namespace.Name, secretNames), 50, interval).Should(BeTrue()) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) - }) - }) - }) - - // Note, that this test doesn't work with "DevMode=true" as requires the deployment to get created - Describe("Check the reverse order of deployment-user creation (user - first, then - the deployment)", func() { - It("Should succeed", func() { - // Here we create a database user first - then the deployment - By("Creating database user", func() { - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) - - Expect(k8sClient.Create(context.Background(), createdDBUser)).To(Succeed()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) - }) - By("Creating deployment", func() { - createdDeploymentAWS = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAWS)).ToNot(HaveOccurred()) - - // We don't wait for the full deployment creation - only when it has started the process - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAWS, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(interval).Should(BeTrue()) - }) - By("Updating the database user while the deployment is being created", func() { - createdDBUser = createdDBUser.WithRole("read", "test", "somecollection") - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - - // DatabaseUser will wait for the deployment to get created. - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - expectedConditionsMatchers := testutil.MatchConditions( - status.TrueCondition(status.DatabaseUserReadyType), - status.TrueCondition(status.ReadyType), - status.TrueCondition(status.ValidationSucceeded), - status.TrueCondition(status.ResourceVersionStatus), - ) - Expect(createdDBUser.Status.Conditions).To(ConsistOf(expectedConditionsMatchers)) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).Should(Succeed()) - By("Checking connection Secrets", func() { - validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 1) - }) - }) - }) - }) - Describe("Check the password Secret is watched", func() { - It("Should succeed", func() { - By("Creating deployments", func() { - createdDeploymentAWS = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAWS)).ToNot(HaveOccurred()) - - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAWS, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - }) - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) - var connSecretInitial corev1.Secret - var pwdSecret corev1.Secret - - By(fmt.Sprintf("Creating the Database User %s", kube.ObjectKeyFromObject(createdDBUser)), func() { - Expect(k8sClient.Create(context.Background(), createdDBUser)).ToNot(HaveOccurred()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - testutil.EventExists(k8sClient, createdDBUser, "Normal", "Ready", "") - - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).Should(Succeed()) - - connSecretInitial = validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - Expect(k8sClient.Get(context.Background(), kube.ObjectKey(namespace.Name, UserPasswordSecret), &pwdSecret)).To(Succeed()) - Expect(createdDBUser.Status.PasswordVersion).To(Equal(pwdSecret.ResourceVersion)) - }) - - By("Breaking the password secret", func() { - passwordSecret := buildPasswordSecret(UserPasswordSecret, "") - Expect(k8sClient.Update(context.Background(), &passwordSecret)).To(Succeed()) - - expectedCondition := status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.Internal)).WithMessageRegexp("the 'password' field is empty") - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, expectedCondition) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - testutil.EventExists(k8sClient, createdDBUser, "Warning", string(workflow.Internal), "the 'password' field is empty") - }) - By("Fixing the password secret", func() { - passwordSecret := buildPasswordSecret(UserPasswordSecret, "someNewPassw00rd") - Expect(k8sClient.Update(context.Background(), &passwordSecret)).To(Succeed()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - // We need to make sure that the new connection secret is different from the initial one - connSecretUpdated := validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - Expect(string(connSecretInitial.Data["password"])).To(Equal(DBUserPassword)) - Expect(string(connSecretUpdated.Data["password"])).To(Equal("someNewPassw00rd")) - - var updatedPwdSecret corev1.Secret - Expect(k8sClient.Get(context.Background(), kube.ObjectKey(namespace.Name, UserPasswordSecret), &updatedPwdSecret)).To(Succeed()) - Expect(updatedPwdSecret.ResourceVersion).NotTo(Equal(pwdSecret.ResourceVersion)) - Expect(createdDBUser.Status.PasswordVersion).To(Equal(updatedPwdSecret.ResourceVersion)) - - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).Should(Succeed()) - }) - }) - }) - Describe("Change database users (make sure all stale secrets are removed)", func() { - It("Should succeed", func() { - byCreatingDefaultAWSandAzureDeployments() - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) - - By(fmt.Sprintf("Creating the Database User %s (no scopes)", kube.ObjectKeyFromObject(createdDBUser)), func() { - Expect(k8sClient.Create(context.Background(), createdDBUser)).To(Succeed()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 2) - - s1 := validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - s2 := validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *createdDBUser) - - testutil.EventExists(k8sClient, createdDBUser, "Normal", connectionsecret.ConnectionSecretsEnsuredEvent, - fmt.Sprintf("Connection Secrets were created/updated: (%s|%s|, ){3}", s1.Name, s2.Name)) - }) - By("Changing the db user name - two stale secret are expected to be removed, two added instead", func() { - oldName := createdDBUser.Spec.Username - createdDBUser = createdDBUser.WithAtlasUserName("new-user") - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - // Old user has been removed - _, _, err := atlasClient.DatabaseUsers.Get(context.Background(), createdDBUser.Spec.DatabaseName, createdProject.ID(), oldName) - Expect(err).To(HaveOccurred()) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 2) - secret := validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *createdDBUser) - Expect(secret.Name).To(Equal(connSecretname("-test-deployment-azure-new-user"))) - secret = validateSecret(k8sClient, *createdProject, *createdDeploymentAWS, *createdDBUser) - Expect(secret.Name).To(Equal(connSecretname("-test-deployment-aws-new-user"))) - - Expect(tryConnect(createdProject.ID(), *createdDeploymentAzure, *createdDBUser)).Should(Succeed()) - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).Should(Succeed()) - }) - By("Changing the scopes - one stale secret is expected to be removed", func() { - createdDBUser = createdDBUser.ClearScopes().WithScope(mdbv1.DeploymentScopeType, createdDeploymentAzure.GetDeploymentName()) - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 1) - validateSecret(k8sClient, *createdProject, *createdDeploymentAzure, *createdDBUser) - - Expect(tryConnect(createdProject.ID(), *createdDeploymentAzure, *createdDBUser)).Should(Succeed()) - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).ShouldNot(Succeed()) - }) - }) - }) - Describe("Check the user expiration", func() { - It("Should succeed", func() { - By("Creating a AWS deployment", func() { - createdDeploymentAWS = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAWS)).To(Succeed()) - - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAWS, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(intervalShort).Should(BeTrue()) - }) - - By("Creating the expired Database User - no user created in Atlas", func() { - before := time.Now().UTC().Add(time.Minute * -10).Format("2006-01-02T15:04:05") - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name). - WithPasswordSecret(UserPasswordSecret). - WithDeleteAfterDate(before) - - Expect(k8sClient.Create(context.Background(), createdDBUser)).To(Succeed()) - - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, - status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserExpired))) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(intervalShort).Should(BeTrue()) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) - - // no user in Atlas - _, _, err := atlasClient.DatabaseUsers.Get(context.Background(), createdDBUser.Spec.DatabaseName, createdProject.ID(), createdDBUser.Spec.Username) - Expect(err).To(HaveOccurred()) - }) - By("Fixing the Database User - setting the expiration to future", func() { - after := time.Now().UTC().Add(time.Hour * 10).Format("2006-01-02T15:04:05") - createdDBUser = createdDBUser.WithDeleteAfterDate(after) - - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 1) - Expect(tryConnect(createdProject.ID(), *createdDeploymentAWS, *createdDBUser)).Should(Succeed()) - }) - By("Extending the expiration", func() { - after := time.Now().UTC().Add(time.Hour * 30).Format("2006-01-02T15:04:05") - createdDBUser = createdDBUser.WithDeleteAfterDate(after) - - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - checkUserInAtlas(createdProject.ID(), *createdDBUser) - }) - By("Emulating expiration of the User - connection secret must be removed", func() { - before := time.Now().UTC().Add(time.Minute * -5).Format("2006-01-02T15:04:05") - createdDBUser = createdDBUser.WithDeleteAfterDate(before) - - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserExpired))) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(intervalShort).Should(BeTrue()) - - expectedConditionsMatchers := testutil.MatchConditions( - status.FalseCondition(status.DatabaseUserReadyType), - status.FalseCondition(status.ReadyType), - status.TrueCondition(status.ValidationSucceeded), - status.TrueCondition(status.ResourceVersionStatus), - ) - Expect(createdDBUser.Status.Conditions).To(ConsistOf(expectedConditionsMatchers)) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) - }) - }) - Describe("Deleting the db user (not cleaning Atlas)", func() { - It("Should Succeed", func() { - By(`Creating the db user with retention policy "keep" first`, func() { - createdDeploymentAWS = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAWS)).ToNot(HaveOccurred()) - - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAWS, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) - createdDBUser.ObjectMeta.Annotations = map[string]string{customresource.ResourcePolicyAnnotation: customresource.ResourcePolicyKeep} - Expect(k8sClient.Create(context.Background(), createdDBUser)).To(Succeed()) - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - }) - By("Deleting the db user - stays in Atlas", func() { - Expect(k8sClient.Delete(context.Background(), createdDBUser)).To(Succeed()) - - time.Sleep(1 * time.Minute) - Expect(checkAtlasDatabaseUserRemoved(createdProject.Status.ID, *createdDBUser)()).Should(BeFalse()) - - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) - }) - }) - }) - - Describe("Setting the user skip annotation should skip reconciliations.", func() { - It("Should Succeed", func() { - - By(`Creating the user with reconciliation policy "skip" first`, func() { - createdDeploymentAWS = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name).Lightweight() - Expect(k8sClient.Create(context.Background(), createdDeploymentAWS)).ToNot(HaveOccurred()) - Eventually(func(g Gomega) bool { - return testutil.CheckCondition(k8sClient, createdDeploymentAWS, status.TrueCondition(status.ReadyType), validateDeploymentCreatingFunc(g)) - }).WithTimeout(DeploymentUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - createdDBUser = mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) - - Expect(k8sClient.Create(context.Background(), createdDBUser)).To(Succeed()) - Eventually(func() bool { - return testutil.CheckCondition(k8sClient, createdDBUser, status.TrueCondition(status.ReadyType)) - }).WithTimeout(DBUserUpdateTimeout).WithPolling(interval).Should(BeTrue()) - - createdDBUser.ObjectMeta.Annotations = map[string]string{customresource.ReconciliationPolicyAnnotation: customresource.ReconciliationPolicySkip} - createdDBUser.Spec.Roles = append(createdDBUser.Spec.Roles, mdbv1.RoleSpec{ - RoleName: "new-role", - DatabaseName: "new-database", - CollectionName: "new-collection", - }) - - // add the annotation to skip reconciliation and a new role. This new role should not be seen in - // atlas. - Expect(k8sClient.Update(context.Background(), createdDBUser)).To(Succeed()) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*2) - defer cancel() - - containsDatabaseUser := func(dbUser *mongodbatlas.DatabaseUser) bool { - for _, role := range dbUser.Roles { - if role.RoleName == "new-role" && role.DatabaseName == "new-database" && role.CollectionName == "new-collection" { - return true - } - } - return false - } - - Eventually(testutil.WaitForAtlasDatabaseUserStateToNotBeReached(ctx, atlasClient, "admin", createdProject.Name, createdDeploymentAWS.GetDeploymentName(), containsDatabaseUser)) - }) - }) - }) - }) -}) - -func buildPasswordSecret(name, password string) corev1.Secret { - return corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace.Name, - Labels: map[string]string{ - connectionsecret.TypeLabelKey: connectionsecret.CredLabelVal, - }, - }, - StringData: map[string]string{"password": password}, - } -} - -// normalize brings the operator 'user' to the user returned by Atlas that allows to perform comparison for equality -func normalize(user mongodbatlas.DatabaseUser, projectID string) mongodbatlas.DatabaseUser { - if user.Scopes == nil { - user.Scopes = []mongodbatlas.Scope{} - } - if user.Labels == nil { - user.Labels = []mongodbatlas.Label{} - } - if user.LDAPAuthType == "" { - user.LDAPAuthType = "NONE" - } - if user.AWSIAMType == "" { - user.AWSIAMType = "NONE" - } - if user.X509Type == "" { - user.X509Type = "NONE" - } - if user.DeleteAfterDate != "" { - user.DeleteAfterDate = timeutil.FormatISO8601(timeutil.MustParseISO8601(user.DeleteAfterDate)) - } - user.GroupID = projectID - user.Password = "" - return user -} - -func tryConnect(projectID string, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser) error { - _, err := mongoClient(projectID, deployment, user) - return err -} - -func mongoClient(projectID string, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser) (*mongo.Client, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - c, _, err := atlasClient.AdvancedClusters.Get(context.Background(), projectID, deployment.GetDeploymentName()) - Expect(err).NotTo(HaveOccurred()) - - if c.ConnectionStrings == nil { - return nil, errors.New("Connection strings are not provided!") - } - - cs, err := url.Parse(c.ConnectionStrings.StandardSrv) - Expect(err).NotTo(HaveOccurred()) - - password, err := user.ReadPassword(k8sClient) - Expect(err).NotTo(HaveOccurred()) - cs.User = url.UserPassword(user.Spec.Username, password) - - dbClient, err := mongo.Connect(ctx, options.Client().ApplyURI(cs.String())) - if err != nil { - return nil, err - } - err = dbClient.Ping(context.TODO(), nil) - - return dbClient, err -} - -type Person struct { - Name string `json:"name"` - Age int `json:"age"` -} - -func tryWrite(projectID string, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser, dbName, collectionName string) error { - dbClient, err := mongoClient(projectID, deployment, user) - Expect(err).NotTo(HaveOccurred()) - defer func() { - if err = dbClient.Disconnect(context.Background()); err != nil { - panic(err) - } - }() - - collection := dbClient.Database(dbName).Collection(collectionName) - - p := Person{ - Name: "Patrick", - Age: 32, - } - - _, err = collection.InsertOne(context.Background(), p) - if err != nil { - return err - } - filter := bson.D{{Key: "name", Value: "Patrick"}} - - var s Person - - err = collection.FindOne(context.Background(), filter).Decode(&s) - Expect(err).NotTo(HaveOccurred()) - // Shouldn't return the error - by this step the roles should be propagated - Expect(s).To(Equal(p)) - fmt.Fprintf(GinkgoWriter, "User %s (deployment %s) has inserted a single document to %s/%s\n", user.Spec.Username, deployment.GetDeploymentName(), dbName, collectionName) - return nil -} - -func validateSecret(k8sClient client.Client, project mdbv1.AtlasProject, deployment mdbv1.AtlasDeployment, user mdbv1.AtlasDatabaseUser) corev1.Secret { - secret := corev1.Secret{} - username := user.Spec.Username - secretName := fmt.Sprintf("%s-%s-%s", kube.NormalizeIdentifier(project.Spec.Name), kube.NormalizeIdentifier(deployment.GetDeploymentName()), kube.NormalizeIdentifier(username)) - Expect(k8sClient.Get(context.Background(), kube.ObjectKey(project.Namespace, secretName), &secret)).To(Succeed()) - GinkgoWriter.Write([]byte(fmt.Sprintf("!! Secret: %v (%v)\n", kube.ObjectKey(project.Namespace, secretName), secret.Namespace+"/"+secret.Name))) - - password, err := user.ReadPassword(k8sClient) - Expect(err).NotTo(HaveOccurred()) - - c, _, err := atlasClient.AdvancedClusters.Get(context.Background(), project.ID(), deployment.GetDeploymentName()) - Expect(err).NotTo(HaveOccurred()) - - expectedData := map[string][]byte{ - "connectionStringStandard": []byte(buildConnectionURL(c.ConnectionStrings.Standard, username, password)), - "connectionStringStandardSrv": []byte(buildConnectionURL(c.ConnectionStrings.StandardSrv, username, password)), - "connectionStringPrivate": []byte(buildConnectionURL(c.ConnectionStrings.Private, username, password)), - "connectionStringPrivateSrv": []byte(buildConnectionURL(c.ConnectionStrings.PrivateSrv, username, password)), - "username": []byte(username), - "password": []byte(password), - } - expectedLabels := map[string]string{ - "atlas.mongodb.com/project-id": project.ID(), - "atlas.mongodb.com/cluster-name": deployment.GetDeploymentName(), - connectionsecret.TypeLabelKey: connectionsecret.CredLabelVal, - } - Expect(secret.Data).To(Equal(expectedData)) - Expect(secret.Labels).To(Equal(expectedLabels)) - GinkgoWriter.Write([]byte(fmt.Sprintf("!! Secret 2: %v \n", secret.Namespace+"/"+secret.Name))) - return secret -} - -func checkNumberOfConnectionSecrets(k8sClient client.Client, project mdbv1.AtlasProject, length int) { - secretList := corev1.SecretList{} - Expect(k8sClient.List(context.Background(), &secretList, client.InNamespace(namespace.Name))).To(Succeed()) - - names := make([]string, 0) - for _, item := range secretList.Items { - if strings.HasPrefix(item.Name, kube.NormalizeIdentifier(project.Spec.Name)) { - names = append(names, item.Name) - } - } - Expect(names).To(HaveLen(length), fmt.Sprintf("Expected %d items, but found %d (%v)", length, len(names), names)) -} - -func buildConnectionURL(connURL, userName, password string) string { - if connURL == "" { - return "" - } - - u, err := connectionsecret.AddCredentialsToConnectionURL(connURL, userName, password) - Expect(err).NotTo(HaveOccurred()) - return u -} - -func checkAtlasDatabaseUserRemoved(projectID string, user mdbv1.AtlasDatabaseUser) func() bool { - return func() bool { - _, r, err := atlasClient.DatabaseUsers.Get(context.Background(), user.Spec.DatabaseName, projectID, user.Spec.Username) - if err != nil { - if r != nil && r.StatusCode == http.StatusNotFound { - return true - } - } - - return false - } -} - -func checkSecretsDontExist(namespace string, secretNames []string) func() bool { - return func() bool { - nonExisting := 0 - for _, name := range secretNames { - s := corev1.Secret{} - err := k8sClient.Get(context.Background(), kube.ObjectKey(namespace, name), &s) - if err != nil && apiErrors.IsNotFound(err) { - nonExisting++ - } - } - return nonExisting == len(secretNames) - } -} - -func checkUserInAtlas(projectID string, user mdbv1.AtlasDatabaseUser) { - By("Verifying Database User state in Atlas", func() { - atlasDBUser, _, err := atlasClient.DatabaseUsers.Get(context.Background(), user.Spec.DatabaseName, projectID, user.Spec.Username) - Expect(err).ToNot(HaveOccurred()) - operatorDBUser, err := user.ToAtlas(k8sClient) - Expect(err).ToNot(HaveOccurred()) - - Expect(*atlasDBUser).To(Equal(normalize(*operatorDBUser, projectID))) - }) -} - -func validateDatabaseUserUpdatingFunc(g Gomega) func(a mdbv1.AtlasCustomResource) { - return func(a mdbv1.AtlasCustomResource) { - d := a.(*mdbv1.AtlasDatabaseUser) - expectedConditionsMatchers := testutil.MatchConditions( - status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserDeploymentAppliedChanges)), - status.FalseCondition(status.ReadyType), - status.TrueCondition(status.ValidationSucceeded), - status.TrueCondition(status.ResourceVersionStatus), - ) - g.Expect(d.Status.Conditions).To(ConsistOf(expectedConditionsMatchers)) - } -} - -// nolint -func validateDatabaseUserWaitingForCluster() func(a mdbv1.AtlasCustomResource) { - return func(a mdbv1.AtlasCustomResource) { - d := a.(*mdbv1.AtlasDatabaseUser) - // this is the first status that db user gets after update - userChangesApplied := testutil.MatchConditions( - status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserDeploymentAppliedChanges)), - status.FalseCondition(status.ReadyType), - status.TrueCondition(status.ValidationSucceeded), - ) - // this is the status the db user gets to when tries to create connection secrets and sees that the deployment - // is not ready - waitingForDeployment := testutil.MatchConditions( - status.FalseCondition(status.DatabaseUserReadyType). - WithReason(string(workflow.DatabaseUserConnectionSecretsNotCreated)). - WithMessageRegexp("Waiting for deployments to get created/updated"), - status.FalseCondition(status.ReadyType), - status.TrueCondition(status.ResourceVersionStatus), - ) - Expect(d.Status.Conditions).To(Or(ConsistOf(waitingForDeployment), ConsistOf(userChangesApplied))) - } -} diff --git a/test/int/deployment_test.go b/test/int/deployment_test.go index ab63dbe72a..3fea7fd9be 100644 --- a/test/int/deployment_test.go +++ b/test/int/deployment_test.go @@ -61,7 +61,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { ) BeforeEach(func() { - prepareControllers() + prepareControllers(false) createdDeployment = &mdbv1.AtlasDeployment{} @@ -682,7 +682,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { Describe("Create DBUser before deployment & check secrets", func() { It("Should Succeed", func() { By(fmt.Sprintf("Creating password Secret %s", UserPasswordSecret), func() { - passwordSecret := buildPasswordSecret(UserPasswordSecret, DBUserPassword) + passwordSecret := buildPasswordSecret(namespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) }) @@ -707,7 +707,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { return testutil.CheckCondition(k8sClient, createdDBUserFakeScope, status.FalseCondition(status.DatabaseUserReadyType).WithReason(string(workflow.DatabaseUserInvalidSpec))) }).WithTimeout(30 * time.Minute).WithPolling(interval).Should(BeTrue()) }) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) + checkNumberOfConnectionSecrets(k8sClient, *createdProject, namespace.Name, 0) createdDeployment = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name) By(fmt.Sprintf("Creating the Deployment %s", kube.ObjectKeyFromObject(createdDeployment)), func() { @@ -719,7 +719,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { By("Checking connection Secrets", func() { Expect(tryConnect(createdProject.ID(), *createdDeployment, *createdDBUser)).To(Succeed()) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 1) + checkNumberOfConnectionSecrets(k8sClient, *createdProject, namespace.Name, 1) validateSecret(k8sClient, *createdProject, *createdDeployment, *createdDBUser) }) }) @@ -739,7 +739,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { checkAtlasState() }) - passwordSecret := buildPasswordSecret(UserPasswordSecret, DBUserPassword) + passwordSecret := buildPasswordSecret(namespace.Name, UserPasswordSecret, DBUserPassword) Expect(k8sClient.Create(context.Background(), &passwordSecret)).To(Succeed()) createdDBUser := mdbv1.DefaultDBUser(namespace.Name, "test-db-user", createdProject.Name).WithPasswordSecret(UserPasswordSecret) @@ -759,7 +759,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { secretNames := []string{kube.NormalizeIdentifier(fmt.Sprintf("%s-%s-%s", createdProject.Spec.Name, createdDeployment.GetDeploymentName(), createdDBUser.Spec.Username))} createdDeployment = nil // prevent cleanup from failing due to deployment already deleted Eventually(checkSecretsDontExist(namespace.Name, secretNames), 50, interval).Should(BeTrue()) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) + checkNumberOfConnectionSecrets(k8sClient, *createdProject, namespace.Name, 0) }) }) }) @@ -776,7 +776,7 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment"), func() { Expect(k8sClient.Delete(context.Background(), createdDeployment)).To(Succeed()) time.Sleep(5 * time.Minute) Expect(checkAtlasDeploymentRemoved(createdProject.Status.ID, createdDeployment.GetDeploymentName())()).Should(BeFalse()) - checkNumberOfConnectionSecrets(k8sClient, *createdProject, 0) + checkNumberOfConnectionSecrets(k8sClient, *createdProject, namespace.Name, 0) }) }) }) diff --git a/test/int/integration_suite_test.go b/test/int/integration_suite_test.go index e8685ee15b..0ad6fe1daa 100644 --- a/test/int/integration_suite_test.go +++ b/test/int/integration_suite_test.go @@ -92,7 +92,7 @@ func init() { func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Project Controller Suite") + RunSpecs(t, "Atlas Operator Integration Test Suite") } // SynchronizedBeforeSuite uses the parallel "with singleton" pattern described by ginkgo @@ -179,7 +179,7 @@ func prepareAtlasClient() (*mongodbatlas.Client, atlas.Connection) { // prepareControllers is a common function used by all the tests that creates the namespace and registers all the // reconcilers there. Each of them listens only this specific namespace only, otherwise it's not possible to run in parallel -func prepareControllers() { +func prepareControllers(deletionProtection bool) (*corev1.Namespace, context.CancelFunc) { err := mdbv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) @@ -241,13 +241,15 @@ func prepareControllers() { Expect(err).ToNot(HaveOccurred()) err = (&atlasdatabaseuser.AtlasDatabaseUserReconciler{ - Client: k8sManager.GetClient(), - Log: logger.Named("controllers").Named("AtlasDatabaseUser").Sugar(), - AtlasDomain: atlasDomain, - EventRecorder: k8sManager.GetEventRecorderFor("AtlasDatabaseUser"), - ResourceWatcher: watch.NewResourceWatcher(), - GlobalAPISecret: kube.ObjectKey(namespace.Name, "atlas-operator-api-key"), - GlobalPredicates: globalPredicates, + Client: k8sManager.GetClient(), + Log: logger.Named("controllers").Named("AtlasDatabaseUser").Sugar(), + AtlasDomain: atlasDomain, + EventRecorder: k8sManager.GetEventRecorderFor("AtlasDatabaseUser"), + ResourceWatcher: watch.NewResourceWatcher(), + GlobalAPISecret: kube.ObjectKey(namespace.Name, "atlas-operator-api-key"), + GlobalPredicates: globalPredicates, + ObjectDeletionProtection: deletionProtection, + SubObjectDeletionProtection: deletionProtection, }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) @@ -271,6 +273,8 @@ func prepareControllers() { err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred()) }() + + return &namespace, managerCancelFunc } func removeControllersAndNamespace() { diff --git a/test/int/project_test.go b/test/int/project_test.go index 4db2575959..965c373fa6 100644 --- a/test/int/project_test.go +++ b/test/int/project_test.go @@ -42,7 +42,7 @@ var _ = Describe("AtlasProject", Label("int", "AtlasProject"), func() { ) BeforeEach(func() { - prepareControllers() + prepareControllers(false) createdProject = &mdbv1.AtlasProject{}