context.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. /*if creds.Profile == "" {
  138. creds.Profile = opts.Name
  139. }
  140. fmt.Printf("Saving credentials under profile %s\n", creds.Profile)
  141. h.createProfile(creds.Profile, &creds)*/
  142. case 1:
  143. accessKey, secretKey, err := h.askCredentials()
  144. if err != nil {
  145. return nil, "", err
  146. }
  147. creds.AccessKey = accessKey
  148. creds.SecretKey = secretKey
  149. // we need a region set -- either read it from profile or prompt user
  150. // prompt for the region to use with this context
  151. creds.Region, err = h.chooseRegion(creds.Region, creds.Profile)
  152. if err != nil {
  153. return nil, "", err
  154. }
  155. // save as a profile
  156. if creds.Profile == "" {
  157. creds.Profile = opts.Name
  158. }
  159. fmt.Printf("Saving credentials under profile %s\n", creds.Profile)
  160. h.createProfile(creds.Profile, &creds)
  161. case 2:
  162. profilesList, err := h.getProfiles()
  163. if err != nil {
  164. return nil, "", err
  165. }
  166. // choose profile
  167. creds.Profile, err = h.chooseProfile(profilesList)
  168. if err != nil {
  169. return nil, "", err
  170. }
  171. if creds.Region == "" {
  172. creds.Region, err = h.chooseRegion(creds.Region, creds.Profile)
  173. if err != nil {
  174. return nil, "", err
  175. }
  176. }
  177. }
  178. //os.Exit(0)
  179. ecsCtx, descr := h.createContext(&creds, opts.Description)
  180. return ecsCtx, descr, nil
  181. }
  182. func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error {
  183. p := credentials.SharedCredentialsProvider{Profile: profile}
  184. _, err := p.Retrieve()
  185. if err == nil {
  186. return fmt.Errorf("credentials already exist")
  187. }
  188. if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" {
  189. _, err := os.Create(p.Filename)
  190. if err != nil {
  191. return err
  192. }
  193. }
  194. credIni, err := ini.Load(p.Filename)
  195. if err != nil {
  196. return err
  197. }
  198. section, err := credIni.NewSection(profile)
  199. if err != nil {
  200. return err
  201. }
  202. _, err = section.NewKey("aws_access_key_id", accessKeyID)
  203. if err != nil {
  204. return err
  205. }
  206. _, err = section.NewKey("aws_secret_access_key", secretAccessKey)
  207. if err != nil {
  208. return err
  209. }
  210. return credIni.SaveTo(p.Filename)
  211. }
  212. func (h contextCreateAWSHelper) getProfiles() ([]string, error) {
  213. profiles := []string{}
  214. // parse both .aws/credentials and .aws/config for profiles
  215. configFiles := map[string]bool{
  216. defaults.SharedCredentialsFilename(): false,
  217. defaults.SharedConfigFilename(): true,
  218. }
  219. for f, prefix := range configFiles {
  220. sections, err := loadIniFile(f, prefix)
  221. if err != nil {
  222. if os.IsNotExist(err) {
  223. continue
  224. }
  225. return nil, err
  226. }
  227. for key := range sections {
  228. name := strings.ToLower(key)
  229. if !contains(profiles, name) {
  230. profiles = append(profiles, name)
  231. }
  232. }
  233. }
  234. return profiles, nil
  235. }
  236. func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) {
  237. options := []string{}
  238. options = append(options, profiles...)
  239. selected, err := h.user.Select("Select AWS Profile", options)
  240. if err != nil {
  241. if err == terminal.InterruptErr {
  242. return "", errdefs.ErrCanceled
  243. }
  244. return "", err
  245. }
  246. profile := options[selected]
  247. return profile, nil
  248. }
  249. func (h contextCreateAWSHelper) getRegionSuggestion(region string, profile string) (string, error) {
  250. if profile == "" {
  251. profile = "default"
  252. }
  253. // only load ~/.aws/config
  254. awsConfig := defaults.SharedConfigFilename()
  255. configIni, err := ini.Load(awsConfig)
  256. if err != nil {
  257. if !os.IsNotExist(err) {
  258. return "", err
  259. }
  260. configIni = ini.Empty()
  261. }
  262. var f func(string, string) string
  263. f = func(r string, p string) string {
  264. section, err := configIni.GetSection(p)
  265. if err == nil {
  266. reg, err := section.GetKey("region")
  267. if err == nil {
  268. r = reg.Value()
  269. }
  270. }
  271. if r == "" {
  272. switch p {
  273. case "":
  274. return "us-east-1"
  275. case "default":
  276. return f(r, "")
  277. }
  278. return f(r, "default")
  279. }
  280. return r
  281. }
  282. if profile != "default" {
  283. profile = fmt.Sprintf("profile %s", profile)
  284. }
  285. return f(region, profile), nil
  286. }
  287. func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) {
  288. suggestion, err := h.getRegionSuggestion(region, profile)
  289. if err != nil {
  290. return "", err
  291. }
  292. // promp user for region
  293. region, err = h.user.Input("Region", suggestion)
  294. if err != nil {
  295. return "", err
  296. }
  297. if region == "" {
  298. return "", fmt.Errorf("region cannot be empty")
  299. }
  300. return region, nil
  301. }
  302. func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
  303. /*confirm, err := h.user.Confirm("Enter AWS credentials", false)
  304. if err != nil {
  305. return "", "", err
  306. }
  307. if !confirm {
  308. return "", "", nil
  309. }*/
  310. accessKeyID, err := h.user.Input("AWS Access Key ID", "")
  311. if err != nil {
  312. return "", "", err
  313. }
  314. secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key")
  315. if err != nil {
  316. return "", "", err
  317. }
  318. // validate access ID and password
  319. if len(accessKeyID) < 3 || len(secretAccessKey) < 3 {
  320. return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters")
  321. }
  322. return accessKeyID, secretAccessKey, nil
  323. }
  324. func contains(values []string, value string) bool {
  325. for _, v := range values {
  326. if v == value {
  327. return true
  328. }
  329. }
  330. return false
  331. }
  332. func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) {
  333. profiles := map[string]ini.Section{}
  334. credIni, err := ini.Load(path)
  335. if err != nil {
  336. return nil, err
  337. }
  338. for _, section := range credIni.Sections() {
  339. if prefix && strings.HasPrefix(section.Name(), "profile ") {
  340. profiles[section.Name()[len("profile "):]] = *section
  341. } else if !prefix || section.Name() == "default" {
  342. profiles[section.Name()] = *section
  343. }
  344. }
  345. return profiles, nil
  346. }