context.go 9.2 KB

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