context.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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/api/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. credentials, err := ini.Load(file)
  177. if err != nil {
  178. if !os.IsNotExist(err) {
  179. return err
  180. }
  181. credentials = ini.Empty()
  182. }
  183. section, err := credentials.NewSection(profile)
  184. if err != nil {
  185. return err
  186. }
  187. _, err = section.NewKey("aws_access_key_id", accessKeyID)
  188. if err != nil {
  189. return err
  190. }
  191. _, err = section.NewKey("aws_secret_access_key", secretAccessKey)
  192. if err != nil {
  193. return err
  194. }
  195. return credentials.SaveTo(file)
  196. }
  197. func (h contextCreateAWSHelper) saveRegion(profile, region string) error {
  198. if region == "" {
  199. return nil
  200. }
  201. // loads ~/.aws/config
  202. awsConfig := getAWSConfigFile()
  203. configIni, err := ini.Load(awsConfig)
  204. if err != nil {
  205. if !os.IsNotExist(err) {
  206. return err
  207. }
  208. configIni = ini.Empty()
  209. }
  210. profile = fmt.Sprintf("profile %s", profile)
  211. section, err := configIni.GetSection(profile)
  212. if err != nil {
  213. if !strings.Contains(err.Error(), "does not exist") {
  214. return err
  215. }
  216. section, err = configIni.NewSection(profile)
  217. if err != nil {
  218. return err
  219. }
  220. }
  221. // save region under profile section in ~/.aws/config
  222. _, err = section.NewKey("region", region)
  223. if err != nil {
  224. return err
  225. }
  226. return configIni.SaveTo(awsConfig)
  227. }
  228. func getProfiles() ([]string, error) {
  229. profiles := []string{}
  230. // parse both .aws/credentials and .aws/config for profiles
  231. configFiles := map[string]bool{
  232. getAWSCredentialsFile(): false,
  233. getAWSConfigFile(): true,
  234. }
  235. for f, prefix := range configFiles {
  236. sections, err := loadIniFile(f, prefix)
  237. if err != nil {
  238. if os.IsNotExist(err) {
  239. continue
  240. }
  241. return nil, err
  242. }
  243. for key := range sections {
  244. name := strings.ToLower(key)
  245. if !contains(profiles, name) {
  246. profiles = append(profiles, name)
  247. }
  248. }
  249. }
  250. sort.Slice(profiles, func(i, j int) bool {
  251. return profiles[i] < profiles[j]
  252. })
  253. return profiles, nil
  254. }
  255. func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) {
  256. options := []string{}
  257. options = append(options, profiles...)
  258. selected, err := h.user.Select("Select AWS Profile", options)
  259. if err != nil {
  260. if err == terminal.InterruptErr {
  261. return "", errdefs.ErrCanceled
  262. }
  263. return "", err
  264. }
  265. profile := options[selected]
  266. return profile, nil
  267. }
  268. func getRegion(profile string) (string, error) {
  269. if profile == "" {
  270. profile = "default"
  271. }
  272. // only load ~/.aws/config
  273. awsConfig := defaults.SharedConfigFilename()
  274. configIni, err := ini.Load(awsConfig)
  275. if err != nil {
  276. if !os.IsNotExist(err) {
  277. return "", err
  278. }
  279. configIni = ini.Empty()
  280. }
  281. getProfileRegion := func(p string) string {
  282. r := ""
  283. section, err := configIni.GetSection(p)
  284. if err == nil {
  285. reg, err := section.GetKey("region")
  286. if err == nil {
  287. r = reg.Value()
  288. }
  289. }
  290. return r
  291. }
  292. if profile != "default" {
  293. profile = fmt.Sprintf("profile %s", profile)
  294. }
  295. region := getProfileRegion(profile)
  296. if region == "" {
  297. region = getProfileRegion("default")
  298. }
  299. if region == "" {
  300. // fallback to AWS default
  301. region = "us-east-1"
  302. }
  303. return region, nil
  304. }
  305. func (h contextCreateAWSHelper) chooseRegion(opts *ContextParams) error {
  306. regions, err := h.availableRegions(opts)
  307. if err != nil {
  308. return err
  309. }
  310. // promp user for region
  311. selected, err := h.user.Select("Region", regions)
  312. if err != nil {
  313. return err
  314. }
  315. opts.Region = regions[selected]
  316. return nil
  317. }
  318. func listAvailableRegions(opts *ContextParams) ([]string, error) {
  319. // Setup SDK with credentials, will also validate those
  320. session, err := session.NewSessionWithOptions(session.Options{
  321. Config: aws.Config{
  322. Credentials: credentials.NewStaticCredentials(opts.AccessKey, opts.SecretKey, ""),
  323. Region: aws.String("us-east-1"),
  324. },
  325. })
  326. if err != nil {
  327. return nil, err
  328. }
  329. desc, err := ec2.New(session).DescribeRegions(&ec2.DescribeRegionsInput{})
  330. if err != nil {
  331. return nil, err
  332. }
  333. var regions []string
  334. for _, r := range desc.Regions {
  335. regions = append(regions, aws.StringValue(r.RegionName))
  336. }
  337. return regions, nil
  338. }
  339. func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
  340. accessKeyID, err := h.user.Input("AWS Access Key ID", "")
  341. if err != nil {
  342. return "", "", err
  343. }
  344. secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key")
  345. if err != nil {
  346. return "", "", err
  347. }
  348. // validate access ID and password
  349. if len(accessKeyID) < 3 || len(secretAccessKey) < 3 {
  350. return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters")
  351. }
  352. return accessKeyID, secretAccessKey, nil
  353. }
  354. func contains(values []string, value string) bool {
  355. for _, v := range values {
  356. if v == value {
  357. return true
  358. }
  359. }
  360. return false
  361. }
  362. func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) {
  363. profiles := map[string]ini.Section{}
  364. credIni, err := ini.Load(path)
  365. if err != nil {
  366. return nil, err
  367. }
  368. for _, section := range credIni.Sections() {
  369. if prefix && strings.HasPrefix(section.Name(), "profile ") {
  370. profiles[section.Name()[len("profile "):]] = *section
  371. } else if !prefix || section.Name() == "default" {
  372. profiles[section.Name()] = *section
  373. }
  374. }
  375. return profiles, nil
  376. }
  377. func getAWSConfigFile() string {
  378. awsConfig, ok := os.LookupEnv("AWS_CONFIG_FILE")
  379. if !ok {
  380. awsConfig = defaults.SharedConfigFilename()
  381. }
  382. return awsConfig
  383. }
  384. func getAWSCredentialsFile() string {
  385. awsConfig, ok := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE")
  386. if !ok {
  387. awsConfig = defaults.SharedCredentialsFilename()
  388. }
  389. return awsConfig
  390. }