Browse Source

Merge pull request #846 from docker/ecs_context

Nicolas De loof 5 years ago
parent
commit
f66123b34a

+ 7 - 2
cli/cmd/context/create_ecs.go

@@ -18,6 +18,7 @@ package context
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -45,6 +46,10 @@ 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 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)
 			}
@@ -54,8 +59,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().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
 }
 

+ 2 - 2
context/store/contextmetadata.go

@@ -51,8 +51,8 @@ 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"`
 }
 
 // AwsContext is the context for the ecs plugin

+ 41 - 6
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"
@@ -38,9 +39,23 @@ const backendType = store.EcsContextType
 
 // ContextParams options for creating AWS context
 type ContextParams struct {
-	Description string
-	Region      string
-	Profile     string
+	Name         string
+	Description  string
+	AccessKey    string
+	SecretKey    string
+	Profile      string
+	Region       string
+	CredsFromEnv bool
+}
+
+func (c ContextParams) haveRequiredEnvVars() bool {
+	if c.Profile != "" {
+		return true
+	}
+	if c.AccessKey != "" && c.SecretKey != "" {
+		return true
+	}
+	return false
 }
 
 func init() {
@@ -60,11 +75,31 @@ func service(ctx context.Context) (backend.Service, error) {
 }
 
 func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) {
+	region := ""
+	profile := ecsCtx.Profile
+
+	if ecsCtx.CredentialsFromEnv {
+		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:           ecsCtx.Profile,
+		Profile:           profile,
 		SharedConfigState: session.SharedConfigEnable,
 		Config: aws.Config{
-			Region: aws.String(ecsCtx.Region),
+			Region: aws.String(region),
 		},
 	})
 	if err != nil {
@@ -74,7 +109,7 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) {
 	sdk := newSDK(sess)
 	return &ecsAPIService{
 		ctx:    ecsCtx,
-		Region: ecsCtx.Region,
+		Region: region,
 		aws:    sdk,
 	}, nil
 }

+ 250 - 104
ecs/context.go

@@ -20,100 +20,186 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"path/filepath"
+	"sort"
 	"strings"
 
+	"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/awserr"
+	"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"
-
-	"github.com/docker/compose-cli/context/store"
-	"github.com/docker/compose-cli/errdefs"
-	"github.com/docker/compose-cli/prompt"
 )
 
-type contextCreateAWSHelper struct {
-	user prompt.UI
-}
-
-func newContextCreateHelper() contextCreateAWSHelper {
-	return contextCreateAWSHelper{
-		user: prompt.User{},
+func getEnvVars() ContextParams {
+	c := ContextParams{
+		Profile: os.Getenv("AWS_PROFILE"),
+		Region:  os.Getenv("AWS_REGION"),
+	}
+	if c.Region == "" {
+		defaultRegion := os.Getenv("AWS_DEFAULT_REGION")
+		if defaultRegion == "" {
+			defaultRegion = "us-east-1"
+		}
+		c.Region = defaultRegion
 	}
-}
 
-func (h contextCreateAWSHelper) createProfile(name string) error {
-	accessKey, secretKey, err := h.askCredentials()
+	p := credentials.EnvProvider{}
+	creds, err := p.Retrieve()
 	if err != nil {
-		return err
-	}
-	if accessKey != "" && secretKey != "" {
-		return h.saveCredentials(name, accessKey, secretKey)
+		return c
 	}
-	return nil
+	c.AccessKey = creds.AccessKeyID
+	c.SecretKey = creds.SecretAccessKey
+	return c
 }
 
-func (h contextCreateAWSHelper) createContext(profile, region, description string) (interface{}, string) {
-	if profile == "default" {
-		profile = ""
+type contextCreateAWSHelper struct {
+	user             prompt.UI
+	availableRegions func(opts *ContextParams) ([]string, error)
+}
+
+func newContextCreateHelper() contextCreateAWSHelper {
+	return contextCreateAWSHelper{
+		user:             prompt.User{},
+		availableRegions: listAvailableRegions,
 	}
-	description = strings.TrimSpace(
-		fmt.Sprintf("%s (%s)", description, region))
-	return store.EcsContext{
-		Profile: profile,
-		Region:  region,
-	}, description
 }
 
 func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) {
-	profile := opts.Profile
-	region := opts.Region
-
-	profilesList, err := h.getProfiles()
-	if err != nil {
-		return nil, "", err
-	}
-	if profile != "" {
-		// validate profile
-		if profile != "default" && !contains(profilesList, profile) {
-			return nil, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q", profile)
+	if opts.CredsFromEnv {
+		// Explicit creation from ENV variables
+		ecsCtx, descr := h.createContext(&opts)
+		return ecsCtx, descr, nil
+	} 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, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q not found", opts.Profile)
 		}
 	} else {
-		// choose profile
-		profile, err = h.chooseProfile(profilesList)
+		// interactive
+		var options []string
+		var actions []func(params *ContextParams) error
+
+		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
 		}
-	}
-	if region == "" {
-		region, err = h.chooseRegion(region, profile)
+
+		err = actions[selected](&opts)
 		if err != nil {
 			return nil, "", err
 		}
 	}
-	ecsCtx, descr := h.createContext(profile, region, opts.Description)
+
+	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()
-	if err == nil {
-		return fmt.Errorf("credentials already exist")
+func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, string) {
+	var description string
+
+	if c.CredsFromEnv {
+		if c.Description == "" {
+			description = "credentials read from environment"
+		}
+		return store.EcsContext{
+			CredentialsFromEnv: c.CredsFromEnv,
+			Profile:            c.Profile,
+		}, description
 	}
 
-	if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" {
-		_, err := os.Create(p.Filename)
+	if c.Region != "" {
+		description = strings.TrimSpace(
+			fmt.Sprintf("%s (%s)", c.Description, c.Region))
+	}
+	return store.EcsContext{
+		Profile: c.Profile,
+	}, description
+}
+
+func (h contextCreateAWSHelper) selectFromLocalProfile(opts *ContextParams) error {
+	profilesList, err := getProfiles()
+	if err != nil {
+		return err
+	}
+	opts.Profile, err = h.chooseProfile(profilesList)
+	return err
+}
+
+func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams) error {
+	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 opts.Region == "" {
+		err := h.chooseRegion(opts)
+		if err != nil {
+			return err
+		}
+	}
+	// 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
 	}
-	credIni, err := ini.Load(p.Filename)
+	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
@@ -126,15 +212,47 @@ 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) getProfiles() ([]string, error) {
+func (h contextCreateAWSHelper) saveRegion(profile, region string) error {
+	if region == "" {
+		return nil
+	}
+	// loads ~/.aws/config
+	awsConfig := getAWSConfigFile()
+	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
 	configFiles := map[string]bool{
-		defaults.SharedCredentialsFilename(): false,
-		defaults.SharedConfigFilename():      true,
+		getAWSCredentialsFile(): false,
+		getAWSConfigFile():      true,
 	}
 	for f, prefix := range configFiles {
 		sections, err := loadIniFile(f, prefix)
@@ -151,11 +269,15 @@ func (h contextCreateAWSHelper) getProfiles() ([]string, error) {
 			}
 		}
 	}
+	sort.Slice(profiles, func(i, j int) bool {
+		return profiles[i] < profiles[j]
+	})
+
 	return profiles, nil
 }
 
 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)
