diff --git a/internal/cli/atlas_alert_config_delete.go b/internal/cli/atlas_alert_config_delete.go index a1ae10b6b3..596db2d022 100644 --- a/internal/cli/atlas_alert_config_delete.go +++ b/internal/cli/atlas_alert_config_delete.go @@ -38,7 +38,7 @@ func (opts *atlasAlertConfigDeleteOpts) init() error { } func (opts *atlasAlertConfigDeleteOpts) Run() error { - return opts.DeleteFromProject(opts.store.DeleteAlertConfiguration, opts.ProjectID()) + return opts.Delete(opts.store.DeleteAlertConfiguration, opts.ProjectID()) } // mongocli atlas alerts config(s) delete id --projectId projectId [--confirm] diff --git a/internal/cli/atlas_clusters_delete.go b/internal/cli/atlas_clusters_delete.go index 2a4595e543..621066cec2 100644 --- a/internal/cli/atlas_clusters_delete.go +++ b/internal/cli/atlas_clusters_delete.go @@ -38,7 +38,7 @@ func (opts *atlasClustersDeleteOpts) init() error { } func (opts *atlasClustersDeleteOpts) Run() error { - return opts.DeleteFromProject(opts.store.DeleteCluster, opts.ProjectID()) + return opts.Delete(opts.store.DeleteCluster, opts.ProjectID()) } // mongocli atlas cluster(s) delete name --projectId projectId [--confirm] diff --git a/internal/cli/atlas_dbusers_create.go b/internal/cli/atlas_dbusers_create.go index 6f6815aec5..22cc6a321b 100644 --- a/internal/cli/atlas_dbusers_create.go +++ b/internal/cli/atlas_dbusers_create.go @@ -31,6 +31,7 @@ type atlasDBUsersCreateOpts struct { *globalOpts username string password string + authDB string roles []string store store.DatabaseUserCreator } @@ -58,7 +59,7 @@ func (opts *atlasDBUsersCreateOpts) Run() error { func (opts *atlasDBUsersCreateOpts) newDatabaseUser() *atlas.DatabaseUser { return &atlas.DatabaseUser{ - DatabaseName: convert.AdminDB, + DatabaseName: opts.authDB, Roles: convert.BuildAtlasRoles(opts.roles), GroupID: opts.ProjectID(), Username: opts.username, @@ -105,6 +106,7 @@ func AtlasDBUsersCreateBuilder() *cobra.Command { cmd.Flags().StringVar(&opts.username, flags.Username, "", usage.Username) cmd.Flags().StringVar(&opts.password, flags.Password, "", usage.Password) cmd.Flags().StringSliceVar(&opts.roles, flags.Role, []string{}, usage.Roles) + cmd.Flags().StringVar(&opts.authDB, flags.AuthDB, convert.AdminDB, usage.AuthDB) cmd.Flags().StringVar(&opts.projectID, flags.ProjectID, "", usage.ProjectID) diff --git a/internal/cli/atlas_dbusers_delete.go b/internal/cli/atlas_dbusers_delete.go index 1e21d05ce8..ae16fa5344 100644 --- a/internal/cli/atlas_dbusers_delete.go +++ b/internal/cli/atlas_dbusers_delete.go @@ -15,6 +15,7 @@ package cli import ( + "github.com/mongodb/mongocli/internal/convert" "github.com/mongodb/mongocli/internal/flags" "github.com/mongodb/mongocli/internal/store" "github.com/mongodb/mongocli/internal/usage" @@ -39,7 +40,7 @@ func (opts *atlasDBUsersDeleteOpts) init() error { } func (opts *atlasDBUsersDeleteOpts) Run() error { - return opts.DeleterFromProjectAuthDB(opts.store.DeleteDatabaseUser, opts.authDB, opts.ProjectID()) + return opts.Delete(opts.store.DeleteDatabaseUser, opts.authDB, opts.ProjectID()) } // mongocli atlas dbuser(s) delete --force @@ -69,9 +70,9 @@ func AtlasDBUsersDeleteBuilder() *cobra.Command { } cmd.Flags().BoolVar(&opts.confirm, flags.Force, false, usage.Force) + cmd.Flags().StringVar(&opts.authDB, flags.AuthDB, convert.AdminDB, usage.AuthDB) cmd.Flags().StringVar(&opts.projectID, flags.ProjectID, "", usage.ProjectID) - cmd.Flags().StringVar(&opts.authDB, flags.AuthDB, "admin", usage.AuthDB) return cmd } diff --git a/internal/cli/atlas_whitelist_delete.go b/internal/cli/atlas_whitelist_delete.go index 6dd8a43484..36572abf6e 100644 --- a/internal/cli/atlas_whitelist_delete.go +++ b/internal/cli/atlas_whitelist_delete.go @@ -38,7 +38,7 @@ func (opts *atlasWhitelistDeleteOpts) init() error { } func (opts *atlasWhitelistDeleteOpts) Run() error { - return opts.DeleteFromProject(opts.store.DeleteProjectIPWhitelist, opts.ProjectID()) + return opts.Delete(opts.store.DeleteProjectIPWhitelist, opts.ProjectID()) } // mongocli atlas whitelist delete --force diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 6b998a3c18..6d0a33ca41 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -15,6 +15,7 @@ package cli import ( + "errors" "fmt" "github.com/AlecAivazis/survey/v2" @@ -24,7 +25,7 @@ import ( const ( fallbackSuccessMessage = "'%s' deleted\n" - fallbackFailMessage = "'%s' not deleted\n" + fallbackFailMessage = "entry not deleted" ) type globalOpts struct { @@ -58,7 +59,7 @@ func (opts *globalOpts) OrgID() string { } // deleteOpts options required when deleting a resource. -// A command can embed this structure and then safely rely on the methods Confirm, DeleteFromProject or Delete +// A command can compose this struct and then safely rely on the methods Confirm, or Delete // to manage the interactions with the user type deleteOpts struct { entry string @@ -67,83 +68,31 @@ type deleteOpts struct { failMessage string } -// DeleterFromProject a function to delete from the store. -type DeleterFromProject func(projectID string, entry string) error - -// DeleterFromProjectAuthDB a function to delete from the store. -type DeleterFromProjectAuthDB func(authDB string, projectID string, entry string) error - -// DeleteFromProject deletes a resource from a project, it expects a callback +// Delete deletes a resource not associated to a project, it expects a callback // that should perform the deletion from the store. -func (opts *deleteOpts) DeleteFromProject(d DeleterFromProject, projectID string) error { +func (opts *deleteOpts) Delete(d interface{}, a ...string) error { if !opts.confirm { - opts.printFailMessage() + fmt.Println(opts.FailMessage()) return nil } - err := d(projectID, opts.entry) - - if err != nil { - return err - } - - opts.printSuccessMessage() - - return nil -} -// DeleterFromProjectAuthDB deletes a resource from a project, it expects a callback -// that should perform the deletion from the store. -func (opts *deleteOpts) DeleterFromProjectAuthDB(d DeleterFromProjectAuthDB, authDB, projectID string) error { - if !opts.confirm { - opts.printFailMessage() - return nil + var err error + switch f := d.(type) { + case func(string) error: + err = f(opts.entry) + case func(string, string) error: + err = f(a[0], opts.entry) + case func(string, string, string) error: + err = f(a[0], a[1], opts.entry) + default: + return errors.New("invalid") } - err := d(authDB, projectID, opts.entry) if err != nil { return err } - opts.printSuccessMessage() - - return nil -} - -// printSuccessMessage prints a success message -func (opts *deleteOpts) printSuccessMessage() { - if opts.successMessage != "" { - fmt.Printf(opts.successMessage, opts.entry) - } else { - fmt.Printf(fallbackSuccessMessage, opts.entry) - } -} - -// printFailMessage prints a fail message -func (opts *deleteOpts) printFailMessage() { - if opts.successMessage != "" { - fmt.Printf(opts.failMessage, opts.entry) - } else { - fmt.Printf(fallbackFailMessage, opts.entry) - } -} - -// Deleter a function to delete from the store. -type Deleter func(entry string) error - -// Delete deletes a resource not associated to a project, it expects a callback -//// that should perform the deletion from the store. -func (opts *deleteOpts) Delete(d Deleter) error { - if !opts.confirm { - opts.printFailMessage() - return nil - } - err := d(opts.entry) - - if err != nil { - return err - } - - opts.printSuccessMessage() + fmt.Printf(opts.SuccessMessage(), opts.entry) return nil } @@ -157,3 +106,19 @@ func (opts *deleteOpts) Confirm() error { prompt := prompts.NewDeleteConfirm(opts.entry) return survey.AskOne(prompt, &opts.confirm) } + +// SuccessMessage gets the set success message or the default value +func (opts *deleteOpts) SuccessMessage() string { + if opts.successMessage != "" { + return opts.successMessage + } + return fallbackSuccessMessage +} + +// FailMessage gets the set fail message or the default value +func (opts *deleteOpts) FailMessage() string { + if opts.failMessage != "" { + return opts.failMessage + } + return fallbackFailMessage +} diff --git a/internal/cli/ops_manager_dbusers.go b/internal/cli/ops_manager_dbusers.go index ffcc9592e9..cefa609b57 100644 --- a/internal/cli/ops_manager_dbusers.go +++ b/internal/cli/ops_manager_dbusers.go @@ -31,6 +31,7 @@ A user’s roles apply to all the clusters in the project.`, cmd.AddCommand(OpsManagerDBUsersCreateBuilder()) cmd.AddCommand(OpsManagerDBUsersListBuilder()) + cmd.AddCommand(OpsManagerDBUsersDeleteBuilder()) return cmd } diff --git a/internal/cli/ops_manager_dbusers_create.go b/internal/cli/ops_manager_dbusers_create.go index 41bf22965e..04af986ec1 100644 --- a/internal/cli/ops_manager_dbusers_create.go +++ b/internal/cli/ops_manager_dbusers_create.go @@ -57,7 +57,7 @@ func (opts *opsManagerDBUsersCreateOpts) Run() error { return err } - current.Auth.Users = append(current.Auth.Users, opts.newDBUser()) + convert.AddUser(current, opts.newDBUser()) if err = opts.store.UpdateAutomationConfig(opts.ProjectID(), current); err != nil { return err diff --git a/internal/cli/ops_manager_dbusers_delete.go b/internal/cli/ops_manager_dbusers_delete.go new file mode 100644 index 0000000000..2950aa7af0 --- /dev/null +++ b/internal/cli/ops_manager_dbusers_delete.go @@ -0,0 +1,95 @@ +// Copyright 2020 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "fmt" + + "github.com/mongodb/mongocli/internal/config" + "github.com/mongodb/mongocli/internal/convert" + "github.com/mongodb/mongocli/internal/flags" + "github.com/mongodb/mongocli/internal/messages" + "github.com/mongodb/mongocli/internal/store" + "github.com/mongodb/mongocli/internal/usage" + "github.com/spf13/cobra" +) + +type opsManagerDBUsersDeleteOpts struct { + *globalOpts + *deleteOpts + authDB string + store store.AutomationPatcher +} + +func (opts *opsManagerDBUsersDeleteOpts) init() error { + if opts.ProjectID() == "" { + return errMissingProjectID + } + + var err error + opts.store, err = store.New() + return err +} + +func (opts *opsManagerDBUsersDeleteOpts) Run() error { + current, err := opts.store.GetAutomationConfig(opts.ProjectID()) + + if err != nil { + return err + } + + convert.RemoveUser(current, opts.entry, opts.authDB) + + if err = opts.store.UpdateAutomationConfig(opts.ProjectID(), current); err != nil { + return err + } + + fmt.Print(messages.DeploymentStatus(config.OpsManagerURL(), opts.ProjectID())) + + return nil +} + +// mongocli atlas dbuser(s) delete [--projectId projectId] +func OpsManagerDBUsersDeleteBuilder() *cobra.Command { + opts := &opsManagerDBUsersDeleteOpts{ + globalOpts: newGlobalOpts(), + deleteOpts: &deleteOpts{ + successMessage: "DB user '%s' deleted\n", + failMessage: "DB user not deleted", + }, + } + cmd := &cobra.Command{ + Use: "delete [username]", + Short: "Delete a database user for a project.", + Aliases: []string{"rm"}, + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := opts.init(); err != nil { + return err + } + opts.entry = args[0] + return opts.Confirm() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return opts.Run() + }, + } + + cmd.Flags().StringVar(&opts.authDB, flags.AuthDB, convert.AdminDB, usage.AuthDB) + + cmd.Flags().StringVar(&opts.projectID, flags.ProjectID, "", usage.ProjectID) + + return cmd +} diff --git a/internal/cli/ops_manager_dbusers_delete_test.go b/internal/cli/ops_manager_dbusers_delete_test.go new file mode 100644 index 0000000000..7bfb7fd18c --- /dev/null +++ b/internal/cli/ops_manager_dbusers_delete_test.go @@ -0,0 +1,60 @@ +// Copyright 2020 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/mongodb/mongocli/internal/fixtures" + "github.com/mongodb/mongocli/internal/mocks" +) + +func TestOpsManagerDBUserDelete_Run(t *testing.T) { + ctrl := gomock.NewController(t) + mockStore := mocks.NewMockAutomationPatcher(ctrl) + + defer ctrl.Finish() + + expected := fixtures.AutomationConfig() + + createOpts := &opsManagerDBUsersDeleteOpts{ + globalOpts: newGlobalOpts(), + deleteOpts: &deleteOpts{ + confirm: true, + entry: "test", + successMessage: "DB user '%s' deleted\n", + }, + authDB: "admin", + store: mockStore, + } + + mockStore. + EXPECT(). + GetAutomationConfig(createOpts.projectID). + Return(expected, nil). + Times(1) + + mockStore. + EXPECT(). + UpdateAutomationConfig(createOpts.projectID, expected). + Return(nil). + Times(1) + + err := createOpts.Run() + if err != nil { + t.Fatalf("Run() unexpected error: %v", err) + } +} diff --git a/internal/convert/automation_config.go b/internal/convert/automation_config.go index 3437a2b778..8b3b8667b5 100644 --- a/internal/convert/automation_config.go +++ b/internal/convert/automation_config.go @@ -18,6 +18,7 @@ import ( "fmt" om "github.com/mongodb/go-client-mongodb-ops-manager/opsmngr" + "github.com/mongodb/mongocli/internal/search" ) const ( @@ -62,6 +63,21 @@ func Startup(out *om.AutomationConfig, name string) { setDisabledByClusterName(out, name, false) } +// AddUser adds a MongoDBUser to the config +func AddUser(out *om.AutomationConfig, u *om.MongoDBUser) { + out.Auth.Users = append(out.Auth.Users, u) +} + +// RemoveUser removes a MongoDBUser from the config +func RemoveUser(out *om.AutomationConfig, username string, database string) { + pos, found := search.MongoDBUsers(out.Auth.Users, func(p *om.MongoDBUser) bool { + return p.Username == username && p.Database == database + }) + if found { + out.Auth.Users = append(out.Auth.Users[:pos], out.Auth.Users[pos+1:]...) + } +} + func setDisabledByClusterName(out *om.AutomationConfig, name string, disabled bool) { // This value may not be present and is mandatory if out.Auth.DeploymentAuthMechanisms == nil { diff --git a/internal/convert/automation_config_test.go b/internal/convert/automation_config_test.go index ce250c8fd4..aa10f234a9 100644 --- a/internal/convert/automation_config_test.go +++ b/internal/convert/automation_config_test.go @@ -23,7 +23,7 @@ import ( func TestFromAutomationConfig(t *testing.T) { name := "cluster_1" - cloud := fixtures.AutomationConfigWithOneReplicaSet(name, false) + config := fixtures.AutomationConfigWithOneReplicaSet(name, false) buildIndexes := true expected := []ClusterConfig{ @@ -52,7 +52,7 @@ func TestFromAutomationConfig(t *testing.T) { }, } - result := FromAutomationConfig(cloud) + result := FromAutomationConfig(config) if diff := deep.Equal(result, expected); diff != nil { t.Error(diff) } @@ -60,11 +60,11 @@ func TestFromAutomationConfig(t *testing.T) { func TestShutdown(t *testing.T) { name := "cluster_1" - cloud := fixtures.AutomationConfigWithOneReplicaSet(name, false) + config := fixtures.AutomationConfigWithOneReplicaSet(name, false) - Shutdown(cloud, name) - if !cloud.Processes[0].Disabled { - t.Errorf("TestShutdown\n got=%#v\nwant=%#v\n", cloud.Processes[0].Disabled, true) + Shutdown(config, name) + if !config.Processes[0].Disabled { + t.Errorf("TestShutdown\n got=%#v\nwant=%#v\n", config.Processes[0].Disabled, true) } } @@ -77,3 +77,21 @@ func TestStartup(t *testing.T) { t.Errorf("TestStartup\n got=%#v\nwant=%#v\n", cloud.Processes[0].Disabled, false) } } + +func TestAddUser(t *testing.T) { + config := fixtures.AutomationConfigWithoutMongoDBUsers() + u := fixtures.MongoDBUsers() + AddUser(config, u) + if len(config.Auth.Users) != 1 { + t.Error("User not added\n") + } +} + +func TestRemoveUser(t *testing.T) { + config := fixtures.AutomationConfigWithMongoDBUsers() + u := fixtures.MongoDBUsers() + RemoveUser(config, u.Username, u.Database) + if len(config.Auth.Users) != 0 { + t.Error("User not removed\n") + } +} diff --git a/internal/convert/database_user.go b/internal/convert/database_user.go index 1992b0a350..0a4944ce89 100644 --- a/internal/convert/database_user.go +++ b/internal/convert/database_user.go @@ -21,12 +21,12 @@ import ( om "github.com/mongodb/go-client-mongodb-ops-manager/opsmngr" ) -//Public constants +// Public constants const ( AdminDB = "admin" ) -//Private constants +// Private constants const ( roleSep = "@" ) diff --git a/internal/convert/database_user_test.go b/internal/convert/database_user_test.go new file mode 100644 index 0000000000..4e4679c65a --- /dev/null +++ b/internal/convert/database_user_test.go @@ -0,0 +1,99 @@ +package convert + +import ( + "testing" + + "github.com/go-test/deep" + "github.com/mongodb/go-client-mongodb-atlas/mongodbatlas" + "github.com/mongodb/go-client-mongodb-ops-manager/opsmngr" +) + +func TestBuildAtlasRoles(t *testing.T) { + t.Run("No database defaults to admin", func(t *testing.T) { + r := BuildAtlasRoles([]string{"admin"}) + expected := []mongodbatlas.Role{ + { + RoleName: "admin", + DatabaseName: "admin", + }, + } + if err := deep.Equal(r, expected); err != nil { + t.Error(err) + } + }) + + t.Run("should split by @", func(t *testing.T) { + r := BuildAtlasRoles([]string{"admin@test"}) + expected := []mongodbatlas.Role{ + { + RoleName: "admin", + DatabaseName: "test", + }, + } + if err := deep.Equal(r, expected); err != nil { + t.Error(err) + } + }) + + t.Run("all", func(t *testing.T) { + r := BuildAtlasRoles([]string{"admin@test", "something"}) + expected := []mongodbatlas.Role{ + { + RoleName: "admin", + DatabaseName: "test", + }, + { + RoleName: "something", + DatabaseName: "admin", + }, + } + if err := deep.Equal(r, expected); err != nil { + t.Error(err) + } + }) +} + +func TestBuildOMRoles(t *testing.T) { + t.Run("No database defaults to admin", func(t *testing.T) { + r := BuildOMRoles([]string{"admin"}) + expected := []*opsmngr.Role{ + { + Role: "admin", + Database: "admin", + }, + } + if err := deep.Equal(r, expected); err != nil { + t.Error(err) + } + }) + + t.Run("should split by @", func(t *testing.T) { + r := BuildOMRoles([]string{"admin@test"}) + expected := []*opsmngr.Role{ + { + Role: "admin", + Database: "test", + }, + } + if err := deep.Equal(r, expected); err != nil { + t.Error(err) + } + }) + + t.Run("all", func(t *testing.T) { + r := BuildOMRoles([]string{"admin@test", "something"}) + expected := []*opsmngr.Role{ + { + Role: "admin", + Database: "test", + }, + { + Role: "something", + Database: "admin", + }, + } + if err := deep.Equal(r, expected); err != nil { + t.Error(err) + } + }) +} diff --git a/internal/fixtures/automation_configs.go b/internal/fixtures/automation_configs.go index 2420dac26b..4d9f21bdaf 100644 --- a/internal/fixtures/automation_configs.go +++ b/internal/fixtures/automation_configs.go @@ -214,6 +214,44 @@ func AutomationConfigWithOneReplicaSet(name string, disabled bool) *opsmngr.Auto } } +func MongoDBUsers() *opsmngr.MongoDBUser { + return &opsmngr.MongoDBUser{ + Mechanisms: []string{"SCRAM-SHA-1"}, + Roles: []*opsmngr.Role{ + { + Role: "test", + Database: "test", + }, + }, + Username: "test", + Database: "test", + } +} + +func AutomationConfigWithoutMongoDBUsers() *opsmngr.AutomationConfig { + return &opsmngr.AutomationConfig{ + Auth: opsmngr.Auth{ + AutoAuthMechanism: "MONGODB-CR", + Disabled: true, + AuthoritativeSet: false, + Users: make([]*opsmngr.MongoDBUser, 0), + }, + } +} + +func AutomationConfigWithMongoDBUsers() *opsmngr.AutomationConfig { + return &opsmngr.AutomationConfig{ + Auth: opsmngr.Auth{ + AutoAuthMechanism: "MONGODB-CR", + Disabled: true, + AuthoritativeSet: false, + Users: []*opsmngr.MongoDBUser{ + MongoDBUsers(), + }, + }, + } +} + func EmptyAutomationConfig() *opsmngr.AutomationConfig { return &opsmngr.AutomationConfig{ Processes: make([]*opsmngr.Process, 0), diff --git a/internal/search/example_search_test.go b/internal/search/example_search_test.go index 39f3b5079e..2c86bce747 100644 --- a/internal/search/example_search_test.go +++ b/internal/search/example_search_test.go @@ -78,6 +78,20 @@ func ExampleMembers() { // found myReplicaSet_2 at index 1 } +// This example demonstrates searching a list of db users by username. +func ExampleMongoDBUsers() { + a := fixtures.AutomationConfigWithMongoDBUsers().Auth.Users + x := "test" + i, found := search.MongoDBUsers(a, func(m *om.MongoDBUser) bool { return m.Username == x }) + if i < len(a) && found { + fmt.Printf("found %v at index %d\n", x, i) + } else { + fmt.Printf("%s not found\n", x) + } + // Output: + // found test at index 0 +} + // This example demonstrates searching a cluster in an automation config. func ExampleClusterExists() { a := fixtures.AutomationConfig() diff --git a/internal/search/search.go b/internal/search/search.go index d2e7bf9b6e..a6af645d37 100644 --- a/internal/search/search.go +++ b/internal/search/search.go @@ -53,10 +53,10 @@ func Members(a []om.Member, f func(om.Member) bool) (int, bool) { return len(a), false } -// Members return the smallest index i +// ReplicaSets return the smallest index i // in [0, n) at which f(i) is true, assuming that on the range [0, n), // f(i) == true implies f(i+1) == true. -// returns the first true index. If there is no such index, Members returns n and false +// returns the first true index. If there is no such index, ReplicaSets returns n and false func ReplicaSets(a []*om.ReplicaSet, f func(*om.ReplicaSet) bool) (int, bool) { for i, m := range a { if f(m) { @@ -66,6 +66,19 @@ func ReplicaSets(a []*om.ReplicaSet, f func(*om.ReplicaSet) bool) (int, bool) { return len(a), false } +// MongoDBUser return the smallest index i +// in [0, n) at which f(i) is true, assuming that on the range [0, n), +// f(i) == true implies f(i+1) == true. +// returns the first true index. If there is no such index, MongoDBUser returns n and false +func MongoDBUsers(a []*om.MongoDBUser, f func(*om.MongoDBUser) bool) (int, bool) { + for i, m := range a { + if f(m) { + return i, true + } + } + return len(a), false +} + // ClusterExists return true if a cluster exists for the given name func ClusterExists(c *om.AutomationConfig, name string) bool { _, found := ReplicaSets(c.ReplicaSets, func(r *om.ReplicaSet) bool {