From c2af0a136a68299b953d1589a83e36506236aea2 Mon Sep 17 00:00:00 2001 From: aiordache Date: Fri, 9 Oct 2020 19:24:37 +0200 Subject: [PATCH] Populate ~/.aws/config(credentials) on ecs context create Signed-off-by: aiordache --- cli/cmd/context/create_ecs.go | 2 - ecs/backend.go | 3 - ecs/context.go | 255 ++++++++++++++++++++++------------ tests/ecs-e2e/e2e-ecs_test.go | 6 +- 4 files changed, 167 insertions(+), 99 deletions(-) diff --git a/cli/cmd/context/create_ecs.go b/cli/cmd/context/create_ecs.go index 48c7b6800..8dde7b049 100644 --- a/cli/cmd/context/create_ecs.go +++ b/cli/cmd/context/create_ecs.go @@ -56,8 +56,6 @@ 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().StringVar(&opts.AwsID, "key-id", "", "AWS Access Key ID") - cmd.Flags().StringVar(&opts.AwsSecret, "secret-key", "", "AWS Secret Access Key") return cmd } diff --git a/ecs/backend.go b/ecs/backend.go index e96803342..355fef9fe 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -40,9 +40,6 @@ type ContextParams struct { Description string Region string Profile string - - AwsID string - AwsSecret string } func init() { diff --git a/ecs/context.go b/ecs/context.go index 65096ea8d..ce91a853d 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -20,13 +20,13 @@ import ( "context" "fmt" "os" - "reflect" "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" + "github.com/pkg/errors" "gopkg.in/ini.v1" "github.com/docker/compose-cli/context/store" @@ -44,60 +44,57 @@ func newContextCreateHelper() contextCreateAWSHelper { } } -func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) { - accessKey := opts.AwsID - secretKey := opts.AwsSecret - - ecsCtx := store.EcsContext{ - Profile: opts.Profile, - Region: opts.Region, - } - - if h.missingRequiredFlags(ecsCtx) { - profilesList, err := h.getProfiles() - if err != nil { - return nil, "", err - } - // get profile - _, ok := profilesList[ecsCtx.Profile] - if !ok { - profile, err := h.chooseProfile(profilesList) - if err != nil { - return nil, "", err - } - ecsCtx.Profile = profile - } - // set region - region, err := h.chooseRegion(ecsCtx.Region, profilesList[ecsCtx.Profile]) - if err != nil { - return nil, "", err - } - ecsCtx.Region = region - - accessKey, secretKey, err = h.askCredentials() - if err != nil { - return nil, "", err - } +func (h contextCreateAWSHelper) createProfile(name string) error { + accessKey, secretKey, err := h.askCredentials() + if err != nil { + return err } if accessKey != "" && secretKey != "" { - if err := h.saveCredentials(ecsCtx.Profile, accessKey, secretKey); err != nil { + return h.saveCredentials(name, accessKey, secretKey) + } + return nil +} + +func (h contextCreateAWSHelper) createContext(profile, region, description string) (interface{}, string) { + if profile == "default" { + profile = "" + } + 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) + } + } else { + // choose profile + profile, err = h.chooseProfile(profilesList) + if err != nil { return nil, "", err } } - - description := ecsCtx.Region - if opts.Description != "" { - description = fmt.Sprintf("%s (%s)", opts.Description, description) + if region == "" { + region, err = h.chooseRegion(region, profile) + if err != nil { + return nil, "", err + } } - - return ecsCtx, description, nil -} - -func (h contextCreateAWSHelper) missingRequiredFlags(ctx store.EcsContext) bool { - if ctx.Profile == "" || ctx.Region == "" { - return true - } - return false + ecsCtx, descr := h.createContext(profile, region, opts.Description) + return ecsCtx, descr, nil } func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { @@ -132,75 +129,151 @@ func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID stri return credIni.SaveTo(p.Filename) } -func (h contextCreateAWSHelper) getProfiles() (map[string]ini.Section, error) { - profiles := map[string]ini.Section{"new profile": {}} - credIni, err := ini.Load(defaults.SharedConfigFilename()) - if err != nil { - return nil, err +func (h contextCreateAWSHelper) getProfiles() ([]string, error) { + profiles := []string{} + // parse both .aws/credentials and .aws/config for profiles + configFiles := map[string]bool{ + defaults.SharedCredentialsFilename(): false, + defaults.SharedConfigFilename(): true, } - for _, section := range credIni.Sections() { - if strings.HasPrefix(section.Name(), "profile") { - profiles[section.Name()[len("profile "):]] = *section + for f, prefix := range configFiles { + sections, err := loadIniFile(f, prefix) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + for key := range sections { + name := strings.ToLower(key) + if !contains(profiles, name) { + profiles = append(profiles, name) + } } } return profiles, nil } -func (h contextCreateAWSHelper) chooseProfile(section map[string]ini.Section) (string, error) { - keys := reflect.ValueOf(section).MapKeys() - profiles := make([]string, len(keys)) - for i := 0; i < len(keys); i++ { - profiles[i] = keys[i].String() - } +func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) { + options := []string{"new profile"} + options = append(options, profiles...) - selected, err := h.user.Select("Select AWS Profile", profiles) + selected, err := h.user.Select("Select AWS Profile", options) if err != nil { if err == terminal.InterruptErr { return "", errdefs.ErrCanceled } return "", err } - profile := profiles[selected] - if profiles[selected] == "new profile" { - return h.user.Input("profile name", "") + 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, section ini.Section) (string, error) { - defaultRegion := region - if defaultRegion == "" && section.Name() != "" { - reg, err := section.GetKey("region") - if err == nil { - defaultRegion = reg.Value() +func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) { + suggestion := region + + // only load ~/.aws/config + awsConfig := defaults.SharedConfigFilename() + configIni, err := ini.Load(awsConfig) + + if err != nil { + if !os.IsNotExist(err) { + return "", err + } + configIni = ini.Empty() + } + 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 } } - result, err := h.user.Input("Region", defaultRegion) + reg, err := section.GetKey("region") + if err == nil { + suggestion = reg.Value() + } + // promp user for region + region, err = h.user.Input("Region", suggestion) if err != nil { return "", err } - return result, nil + 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) } func (h contextCreateAWSHelper) askCredentials() (string, string, error) { - confirm, err := h.user.Confirm("Enter credentials", false) + confirm, err := h.user.Confirm("Enter AWS credentials", false) if err != nil { return "", "", err } - if confirm { - accessKeyID, err := h.user.Input("AWS Access Key ID", "") - if err != nil { - return "", "", err - } - secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key") - if err != nil { - return "", "", err - } - // validate password - if len(secretAccessKey) < 3 { - return "", "", fmt.Errorf("AWS Secret Access Key must have more than 3 characters") - } - return accessKeyID, secretAccessKey, nil + if !confirm { + return "", "", nil } - return "", "", nil + + accessKeyID, err := h.user.Input("AWS Access Key ID", "") + if err != nil { + return "", "", err + } + secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key") + if err != nil { + return "", "", err + } + // validate access ID and password + if len(accessKeyID) < 3 || len(secretAccessKey) < 3 { + return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters") + } + return accessKeyID, secretAccessKey, nil +} + +func contains(values []string, value string) bool { + for _, v := range values { + if v == value { + return true + } + } + return false +} + +func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) { + profiles := map[string]ini.Section{} + credIni, err := ini.Load(path) + if err != nil { + return nil, err + } + for _, section := range credIni.Sections() { + if prefix && strings.HasPrefix(section.Name(), "profile ") { + profiles[section.Name()[len("profile "):]] = *section + } else if !prefix || section.Name() == "default" { + profiles[section.Name()] = *section + } + } + return profiles, nil } diff --git a/tests/ecs-e2e/e2e-ecs_test.go b/tests/ecs-e2e/e2e-ecs_test.go index 60393cd67..18460a926 100644 --- a/tests/ecs-e2e/e2e-ecs_test.go +++ b/tests/ecs-e2e/e2e-ecs_test.go @@ -152,16 +152,16 @@ 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", localTestProfile, "--region", region) + res = c.RunDockerCmd("context", "create", "ecs", contextName, "--profile", "default", "--region", region) } else { - profile := contextName + 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, "--secret-key", secretKey, "--key-id", keyID) + res = c.RunDockerCmd("context", "create", "ecs", contextName, "--profile", profile, "--region", region) } res.Assert(t, icmd.Expected{Out: "Successfully created ecs context \"" + contextName + "\""}) res = c.RunDockerCmd("context", "use", contextName)