Skip to content

Commit 87da562

Browse files
authored
CLOUDP-114891: Improve org/project selection when too many orgs/projects (#1025)
1 parent 1469135 commit 87da562

File tree

10 files changed

+200
-148
lines changed

10 files changed

+200
-148
lines changed

Makefile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
22

3-
43
GOLANGCI_VERSION=v1.43.0
54
COVERAGE=coverage.out
65

7-
86
MCLI_SOURCE_FILES?=./cmd/mongocli
97
MCLI_BINARY_NAME=mongocli
108
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)
1311
MCLI_INSTALL_PATH="${GOPATH}/bin/$(MCLI_BINARY_NAME)"
1412
MCLI_E2E_BINARY?=../../bin/${MCLI_BINARY_NAME}
1513

16-
1714
ATLAS_SOURCE_FILES?=./cmd/atlas
1815
ATLAS_BINARY_NAME=atlas
1916
ATLAS_VERSION?=$(shell git tag --list 'atlascli/v*' --sort=committerdate | tail -1 | cut -d "v" -f 2 | xargs -I % sh -c 'echo %-next' )

internal/cli/atlas/config/init.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (opts *initOpts) SetUpAccess() {
4545
}
4646

4747
func (opts *initOpts) Run(ctx context.Context) error {
48-
fmt.Printf(`You are configuring a profile for %s.
48+
_, _ = fmt.Fprintf(opts.OutWriter, `You are configuring a profile for %s.
4949
5050
All values are optional and you can use environment variables (MONGODB_ATLAS_*) instead.
5151
@@ -89,11 +89,11 @@ Enter [?] on any option to get help.
8989
return err
9090
}
9191

92-
fmt.Printf("\nYour profile is now configured.\n")
92+
_, _ = fmt.Fprintf(opts.OutWriter, "\nYour profile is now configured.\n")
9393
if config.Name() != config.DefaultProfile {
94-
fmt.Printf("To use this profile, you must set the flag [-%s %s] for every command.\n", flag.ProfileShort, config.Name())
94+
_, _ = fmt.Fprintf(opts.OutWriter, "To use this profile, you must set the flag [-%s %s] for every command.\n", flag.ProfileShort, config.Name())
9595
}
96-
fmt.Printf("You can use [%s config set] to change these settings at a later time.\n", atlas)
96+
_, _ = fmt.Fprintf(opts.OutWriter, "You can use [%s config set] to change these settings at a later time.\n", atlas)
9797
return nil
9898
}
9999

internal/cli/auth/login.go

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ package auth
1616

