context.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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. "strings"
  19. "github.com/AlecAivazis/survey/v2/terminal"
  20. "github.com/aws/aws-sdk-go/aws/awserr"
  21. "github.com/aws/aws-sdk-go/aws/credentials"
  22. "github.com/aws/aws-sdk-go/aws/defaults"
  23. "gopkg.in/ini.v1"
  24. "github.com/docker/compose-cli/context/store"
  25. "github.com/docker/compose-cli/errdefs"
  26. "github.com/docker/compose-cli/prompt"
  27. )
  28. func getEnvVars() ContextParams {
  29. c := ContextParams{}
  30. //check profile env vars
  31. profile := os.Getenv("AWS_PROFILE")
  32. if profile != "" {
  33. c.Profile = profile
  34. }
  35. // check REGION env vars
  36. region := os.Getenv("AWS_REGION")
  37. c.Region = region
  38. if c.Region == "" {
  39. region = os.Getenv("AWS_DEFAULT_REGION")
  40. if region == "" {
  41. region = "us-east-1"
  42. }
  43. c.Region = region
  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. c.SessionToken = creds.SessionToken
  53. return c
  54. }
  55. type contextCreateAWSHelper struct {
  56. user prompt.UI
  57. }
  58. func newContextCreateHelper() contextCreateAWSHelper {
  59. return contextCreateAWSHelper{
  60. user: prompt.User{},
  61. }
  62. }
  63. func (h contextCreateAWSHelper) createProfile(name string, c *ContextParams) error {
  64. if c != nil {
  65. if c.AccessKey != "" && c.SecretKey != "" {
  66. return h.saveCredentials(name, c.AccessKey, c.SecretKey)
  67. }
  68. accessKey, secretKey, err := h.askCredentials()
  69. if err != nil {
  70. return err
  71. }
  72. c.AccessKey = accessKey
  73. c.SecretKey = secretKey
  74. return h.saveCredentials(name, c.AccessKey, c.SecretKey)
  75. }
  76. accessKey, secretKey, err := h.askCredentials()
  77. if err != nil {
  78. return err
  79. }
  80. if accessKey != "" && secretKey != "" {
  81. return h.saveCredentials(name, accessKey, secretKey)
  82. }
  83. return nil
  84. }
  85. func (h contextCreateAWSHelper) createContext(c *ContextParams) (interface{}, string) {
  86. if c.Profile == "default" {
  87. c.Profile = ""
  88. }
  89. var description string
  90. if c.CredsFromEnv {
  91. if c.Description == "" {
  92. description = "credentials read from environment"
  93. }
  94. return store.EcsContext{
  95. CredentialsFromEnv: c.CredsFromEnv,
  96. Profile: c.Profile,
  97. Region: c.Region,
  98. }, description
  99. }
  100. if c.Region != "" {
  101. description = strings.TrimSpace(
  102. fmt.Sprintf("%s (%s)", c.Description, c.Region))
  103. }
  104. return store.EcsContext{
  105. Profile: c.Profile,
  106. Region: c.Region,
  107. }, description
  108. }
  109. func (h contextCreateAWSHelper) selectFromLocalProfile(opts *ContextParams) error {
  110. profilesList, err := getProfiles()
  111. if err != nil {
  112. return err
  113. }
  114. // choose profile
  115. opts.Profile, err = h.chooseProfile(profilesList)
  116. if err != nil {
  117. return err
  118. }
  119. if opts.Region == "" {
  120. region, isDefinedInProfile, err := getRegion(opts.Profile)
  121. if err != nil {
  122. return err
  123. }
  124. if isDefinedInProfile {
  125. opts.Region = region
  126. } else {
  127. fmt.Println("No region defined in the profile. Choose the region to use.")
  128. opts.Region, err = h.chooseRegion(opts.Region, opts.Profile)
  129. if err != nil {
  130. return err
  131. }
  132. }
  133. }
  134. return nil
  135. }
  136. func (h contextCreateAWSHelper) createProfileFromCredentials(opts *ContextParams) error {
  137. accessKey, secretKey, err := h.askCredentials()
  138. if err != nil {
  139. return err
  140. }
  141. opts.AccessKey = accessKey
  142. opts.SecretKey = secretKey
  143. // we need a region set -- either read it from profile or prompt user
  144. // prompt for the region to use with this context
  145. opts.Region, err = h.chooseRegion(opts.Region, opts.Profile)
  146. if err != nil {
  147. return err
  148. }
  149. // save as a profile
  150. if opts.Profile == "" {
  151. opts.Profile = opts.Name
  152. }
  153. fmt.Printf("Saving credentials under profile %s\n", opts.Profile)
  154. return h.createProfile(opts.Profile, opts)
  155. }
  156. func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) {
  157. if opts.CredsFromEnv {
  158. ecsCtx, descr := h.createContext(&opts)
  159. return ecsCtx, descr, nil
  160. }
  161. options := []string{
  162. "Use AWS credentials from environment",
  163. "Select from existing AWS profiles",
  164. "Create new profile from AWS credentials",
  165. }
  166. selected, err := h.user.Select("Would you like to create your context based on", options)
  167. if err != nil {
  168. if err == terminal.InterruptErr {
  169. return nil, "", errdefs.ErrCanceled
  170. }
  171. return nil, "", err
  172. }
  173. switch selected {
  174. case 0:
  175. opts.CredsFromEnv = true
  176. case 1:
  177. err = h.selectFromLocalProfile(&opts)
  178. case 2:
  179. err = h.createProfileFromCredentials(&opts)
  180. }
  181. if err != nil {
  182. return nil, "", err
  183. }
  184. ecsCtx, descr := h.createContext(&opts)
  185. return ecsCtx, descr, nil
  186. }
  187. func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error {
  188. p := credentials.SharedCredentialsProvider{Profile: profile}
  189. _, err := p.Retrieve()
  190. if err == nil {
  191. return fmt.Errorf("credentials already exist")
  192. }
  193. if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" {
  194. _, err := os.Create(p.Filename)
  195. if err != nil {
  196. return err
  197. }
  198. }
  199. credIni, err := ini.Load(p.Filename)
  200. if err != nil {
  201. return err
  202. }
  203. section, err := credIni.NewSection(profile)
  204. if err != nil {
  205. return err
  206. }
  207. _, err = section.NewKey("aws_access_key_id", accessKeyID)
  208. if err != nil {
  209. return err
  210. }
  211. _, err = section.NewKey("aws_secret_access_key", secretAccessKey)
  212. if err != nil {
  213. return err
  214. }
  215. return credIni.SaveTo(p.Filename)
  216. }
  217. func getProfiles() ([]string, error) {
  218. profiles := []string{}
  219. // parse both .aws/credentials and .aws/config for profiles
  220. configFiles := map[string]bool{
  221. defaults.SharedCredentialsFilename(): false,
  222. defaults.SharedConfigFilename(): true,
  223. }
  224. for f, prefix := range configFiles {
  225. sections, err := loadIniFile(f, prefix)
  226. if err != nil {
  227. if os.IsNotExist(err) {
  228. continue
  229. }
  230. return nil, err
  231. }
  232. for key := range sections {
  233. name := strings.ToLower(key)
  234. if !contains(profiles, name) {
  235. profiles = append(profiles, name)
  236. }
  237. }
  238. }
  239. return profiles, nil
  240. }
  241. func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) {
  242. options := []string{}
  243. options = append(options, profiles...)
  244. selected, err := h.user.Select("Select AWS Profile", options)
  245. if err != nil {
  246. if err == terminal.InterruptErr {
  247. return "", errdefs.ErrCanceled
  248. }
  249. return "", err
  250. }
  251. profile := options[selected]
  252. return profile, nil
  253. }
  254. func getRegion(profile string) (string, bool, error) {
  255. if profile == "" {
  256. profile = "default"
  257. }
  258. // only load ~/.aws/config
  259. awsConfig := defaults.SharedConfigFilename()
  260. configIni, err := ini.Load(awsConfig)
  261. if err != nil {
  262. if !os.IsNotExist(err) {
  263. return "", false, err
  264. }
  265. configIni = ini.Empty()
  266. }
  267. var f func(string) (string, string)
  268. f = func(p string) (string, string) {
  269. r := ""
  270. section, err := configIni.GetSection(p)
  271. if err == nil {
  272. reg, err := section.GetKey("region")
  273. if err == nil {
  274. r = reg.Value()
  275. }
  276. }
  277. if r == "" {
  278. switch p {
  279. case "":
  280. return "us-east-1", ""
  281. case "default":
  282. return f("")
  283. }
  284. return f("default")
  285. }
  286. return r, p
  287. }
  288. if profile != "default" {
  289. profile = fmt.Sprintf("profile %s", profile)
  290. }
  291. region, p := f(profile)
  292. return region, p == profile, nil
  293. }
  294. func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) {
  295. suggestion := region
  296. if suggestion == "" {
  297. region, _, err := getRegion(profile)
  298. if err != nil {
  299. return "", err
  300. }
  301. suggestion = region
  302. }
  303. // promp user for region
  304. region, err := h.user.Input("Region", suggestion)
  305. if err != nil {
  306. return "", err
  307. }
  308. if region == "" {
  309. return "", fmt.Errorf("region cannot be empty")
  310. }
  311. return region, nil
  312. }
  313. func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
  314. accessKeyID, err := h.user.Input("AWS Access Key ID", "")
  315. if err != nil {
  316. return "", "", err
  317. }
  318. secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key")
  319. if err != nil {
  320. return "", "", err
  321. }
  322. // validate access ID and password
  323. if len(accessKeyID) < 3 || len(secretAccessKey) < 3 {
  324. return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters")
  325. }
  326. return accessKeyID, secretAccessKey, nil
  327. }
  328. func contains(values []string, value string) bool {
  329. for _, v := range values {
  330. if v == value {
  331. return true
  332. }
  333. }
  334. return false
  335. }
  336. func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) {
  337. profiles := map[string]ini.Section{}
  338. credIni, err := ini.Load(path)
  339. if err != nil {
  340. return nil, err
  341. }
  342. for _, section := range credIni.Sections() {
  343. if prefix && strings.HasPrefix(section.Name(), "profile ") {
  344. profiles[section.Name()[len("profile "):]] = *section
  345. } else if !prefix || section.Name() == "default" {
  346. profiles[section.Name()] = *section
  347. }
  348. }
  349. return profiles, nil
  350. }