context.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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. "github.com/pkg/errors"
  24. "gopkg.in/ini.v1"
  25. "github.com/docker/compose-cli/context/store"
  26. "github.com/docker/compose-cli/errdefs"
  27. "github.com/docker/compose-cli/prompt"
  28. )
  29. type contextCreateAWSHelper struct {
  30. user prompt.UI
  31. }
  32. func newContextCreateHelper() contextCreateAWSHelper {
  33. return contextCreateAWSHelper{
  34. user: prompt.User{},
  35. }
  36. }
  37. func (h contextCreateAWSHelper) createProfile(name string) error {
  38. accessKey, secretKey, err := h.askCredentials()
  39. if err != nil {
  40. return err
  41. }
  42. if accessKey != "" && secretKey != "" {
  43. return h.saveCredentials(name, accessKey, secretKey)
  44. }
  45. return nil
  46. }
  47. func (h contextCreateAWSHelper) createContext(profile, region, description string) (interface{}, string) {
  48. if profile == "default" {
  49. profile = ""
  50. }
  51. description = strings.TrimSpace(
  52. fmt.Sprintf("%s (%s)", description, region))
  53. return store.EcsContext{
  54. Profile: profile,
  55. Region: region,
  56. }, description
  57. }
  58. func (h contextCreateAWSHelper) createContextData(_ context.Context, opts ContextParams) (interface{}, string, error) {
  59. profile := opts.Profile
  60. region := opts.Region
  61. profilesList, err := h.getProfiles()
  62. if err != nil {
  63. return nil, "", err
  64. }
  65. if profile != "" {
  66. // validate profile
  67. if profile != "default" && !contains(profilesList, profile) {
  68. return nil, "", errors.Wrapf(errdefs.ErrNotFound, "profile %q", profile)
  69. }
  70. } else {
  71. // choose profile
  72. profile, err = h.chooseProfile(profilesList)
  73. if err != nil {
  74. return nil, "", err
  75. }
  76. }
  77. if region == "" {
  78. region, err = h.chooseRegion(region, profile)
  79. if err != nil {
  80. return nil, "", err
  81. }
  82. }
  83. ecsCtx, descr := h.createContext(profile, region, opts.Description)
  84. return ecsCtx, descr, nil
  85. }
  86. func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID string, secretAccessKey string) error {
  87. p := credentials.SharedCredentialsProvider{Profile: profile}
  88. _, err := p.Retrieve()
  89. if err == nil {
  90. return fmt.Errorf("credentials already exist")
  91. }
  92. if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" {
  93. _, err := os.Create(p.Filename)
  94. if err != nil {
  95. return err
  96. }
  97. }
  98. credIni, err := ini.Load(p.Filename)
  99. if err != nil {
  100. return err
  101. }
  102. section, err := credIni.NewSection(profile)
  103. if err != nil {
  104. return err
  105. }
  106. _, err = section.NewKey("aws_access_key_id", accessKeyID)
  107. if err != nil {
  108. return err
  109. }
  110. _, err = section.NewKey("aws_secret_access_key", secretAccessKey)
  111. if err != nil {
  112. return err
  113. }
  114. return credIni.SaveTo(p.Filename)
  115. }
  116. func (h contextCreateAWSHelper) getProfiles() ([]string, error) {
  117. profiles := []string{}
  118. // parse both .aws/credentials and .aws/config for profiles
  119. configFiles := map[string]bool{
  120. defaults.SharedCredentialsFilename(): false,
  121. defaults.SharedConfigFilename(): true,
  122. }
  123. for f, prefix := range configFiles {
  124. sections, err := loadIniFile(f, prefix)
  125. if err != nil {
  126. if os.IsNotExist(err) {
  127. continue
  128. }
  129. return nil, err
  130. }
  131. for key := range sections {
  132. name := strings.ToLower(key)
  133. if !contains(profiles, name) {
  134. profiles = append(profiles, name)
  135. }
  136. }
  137. }
  138. return profiles, nil
  139. }
  140. func (h contextCreateAWSHelper) chooseProfile(profiles []string) (string, error) {
  141. options := []string{"new profile"}
  142. options = append(options, profiles...)
  143. selected, err := h.user.Select("Select AWS Profile", options)
  144. if err != nil {
  145. if err == terminal.InterruptErr {
  146. return "", errdefs.ErrCanceled
  147. }
  148. return "", err
  149. }
  150. profile := options[selected]
  151. if options[selected] == "new profile" {
  152. suggestion := ""
  153. if !contains(profiles, "default") {
  154. suggestion = "default"
  155. }
  156. name, err := h.user.Input("profile name", suggestion)
  157. if err != nil {
  158. return "", err
  159. }
  160. if name == "" {
  161. return "", fmt.Errorf("profile name cannot be empty")
  162. }
  163. return name, h.createProfile(name)
  164. }
  165. return profile, nil
  166. }
  167. func (h contextCreateAWSHelper) chooseRegion(region string, profile string) (string, error) {
  168. suggestion := region
  169. // only load ~/.aws/config
  170. awsConfig := defaults.SharedConfigFilename()
  171. configIni, err := ini.Load(awsConfig)
  172. if err != nil {
  173. if !os.IsNotExist(err) {
  174. return "", err
  175. }
  176. configIni = ini.Empty()
  177. }
  178. if profile != "default" {
  179. profile = fmt.Sprintf("profile %s", profile)
  180. }
  181. section, err := configIni.GetSection(profile)
  182. if err != nil {
  183. if !strings.Contains(err.Error(), "does not exist") {
  184. return "", err
  185. }
  186. section, err = configIni.NewSection(profile)
  187. if err != nil {
  188. return "", err
  189. }
  190. }
  191. reg, err := section.GetKey("region")
  192. if err == nil {
  193. suggestion = reg.Value()
  194. }
  195. // promp user for region
  196. region, err = h.user.Input("Region", suggestion)
  197. if err != nil {
  198. return "", err
  199. }
  200. if region == "" {
  201. return "", fmt.Errorf("region cannot be empty")
  202. }
  203. // save selected/typed region under profile in ~/.aws/config
  204. _, err = section.NewKey("region", region)
  205. if err != nil {
  206. return "", err
  207. }
  208. return region, configIni.SaveTo(awsConfig)
  209. }
  210. func (h contextCreateAWSHelper) askCredentials() (string, string, error) {
  211. confirm, err := h.user.Confirm("Enter AWS credentials", false)
  212. if err != nil {
  213. return "", "", err
  214. }
  215. if !confirm {
  216. return "", "", nil
  217. }
  218. accessKeyID, err := h.user.Input("AWS Access Key ID", "")
  219. if err != nil {
  220. return "", "", err
  221. }
  222. secretAccessKey, err := h.user.Password("Enter AWS Secret Access Key")
  223. if err != nil {
  224. return "", "", err
  225. }
  226. // validate access ID and password
  227. if len(accessKeyID) < 3 || len(secretAccessKey) < 3 {
  228. return "", "", fmt.Errorf("AWS Access/Secret Access Key must have more than 3 characters")
  229. }
  230. return accessKeyID, secretAccessKey, nil
  231. }
  232. func contains(values []string, value string) bool {
  233. for _, v := range values {
  234. if v == value {
  235. return true
  236. }
  237. }
  238. return false
  239. }
  240. func loadIniFile(path string, prefix bool) (map[string]ini.Section, error) {
  241. profiles := map[string]ini.Section{}
  242. credIni, err := ini.Load(path)
  243. if err != nil {
  244. return nil, err
  245. }
  246. for _, section := range credIni.Sections() {
  247. if prefix && strings.HasPrefix(section.Name(), "profile ") {
  248. profiles[section.Name()[len("profile "):]] = *section
  249. } else if !prefix || section.Name() == "default" {
  250. profiles[section.Name()] = *section
  251. }
  252. }
  253. return profiles, nil
  254. }