Merge pull request #765 from docker/ecs_default_profile

Populate ~/.aws/config(credentials) on ecs context create
This commit is contained in:
Anca Iordache 2020-10-16 11:33:03 +02:00 committed by GitHub
commit 05295b5e8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 167 additions and 99 deletions

View File

@ -56,8 +56,6 @@ func createEcsCommand() *cobra.Command {
cmd.Flags().BoolVar(&localSimulation, "local-simulation", false, "Create context for ECS local simulation endpoints") 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.Profile, "profile", "", "Profile")
cmd.Flags().StringVar(&opts.Region, "region", "", "Region") 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 return cmd
} }

View File

@ -41,9 +41,6 @@ type ContextParams struct {
Description string Description string
Region string Region string
Profile string Profile string
AwsID string
AwsSecret string
} }
func init() { func init() {

View File

@ -20,13 +20,13 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"reflect"
"strings" "strings"
"github.com/AlecAivazis/survey/v2/terminal" "github.com/AlecAivazis/survey/v2/terminal"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/defaults" "github.com/aws/aws-sdk-go/aws/defaults"
"github.com/pkg/errors"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"github.com/docker/compose-cli/context/store" "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) { func (h contextCreateAWSHelper) createProfile(name string) error {
accessKey := opts.AwsID accessKey, secretKey, err := h.askCredentials()
secretKey := opts.AwsSecret if err != nil {
return err
ecsCtx := store.EcsContext{
Profile: opts.Profile,
Region: opts.Region,
} }
if accessKey != "" && secretKey != "" {
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
if h.missingRequiredFlags(ecsCtx) {
profilesList, err := h.getProfiles() profilesList, err := h.getProfiles()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
// get profile if profile != "" {
_, ok := profilesList[ecsCtx.Profile] // validate profile
if !ok { if profile != "default" && !contains(profilesList, profile) {
profile, err := h.chooseProfile(profilesList) return nil, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q", profile)
if err != nil {
return nil, "", err
} }
ecsCtx.Profile = profile } else {
} // choose profile
// set region profile, err = h.chooseProfile(profilesList)
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 { if err != nil {
return nil, "", err return nil, "", err
} }
} }
if accessKey != "" && secretKey != "" { if region == "" {
if err := h.saveCredentials(ecsCtx.Profile, accessKey, secretKey); err != nil { region, err = h.chooseRegion(region, profile)
if err != nil {
return nil, "", err return nil, "", err
} }
} }
ecsCtx, descr := h.createContext(profile, region, opts.Description)
description := ecsCtx.Region return ecsCtx, descr, nil
if opts.Description != "" {
description = fmt.Sprintf("%s (%s)", opts.Description, description)
}
return ecsCtx, description, nil
}
func (h contextCreateAWSHelper) missingRequiredFlags(ctx store.EcsContext) bool {
if ctx.Profile == "" || ctx.Region == "" {
return true
}
return false
} }
func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error { func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error {
@ -132,62 +129,115 @@ func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID stri
return credIni.SaveTo(p.Filename) return credIni.SaveTo(p.Filename)
} }
func (h contextCreateAWSHelper) getProfiles() (map[string]ini.Section, error) { func (h contextCreateAWSHelper) getProfiles() ([]string, error) {
profiles := map[string]ini.Section{"new profile": {}} profiles := []string{}
credIni, err := ini.Load(defaults.SharedConfigFilename()) // parse both .aws/credentials and .aws/config for profiles
configFiles := map[string]bool{
defaults.SharedCredentialsFilename(): false,
defaults.SharedConfigFilename(): true,
}
for f, prefix := range configFiles {
sections, err := loadIniFile(f, prefix)
if err != nil { if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err return nil, err
} }
for _, section := range credIni.Sections() { for key := range sections {
if strings.HasPrefix(section.Name(), "profile") { name := strings.ToLower(key)
profiles[section.Name()[len("profile "):]] = *section if !contains(profiles, name) {
profiles = append(profiles, name)
}
} }
} }
return profiles, nil return profiles, nil
} }
func (h contextCreateAWSHelper) chooseProfile(section map[string]ini.Section) (string, error) { func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) {
keys := reflect.ValueOf(section).MapKeys() options := []string{"new profile"}
profiles := make([]string, len(keys)) options = append(options, profiles...)
for i := 0; i < len(keys); i++ {
profiles[i] = keys[i].String()
}
selected, err := h.user.Select("Select AWS Profile", profiles) selected, err := h.user.Select("Select AWS Profile", options)
if err != nil { if err != nil {
if err == terminal.InterruptErr { if err == terminal.InterruptErr {
return "", errdefs.ErrCanceled return "", errdefs.ErrCanceled
} }
return "", err return "", err
} }
profile := profiles[selected] profile := options[selected]
if profiles[selected] == "new profile" { if options[selected] == "new profile" {
return h.user.Input("profile name", "") 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 return profile, nil
} }
func (h contextCreateAWSHelper) chooseRegion(region string, section ini.Section) (string, error) { func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) {
defaultRegion := region suggestion := region
if defaultRegion == "" && section.Name() != "" {
reg, err := section.GetKey("region") // only load ~/.aws/config
if err == nil { awsConfig := defaults.SharedConfigFilename()
defaultRegion = reg.Value() configIni, err := ini.Load(awsConfig)
if err != nil {
if !os.IsNotExist(err) {
return "", err
} }
configIni = ini.Empty()
} }
result, err := h.user.Input("Region", defaultRegion) 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 { if err != nil {
return "", err return "", err
} }
return result, nil }
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
}
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) { 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 { if err != nil {
return "", "", err return "", "", err
} }
if confirm { if !confirm {
return "", "", nil
}
accessKeyID, err := h.user.Input("AWS Access Key ID", "") accessKeyID, err := h.user.Input("AWS Access Key ID", "")
if err != nil { if err != nil {
return "", "", err return "", "", err
@ -196,11 +246,34 @@ func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
// validate password // validate access ID and password
if len(secretAccessKey) < 3 { if len(accessKeyID) < 3 || len(secretAccessKey) < 3 {
return "", "", fmt.Errorf("AWS Secret Access Key must have more than 3 characters") return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters")
} }
return accessKeyID, secretAccessKey, nil return accessKeyID, secretAccessKey, nil
} }
return "", "", 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
} }

View File

@ -152,16 +152,16 @@ func setupTest(t *testing.T) (*E2eCLI, string) {
if localTestProfile != "" { if localTestProfile != "" {
region := os.Getenv("TEST_AWS_REGION") region := os.Getenv("TEST_AWS_REGION")
assert.Check(t, 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 { } else {
profile := contextName profile := "default"
region := os.Getenv("AWS_DEFAULT_REGION") region := os.Getenv("AWS_DEFAULT_REGION")
secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
keyID := os.Getenv("AWS_ACCESS_KEY_ID") keyID := os.Getenv("AWS_ACCESS_KEY_ID")
assert.Check(t, keyID != "") assert.Check(t, keyID != "")
assert.Check(t, secretKey != "") assert.Check(t, secretKey != "")
assert.Check(t, region != "") 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.Assert(t, icmd.Expected{Out: "Successfully created ecs context \"" + contextName + "\""})
res = c.RunDockerCmd("context", "use", contextName) res = c.RunDockerCmd("context", "use", contextName)