From af7aebf8cf12c4762adbcf7a5dafd56f303fef14 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 2 Nov 2020 16:24:35 +0100 Subject: [PATCH] Make newcomer experience smooth Signed-off-by: Nicolas De Loof --- ecs/context.go | 241 ++++++++++-------- ecs/context_test.go | 173 +++++++++++++ ecs/testdata/context/by-keys/config.golden | 3 + .../context/by-keys/credentials.golden | 4 + ecs/testdata/context/by-profile/config.golden | 3 + .../context/by-profile/credentials.golden | 4 + go.sum | 1 + prompt/prompt.go | 2 + prompt/prompt_mock.go | 93 +++++++ 9 files changed, 423 insertions(+), 101 deletions(-) create mode 100644 ecs/context_test.go create mode 100644 ecs/testdata/context/by-keys/config.golden create mode 100644 ecs/testdata/context/by-keys/credentials.golden create mode 100644 ecs/testdata/context/by-profile/config.golden create mode 100644 ecs/testdata/context/by-profile/credentials.golden create mode 100644 prompt/prompt_mock.go diff --git a/ecs/context.go b/ecs/context.go index 15d5d0681..5d636ac37 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -20,17 +20,22 @@ import ( "context" "fmt" "os" + "path/filepath" + "sort" "strings" - "github.com/AlecAivazis/survey/v2/terminal" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/defaults" - "gopkg.in/ini.v1" - "github.com/docker/compose-cli/context/store" "github.com/docker/compose-cli/errdefs" "github.com/docker/compose-cli/prompt" + + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/pkg/errors" + "gopkg.in/ini.v1" ) func getEnvVars() ContextParams { @@ -57,66 +62,77 @@ func getEnvVars() ContextParams { } type contextCreateAWSHelper struct { - user prompt.UI + user prompt.UI + availableRegions func(opts *ContextParams) ([]string, error) } func newContextCreateHelper() contextCreateAWSHelper { return contextCreateAWSHelper{ - user: prompt.User{}, + user: prompt.User{}, + availableRegions: listAvailableRegions, } } func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { if opts.CredsFromEnv { + // Explicit creation from ENV variables ecsCtx, descr := h.createContext(&opts) return ecsCtx, descr, nil - } - if opts.Profile != "" { + } else if opts.AccessKey != "" && opts.SecretKey != "" { + // Explicit creation using keys + err := h.createProfileFromCredentials(&opts) + if err != nil { + return nil, "", err + } + } else if opts.Profile != "" { + // Excplicit creation by selecting a profile // check profile exists profilesList, err := getProfiles() if err != nil { return nil, "", err } if !contains(profilesList, opts.Profile) { - return nil, "", fmt.Errorf("profile %q not found", opts.Profile) + return nil, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q not found", opts.Profile) } - ecsCtx, descr := h.createContext(&opts) - return ecsCtx, descr, nil - } - options := []string{ - "An existing AWS profile", - "A new AWS profile", - "AWS environment variables", - } + } else { + // interactive + var options []string + var actions []func(params *ContextParams) error - selected, err := h.user.Select("Create a Docker context using:", options) - if err != nil { - if err == terminal.InterruptErr { - return nil, "", errdefs.ErrCanceled + if _, err := os.Stat(getAWSConfigFile()); err == nil { + // User has .aws/config file, so we can offer to select one of his profiles + options = append(options, "An existing AWS profile") + actions = append(actions, h.selectFromLocalProfile) + } + + options = append(options, "AWS secret and token credentials") + actions = append(actions, h.createProfileFromCredentials) + + options = append(options, "AWS environment variables") + actions = append(actions, func(params *ContextParams) error { + opts.CredsFromEnv = true + return nil + }) + + selected, err := h.user.Select("Create a Docker context using:", options) + if err != nil { + if err == terminal.InterruptErr { + return nil, "", errdefs.ErrCanceled + } + return nil, "", err + } + + err = actions[selected](&opts) + if err != nil { + return nil, "", err } - return nil, "", err } - switch selected { - case 0: - err = h.selectFromLocalProfile(&opts) - case 1: - err = h.createProfileFromCredentials(&opts) - case 2: - opts.CredsFromEnv = true - - } - if err != nil { - return nil, "", err - } ecsCtx, descr := h.createContext(&opts) return ecsCtx, descr, nil } func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, string) { - if c.Profile == "default" { - c.Profile = "" - } var description string if c.CredsFromEnv { @@ -148,56 +164,42 @@ func (h contextCreateAWSHelper) selectFromLocalProfile(opts *ContextParams) erro } func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams) error { - accessKey, secretKey, err := h.askCredentials() - if err != nil { - return err - } - opts.AccessKey = accessKey - opts.SecretKey = secretKey - // we need a region set -- either read it from profile or prompt user - // prompt for the region to use with this context - opts.Region, err = h.chooseRegion(opts.Region, opts.Profile) - if err != nil { - return err - } - // save as a profile - if opts.Profile == "" { - opts.Profile = opts.Name - } - // check profile does not already exist - profilesList, err := getProfiles() - if err != nil { - return err - } - if contains(profilesList, opts.Profile) { - return fmt.Errorf("profile %q already exists", opts.Profile) - } - fmt.Printf("Saving to profile %q\n", opts.Profile) - // context name used as profile name - err = h.saveCredentials(opts.Name, opts.AccessKey, opts.SecretKey) - if err != nil { - return err - } - return h.saveRegion(opts.Name, opts.Region) -} - -func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { - p := credentials.SharedCredentialsProvider{Profile: profile} - _, err := p.Retrieve() - if err == nil { - return fmt.Errorf("credentials already exist") + if opts.AccessKey == "" || opts.SecretKey == "" { + fmt.Println("Retrieve or create AWS Access Key and Secret on https://console.aws.amazon.com/iam/home?#security_credential") + accessKey, secretKey, err := h.askCredentials() + if err != nil { + return err + } + opts.AccessKey = accessKey + opts.SecretKey = secretKey } - if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" { - _, err := os.Create(p.Filename) + if opts.Region == "" { + err := h.chooseRegion(opts) if err != nil { return err } } - credIni, err := ini.Load(p.Filename) + // save as a profile + if opts.Profile == "" { + opts.Profile = "default" + } + // context name used as profile name + err := h.saveCredentials(opts.Profile, opts.AccessKey, opts.SecretKey) if err != nil { return err } + return h.saveRegion(opts.Profile, opts.Region) +} + +func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { + file := getAWSCredentialsFile() + err := os.MkdirAll(filepath.Dir(file), 0700) + if err != nil { + return err + } + + credIni := ini.Empty() section, err := credIni.NewSection(profile) if err != nil { return err @@ -210,7 +212,7 @@ func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID stri if err != nil { return err } - return credIni.SaveTo(p.Filename) + return credIni.SaveTo(file) } func (h contextCreateAWSHelper) saveRegion(profile, region string) error { @@ -218,7 +220,7 @@ func (h contextCreateAWSHelper) saveRegion(profile, region string) error { return nil } // loads ~/.aws/config - awsConfig := defaults.SharedConfigFilename() + awsConfig := getAWSConfigFile() configIni, err := ini.Load(awsConfig) if err != nil { if !os.IsNotExist(err) { @@ -249,8 +251,8 @@ func getProfiles() ([]string, error) { profiles := []string{} // parse both .aws/credentials and .aws/config for profiles configFiles := map[string]bool{ - defaults.SharedCredentialsFilename(): false, - defaults.SharedConfigFilename(): true, + getAWSCredentialsFile(): false, + getAWSConfigFile(): true, } for f, prefix := range configFiles { sections, err := loadIniFile(f, prefix) @@ -267,6 +269,10 @@ func getProfiles() ([]string, error) { } } } + sort.Slice(profiles, func(i, j int) bool { + return profiles[i] < profiles[j] + }) + return profiles, nil } @@ -316,32 +322,49 @@ func getRegion(profile string) (string, error) { region := getProfileRegion(profile) if region == "" { region = getProfileRegion("default") - if region == "" { - return "us-east-1", nil - } - return region, nil + } + if region == "" { + // fallback to AWS default + region = "us-east-1" } return region, nil } -func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { - suggestion := region - if suggestion == "" { - region, err := getRegion(profile) - if err != nil { - return "", err - } - suggestion = region +func (h contextCreateAWSHelper) chooseRegion(opts *ContextParams) error { + regions, err := h.availableRegions(opts) + if err != nil { + return err } // promp user for region - region, err := h.user.Input("Region", suggestion) + selected, err := h.user.Select("Region", regions) if err != nil { - return "", err + return err } - if region == "" { - return "", fmt.Errorf("region cannot be empty") + opts.Region = regions[selected] + return nil +} + +func listAvailableRegions(opts *ContextParams) ([]string, error) { + // Setup SDK with credentials, will also validate those + session, err := session.NewSessionWithOptions(session.Options{ + Config: aws.Config{ + Credentials: credentials.NewStaticCredentials(opts.AccessKey, opts.SecretKey, ""), + Region: aws.String("us-east-1"), + }, + }) + if err != nil { + return nil, err } - return region, nil + + desc, err := ec2.New(session).DescribeRegions(&ec2.DescribeRegionsInput{}) + if err != nil { + return nil, err + } + var regions []string + for _, r := range desc.Regions { + regions = append(regions, aws.StringValue(r.RegionName)) + } + return regions, nil } func (h contextCreateAWSHelper) askCredentials() (string, string, error) { @@ -384,3 +407,19 @@ func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) { } return profiles, nil } + +func getAWSConfigFile() string { + awsConfig, ok := os.LookupEnv("AWS_CONFIG_FILE") + if !ok { + awsConfig = defaults.SharedConfigFilename() + } + return awsConfig +} + +func getAWSCredentialsFile() string { + awsConfig, ok := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE") + if !ok { + awsConfig = defaults.SharedCredentialsFilename() + } + return awsConfig +} diff --git a/ecs/context_test.go b/ecs/context_test.go new file mode 100644 index 000000000..a69385e75 --- /dev/null +++ b/ecs/context_test.go @@ -0,0 +1,173 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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 ecs + +import ( + "context" + "os" + "testing" + + "github.com/docker/compose-cli/context/store" + "github.com/docker/compose-cli/prompt" + + "github.com/golang/mock/gomock" + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" + "gotest.tools/v3/golden" +) + +func TestCreateContextDataFromEnv(t *testing.T) { + c := contextCreateAWSHelper{ + user: nil, + } + data, desc, err := c.createContextData(context.TODO(), ContextParams{ + Name: "test", + CredsFromEnv: true, + }) + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).CredentialsFromEnv, true) + assert.Equal(t, desc, "credentials read from environment") +} + +func TestCreateContextDataByKeys(t *testing.T) { + dir := fs.NewDir(t, "aws") + os.Setenv("AWS_CONFIG_FILE", dir.Join("config")) // nolint:errcheck + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", dir.Join("credentials")) // nolint:errcheck + + defer os.Unsetenv("AWS_CONFIG_FILE") // nolint:errcheck + defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") // nolint:errcheck + + c := contextCreateAWSHelper{ + user: nil, + } + + data, _, err := c.createContextData(context.TODO(), ContextParams{ + Name: "test", + AccessKey: "ABCD", + SecretKey: "X&123", + Region: "eu-west-3", + }) + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).Profile, "default") + + s := golden.Get(t, dir.Join("config")) + golden.Assert(t, string(s), "context/by-keys/config.golden") + + s = golden.Get(t, dir.Join("credentials")) + golden.Assert(t, string(s), "context/by-keys/credentials.golden") +} + +func TestCreateContextDataFromProfile(t *testing.T) { + os.Setenv("AWS_CONFIG_FILE", "testdata/context/by-profile/config.golden") // nolint:errcheck + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/context/by-profile/credentials.golden") // nolint:errcheck + + defer os.Unsetenv("AWS_CONFIG_FILE") // nolint:errcheck + defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") // nolint:errcheck + + c := contextCreateAWSHelper{ + user: nil, + } + + data, _, err := c.createContextData(context.TODO(), ContextParams{ + Name: "test", + Profile: "foo", + }) + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).Profile, "foo") +} + +func TestCreateContextDataFromEnvInteractive(t *testing.T) { + dir := fs.NewDir(t, "aws") + os.Setenv("AWS_CONFIG_FILE", dir.Join("config")) // nolint:errcheck + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", dir.Join("credentials")) // nolint:errcheck + + defer os.Unsetenv("AWS_CONFIG_FILE") // nolint:errcheck + defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") // nolint:errcheck + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ui := prompt.NewMockUI(ctrl) + c := contextCreateAWSHelper{ + user: ui, + } + + ui.EXPECT().Select("Create a Docker context using:", gomock.Any()).Return(1, nil) + data, _, err := c.createContextData(context.TODO(), ContextParams{}) + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).CredentialsFromEnv, true) +} + +func TestCreateContextDataByKeysInteractive(t *testing.T) { + dir := fs.NewDir(t, "aws") + os.Setenv("AWS_CONFIG_FILE", dir.Join("config")) // nolint:errcheck + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", dir.Join("credentials")) // nolint:errcheck + + defer os.Unsetenv("AWS_CONFIG_FILE") // nolint:errcheck + defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") // nolint:errcheck + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ui := prompt.NewMockUI(ctrl) + c := contextCreateAWSHelper{ + user: ui, + availableRegions: func(opts *ContextParams) ([]string, error) { + return []string{"us-east-1", "eu-west-3"}, nil + }, + } + + ui.EXPECT().Select("Create a Docker context using:", gomock.Any()).Return(0, nil) + ui.EXPECT().Input("AWS Access Key ID", gomock.Any()).Return("ABCD", nil) + ui.EXPECT().Password("Enter AWS Secret Access Key").Return("X&123", nil) + ui.EXPECT().Select("Region", []string{"us-east-1", "eu-west-3"}).Return(1, nil) + + data, _, err := c.createContextData(context.TODO(), ContextParams{}) + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).Profile, "default") + + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).Profile, "default") + + s := golden.Get(t, dir.Join("config")) + golden.Assert(t, string(s), "context/by-keys/config.golden") + + s = golden.Get(t, dir.Join("credentials")) + golden.Assert(t, string(s), "context/by-keys/credentials.golden") +} + +func TestCreateContextDataByProfileInteractive(t *testing.T) { + os.Setenv("AWS_CONFIG_FILE", "testdata/context/by-profile/config.golden") // nolint:errcheck + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/context/by-profile/credentials.golden") // nolint:errcheck + + defer os.Unsetenv("AWS_CONFIG_FILE") // nolint:errcheck + defer os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE") // nolint:errcheck + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ui := prompt.NewMockUI(ctrl) + c := contextCreateAWSHelper{ + user: ui, + } + ui.EXPECT().Select("Create a Docker context using:", gomock.Any()).Return(0, nil) + ui.EXPECT().Select("Select AWS Profile", []string{"default", "foo"}).Return(1, nil) + + data, _, err := c.createContextData(context.TODO(), ContextParams{}) + assert.NilError(t, err) + assert.Equal(t, data.(store.EcsContext).Profile, "foo") +} diff --git a/ecs/testdata/context/by-keys/config.golden b/ecs/testdata/context/by-keys/config.golden new file mode 100644 index 000000000..6d2318321 --- /dev/null +++ b/ecs/testdata/context/by-keys/config.golden @@ -0,0 +1,3 @@ +[profile default] +region = eu-west-3 + diff --git a/ecs/testdata/context/by-keys/credentials.golden b/ecs/testdata/context/by-keys/credentials.golden new file mode 100644 index 000000000..2c69e47ef --- /dev/null +++ b/ecs/testdata/context/by-keys/credentials.golden @@ -0,0 +1,4 @@ +[default] +aws_access_key_id = ABCD +aws_secret_access_key = X&123 + diff --git a/ecs/testdata/context/by-profile/config.golden b/ecs/testdata/context/by-profile/config.golden new file mode 100644 index 000000000..e388f9b02 --- /dev/null +++ b/ecs/testdata/context/by-profile/config.golden @@ -0,0 +1,3 @@ +[profile foo] +region = eu-west-3 + diff --git a/ecs/testdata/context/by-profile/credentials.golden b/ecs/testdata/context/by-profile/credentials.golden new file mode 100644 index 000000000..7146be5fe --- /dev/null +++ b/ecs/testdata/context/by-profile/credentials.golden @@ -0,0 +1,4 @@ +[foo] +aws_access_key_id = ABCD +aws_secret_access_key = X&123 + diff --git a/go.sum b/go.sum index ca4c41b57..b44dfaccd 100644 --- a/go.sum +++ b/go.sum @@ -746,6 +746,7 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/prompt/prompt.go b/prompt/prompt.go index ac0ff9423..f30fbce3b 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -20,6 +20,8 @@ import ( "github.com/AlecAivazis/survey/v2" ) +//go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose-cli/prompt" -package=prompt . UI + // UI - prompt user input type UI interface { Select(message string, options []string) (int, error) diff --git a/prompt/prompt_mock.go b/prompt/prompt_mock.go new file mode 100644 index 000000000..06886e0fb --- /dev/null +++ b/prompt/prompt_mock.go @@ -0,0 +1,93 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/docker/compose-cli/prompt (interfaces: UI) + +// Package prompt is a generated GoMock package. +package prompt + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockUI is a mock of UI interface +type MockUI struct { + ctrl *gomock.Controller + recorder *MockUIMockRecorder +} + +// MockUIMockRecorder is the mock recorder for MockUI +type MockUIMockRecorder struct { + mock *MockUI +} + +// NewMockUI creates a new mock instance +func NewMockUI(ctrl *gomock.Controller) *MockUI { + mock := &MockUI{ctrl: ctrl} + mock.recorder = &MockUIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockUI) EXPECT() *MockUIMockRecorder { + return m.recorder +} + +// Confirm mocks base method +func (m *MockUI) Confirm(arg0 string, arg1 bool) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Confirm", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Confirm indicates an expected call of Confirm +func (mr *MockUIMockRecorder) Confirm(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Confirm", reflect.TypeOf((*MockUI)(nil).Confirm), arg0, arg1) +} + +// Input mocks base method +func (m *MockUI) Input(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Input", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Input indicates an expected call of Input +func (mr *MockUIMockRecorder) Input(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Input", reflect.TypeOf((*MockUI)(nil).Input), arg0, arg1) +} + +// Password mocks base method +func (m *MockUI) Password(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Password", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Password indicates an expected call of Password +func (mr *MockUIMockRecorder) Password(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Password", reflect.TypeOf((*MockUI)(nil).Password), arg0) +} + +// Select mocks base method +func (m *MockUI) Select(arg0 string, arg1 []string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Select", arg0, arg1) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Select indicates an expected call of Select +func (mr *MockUIMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockUI)(nil).Select), arg0, arg1) +}