context.go 9.0 KB

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