diff --git a/Makefile b/Makefile index d63e6b29bd..3d85036944 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,8 @@ # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html - GOLANGCI_VERSION=v1.43.0 COVERAGE=coverage.out - MCLI_SOURCE_FILES?=./cmd/mongocli MCLI_BINARY_NAME=mongocli MCLI_VERSION?=$(shell git tag --list 'mongocli/v*' --sort=committerdate | tail -1 | cut -d "v" -f 2 | xargs -I % sh -c 'echo %-next' ) @@ -13,7 +11,6 @@ MCLI_DESTINATION=./bin/$(MCLI_BINARY_NAME) MCLI_INSTALL_PATH="${GOPATH}/bin/$(MCLI_BINARY_NAME)" MCLI_E2E_BINARY?=../../bin/${MCLI_BINARY_NAME} - ATLAS_SOURCE_FILES?=./cmd/atlas ATLAS_BINARY_NAME=atlas ATLAS_VERSION?=$(shell git tag --list 'atlascli/v*' --sort=committerdate | tail -1 | cut -d "v" -f 2 | xargs -I % sh -c 'echo %-next' ) diff --git a/internal/cli/atlas/config/init.go b/internal/cli/atlas/config/init.go index 46d817300a..fd6498d535 100644 --- a/internal/cli/atlas/config/init.go +++ b/internal/cli/atlas/config/init.go @@ -45,7 +45,7 @@ func (opts *initOpts) SetUpAccess() { } func (opts *initOpts) Run(ctx context.Context) error { - fmt.Printf(`You are configuring a profile for %s. + _, _ = fmt.Fprintf(opts.OutWriter, `You are configuring a profile for %s. All values are optional and you can use environment variables (MONGODB_ATLAS_*) instead. @@ -89,11 +89,11 @@ Enter [?] on any option to get help. return err } - fmt.Printf("\nYour profile is now configured.\n") + _, _ = fmt.Fprintf(opts.OutWriter, "\nYour profile is now configured.\n") if config.Name() != config.DefaultProfile { - fmt.Printf("To use this profile, you must set the flag [-%s %s] for every command.\n", flag.ProfileShort, config.Name()) + _, _ = fmt.Fprintf(opts.OutWriter, "To use this profile, you must set the flag [-%s %s] for every command.\n", flag.ProfileShort, config.Name()) } - fmt.Printf("You can use [%s config set] to change these settings at a later time.\n", atlas) + _, _ = fmt.Fprintf(opts.OutWriter, "You can use [%s config set] to change these settings at a later time.\n", atlas) return nil } diff --git a/internal/cli/auth/login.go b/internal/cli/auth/login.go index 4b7e992935..060536fcb9 100644 --- a/internal/cli/auth/login.go +++ b/internal/cli/auth/login.go @@ -16,7 +16,6 @@ package auth import ( "context" - "errors" "fmt" "os" "time" @@ -27,7 +26,6 @@ import ( "github.com/mongodb/mongocli/internal/config" "github.com/mongodb/mongocli/internal/flag" "github.com/mongodb/mongocli/internal/oauth" - "github.com/mongodb/mongocli/internal/prompt" "github.com/pkg/browser" "github.com/spf13/cobra" "go.mongodb.org/atlas/auth" @@ -107,11 +105,11 @@ func (opts *loginOpts) Run(ctx context.Context) error { } _, _ = fmt.Fprint(opts.OutWriter, "Press Enter to continue your profile configuration") _, _ = fmt.Scanln() - if err := opts.askOrg(); err != nil { + if err := opts.AskOrg(); err != nil { return err } opts.SetUpOrg() - if err := opts.askProject(); err != nil { + if err := opts.AskProject(); err != nil { return err } opts.SetUpProject() @@ -168,36 +166,6 @@ Your code will expire after %.0f minutes. return nil } -func (opts *loginOpts) askOrg() error { - oMap, oSlice, err := opts.Orgs() - if err != nil || len(oSlice) == 0 { - return errors.New("no orgs") - } - - p := prompt.NewOrgSelect(oSlice) - var orgID string - if err := survey.AskOne(p, &orgID); err != nil { - return err - } - opts.OrgID = oMap[orgID] - return nil -} - -func (opts *loginOpts) askProject() error { - pMap, pSlice, err := opts.Projects() - if err != nil || len(pSlice) == 0 { - return errors.New("no projects") - } - - p := prompt.NewProjectSelect(pSlice) - var projectID string - if err := survey.AskOne(p, &projectID); err != nil { - return err - } - opts.ProjectID = pMap[projectID] - return nil -} - func LoginBuilder() *cobra.Command { opts := &loginOpts{} cmd := &cobra.Command{ diff --git a/internal/cli/config_opts.go b/internal/cli/config_opts.go deleted file mode 100644 index 9a4fa52cd2..0000000000 --- a/internal/cli/config_opts.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2022 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 ( - "github.com/AlecAivazis/survey/v2" - "github.com/mongodb/mongocli/internal/config" - "github.com/mongodb/mongocli/internal/prompt" - "github.com/mongodb/mongocli/internal/validate" -) - -type DigestConfigOpts struct { - DefaultSetterOpts - PublicAPIKey string - PrivateAPIKey string -} - -func (opts *DigestConfigOpts) SetUpServiceAndKeys() { - config.SetService(opts.Service) - if opts.PublicAPIKey != "" { - config.SetPublicAPIKey(opts.PublicAPIKey) - } - if opts.PrivateAPIKey != "" { - config.SetPrivateAPIKey(opts.PrivateAPIKey) - } -} - -func (opts *DigestConfigOpts) SetUpDigestAccess() { - opts.SetUpServiceAndKeys() - if opts.OpsManagerURL != "" { - config.SetOpsManagerURL(opts.OpsManagerURL) - } -} - -// AskProject will try to construct a select based on fetched projects. -// If it fails or there are no projects to show we fallback to ask for project by ID. -func (opts *DigestConfigOpts) AskProject() error { - pMap, pSlice, err := opts.Projects() - if err != nil || len(pSlice) == 0 { - p := prompt.NewProjectIDInput() - return survey.AskOne(p, &opts.ProjectID, survey.WithValidator(validate.OptionalObjectID)) - } - - p := prompt.NewProjectSelect(pSlice) - var projectID string - if err := survey.AskOne(p, &projectID); err != nil { - return err - } - opts.ProjectID = pMap[projectID] - return nil -} - -// AskOrg will try to construct a select based on fetched organizations. -// If it fails or there are no organizations to show we fallback to ask for org by ID. -func (opts *DigestConfigOpts) AskOrg() error { - oMap, oSlice, err := opts.Orgs() - if err != nil || len(oSlice) == 0 { - p := prompt.NewOrgIDInput() - return survey.AskOne(p, &opts.OrgID, survey.WithValidator(validate.OptionalObjectID)) - } - - p := prompt.NewOrgSelect(oSlice) - var orgID string - if err := survey.AskOne(p, &orgID); err != nil { - return err - } - opts.OrgID = oMap[orgID] - return nil -} diff --git a/internal/cli/default_setter_opts.go b/internal/cli/default_setter_opts.go index deab4124b4..2411308d36 100644 --- a/internal/cli/default_setter_opts.go +++ b/internal/cli/default_setter_opts.go @@ -16,13 +16,14 @@ package cli import ( "context" + "errors" "fmt" "io" - "os" "github.com/AlecAivazis/survey/v2" "github.com/mongodb/mongocli/internal/config" "github.com/mongodb/mongocli/internal/mongosh" + "github.com/mongodb/mongocli/internal/prompt" "github.com/mongodb/mongocli/internal/store" "github.com/mongodb/mongocli/internal/validate" atlas "go.mongodb.org/atlas/mongodbatlas" @@ -34,6 +35,7 @@ import ( type ProjectOrgsLister interface { Projects(*atlas.ListOptions) (interface{}, error) Organizations(*atlas.OrganizationsListOptions) (*atlas.Organizations, error) + GetOrgProjects(string, *atlas.ListOptions) (interface{}, error) } type DefaultSetterOpts struct { @@ -61,21 +63,46 @@ func (opts *DefaultSetterOpts) IsOpsManager() bool { return opts.Service == config.OpsManagerService } +const resultsLimit = 500 + +var ( + errTooManyResults = errors.New("too many results") + errNoResults = errors.New("no results") +) + // Projects fetches projects and returns then as a slice of the format `nameIDFormat`, // and a map such as `map[nameIDFormat]=ID`. // This is necessary as we can only prompt using `nameIDFormat` -// and we want them to get the ID mapping to store on the config. -func (opts *DefaultSetterOpts) Projects() (pMap map[string]string, pSlice []string, err error) { - projects, err := opts.Store.Projects(nil) +// and we want them to get the ID mapping to store in the config. +func (opts *DefaultSetterOpts) projects() (pMap map[string]string, pSlice []string, err error) { + var projects interface{} + if opts.OrgID == "" { + projects, err = opts.Store.Projects(nil) + } else { + projects, err = opts.Store.GetOrgProjects(opts.OrgID, &atlas.ListOptions{ItemsPerPage: resultsLimit}) + } if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "there was a problem fetching projects: %s\n", err) return nil, nil, err } - if opts.IsCloud() { - pMap, pSlice = atlasProjects(projects.(*atlas.Projects).Results) - } else { - pMap, pSlice = omProjects(projects.(*opsmngr.Projects).Results) + switch r := projects.(type) { + case *atlas.Projects: + if r.TotalCount == 0 { + return nil, nil, errNoResults + } + if r.TotalCount > resultsLimit { + return nil, nil, errTooManyResults + } + pMap, pSlice = atlasProjects(r.Results) + case *opsmngr.Projects: + if r.TotalCount == 0 { + return nil, nil, errNoResults + } + if r.TotalCount > resultsLimit { + return nil, nil, errTooManyResults + } + pMap, pSlice = omProjects(r.Results) } + return pMap, pSlice, nil } @@ -83,16 +110,20 @@ func (opts *DefaultSetterOpts) Projects() (pMap map[string]string, pSlice []stri // and a map such as `map[nameIDFormat]=ID`. // This is necessary as we can only prompt using `nameIDFormat` // and we want them to get the ID mapping to store on the config. -func (opts *DefaultSetterOpts) Orgs() (oMap map[string]string, oSlice []string, err error) { +func (opts *DefaultSetterOpts) orgs() (oMap map[string]string, oSlice []string, err error) { includeDeleted := false - orgs, err := opts.Store.Organizations(&atlas.OrganizationsListOptions{IncludeDeletedOrgs: &includeDeleted}) - if orgs != nil && orgs.TotalCount > len(orgs.Results) { - orgs, err = opts.Store.Organizations(&atlas.OrganizationsListOptions{IncludeDeletedOrgs: &includeDeleted, ListOptions: atlas.ListOptions{ItemsPerPage: orgs.TotalCount}}) - } + pagination := &atlas.OrganizationsListOptions{IncludeDeletedOrgs: &includeDeleted} + pagination.ItemsPerPage = resultsLimit + orgs, err := opts.Store.Organizations(pagination) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "there was a problem fetching orgs: %s\n", err) return nil, nil, err } + if orgs.TotalCount == 0 { + return nil, nil, errNoResults + } + if orgs.TotalCount > resultsLimit { + return nil, nil, errTooManyResults + } oMap = make(map[string]string, len(orgs.Results)) oSlice = make([]string, len(orgs.Results)) for i, o := range orgs.Results { @@ -103,6 +134,85 @@ func (opts *DefaultSetterOpts) Orgs() (oMap map[string]string, oSlice []string, return oMap, oSlice, nil } +// AskProject will try to construct a select based on fetched projects. +// If it fails or there are no projects to show we fallback to ask for project by ID. +func (opts *DefaultSetterOpts) AskProject() error { + pMap, pSlice, err := opts.projects() + if err != nil { + var target *atlas.ErrorResponse + switch { + case errors.Is(err, errNoResults): + _, _ = fmt.Fprintln(opts.OutWriter, "You don't seem to have access to any project") + case errors.Is(err, errTooManyResults): + _, _ = fmt.Fprintf(opts.OutWriter, "You have access to more than %d projects\n", resultsLimit) + case errors.As(err, &target): + _, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your projects: %s\n", target.Detail) + default: + _, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your projects: %s\n", err) + } + p := &survey.Confirm{ + Message: "Do you want to enter the Project ID manually?", + } + manually := true + if err2 := survey.AskOne(p, &manually); err2 != nil { + return err2 + } + if manually { + p := prompt.NewProjectIDInput() + return survey.AskOne(p, &opts.ProjectID, survey.WithValidator(validate.OptionalObjectID)) + } + _, _ = fmt.Fprint(opts.OutWriter, "Skipping default project setting\n") + return nil + } + + p := prompt.NewProjectSelect(pSlice) + var projectID string + if err := survey.AskOne(p, &projectID); err != nil { + return err + } + opts.ProjectID = pMap[projectID] + return nil +} + +// AskOrg will try to construct a select based on fetched organizations. +// If it fails or there are no organizations to show we fallback to ask for org by ID. +func (opts *DefaultSetterOpts) AskOrg() error { + oMap, oSlice, err := opts.orgs() + if err != nil { + var target *atlas.ErrorResponse + switch { + case errors.Is(err, errNoResults): + _, _ = fmt.Fprintln(opts.OutWriter, "You don't seem to have access to any organization") + case errors.Is(err, errTooManyResults): + _, _ = fmt.Fprintf(opts.OutWriter, "You have access to more than %d organizations\n", resultsLimit) + case errors.As(err, &target): + _, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your organizations: %s\n", target.Detail) + default: + _, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your organizations: %s\n", err) + } + p := &survey.Confirm{ + Message: "Do you want to enter the Org ID manually?", + } + manually := true + if err2 := survey.AskOne(p, &manually); err2 != nil { + return err2 + } + if manually { + p := prompt.NewOrgIDInput() + return survey.AskOne(p, &opts.OrgID, survey.WithValidator(validate.OptionalObjectID)) + } + _, _ = fmt.Fprint(opts.OutWriter, "Skipping default organization setting\n") + return nil + } + p := prompt.NewOrgSelect(oSlice) + var orgID string + if err := survey.AskOne(p, &orgID); err != nil { + return err + } + opts.OrgID = oMap[orgID] + return nil +} + func (opts *DefaultSetterOpts) SetUpProject() { if opts.ProjectID != "" { config.SetProjectID(opts.ProjectID) diff --git a/internal/cli/default_setter_opts_test.go b/internal/cli/default_setter_opts_test.go index 18a13cf992..659d703d63 100644 --- a/internal/cli/default_setter_opts_test.go +++ b/internal/cli/default_setter_opts_test.go @@ -83,10 +83,8 @@ func TestDefaultOpts_Projects(t *testing.T) { t.Run("empty", func(t *testing.T) { expectedProjects := &atlas.Projects{} mockStore.EXPECT().Projects(gomock.Any()).Return(expectedProjects, nil).Times(1) - gotPMap, gotPSlice, err := opts.Projects() - require.NoError(t, err) - assert.Empty(t, gotPMap) - assert.Empty(t, gotPSlice) + _, _, err := opts.projects() + require.Error(t, err) }) t.Run("with one project", func(t *testing.T) { expectedProjects := &atlas.Projects{ @@ -96,9 +94,10 @@ func TestDefaultOpts_Projects(t *testing.T) { Name: "Project 1", }, }, + TotalCount: 1, } mockStore.EXPECT().Projects(gomock.Any()).Return(expectedProjects, nil).Times(1) - gotPMap, gotPSlice, err := opts.Projects() + gotPMap, gotPSlice, err := opts.projects() require.NoError(t, err) assert.Equal(t, map[string]string{"Project 1 (1)": "1"}, gotPMap) assert.Equal(t, []string{"Project 1 (1)"}, gotPSlice) @@ -116,10 +115,8 @@ func TestDefaultOpts_Orgs(t *testing.T) { t.Run("empty", func(t *testing.T) { expectedOrgs := &atlas.Organizations{} mockStore.EXPECT().Organizations(gomock.Any()).Return(expectedOrgs, nil).Times(1) - gotOMap, gotOSlice, err := opts.Orgs() - require.NoError(t, err) - assert.Empty(t, gotOMap) - assert.Empty(t, gotOSlice) + _, _, err := opts.orgs() + require.Error(t, err) }) t.Run("with one org", func(t *testing.T) { expectedOrgs := &atlas.Organizations{ @@ -129,9 +126,10 @@ func TestDefaultOpts_Orgs(t *testing.T) { Name: "Org 1", }, }, + TotalCount: 1, } mockStore.EXPECT().Organizations(gomock.Any()).Return(expectedOrgs, nil).Times(1) - gotOMap, gotOSlice, err := opts.Orgs() + gotOMap, gotOSlice, err := opts.orgs() require.NoError(t, err) assert.Equal(t, map[string]string{"Org 1 (1)": "1"}, gotOMap) assert.Equal(t, []string{"Org 1 (1)"}, gotOSlice) diff --git a/internal/cli/digest_config_opts.go b/internal/cli/digest_config_opts.go new file mode 100644 index 0000000000..c9de3a5da5 --- /dev/null +++ b/internal/cli/digest_config_opts.go @@ -0,0 +1,42 @@ +// Copyright 2022 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 ( + "github.com/mongodb/mongocli/internal/config" +) + +type DigestConfigOpts struct { + DefaultSetterOpts + PublicAPIKey string + PrivateAPIKey string +} + +func (opts *DigestConfigOpts) SetUpServiceAndKeys() { + config.SetService(opts.Service) + if opts.PublicAPIKey != "" { + config.SetPublicAPIKey(opts.PublicAPIKey) + } + if opts.PrivateAPIKey != "" { + config.SetPrivateAPIKey(opts.PrivateAPIKey) + } +} + +func (opts *DigestConfigOpts) SetUpDigestAccess() { + opts.SetUpServiceAndKeys() + if opts.OpsManagerURL != "" { + config.SetOpsManagerURL(opts.OpsManagerURL) + } +} diff --git a/internal/mocks/mock_default_opts.go b/internal/mocks/mock_default_opts.go index aed34279d7..2863c00c6e 100644 --- a/internal/mocks/mock_default_opts.go +++ b/internal/mocks/mock_default_opts.go @@ -34,6 +34,21 @@ func (m *MockProjectOrgsLister) EXPECT() *MockProjectOrgsListerMockRecorder { return m.recorder } +// GetOrgProjects mocks base method. +func (m *MockProjectOrgsLister) GetOrgProjects(arg0 string, arg1 *mongodbatlas.ListOptions) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOrgProjects", arg0, arg1) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOrgProjects indicates an expected call of GetOrgProjects. +func (mr *MockProjectOrgsListerMockRecorder) GetOrgProjects(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgProjects", reflect.TypeOf((*MockProjectOrgsLister)(nil).GetOrgProjects), arg0, arg1) +} + // Organizations mocks base method. func (m *MockProjectOrgsLister) Organizations(arg0 *mongodbatlas.OrganizationsListOptions) (*mongodbatlas.Organizations, error) { m.ctrl.T.Helper() diff --git a/internal/store/logs.go b/internal/store/logs.go index 4d7ca27abc..c6ebd7ab95 100644 --- a/internal/store/logs.go +++ b/internal/store/logs.go @@ -78,7 +78,7 @@ func (s *Store) Collect(groupID string, newLog *opsmngr.LogCollectionJob) (*opsm } } -// ProcessDisks encapsulate the logic to manage different cloud providers. +// DownloadLog encapsulates the logic to manage different cloud providers. func (s *Store) DownloadLog(groupID, host, name string, out io.Writer, opts *atlas.DateRangetOptions) error { switch s.service { case config.CloudService, config.CloudGovService: diff --git a/internal/store/projects.go b/internal/store/projects.go index b8a18e4320..5602258530 100644 --- a/internal/store/projects.go +++ b/internal/store/projects.go @@ -83,6 +83,9 @@ func (s *Store) Projects(opts *atlas.ListOptions) (interface{}, error) { // GetOrgProjects encapsulates the logic to manage different cloud providers. func (s *Store) GetOrgProjects(orgID string, opts *atlas.ListOptions) (interface{}, error) { switch s.service { + case config.CloudService, config.CloudGovService: + result, _, err := s.client.(*atlas.Client).Organizations.Projects(s.ctx, orgID, opts) + return result, err case config.CloudManagerService, config.OpsManagerService: result, _, err := s.client.(*opsmngr.Client).Organizations.Projects(s.ctx, orgID, opts) return result, err