@@ -166,78 +288,86 @@ 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)
-	}
 	return profile, nil
 }
 
-func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) {
-	suggestion := region
-
+func getRegion(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()
 	}
+
+	getProfileRegion := func(p string) string {
+		r := ""
+		section, err := configIni.GetSection(p)
+		if err == nil {
+			reg, err := section.GetKey("region")
+			if err == nil {
+				r = reg.Value()
+			}
+		}
+		return r
+	}
 	if profile != "default" {
 		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
-		}
+	region := getProfileRegion(profile)
+	if region == "" {
+		region = getProfileRegion("default")
 	}
-	reg, err := section.GetKey("region")
-	if err == nil {
-		suggestion = reg.Value()
+	if region == "" {
+		// fallback to AWS default
+		region = "us-east-1"
 	}
-	// promp user for region
-	region, err = h.user.Input("Region", suggestion)
+	return region, nil
+}
+
+func (h contextCreateAWSHelper) chooseRegion(opts *ContextParams) error {
+	regions, err := h.availableRegions(opts)
 	if err != nil {
-		return "", err
-	}
-	if region == "" {
-		return "", fmt.Errorf("region cannot be empty")
+		return err
 	}
-	// save selected/typed region under profile in ~/.aws/config
-	_, err = section.NewKey("region", region)
+	// promp user for region
+	selected, err := h.user.Select("Region", regions)
 	if err != nil {
-		return "", err
+		return err
 	}
-	return region, configIni.SaveTo(awsConfig)
+	opts.Region = regions[selected]
+	return nil
 }
 
-func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
-	confirm, err := h.user.Confirm("Enter AWS credentials", false)
+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 "", "", err
+		return nil, err
+	}
+
+	desc, err := ec2.New(session).DescribeRegions(&ec2.DescribeRegionsInput{})
+	if err != nil {
+		return nil, err
 	}
-	if !confirm {
-		return "", "", nil
+	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) {
 	accessKeyID, err := h.user.Input("AWS Access Key ID", "")
 	if err != nil {
 		return "", "", err
@@ -277,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
+}

+ 173 - 0
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")
+}

+ 3 - 0
ecs/testdata/context/by-keys/config.golden

@@ -0,0 +1,3 @@
+[profile default]
+region = eu-west-3
+

+ 4 - 0
ecs/testdata/context/by-keys/credentials.golden

@@ -0,0 +1,4 @@
+[default]
+aws_access_key_id     = ABCD
+aws_secret_access_key = X&123
+

+ 3 - 0
ecs/testdata/context/by-profile/config.golden

@@ -0,0 +1,3 @@
+[profile foo]
+region = eu-west-3
+

+ 4 - 0
ecs/testdata/context/by-profile/credentials.golden

@@ -0,0 +1,4 @@
+[foo]
+aws_access_key_id     = ABCD
+aws_secret_access_key = X&123
+

+ 1 - 0
go.sum

@@ -748,6 +748,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=

+ 2 - 0
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)

+ 93 - 0
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)
+}

+ 2 - 3
tests/ecs-e2e/e2e-ecs_test.go

@@ -168,16 +168,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)