From 2dcd1a5826282d56bd6b10182afb6e06c635de98 Mon Sep 17 00:00:00 2001 From: aiordache Date: Mon, 26 Oct 2020 22:01:54 +0100 Subject: [PATCH 1/9] refactor ecs context creation Signed-off-by: aiordache --- cli/cmd/context/create_ecs.go | 1 + ecs/backend.go | 1 + ecs/context.go | 155 +++++++++++++++++++++++++++++----- 3 files changed, 135 insertions(+), 22 deletions(-) diff --git a/cli/cmd/context/create_ecs.go b/cli/cmd/context/create_ecs.go index 8dde7b04..fa583795 100644 --- a/cli/cmd/context/create_ecs.go +++ b/cli/cmd/context/create_ecs.go @@ -45,6 +45,7 @@ func createEcsCommand() *cobra.Command { Short: "Create a context for Amazon ECS", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + opts.Name = args[0] if localSimulation { return runCreateLocalSimulation(cmd.Context(), args[0], opts) } diff --git a/ecs/backend.go b/ecs/backend.go index 5d18bbc8..faa4da4e 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -38,6 +38,7 @@ const backendType = store.EcsContextType // ContextParams options for creating AWS context type ContextParams struct { + Name string Description string Region string Profile string diff --git a/ecs/context.go b/ecs/context.go index ce91a853..aa8e83b0 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -26,7 +26,6 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/defaults" - "github.com/pkg/errors" "gopkg.in/ini.v1" "github.com/docker/compose-cli/context/store" @@ -34,6 +33,21 @@ import ( "github.com/docker/compose-cli/prompt" ) +type contextElements struct { + AccessKey string + SecretKey string + SessionToken string + Profile string + Region string +} + +func (c contextElements) HaveRequiredProps() bool { + if c.AccessKey != "" && c.SecretKey != "" { //} && c.Region != "" { + return true + } + return false +} + type contextCreateAWSHelper struct { user prompt.UI } @@ -44,7 +58,21 @@ func newContextCreateHelper() contextCreateAWSHelper { } } -func (h contextCreateAWSHelper) createProfile(name string) error { +func (h contextCreateAWSHelper) createProfile(name string, c *contextElements) error { + if c != nil { + if c.AccessKey != "" && c.SecretKey != "" { + return h.saveCredentials(name, c.AccessKey, c.SecretKey) + } + accessKey, secretKey, err := h.askCredentials() + + if err != nil { + return err + } + c.AccessKey = accessKey + c.SecretKey = secretKey + return h.saveCredentials(name, c.AccessKey, c.SecretKey) + } + accessKey, secretKey, err := h.askCredentials() if err != nil { return err @@ -67,33 +95,114 @@ func (h contextCreateAWSHelper) createContext(profile, region, description strin }, description } -func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { - profile := opts.Profile - region := opts.Region +func (h contextCreateAWSHelper) ParseEnvVars(c *contextElements) { + profile := os.Getenv("AWS_PROFILE") + if profile != "" { + c.Profile = profile + } + region := os.Getenv("AWS_REGION") + if region != "" { + c.Region = region + } - profilesList, err := h.getProfiles() + p := credentials.EnvProvider{} + creds, err := p.Retrieve() if err != nil { + return + } + c.AccessKey = creds.AccessKeyID + c.SecretKey = creds.SecretAccessKey + c.SessionToken = creds.SessionToken +} + +func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { + creds := contextElements{} + + h.ParseEnvVars(&creds) + + options := []string{} + if creds.HaveRequiredProps() { + options = append(options, "existing AWS environment variables (detected credentials)") + } + options = append(options, "enter AWS credentials", "use an AWS profile or create a new profile (advanced)") + selected, err := h.user.Select("Would you like to create your context based on", options) + if err != nil { + if err == terminal.InterruptErr { + return nil, "", errdefs.ErrCanceled + } return nil, "", err } - if profile != "" { - // validate profile - if profile != "default" && !contains(profilesList, profile) { - return nil, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q", profile) + + if len(options) == 2 { + selected = selected + 1 + } + if selected != 0 { + creds = contextElements{} + } + + if creds.Region == "" { + creds.Region = opts.Region + } + if creds.Profile == "" { + creds.Profile = opts.Profile + } + + switch selected { + case 0: + if creds.Region == "" { + // prompt for the region to use with this context + creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) + if err != nil { + return nil, "", err + } + } + if creds.Profile == "" { + creds.Profile = opts.Name + + } + fmt.Printf("Saving credentials under profile %s\n", creds.Profile) + h.createProfile(creds.Profile, &creds) + case 1: + accessKey, secretKey, err := h.askCredentials() + if err != nil { + return nil, "", err + } + creds.AccessKey = accessKey + creds.SecretKey = secretKey + // we need a region set -- either read it from profile or prompt user + + // prompt for the region to use with this context + creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) + if err != nil { + return nil, "", err + } + // save as a profile + if creds.Profile == "" { + creds.Profile = opts.Name + } + fmt.Printf("Saving credentials under profile %s\n", creds.Profile) + h.createProfile(creds.Profile, &creds) + + case 2: + profilesList, err := h.getProfiles() + if err != nil { + return nil, "", err } - } else { // choose profile - profile, err = h.chooseProfile(profilesList) + creds.Profile, err = h.chooseProfile(profilesList) if err != nil { return nil, "", err } - } - if region == "" { - region, err = h.chooseRegion(region, profile) - if err != nil { - return nil, "", err + if creds.Region == "" { + creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) + if err != nil { + return nil, "", err + } } } - ecsCtx, descr := h.createContext(profile, region, opts.Description) + + os.Exit(0) + ecsCtx, descr := h.createContext(creds.Profile, creds.Region, opts.Description) return ecsCtx, descr, nil } @@ -178,14 +287,16 @@ func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) if name == "" { return "", fmt.Errorf("profile name cannot be empty") } - return name, h.createProfile(name) + return name, h.createProfile(name, nil) } return profile, nil } func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { suggestion := region - + if profile == "" { + profile = "default" + } // only load ~/.aws/config awsConfig := defaults.SharedConfigFilename() configIni, err := ini.Load(awsConfig) @@ -230,13 +341,13 @@ func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (str } func (h contextCreateAWSHelper) askCredentials() (string, string, error) { - confirm, err := h.user.Confirm("Enter AWS credentials", false) + /*confirm, err := h.user.Confirm("Enter AWS credentials", false) if err != nil { return "", "", err } if !confirm { return "", "", nil - } + }*/ accessKeyID, err := h.user.Input("AWS Access Key ID", "") if err != nil { From 902b660de17faf19879c7009b705d837c644d371 Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 28 Oct 2020 10:54:10 +0100 Subject: [PATCH 2/9] revisit context creation Signed-off-by: aiordache --- context/store/contextmetadata.go | 5 +- ecs/backend.go | 8 ++ ecs/context.go | 159 +++++++++++++++---------------- 3 files changed, 87 insertions(+), 85 deletions(-) diff --git a/context/store/contextmetadata.go b/context/store/contextmetadata.go index 771c074d..6bb19111 100644 --- a/context/store/contextmetadata.go +++ b/context/store/contextmetadata.go @@ -51,8 +51,9 @@ type AciContext struct { // EcsContext is the context for the AWS backend type EcsContext struct { - Profile string `json:",omitempty"` - Region string `json:",omitempty"` + CredentialsFromEnv bool `json:",omitempty"` + Profile string `json:",omitempty"` + Region string `json:",omitempty"` } // AwsContext is the context for the ecs plugin diff --git a/ecs/backend.go b/ecs/backend.go index faa4da4e..010cb239 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -18,6 +18,7 @@ package ecs import ( "context" + "fmt" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" @@ -61,6 +62,13 @@ func service(ctx context.Context) (backend.Service, error) { } func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { + if ecsCtx.CredentialsFromEnv { + creds := getEnvVars() + if !creds.HaveRequiredCredentials() { + return nil, fmt.Errorf(`context requires credentials to be passed as environment variable.`) + } + } + sess, err := session.NewSessionWithOptions(session.Options{ Profile: ecsCtx.Profile, SharedConfigState: session.SharedConfigEnable, diff --git a/ecs/context.go b/ecs/context.go index aa8e83b0..553c553c 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -39,10 +39,11 @@ type contextElements struct { SessionToken string Profile string Region string + CredsFromEnv bool } -func (c contextElements) HaveRequiredProps() bool { - if c.AccessKey != "" && c.SecretKey != "" { //} && c.Region != "" { +func (c contextElements) HaveRequiredCredentials() bool { + if c.AccessKey != "" && c.SecretKey != "" { return true } return false @@ -58,6 +59,24 @@ func newContextCreateHelper() contextCreateAWSHelper { } } +func getEnvVars() contextElements { + c := contextElements{} + profile := os.Getenv("AWS_PROFILE") + if profile != "" { + c.Profile = profile + } + + p := credentials.EnvProvider{} + creds, err := p.Retrieve() + if err != nil { + return c + } + c.AccessKey = creds.AccessKeyID + c.SecretKey = creds.SecretAccessKey + c.SessionToken = creds.SessionToken + return c +} + func (h contextCreateAWSHelper) createProfile(name string, c *contextElements) error { if c != nil { if c.AccessKey != "" && c.SecretKey != "" { @@ -83,48 +102,34 @@ func (h contextCreateAWSHelper) createProfile(name string, c *contextElements) e return nil } -func (h contextCreateAWSHelper) createContext(profile, region, description string) (interface{}, string) { - if profile == "default" { - profile = "" +func (h contextCreateAWSHelper) createContext(c *contextElements, description string) (interface{}, string) { + if c.Profile == "default" { + c.Profile = "" } description = strings.TrimSpace( - fmt.Sprintf("%s (%s)", description, region)) + fmt.Sprintf("%s (%s)", description, c.Region)) + if c.CredsFromEnv { + return store.EcsContext{ + CredentialsFromEnv: c.CredsFromEnv, + Profile: c.Profile, + Region: c.Region, + }, description + } return store.EcsContext{ - Profile: profile, - Region: region, + Profile: c.Profile, + Region: c.Region, }, description } -func (h contextCreateAWSHelper) ParseEnvVars(c *contextElements) { - profile := os.Getenv("AWS_PROFILE") - if profile != "" { - c.Profile = profile - } - region := os.Getenv("AWS_REGION") - if region != "" { - c.Region = region - } - - p := credentials.EnvProvider{} - creds, err := p.Retrieve() - if err != nil { - return - } - c.AccessKey = creds.AccessKeyID - c.SecretKey = creds.SecretAccessKey - c.SessionToken = creds.SessionToken -} - func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { creds := contextElements{} - h.ParseEnvVars(&creds) - - options := []string{} - if creds.HaveRequiredProps() { - options = append(options, "existing AWS environment variables (detected credentials)") + options := []string{ + "Use AWS credentials set via environment variables", + "Create a new profile with AWS credentials", + "Select from existing local AWS profiles", } - options = append(options, "enter AWS credentials", "use an AWS profile or create a new profile (advanced)") + //if creds.HaveRequiredProps() { selected, err := h.user.Select("Would you like to create your context based on", options) if err != nil { if err == terminal.InterruptErr { @@ -132,14 +137,6 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex } return nil, "", err } - - if len(options) == 2 { - selected = selected + 1 - } - if selected != 0 { - creds = contextElements{} - } - if creds.Region == "" { creds.Region = opts.Region } @@ -149,19 +146,20 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex switch selected { case 0: + creds.CredsFromEnv = true + // confirm region profile should target if creds.Region == "" { - // prompt for the region to use with this context creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) if err != nil { return nil, "", err } } - if creds.Profile == "" { + /*if creds.Profile == "" { creds.Profile = opts.Name } fmt.Printf("Saving credentials under profile %s\n", creds.Profile) - h.createProfile(creds.Profile, &creds) + h.createProfile(creds.Profile, &creds)*/ case 1: accessKey, secretKey, err := h.askCredentials() if err != nil { @@ -201,8 +199,8 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex } } - os.Exit(0) - ecsCtx, descr := h.createContext(creds.Profile, creds.Region, opts.Description) + //os.Exit(0) + ecsCtx, descr := h.createContext(&creds, opts.Description) return ecsCtx, descr, nil } @@ -264,7 +262,7 @@ func (h contextCreateAWSHelper) getProfiles() ([]string, error) { } func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) { - options := []string{"new profile"} + options := []string{} options = append(options, profiles...) selected, err := h.user.Select("Select AWS Profile", options) @@ -275,54 +273,54 @@ func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) return "", err } profile := options[selected] - if options[selected] == "new profile" { - suggestion := "" - if !contains(profiles, "default") { - suggestion = "default" - } - name, err := h.user.Input("profile name", suggestion) - if err != nil { - return "", err - } - if name == "" { - return "", fmt.Errorf("profile name cannot be empty") - } - return name, h.createProfile(name, nil) - } return profile, nil } -func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { - suggestion := region +func (h contextCreateAWSHelper) getRegionSuggestion(region string, profile string) (string, error) { if profile == "" { profile = "default" } // only load ~/.aws/config awsConfig := defaults.SharedConfigFilename() configIni, err := ini.Load(awsConfig) - if err != nil { if !os.IsNotExist(err) { return "", err } configIni = ini.Empty() } + + var f func(string, string) string + f = func(r string, p string) string { + section, err := configIni.GetSection(p) + if err == nil { + reg, err := section.GetKey("region") + if err == nil { + r = reg.Value() + } + } + if r == "" { + switch p { + case "": + return "us-east-1" + case "default": + return f(r, "") + } + return f(r, "default") + } + return r + } + if profile != "default" { profile = fmt.Sprintf("profile %s", profile) } - section, err := configIni.GetSection(profile) + return f(region, profile), nil +} + +func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { + suggestion, err := h.getRegionSuggestion(region, profile) if err != nil { - if !strings.Contains(err.Error(), "does not exist") { - return "", err - } - section, err = configIni.NewSection(profile) - if err != nil { - return "", err - } - } - reg, err := section.GetKey("region") - if err == nil { - suggestion = reg.Value() + return "", err } // promp user for region region, err = h.user.Input("Region", suggestion) @@ -332,12 +330,7 @@ func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (str if region == "" { return "", fmt.Errorf("region cannot be empty") } - // save selected/typed region under profile in ~/.aws/config - _, err = section.NewKey("region", region) - if err != nil { - return "", err - } - return region, configIni.SaveTo(awsConfig) + return region, nil } func (h contextCreateAWSHelper) askCredentials() (string, string, error) { From e44d0b922af8f1c9772d8b3848a64c890e7c0f77 Mon Sep 17 00:00:00 2001 From: aiordache Date: Wed, 28 Oct 2020 10:59:09 +0100 Subject: [PATCH 3/9] cleanup Signed-off-by: aiordache --- ecs/context.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ecs/context.go b/ecs/context.go index 553c553c..3a19d3c8 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -154,12 +154,6 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex return nil, "", err } } - /*if creds.Profile == "" { - creds.Profile = opts.Name - - } - fmt.Printf("Saving credentials under profile %s\n", creds.Profile) - h.createProfile(creds.Profile, &creds)*/ case 1: accessKey, secretKey, err := h.askCredentials() if err != nil { @@ -199,7 +193,6 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex } } - //os.Exit(0) ecsCtx, descr := h.createContext(&creds, opts.Description) return ecsCtx, descr, nil } From 53efa312c809bff00d9f0f881f92313fc7e369a2 Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 29 Oct 2020 10:08:41 +0100 Subject: [PATCH 4/9] Refactor context create options Signed-off-by: aiordache --- cli/cmd/context/create_ecs.go | 1 + ecs/backend.go | 46 ++++++-- ecs/context.go | 200 +++++++++++++++++----------------- tests/ecs-e2e/e2e-ecs_test.go | 5 +- 4 files changed, 139 insertions(+), 113 deletions(-) diff --git a/cli/cmd/context/create_ecs.go b/cli/cmd/context/create_ecs.go index fa583795..1b2cd8bc 100644 --- a/cli/cmd/context/create_ecs.go +++ b/cli/cmd/context/create_ecs.go @@ -57,6 +57,7 @@ func createEcsCommand() *cobra.Command { cmd.Flags().BoolVar(&localSimulation, "local-simulation", false, "Create context for ECS local simulation endpoints") cmd.Flags().StringVar(&opts.Profile, "profile", "", "Profile") cmd.Flags().StringVar(&opts.Region, "region", "", "Region") + cmd.Flags().BoolVar(&opts.CredsFromEnv, "from-env", false, "Use credentials and region from environment") return cmd } diff --git a/ecs/backend.go b/ecs/backend.go index 010cb239..e16e3718 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -39,10 +39,24 @@ const backendType = store.EcsContextType // ContextParams options for creating AWS context type ContextParams struct { - Name string - Description string - Region string - Profile string + Name string + Description string + AccessKey string + SecretKey string + SessionToken string + Profile string + Region string + CredsFromEnv bool +} + +func (c ContextParams) HaveRequiredCredentials() bool { + if c.AccessKey == "" || c.SecretKey == "" { + return false + } + if c.Region == "" && c.Profile == "" { + return false + } + return true } func init() { @@ -62,18 +76,34 @@ func service(ctx context.Context) (backend.Service, error) { } func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { + var region string + var profile string + if ecsCtx.CredentialsFromEnv { creds := getEnvVars() if !creds.HaveRequiredCredentials() { return nil, fmt.Errorf(`context requires credentials to be passed as environment variable.`) } + region = creds.Region + profile = creds.Profile + } else { + // get region + profile = ecsCtx.Profile + if ecsCtx.Region != "" { + region = ecsCtx.Region + } else { + r, _, err := getRegion(ecsCtx.Profile) + if err != nil { + return nil, err + } + region = r + } } - sess, err := session.NewSessionWithOptions(session.Options{ - Profile: ecsCtx.Profile, + Profile: profile, SharedConfigState: session.SharedConfigEnable, Config: aws.Config{ - Region: aws.String(ecsCtx.Region), + Region: aws.String(region), }, }) if err != nil { @@ -83,7 +113,7 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { sdk := newSDK(sess) return &ecsAPIService{ ctx: ecsCtx, - Region: ecsCtx.Region, + Region: region, aws: sdk, }, nil } diff --git a/ecs/context.go b/ecs/context.go index 3a19d3c8..9fbc8fc1 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -33,38 +33,23 @@ import ( "github.com/docker/compose-cli/prompt" ) -type contextElements struct { - AccessKey string - SecretKey string - SessionToken string - Profile string - Region string - CredsFromEnv bool -} +func getEnvVars() ContextParams { + c := ContextParams{} -func (c contextElements) HaveRequiredCredentials() bool { - if c.AccessKey != "" && c.SecretKey != "" { - return true - } - return false -} - -type contextCreateAWSHelper struct { - user prompt.UI -} - -func newContextCreateHelper() contextCreateAWSHelper { - return contextCreateAWSHelper{ - user: prompt.User{}, - } -} - -func getEnvVars() contextElements { - c := contextElements{} + //check profile env vars profile := os.Getenv("AWS_PROFILE") if profile != "" { c.Profile = profile } + // check REGION env vars + region := os.Getenv("AWS_REGION") + if region == "" { + region = os.Getenv("AWS_DEFAULT_REGION") + if region == "" { + region = "us-east-1" + } + c.Region = region + } p := credentials.EnvProvider{} creds, err := p.Retrieve() @@ -77,7 +62,17 @@ func getEnvVars() contextElements { return c } -func (h contextCreateAWSHelper) createProfile(name string, c *contextElements) error { +type contextCreateAWSHelper struct { + user prompt.UI +} + +func newContextCreateHelper() contextCreateAWSHelper { + return contextCreateAWSHelper{ + user: prompt.User{}, + } +} + +func (h contextCreateAWSHelper) createProfile(name string, c *ContextParams) error { if c != nil { if c.AccessKey != "" && c.SecretKey != "" { return h.saveCredentials(name, c.AccessKey, c.SecretKey) @@ -102,19 +97,27 @@ func (h contextCreateAWSHelper) createProfile(name string, c *contextElements) e return nil } -func (h contextCreateAWSHelper) createContext(c *contextElements, description string) (interface{}, string) { +func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, string) { if c.Profile == "default" { c.Profile = "" } - description = strings.TrimSpace( - fmt.Sprintf("%s (%s)", description, c.Region)) + var description string + if c.CredsFromEnv { + if c.Description == "" { + description = "credentials read from environment" + } return store.EcsContext{ CredentialsFromEnv: c.CredsFromEnv, Profile: c.Profile, Region: c.Region, }, description } + + if c.Region != "" { + description = strings.TrimSpace( + fmt.Sprintf("%s (%s)", c.Description, c.Region)) + } return store.EcsContext{ Profile: c.Profile, Region: c.Region, @@ -122,14 +125,16 @@ func (h contextCreateAWSHelper) createContext(c *contextElements, description st } func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { - creds := contextElements{} - - options := []string{ - "Use AWS credentials set via environment variables", - "Create a new profile with AWS credentials", - "Select from existing local AWS profiles", + if opts.CredsFromEnv { + ecsCtx, descr := h.createContext(&opts) + return ecsCtx, descr, nil } - //if creds.HaveRequiredProps() { + options := []string{ + "Use AWS credentials from environment", + "Select from existing AWS profiles", + "Create new profile from AWS credentials", + } + selected, err := h.user.Select("Would you like to create your context based on", options) if err != nil { if err == terminal.InterruptErr { @@ -137,63 +142,56 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex } return nil, "", err } - if creds.Region == "" { - creds.Region = opts.Region - } - if creds.Profile == "" { - creds.Profile = opts.Profile - } switch selected { case 0: - creds.CredsFromEnv = true - // confirm region profile should target - if creds.Region == "" { - creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) - if err != nil { - return nil, "", err - } - } + opts.CredsFromEnv = true + case 1: - accessKey, secretKey, err := h.askCredentials() - if err != nil { - return nil, "", err - } - creds.AccessKey = accessKey - creds.SecretKey = secretKey - // we need a region set -- either read it from profile or prompt user - - // prompt for the region to use with this context - creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) - if err != nil { - return nil, "", err - } - // save as a profile - if creds.Profile == "" { - creds.Profile = opts.Name - } - fmt.Printf("Saving credentials under profile %s\n", creds.Profile) - h.createProfile(creds.Profile, &creds) - - case 2: - profilesList, err := h.getProfiles() + profilesList, err := getProfiles() if err != nil { return nil, "", err } // choose profile - creds.Profile, err = h.chooseProfile(profilesList) + opts.Profile, err = h.chooseProfile(profilesList) if err != nil { return nil, "", err } - if creds.Region == "" { - creds.Region, err = h.chooseRegion(creds.Region, creds.Profile) - if err != nil { - return nil, "", err + + if opts.Region == "" { + region, isDefinedInProfile, err := getRegion(opts.Profile) + if isDefinedInProfile { + opts.Region = region + } else { + fmt.Println("No region defined in the profile. Choose the region to use.") + opts.Region, err = h.chooseRegion(opts.Region, opts.Profile) + if err != nil { + return nil, "", err + } } } + case 2: + accessKey, secretKey, err := h.askCredentials() + if err != nil { + return nil, "", 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 nil, "", err + } + // save as a profile + if opts.Profile == "" { + opts.Profile = opts.Name + } + fmt.Printf("Saving credentials under profile %s\n", opts.Profile) + h.createProfile(opts.Profile, &opts) } - ecsCtx, descr := h.createContext(&creds, opts.Description) + ecsCtx, descr := h.createContext(&opts) return ecsCtx, descr, nil } @@ -229,7 +227,7 @@ func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID stri return credIni.SaveTo(p.Filename) } -func (h contextCreateAWSHelper) getProfiles() ([]string, error) { +func getProfiles() ([]string, error) { profiles := []string{} // parse both .aws/credentials and .aws/config for profiles configFiles := map[string]bool{ @@ -269,7 +267,7 @@ func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) return profile, nil } -func (h contextCreateAWSHelper) getRegionSuggestion(region string, profile string) (string, error) { +func getRegion(profile string) (string, bool, error) { if profile == "" { profile = "default" } @@ -278,13 +276,14 @@ func (h contextCreateAWSHelper) getRegionSuggestion(region string, profile strin configIni, err := ini.Load(awsConfig) if err != nil { if !os.IsNotExist(err) { - return "", err + return "", false, err } configIni = ini.Empty() } - var f func(string, string) string - f = func(r string, p string) string { + var f func(string) (string, string) + f = func(p string) (string, string) { + r := "" section, err := configIni.GetSection(p) if err == nil { reg, err := section.GetKey("region") @@ -295,28 +294,33 @@ func (h contextCreateAWSHelper) getRegionSuggestion(region string, profile strin if r == "" { switch p { case "": - return "us-east-1" + return "us-east-1", "" case "default": - return f(r, "") + return f("") } - return f(r, "default") + return f("default") } - return r + return r, p } if profile != "default" { profile = fmt.Sprintf("profile %s", profile) } - return f(region, profile), nil + region, p := f(profile) + return region, p == profile, nil } func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { - suggestion, err := h.getRegionSuggestion(region, profile) - if err != nil { - return "", err + suggestion := region + if suggestion == "" { + region, _, err := getRegion(profile) + if err != nil { + return "", err + } + suggestion = region } // promp user for region - region, err = h.user.Input("Region", suggestion) + region, err := h.user.Input("Region", suggestion) if err != nil { return "", err } @@ -327,14 +331,6 @@ func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (str } func (h contextCreateAWSHelper) askCredentials() (string, string, error) { - /*confirm, err := h.user.Confirm("Enter AWS credentials", false) - if err != nil { - return "", "", err - } - if !confirm { - return "", "", nil - }*/ - accessKeyID, err := h.user.Input("AWS Access Key ID", "") if err != nil { return "", "", err diff --git a/tests/ecs-e2e/e2e-ecs_test.go b/tests/ecs-e2e/e2e-ecs_test.go index 9acd30be..57885675 100644 --- a/tests/ecs-e2e/e2e-ecs_test.go +++ b/tests/ecs-e2e/e2e-ecs_test.go @@ -153,16 +153,15 @@ func setupTest(t *testing.T) (*E2eCLI, string) { if localTestProfile != "" { region := os.Getenv("TEST_AWS_REGION") assert.Check(t, region != "") - res = c.RunDockerCmd("context", "create", "ecs", contextName, "--profile", "default", "--region", region) + res = c.RunDockerCmd("context", "create", "ecs", contextName, "--from-env") } else { - profile := "default" region := os.Getenv("AWS_DEFAULT_REGION") secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") keyID := os.Getenv("AWS_ACCESS_KEY_ID") assert.Check(t, keyID != "") assert.Check(t, secretKey != "") assert.Check(t, region != "") - res = c.RunDockerCmd("context", "create", "ecs", contextName, "--profile", profile, "--region", region) + res = c.RunDockerCmd("context", "create", "ecs", contextName, "--from-env") } res.Assert(t, icmd.Expected{Out: "Successfully created ecs context \"" + contextName + "\""}) res = c.RunDockerCmd("context", "use", contextName) From 2fdc93786e9bd0ba29e8afe5f9536e87b74bd4d1 Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 29 Oct 2020 10:51:57 +0100 Subject: [PATCH 5/9] Fix region from environment Signed-off-by: aiordache --- ecs/context.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ecs/context.go b/ecs/context.go index 9fbc8fc1..f773cc60 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -43,7 +43,8 @@ func getEnvVars() ContextParams { } // check REGION env vars region := os.Getenv("AWS_REGION") - if region == "" { + c.Region = region + if c.Region == "" { region = os.Getenv("AWS_DEFAULT_REGION") if region == "" { region = "us-east-1" From 879afa85c06b91405897346cbc397cedd6be272e Mon Sep 17 00:00:00 2001 From: aiordache Date: Thu, 29 Oct 2020 11:02:46 +0100 Subject: [PATCH 6/9] fix lint issues Signed-off-by: aiordache --- ecs/backend.go | 6 ++-- ecs/context.go | 96 +++++++++++++++++++++++++++++--------------------- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/ecs/backend.go b/ecs/backend.go index e16e3718..dacafe1c 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -49,7 +49,7 @@ type ContextParams struct { CredsFromEnv bool } -func (c ContextParams) HaveRequiredCredentials() bool { +func (c ContextParams) haveRequiredCredentials() bool { if c.AccessKey == "" || c.SecretKey == "" { return false } @@ -81,8 +81,8 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { if ecsCtx.CredentialsFromEnv { creds := getEnvVars() - if !creds.HaveRequiredCredentials() { - return nil, fmt.Errorf(`context requires credentials to be passed as environment variable.`) + if !creds.haveRequiredCredentials() { + return nil, fmt.Errorf(`context requires credentials to be passed as environment variable`) } region = creds.Region profile = creds.Profile diff --git a/ecs/context.go b/ecs/context.go index f773cc60..a23dfc60 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -125,6 +125,56 @@ func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, st }, description } +func (h contextCreateAWSHelper) selectFromLocalProfile(opts *ContextParams) error { + profilesList, err := getProfiles() + if err != nil { + return err + } + // choose profile + opts.Profile, err = h.chooseProfile(profilesList) + if err != nil { + return err + } + + if opts.Region == "" { + region, isDefinedInProfile, err := getRegion(opts.Profile) + if err != nil { + return err + } + if isDefinedInProfile { + opts.Region = region + } else { + fmt.Println("No region defined in the profile. Choose the region to use.") + opts.Region, err = h.chooseRegion(opts.Region, opts.Profile) + if err != nil { + return err + } + } + } + return nil +} + +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 + } + fmt.Printf("Saving credentials under profile %s\n", opts.Profile) + return h.createProfile(opts.Profile, opts) +} + func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { if opts.CredsFromEnv { ecsCtx, descr := h.createContext(&opts) @@ -147,51 +197,15 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex switch selected { case 0: opts.CredsFromEnv = true - case 1: - profilesList, err := getProfiles() - if err != nil { - return nil, "", err - } - // choose profile - opts.Profile, err = h.chooseProfile(profilesList) - if err != nil { - return nil, "", err - } + err = h.selectFromLocalProfile(&opts) - if opts.Region == "" { - region, isDefinedInProfile, err := getRegion(opts.Profile) - if isDefinedInProfile { - opts.Region = region - } else { - fmt.Println("No region defined in the profile. Choose the region to use.") - opts.Region, err = h.chooseRegion(opts.Region, opts.Profile) - if err != nil { - return nil, "", err - } - } - } case 2: - accessKey, secretKey, err := h.askCredentials() - if err != nil { - return nil, "", 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 nil, "", err - } - // save as a profile - if opts.Profile == "" { - opts.Profile = opts.Name - } - fmt.Printf("Saving credentials under profile %s\n", opts.Profile) - h.createProfile(opts.Profile, &opts) + err = h.createProfileFromCredentials(&opts) + } + if err != nil { + return nil, "", err } - ecsCtx, descr := h.createContext(&opts) return ecsCtx, descr, nil } From de0be8650ede28151f2c56f860447a12207e7b34 Mon Sep 17 00:00:00 2001 From: aiordache Date: Fri, 30 Oct 2020 17:42:57 +0100 Subject: [PATCH 7/9] Remove region from contexts and cleanup Signed-off-by: aiordache --- cli/cmd/context/create_ecs.go | 5 +- context/store/contextmetadata.go | 1 - ecs/backend.go | 48 ++++----- ecs/context.go | 167 ++++++++++++++----------------- 4 files changed, 101 insertions(+), 120 deletions(-) diff --git a/cli/cmd/context/create_ecs.go b/cli/cmd/context/create_ecs.go index 1b2cd8bc..3c35ddee 100644 --- a/cli/cmd/context/create_ecs.go +++ b/cli/cmd/context/create_ecs.go @@ -55,9 +55,8 @@ func createEcsCommand() *cobra.Command { addDescriptionFlag(cmd, &opts.Description) cmd.Flags().BoolVar(&localSimulation, "local-simulation", false, "Create context for ECS local simulation endpoints") - cmd.Flags().StringVar(&opts.Profile, "profile", "", "Profile") - cmd.Flags().StringVar(&opts.Region, "region", "", "Region") - cmd.Flags().BoolVar(&opts.CredsFromEnv, "from-env", false, "Use credentials and region from environment") + cmd.Flags().StringVar(&opts.Profile, "profile", "", "Use an existing AWS profile") + cmd.Flags().BoolVar(&opts.CredsFromEnv, "from-env", false, "Use AWS environment variables for profile, or credentials and region") return cmd } diff --git a/context/store/contextmetadata.go b/context/store/contextmetadata.go index 6bb19111..a34c8313 100644 --- a/context/store/contextmetadata.go +++ b/context/store/contextmetadata.go @@ -53,7 +53,6 @@ type AciContext struct { type EcsContext struct { CredentialsFromEnv bool `json:",omitempty"` Profile string `json:",omitempty"` - Region string `json:",omitempty"` } // AwsContext is the context for the ecs plugin diff --git a/ecs/backend.go b/ecs/backend.go index dacafe1c..3831ac45 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -43,20 +43,19 @@ type ContextParams struct { Description string AccessKey string SecretKey string - SessionToken string Profile string Region string CredsFromEnv bool } -func (c ContextParams) haveRequiredCredentials() bool { - if c.AccessKey == "" || c.SecretKey == "" { - return false +func (c ContextParams) haveRequiredEnvVars() bool { + if c.Profile != "" { + return true } - if c.Region == "" && c.Profile == "" { - return false + if c.AccessKey != "" && c.SecretKey != "" { + return true } - return true + return false } func init() { @@ -76,29 +75,26 @@ func service(ctx context.Context) (backend.Service, error) { } func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { - var region string - var profile string + region := "" + profile := ecsCtx.Profile if ecsCtx.CredentialsFromEnv { - creds := getEnvVars() - if !creds.haveRequiredCredentials() { - return nil, fmt.Errorf(`context requires credentials to be passed as environment variable`) - } - region = creds.Region - profile = creds.Profile - } else { - // get region - profile = ecsCtx.Profile - if ecsCtx.Region != "" { - region = ecsCtx.Region - } else { - r, _, err := getRegion(ecsCtx.Profile) - if err != nil { - return nil, err - } - region = r + env := getEnvVars() + if !env.haveRequiredEnvVars() { + return nil, fmt.Errorf("context requires credentials to be passed as environment variables") } + profile = env.Profile + region = env.Region } + + if region == "" { + r, err := getRegion(profile) + if err != nil { + return nil, err + } + region = r + } + sess, err := session.NewSessionWithOptions(session.Options{ Profile: profile, SharedConfigState: session.SharedConfigEnable, diff --git a/ecs/context.go b/ecs/context.go index a23dfc60..e91e24b0 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -34,22 +34,16 @@ import ( ) func getEnvVars() ContextParams { - c := ContextParams{} - - //check profile env vars - profile := os.Getenv("AWS_PROFILE") - if profile != "" { - c.Profile = profile + c := ContextParams{ + Profile: os.Getenv("AWS_PROFILE"), + Region: os.Getenv("AWS_REGION"), } - // check REGION env vars - region := os.Getenv("AWS_REGION") - c.Region = region if c.Region == "" { - region = os.Getenv("AWS_DEFAULT_REGION") - if region == "" { - region = "us-east-1" + defaultRegion := os.Getenv("AWS_DEFAULT_REGION") + if defaultRegion == "" { + defaultRegion = "us-east-1" } - c.Region = region + c.Region = defaultRegion } p := credentials.EnvProvider{} @@ -59,7 +53,6 @@ func getEnvVars() ContextParams { } c.AccessKey = creds.AccessKeyID c.SecretKey = creds.SecretAccessKey - c.SessionToken = creds.SessionToken return c } @@ -73,31 +66,6 @@ func newContextCreateHelper() contextCreateAWSHelper { } } -func (h contextCreateAWSHelper) createProfile(name string, c *ContextParams) error { - if c != nil { - if c.AccessKey != "" && c.SecretKey != "" { - return h.saveCredentials(name, c.AccessKey, c.SecretKey) - } - accessKey, secretKey, err := h.askCredentials() - - if err != nil { - return err - } - c.AccessKey = accessKey - c.SecretKey = secretKey - return h.saveCredentials(name, c.AccessKey, c.SecretKey) - } - - accessKey, secretKey, err := h.askCredentials() - if err != nil { - return err - } - if accessKey != "" && secretKey != "" { - return h.saveCredentials(name, accessKey, secretKey) - } - return nil -} - func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, string) { if c.Profile == "default" { c.Profile = "" @@ -111,7 +79,6 @@ func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, st return store.EcsContext{ CredentialsFromEnv: c.CredsFromEnv, Profile: c.Profile, - Region: c.Region, }, description } @@ -121,7 +88,6 @@ func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, st } return store.EcsContext{ Profile: c.Profile, - Region: c.Region, }, description } @@ -130,28 +96,8 @@ func (h contextCreateAWSHelper) selectFromLocalProfile(opts *ContextParams) erro if err != nil { return err } - // choose profile opts.Profile, err = h.chooseProfile(profilesList) - if err != nil { - return err - } - - if opts.Region == "" { - region, isDefinedInProfile, err := getRegion(opts.Profile) - if err != nil { - return err - } - if isDefinedInProfile { - opts.Region = region - } else { - fmt.Println("No region defined in the profile. Choose the region to use.") - opts.Region, err = h.chooseRegion(opts.Region, opts.Profile) - if err != nil { - return err - } - } - } - return nil + return err } func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams) error { @@ -171,8 +117,21 @@ func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams if opts.Profile == "" { opts.Profile = opts.Name } - fmt.Printf("Saving credentials under profile %s\n", opts.Profile) - return h.createProfile(opts.Profile, opts) + // 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) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { @@ -181,12 +140,12 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex return ecsCtx, descr, nil } options := []string{ - "Use AWS credentials from environment", - "Select from existing AWS profiles", - "Create new profile from AWS credentials", + "An existing AWS profile", + "A new AWS profile", + "AWS environment variables", } - selected, err := h.user.Select("Would you like to create your context based on", options) + selected, err := h.user.Select("Create a Docker context using:", options) if err != nil { if err == terminal.InterruptErr { return nil, "", errdefs.ErrCanceled @@ -196,12 +155,12 @@ func (h contextCreateAWSHelper) createContextData(_ context.Context, opts Contex switch selected { case 0: - opts.CredsFromEnv = true - case 1: err = h.selectFromLocalProfile(&opts) - - case 2: + case 1: err = h.createProfileFromCredentials(&opts) + case 2: + opts.CredsFromEnv = true + } if err != nil { return nil, "", err @@ -242,6 +201,38 @@ func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID stri return credIni.SaveTo(p.Filename) } +func (h contextCreateAWSHelper) saveRegion(profile, region string) error { + if region == "" { + return nil + } + // loads ~/.aws/config + awsConfig := defaults.SharedConfigFilename() + configIni, err := ini.Load(awsConfig) + if err != nil { + if !os.IsNotExist(err) { + return err + } + configIni = ini.Empty() + } + profile = fmt.Sprintf("profile %s", profile) + section, err := configIni.GetSection(profile) + if err != nil { + if !strings.Contains(err.Error(), "does not exist") { + return err + } + section, err = configIni.NewSection(profile) + if err != nil { + return err + } + } + // save region under profile section in ~/.aws/config + _, err = section.NewKey("region", region) + if err != nil { + return err + } + return configIni.SaveTo(awsConfig) +} + func getProfiles() ([]string, error) { profiles := []string{} // parse both .aws/credentials and .aws/config for profiles @@ -282,7 +273,7 @@ func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) return profile, nil } -func getRegion(profile string) (string, bool, error) { +func getRegion(profile string) (string, error) { if profile == "" { profile = "default" } @@ -291,13 +282,12 @@ func getRegion(profile string) (string, bool, error) { configIni, err := ini.Load(awsConfig) if err != nil { if !os.IsNotExist(err) { - return "", false, err + return "", err } configIni = ini.Empty() } - var f func(string) (string, string) - f = func(p string) (string, string) { + getProfileRegion := func(p string) string { r := "" section, err := configIni.GetSection(p) if err == nil { @@ -306,29 +296,26 @@ func getRegion(profile string) (string, bool, error) { r = reg.Value() } } - if r == "" { - switch p { - case "": - return "us-east-1", "" - case "default": - return f("") - } - return f("default") - } - return r, p + return r } - if profile != "default" { profile = fmt.Sprintf("profile %s", profile) } - region, p := f(profile) - return region, p == profile, nil + region := getProfileRegion(profile) + if region == "" { + region = getProfileRegion("default") + if region == "" { + return "us-east-1", nil + } + return region, nil + } + return region, nil } func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { suggestion := region if suggestion == "" { - region, _, err := getRegion(profile) + region, err := getRegion(profile) if err != nil { return "", err } From 3066a1cdada2132778d5f9a54a25c6a7b103cc54 Mon Sep 17 00:00:00 2001 From: aiordache Date: Fri, 30 Oct 2020 18:50:56 +0100 Subject: [PATCH 8/9] cleanup Signed-off-by: aiordache --- cli/cmd/context/create_ecs.go | 4 ++ ecs/context.go | 82 ++++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/cli/cmd/context/create_ecs.go b/cli/cmd/context/create_ecs.go index 3c35ddee..5fb68a10 100644 --- a/cli/cmd/context/create_ecs.go +++ b/cli/cmd/context/create_ecs.go @@ -18,6 +18,7 @@ package context import ( "context" + "fmt" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -46,6 +47,9 @@ func createEcsCommand() *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Name = args[0] + if opts.CredsFromEnv && opts.Profile != "" { + return fmt.Errorf("--profile and --from-env flags cannot be set at the same time") + } if localSimulation { return runCreateLocalSimulation(cmd.Context(), args[0], opts) } diff --git a/ecs/context.go b/ecs/context.go index e91e24b0..15d5d068 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -66,6 +66,53 @@ func newContextCreateHelper() contextCreateAWSHelper { } } +func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { + if opts.CredsFromEnv { + ecsCtx, descr := h.createContext(&opts) + return ecsCtx, descr, nil + } + if opts.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) + } + ecsCtx, descr := h.createContext(&opts) + return ecsCtx, descr, nil + } + options := []string{ + "An existing AWS profile", + "A new AWS profile", + "AWS environment variables", + } + + 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 + } + + 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 = "" @@ -134,41 +181,6 @@ func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams return h.saveRegion(opts.Name, opts.Region) } -func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { - if opts.CredsFromEnv { - ecsCtx, descr := h.createContext(&opts) - return ecsCtx, descr, nil - } - options := []string{ - "An existing AWS profile", - "A new AWS profile", - "AWS environment variables", - } - - 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 - } - - 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) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { p := credentials.SharedCredentialsProvider{Profile: profile} _, err := p.Retrieve() From af7aebf8cf12c4762adbcf7a5dafd56f303fef14 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 2 Nov 2020 16:24:35 +0100 Subject: [PATCH 9/9] 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 15d5d068..5d636ac3 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 00000000..a69385e7 --- /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 00000000..6d231832 --- /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 00000000..2c69e47e --- /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 00000000..e388f9b0 --- /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 00000000..7146be5f --- /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 ca4c41b5..b44dfacc 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 ac0ff942..f30fbce3 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 00000000..06886e0f --- /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) +}