helper.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. package helper
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "log"
  7. "net/url"
  8. "os"
  9. "path/filepath"
  10. "regexp"
  11. "strings"
  12. "time"
  13. "github.com/docker/docker-credential-helpers/credentials"
  14. "github.com/porter-dev/porter/cli/cmd"
  15. "github.com/porter-dev/porter/cli/cmd/api"
  16. "github.com/spf13/viper"
  17. "k8s.io/client-go/util/homedir"
  18. )
  19. // PorterHelper implements credentials.Helper: it acts as a credentials
  20. // helper for Docker that allows authentication with different registries.
  21. type PorterHelper struct {
  22. Debug bool
  23. credCache CredentialsCache
  24. }
  25. // Add appends credentials to the store.
  26. func (p *PorterHelper) Add(cr *credentials.Credentials) error {
  27. // Doesn't seem to be called
  28. return nil
  29. }
  30. // Delete removes credentials from the store.
  31. func (p *PorterHelper) Delete(serverURL string) error {
  32. // Doesn't seem to be called
  33. return nil
  34. }
  35. var ecrPattern = regexp.MustCompile(`(^[a-zA-Z0-9][a-zA-Z0-9-_]*)\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?`)
  36. // Get retrieves credentials from the store.
  37. // It returns username and secret as strings.
  38. func (p *PorterHelper) Get(serverURL string) (user string, secret string, err error) {
  39. p.init()
  40. if strings.Contains(serverURL, "gcr.io") {
  41. return p.getGCR(serverURL)
  42. }
  43. return p.getECR(serverURL)
  44. }
  45. func (p *PorterHelper) getGCR(serverURL string) (user string, secret string, err error) {
  46. urlP, err := url.Parse(serverURL)
  47. if err != nil {
  48. return "", "", err
  49. }
  50. credCache := BuildCredentialsCache(urlP.Host)
  51. cachedEntry := credCache.Get(serverURL)
  52. var token string
  53. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  54. token = cachedEntry.AuthorizationToken
  55. } else {
  56. host := viper.GetString("host")
  57. projID := viper.GetUint("project")
  58. client := api.NewClient(host+"/api", "cookie.json")
  59. // get a token from the server
  60. tokenResp, err := client.GetGCRAuthorizationToken(context.Background(), projID, &api.GetGCRTokenRequest{
  61. ServerURL: serverURL,
  62. })
  63. if err != nil {
  64. return "", "", err
  65. }
  66. token = tokenResp.Token
  67. // set the token in cache
  68. credCache.Set(serverURL, &AuthEntry{
  69. AuthorizationToken: token,
  70. RequestedAt: time.Now(),
  71. ExpiresAt: *tokenResp.ExpiresAt,
  72. ProxyEndpoint: serverURL,
  73. })
  74. }
  75. return "oauth2accesstoken", token, nil
  76. }
  77. func (p *PorterHelper) getECR(serverURL string) (user string, secret string, err error) {
  78. // parse the server url for region
  79. matches := ecrPattern.FindStringSubmatch(serverURL)
  80. if len(matches) == 0 {
  81. err := fmt.Errorf("only ECR registry URLs are supported")
  82. if p.Debug {
  83. log.Printf("Error: %s\n", err.Error())
  84. }
  85. return "", "", err
  86. } else if len(matches) < 3 {
  87. err := fmt.Errorf("%s is not a valid ECR repository URI", serverURL)
  88. if p.Debug {
  89. log.Printf("Error: %s\n", err.Error())
  90. }
  91. return "", "", err
  92. }
  93. region := matches[3]
  94. credCache := BuildCredentialsCache(region)
  95. cachedEntry := credCache.Get(serverURL)
  96. var token string
  97. if cachedEntry != nil && cachedEntry.IsValid(time.Now()) {
  98. token = cachedEntry.AuthorizationToken
  99. } else {
  100. host := viper.GetString("host")
  101. projID := viper.GetUint("project")
  102. client := api.NewClient(host+"/api", "cookie.json")
  103. // get a token from the server
  104. tokenResp, err := client.GetECRAuthorizationToken(context.Background(), projID, matches[3])
  105. if err != nil {
  106. return "", "", err
  107. }
  108. token = tokenResp.Token
  109. // set the token in cache
  110. credCache.Set(serverURL, &AuthEntry{
  111. AuthorizationToken: token,
  112. RequestedAt: time.Now(),
  113. ExpiresAt: *tokenResp.ExpiresAt,
  114. ProxyEndpoint: serverURL,
  115. })
  116. }
  117. return p.getAuth(token)
  118. }
  119. // List returns the stored serverURLs and their associated usernames.
  120. func (p *PorterHelper) List() (map[string]string, error) {
  121. p.init()
  122. credCache := BuildCredentialsCache("")
  123. entries := credCache.List()
  124. res := make(map[string]string)
  125. for _, entry := range entries {
  126. user, _, err := p.getAuth(entry.AuthorizationToken)
  127. if err != nil {
  128. continue
  129. }
  130. res[entry.ProxyEndpoint] = user
  131. }
  132. return res, nil
  133. }
  134. func (p *PorterHelper) getAuth(token string) (string, string, error) {
  135. decodedToken, err := base64.StdEncoding.DecodeString(token)
  136. if err != nil {
  137. return "", "", fmt.Errorf("Invalid token: %v", err)
  138. }
  139. parts := strings.SplitN(string(decodedToken), ":", 2)
  140. if len(parts) < 2 {
  141. return "", "", fmt.Errorf("Invalid token: expected two parts, got %d", len(parts))
  142. }
  143. return parts[0], parts[1], nil
  144. }
  145. func (p *PorterHelper) init() {
  146. cmd.Setup()
  147. if p.Debug {
  148. var home = homedir.HomeDir()
  149. file, err := os.OpenFile(filepath.Join(home, ".porter", "logs.txt"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
  150. if err == nil {
  151. log.SetOutput(file)
  152. }
  153. }
  154. }