context.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package ecs
  14. import (
  15. "context"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "sort"
  20. "strings"
  21. "github.com/docker/compose-cli/context/store"
  22. "github.com/docker/compose-cli/errdefs"
  23. "github.com/docker/compose-cli/prompt"
  24. "github.com/AlecAivazis/survey/v2/terminal"
  25. "github.com/aws/aws-sdk-go/aws"
  26. "github.com/aws/aws-sdk-go/aws/credentials"
  27. "github.com/aws/aws-sdk-go/aws/defaults"
  28. "github.com/aws/aws-sdk-go/aws/session"
  29. "github.com/aws/aws-sdk-go/service/ec2"
  30. "github.com/pkg/errors"
  31. "gopkg.in/ini.v1"
  32. )
  33. func getEnvVars() ContextParams {
  34. c := ContextParams{
  35. Profile: os.Getenv("AWS_PROFILE"),
  36. Region: os.Getenv("AWS_REGION"),
  37. }
  38. if c.Region == "" {
  39. defaultRegion := os.Getenv("AWS_DEFAULT_REGION")
  40. if defaultRegion == "" {
  41. defaultRegion = "us-east-1"
  42. }
  43. c.Region = defaultRegion
  44. }
  45. p := credentials.EnvProvider{}
  46. creds, err := p.Retrieve()
  47. if err != nil {
  48. return c
  49. }
  50. c.AccessKey = creds.AccessKeyID
  51. c.SecretKey = creds.SecretAccessKey
  52. return c
  53. }
  54. type contextCreateAWSHelper struct {
  55. user prompt.UI
  56. availableRegions func(opts *ContextParams) ([]string, error)
  57. }
  58. func newContextCreateHelper() contextCreateAWSHelper {
  59. return contextCreateAWSHelper{
  60. user: prompt.User{},
  61. availableRegions: listAvailableRegions,
  62. }
  63. }
  64. func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) {
  65. if opts.CredsFromEnv {
  66. // Explicit creation from ENV variables
  67. ecsCtx, descr := h.createContext(&opts)
  68. return ecsCtx, descr, nil
  69. } else if opts.AccessKey != "" && opts.SecretKey != "" {
  70. // Explicit creation using keys
  71. err := h.createProfileFromCredentials(&opts)
  72. if err != nil {
  73. return nil, "", err
  74. }
  75. } else if opts.Profile != "" {
  76. // Excplicit creation by selecting a profile
  77. // check profile exists
  78. profilesList, err := getProfiles()
  79. if err != nil {
  80. return nil, "", err
  81. }
  82. if !contains(profilesList, opts.Profile) {
  83. return nil, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q not found", opts.Profile)
  84. }
  85. } else {
  86. // interactive
  87. var options []string
  88. var actions []func(params *ContextParams) error
  89. if _, err := os.Stat(getAWSConfigFile()); err == nil {
  90. // User has .aws/config file, so we can offer to select one of his profiles
  91. options = append(options, "An existing AWS profile")
  92. actions = append(actions, h.selectFromLocalProfile)
  93. }
  94. options = append(options, "AWS secret and token credentials")
  95. actions = append(actions, h.createProfileFromCredentials)
  96. options = append(options, "AWS environment variables")
  97. actions = append(actions, func(params *ContextParams) error {
  98. opts.CredsFromEnv = true
  99. return nil
  100. })
  101. selected, err := h.user.Select("Create a Docker context using:", options)
  102. if err != nil {
  103. if err == terminal.InterruptErr {
  104. return nil, "", errdefs.ErrCanceled
  105. }
  106. return nil, "", err
  107. }
  108. err = actions[selected](&opts)
  109. if err != nil {
  110. return nil, "", err
  111. }
  112. }
  113. ecsCtx, descr := h.createContext(&opts)
  114. return ecsCtx, descr, nil
  115. }
  116. func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, string) {
  117. var description string
  118. if c.CredsFromEnv {
  119. if c.Description == "" {
  120. description = "credentials read from environment"
  121. }
  122. return store.EcsContext{
  123. CredentialsFromEnv: c.CredsFromEnv,
  124. Profile: c.Profile,
  125. }, description
  126. }
  127. if c.Region != "" {
  128. description = strings.TrimSpace(
  129. fmt.Sprintf("%s (%s)", c.Description, c.Region))
  130. }
  131. return store.EcsContext{
  132. Profile: c.Profile,
  133. }, description
  134. }
  135. func (h contextCreateAWSHelper) selectFromLocalProfile(opts *ContextParams) error {
  136. profilesList, err := getProfiles()
  137. if err != nil {
  138. return err
  139. }
  140. opts.Profile, err = h.chooseProfile(profilesList)
  141. return err
  142. }
  143. func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams) error {
  144. if opts.AccessKey == "" || opts.SecretKey == "" {
  145. fmt.Println("Retrieve or create AWS Access Key and Secret on https://console.aws.amazon.com/iam/home?#security_credential")
  146. accessKey, secretKey, err := h.askCredentials()
  147. if err != nil {
  148. return err
  149. }
  150. opts.AccessKey = accessKey
  151. opts.SecretKey = secretKey
  152. }
  153. if opts.Region == "" {
  154. err := h.chooseRegion(opts)
  155. if err != nil {
  156. return err
  157. }
  158. }
  159. // save as a profile
  160. if opts.Profile == "" {
  161. opts.Profile = "default"
  162. }
  163. // context name used as profile name
  164. err := h.saveCredentials(opts.Profile, opts.AccessKey, opts.SecretKey)
  165. if err != nil {
  166. return err
  167. }
  168. return h.saveRegion(opts.Profile, opts.Region)
  169. }
  170. func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error {
  171. file := getAWSCredentialsFile()
  172. err := os.MkdirAll(filepath.Dir(file), 0700)
  173. if err != nil {
  174. return err
  175. }
  176. credIni := ini.Empty()
  177. section, err := credIni.NewSection(profile)
  178. if err != nil {
  179. return err
  180. }
  181. _, err = section.NewKey("aws_access_key_id", accessKeyID)
  182. if err != nil {
  183. return err
  184. }
  185. _, err = section.NewKey("aws_secret_access_key", secretAccessKey)
  186. if err != nil {
  187. return err
  188. }
  189. return credIni.SaveTo(file)
  190. }
  191. func (h contextCreateAWSHelper) saveRegion(profile, region string) error {
  192. if region == "" {
  193. return nil
  194. }
  195. // loads ~/.aws/config
  196. awsConfig := getAWSConfigFile()
  197. configIni, err := ini.Load(awsConfig)
  198. if err != nil {
  199. if !os.IsNotExist(err) {
  200. return err
  201. }
  202. configIni = ini.Empty()
  203. }
  204. profile = fmt.Sprintf("profile %s", profile)
  205. section, err := configIni.GetSection(profile)
  206. if err != nil {
  207. if !strings.Contains(err.Error(), "does not exist") {
  208. return err
  209. }
  210. section, err = configIni.NewSection(profile)
  211. if err != nil {
  212. return err
  213. }
  214. }
  215. // save region under profile section in ~/.aws/config
  216. _, err = section.NewKey("region", region)
  217. if err != nil {
  218. return err
  219. }
  220. return configIni.SaveTo(awsConfig)
  221. }
  222. func getProfiles() ([]string, error) {
  223. profiles := []string{}
  224. // parse both .aws/credentials and .aws/config for profiles
  225. configFiles := map[string]bool{
  226. getAWSCredentialsFile(): false,
  227. getAWSConfigFile(): true,
  228. }
  229. for f, prefix := range configFiles {
  230. sections, err := loadIniFile(f, prefix)
  231. if err != nil {
  232. if os.IsNotExist(err) {
  233. continue
  234. }
  235. return nil, err
  236. }
  237. for key := range sections {
  238. name := strings.ToLower(key)
  239. if !contains(profiles, name) {
  240. profiles = append(profiles, name)
  241. }
  242. }
  243. }
  244. sort.Slice(profiles, func(i, j int) bool {
  245. return profiles[i] < profiles[j]
  246. })
  247. return profiles, nil
  248. }
  249. func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) {
  250. options := []string{}
  251. options = append(options, profiles...)
  252. selected, err := h.user.Select("Select AWS Profile", options)
  253. if err != nil {
  254. if err == terminal.InterruptErr {
  255. return "", errdefs.ErrCanceled
  256. }
  257. return "", err
  258. }
  259. profile := options[selected]
  260. return profile, nil
  261. }
  262. func getRegion(profile string) (string, error) {
  263. if profile == "" {
  264. profile = "default"
  265. }
  266. // only load ~/.aws/config
  267. awsConfig := defaults.SharedConfigFilename()
  268. configIni, err := ini.Load(awsConfig)
  269. if err != nil {
  270. if !os.IsNotExist(err) {
  271. return "", err
  272. }
  273. configIni = ini.Empty()
  274. }
  275. getProfileRegion := func(p string) string {
  276. r := ""
  277. section, err := configIni.GetSection(p)
  278. if err == nil {
  279. reg, err := section.GetKey("region")
  280. if err == nil {
  281. r = reg.Value()
  282. }
  283. }
  284. return r
  285. }
  286. if profile != "default" {
  287. profile = fmt.Sprintf("profile %s", profile)
  288. }
  289. region := getProfileRegion(profile)
  290. if region == "" {
  291. region = getProfileRegion("default")
  292. }
  293. if region == "" {
  294. // fallback to AWS default
  295. region = "us-east-1"
  296. }
  297. return region, nil
  298. }
  299. func (h contextCreateAWSHelper) chooseRegion(opts *ContextParams) error {
  300. regions, err := h.availableRegions(opts)
  301. if err != nil {
  302. return err
  303. }
  304. // promp user for region
  305. selected, err := h.user.Select("Region", regions)
  306. if err != nil {
  307. return err
  308. }
  309. opts.Region = regions[selected]
  310. return nil
  311. }
  312. func listAvailableRegions(opts *ContextParams) ([]string, error) {
  313. // Setup SDK with credentials, will also validate those
  314. session, err := session.NewSessionWithOptions(session.Options{
  315. Config: aws.Config{
  316. Credentials: credentials.NewStaticCredentials(opts.AccessKey, opts.SecretKey, ""),
  317. Region: aws.String("us-east-1"),
  318. },
  319. })
  320. if err != nil {
  321. return nil, err
  322. }
  323. desc, err := ec2.New(session).DescribeRegions(&ec2.DescribeRegionsInput{})
  324. if err != nil {
  325. return nil, err
  326. }
  327. var regions []string
  328. for _, r := range desc.Regions {
  329. regions = append(regions, aws.StringValue(r.RegionName))
  330. }
  331. return regions, nil
  332. }
  333. func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
  334. accessKeyID, err := h.user.Input("AWS Access Key ID", "")
  335. if err != nil {
  336. return "", "", err
  337. }
  338. secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key")
  339. if err != nil {
  340. return "", "", err
  341. }
  342. // validate access ID and password
  343. if len(accessKeyID) < 3 || len(secretAccessKey) < 3 {
  344. return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters")
  345. }
  346. return accessKeyID, secretAccessKey, nil
  347. }
  348. func contains(values []string, value string) bool {
  349. for _, v := range values {
  350. if v == value {
  351. return true
  352. }
  353. }
  354. return false
  355. }
  356. func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) {
  357. profiles := map[string]ini.Section{}
  358. credIni, err := ini.Load(path)
  359. if err != nil {
  360. return nil, err
  361. }
  362. for _, section := range credIni.Sections() {
  363. if prefix && strings.HasPrefix(section.Name(), "profile ") {
  364. profiles[section.Name()[len("profile "):]] = *section
  365. } else if !prefix || section.Name() == "default" {
  366. profiles[section.Name()] = *section
  367. }
  368. }
  369. return profiles, nil
  370. }
  371. func getAWSConfigFile() string {
  372. awsConfig, ok := os.LookupEnv("AWS_CONFIG_FILE")
  373. if !ok {
  374. awsConfig = defaults.SharedConfigFilename()
  375. }
  376. return awsConfig
  377. }
  378. func getAWSCredentialsFile() string {
  379. awsConfig, ok := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE")
  380. if !ok {
  381. awsConfig = defaults.SharedCredentialsFilename()
  382. }
  383. return awsConfig
  384. }