1717
import (
1818
"context"
19-
"errors"
2019
"fmt"
2120
"os"
2221
"time"
@@ -27,7 +26,6 @@ import (
2726
"github.com/mongodb/mongocli/internal/config"
2827
"github.com/mongodb/mongocli/internal/flag"
2928
"github.com/mongodb/mongocli/internal/oauth"
30-
"github.com/mongodb/mongocli/internal/prompt"
3129
"github.com/pkg/browser"
3230
"github.com/spf13/cobra"
3331
"go.mongodb.org/atlas/auth"
@@ -107,11 +105,11 @@ func (opts *loginOpts) Run(ctx context.Context) error {
107105
}
108106
_, _ = fmt.Fprint(opts.OutWriter, "Press Enter to continue your profile configuration")
109107
_, _ = fmt.Scanln()
110-
if err := opts.askOrg(); err != nil {
108+
if err := opts.AskOrg(); err != nil {
111109
return err
112110
}
113111
opts.SetUpOrg()
114-
if err := opts.askProject(); err != nil {
112+
if err := opts.AskProject(); err != nil {
115113
return err
116114
}
117115
opts.SetUpProject()
@@ -168,36 +166,6 @@ Your code will expire after %.0f minutes.
168166
return nil
169167
}
170168

171-
func (opts *loginOpts) askOrg() error {
172-
oMap, oSlice, err := opts.Orgs()
173-
if err != nil || len(oSlice) == 0 {
174-
return errors.New("no orgs")
175-
}
176-
177-
p := prompt.NewOrgSelect(oSlice)
178-
var orgID string
179-
if err := survey.AskOne(p, &orgID); err != nil {
180-
return err
181-
}
182-
opts.OrgID = oMap[orgID]
183-
return nil
184-
}
185-
186-
func (opts *loginOpts) askProject() error {
187-
pMap, pSlice, err := opts.Projects()
188-
if err != nil || len(pSlice) == 0 {
189-
return errors.New("no projects")
190-
}
191-
192-
p := prompt.NewProjectSelect(pSlice)
193-
var projectID string
194-
if err := survey.AskOne(p, &projectID); err != nil {
195-
return err
196-
}
197-
opts.ProjectID = pMap[projectID]
198-
return nil
199-
}
200-
201169
func LoginBuilder() *cobra.Command {
202170
opts := &loginOpts{}
203171
cmd := &cobra.Command{

internal/cli/config_opts.go

Lines changed: 0 additions & 81 deletions
This file was deleted.

internal/cli/default_setter_opts.go

Lines changed: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ package cli
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"io"
21-
"os"
2222

2323
"github.com/AlecAivazis/survey/v2"
2424
"github.com/mongodb/mongocli/internal/config"
2525
"github.com/mongodb/mongocli/internal/mongosh"
26+
"github.com/mongodb/mongocli/internal/prompt"
2627
"github.com/mongodb/mongocli/internal/store"
2728
"github.com/mongodb/mongocli/internal/validate"
2829
atlas "go.mongodb.org/atlas/mongodbatlas"
@@ -34,6 +35,7 @@ import (
3435
type ProjectOrgsLister interface {
3536
Projects(*atlas.ListOptions) (interface{}, error)
3637
Organizations(*atlas.OrganizationsListOptions) (*atlas.Organizations, error)
38+
GetOrgProjects(string, *atlas.ListOptions) (interface{}, error)
3739
}
3840

3941
type DefaultSetterOpts struct {
@@ -61,38 +63,67 @@ func (opts *DefaultSetterOpts) IsOpsManager() bool {
6163
return opts.Service == config.OpsManagerService
6264
}
6365

66+
const resultsLimit = 500
67+
68+
var (
69+
errTooManyResults = errors.New("too many results")
70+
errNoResults = errors.New("no results")
71+
)
72+
6473
// Projects fetches projects and returns then as a slice of the format `nameIDFormat`,
6574
// and a map such as `map[nameIDFormat]=ID`.
6675
// This is necessary as we can only prompt using `nameIDFormat`
67-
// and we want them to get the ID mapping to store on the config.
68-
func (opts *DefaultSetterOpts) Projects() (pMap map[string]string, pSlice []string, err error) {
69-
projects, err := opts.Store.Projects(nil)
76+
// and we want them to get the ID mapping to store in the config.
77+
func (opts *DefaultSetterOpts) projects() (pMap map[string]string, pSlice []string, err error) {
78+
var projects interface{}
79+
if opts.OrgID == "" {
80+
projects, err = opts.Store.Projects(nil)
81+
} else {
82+
projects, err = opts.Store.GetOrgProjects(opts.OrgID, &atlas.ListOptions{ItemsPerPage: resultsLimit})
83+
}
7084
if err != nil {
71-
_, _ = fmt.Fprintf(os.Stderr, "there was a problem fetching projects: %s\n", err)
7285
return nil, nil, err
7386
}
74-
if opts.IsCloud() {
75-
pMap, pSlice = atlasProjects(projects.(*atlas.Projects).Results)
76-
} else {
77-
pMap, pSlice = omProjects(projects.(*opsmngr.Projects).Results)
87+
switch r := projects.(type) {
88+
case *atlas.Projects:
89+
if r.TotalCount == 0 {
90+
return nil, nil, errNoResults
91+
}
92+
if r.TotalCount > resultsLimit {
93+
return nil, nil, errTooManyResults
94+
}
95+
pMap, pSlice = atlasProjects(r.Results)
96+
case *opsmngr.Projects:
97+
if r.TotalCount == 0 {
98+
return nil, nil, errNoResults
99+
}
100+
if r.TotalCount > resultsLimit {
101+
return nil, nil, errTooManyResults
102+
}
103+
pMap, pSlice = omProjects(r.Results)
78104
}
105+
79106
return pMap, pSlice, nil
80107
}
81108

82109
// Orgs fetches organizations and returns then as a slice of the format `nameIDFormat`,
83110
// and a map such as `map[nameIDFormat]=ID`.
84111
// This is necessary as we can only prompt using `nameIDFormat`
85112
// and we want them to get the ID mapping to store on the config.
86-
func (opts *DefaultSetterOpts) Orgs() (oMap map[string]string, oSlice []string, err error) {
113+
func (opts *DefaultSetterOpts) orgs() (oMap map[string]string, oSlice []string, err error) {
87114
includeDeleted := false
88-
orgs, err := opts.Store.Organizations(&atlas.OrganizationsListOptions{IncludeDeletedOrgs: &includeDeleted})
89-
if orgs != nil && orgs.TotalCount > len(orgs.Results) {
90-
orgs, err = opts.Store.Organizations(&atlas.OrganizationsListOptions{IncludeDeletedOrgs: &includeDeleted, ListOptions: atlas.ListOptions{ItemsPerPage: orgs.TotalCount}})
91-
}
115+
pagination := &atlas.OrganizationsListOptions{IncludeDeletedOrgs: &includeDeleted}
116+
pagination.ItemsPerPage = resultsLimit
117+
orgs, err := opts.Store.Organizations(pagination)
92118
if err != nil {
93-
_, _ = fmt.Fprintf(os.Stderr, "there was a problem fetching orgs: %s\n", err)
94119
return nil, nil, err
95120
}
121+
if orgs.TotalCount == 0 {
122+
return nil, nil, errNoResults
123+
}
124+
if orgs.TotalCount > resultsLimit {
125+
return nil, nil, errTooManyResults
126+
}
96127
oMap = make(map[string]string, len(orgs.Results))
97128
oSlice = make([]string, len(orgs.Results))
98129
for i, o := range orgs.Results {
@@ -103,6 +134,85 @@ func (opts *DefaultSetterOpts) Orgs() (oMap map[string]string, oSlice []string,
103134
return oMap, oSlice, nil
104135
}
105136

137+
// AskProject will try to construct a select based on fetched projects.
138+
// If it fails or there are no projects to show we fallback to ask for project by ID.
139+
func (opts *DefaultSetterOpts) AskProject() error {
140+
pMap, pSlice, err := opts.projects()
141+
if err != nil {
142+
var target *atlas.ErrorResponse
143+
switch {
144+
case errors.Is(err, errNoResults):
145+
_, _ = fmt.Fprintln(opts.OutWriter, "You don't seem to have access to any project")
146+
case errors.Is(err, errTooManyResults):
147+
_, _ = fmt.Fprintf(opts.OutWriter, "You have access to more than %d projects\n", resultsLimit)
148+
case errors.As(err, &target):
149+
_, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your projects: %s\n", target.Detail)
150+
default:
151+
_, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your projects: %s\n", err)
152+
}
153+
p := &survey.Confirm{
154+
Message: "Do you want to enter the Project ID manually?",
155+
}
156+
manually := true
157+
if err2 := survey.AskOne(p, &manually); err2 != nil {
158+
return err2
159+
}
160+
if manually {
161+
p := prompt.NewProjectIDInput()
162+
return survey.AskOne(p, &opts.ProjectID, survey.WithValidator(validate.OptionalObjectID))
163+
}
164+
_, _ = fmt.Fprint(opts.OutWriter, "Skipping default project setting\n")
165+
return nil
166+
}
167+
168+
p := prompt.NewProjectSelect(pSlice)
169+
var projectID string
170+
if err := survey.AskOne(p, &projectID); err != nil {
171+
return err
172+
}
173+
opts.ProjectID = pMap[projectID]
174+
return nil
175+
}
176+
177+
// AskOrg will try to construct a select based on fetched organizations.
178+
// If it fails or there are no organizations to show we fallback to ask for org by ID.
179+
func (opts *DefaultSetterOpts) AskOrg() error {
180+
oMap, oSlice, err := opts.orgs()
181+
if err != nil {
182+
var target *atlas.ErrorResponse
183+
switch {
184+
case errors.Is(err, errNoResults):
185+
_, _ = fmt.Fprintln(opts.OutWriter, "You don't seem to have access to any organization")
186+
case errors.Is(err, errTooManyResults):
187+
_, _ = fmt.Fprintf(opts.OutWriter, "You have access to more than %d organizations\n", resultsLimit)
188+
case errors.As(err, &target):
189+
_, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your organizations: %s\n", target.Detail)
190+
default:
191+
_, _ = fmt.Fprintf(opts.OutWriter, "There was an error fetching your organizations: %s\n", err)
192+
}
193+
p := &survey.Confirm{
194+
Message: "Do you want to enter the Org ID manually?",
195+
}
196+
manually := true
197+
if err2 := survey.AskOne(p, &manually); err2 != nil {
198+
return err2
199+
}
200+
if manually {
201+
p := prompt.NewOrgIDInput()
202+
return survey.AskOne(p, &opts.OrgID, survey.WithValidator(validate.OptionalObjectID))
203+
}
204+
_, _ = fmt.Fprint(opts.OutWriter, "Skipping default organization setting\n")
205+
return nil
206+
}
207+
p := prompt.NewOrgSelect(oSlice)
208+
var orgID string
209+
if err := survey.AskOne(p, &orgID); err != nil {
210+
return err
211+
}
212+
opts.OrgID = oMap[orgID]
213+
return nil
214+
}
215+
106216
func (opts *DefaultSetterOpts) SetUpProject() {
107217
if opts.ProjectID != "" {
108218
config.SetProjectID(opts.ProjectID)

0 commit comments

Comments
 (0)