cache.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package helper
  2. import (
  3. "crypto/md5"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "io/ioutil"
  8. "os"
  9. "path/filepath"
  10. "time"
  11. "github.com/aws/aws-sdk-go/aws/credentials"
  12. "k8s.io/client-go/util/homedir"
  13. )
  14. type CredentialsCache interface {
  15. Get(registry string) *AuthEntry
  16. Set(registry string, entry *AuthEntry)
  17. List() []*AuthEntry
  18. Clear()
  19. }
  20. type AuthEntry struct {
  21. AuthorizationToken string
  22. RequestedAt time.Time
  23. ExpiresAt time.Time
  24. ProxyEndpoint string
  25. }
  26. // IsValid checks if AuthEntry is still valid at testTime. AuthEntries expire at 1/2 of their original
  27. // requested window.
  28. func (authEntry *AuthEntry) IsValid(testTime time.Time) bool {
  29. validWindow := authEntry.ExpiresAt.Sub(authEntry.RequestedAt)
  30. refreshTime := authEntry.ExpiresAt.Add(-1 * validWindow / time.Duration(2))
  31. return testTime.Before(refreshTime)
  32. }
  33. func BuildCredentialsCache(region string) CredentialsCache {
  34. home := homedir.HomeDir()
  35. cacheDir := filepath.Join(home, ".porter")
  36. cacheFilename := "cache.json"
  37. return NewFileCredentialsCache(cacheDir, cacheFilename, region)
  38. }
  39. // Determine a key prefix for a credentials cache. Because auth tokens are scoped to an account and region, rely on provided
  40. // region, as well as hash of the access key.
  41. func credentialsCachePrefix(region string, credentials *credentials.Value) string {
  42. return fmt.Sprintf("%s-%s-", region, checksum(credentials.AccessKeyID))
  43. }
  44. // Base64 encodes an MD5 checksum. Relied on for uniqueness, and not for cryptographic security.
  45. func checksum(text string) string {
  46. hasher := md5.New()
  47. data := hasher.Sum([]byte(text))
  48. return base64.StdEncoding.EncodeToString(data)
  49. }
  50. const registryCacheVersion = "1.0"
  51. type RegistryCache struct {
  52. Registries map[string]*AuthEntry
  53. Version string
  54. }
  55. type fileCredentialCache struct {
  56. path string
  57. filename string
  58. cachePrefixKey string
  59. }
  60. func newRegistryCache() *RegistryCache {
  61. return &RegistryCache{
  62. Registries: make(map[string]*AuthEntry),
  63. Version: registryCacheVersion,
  64. }
  65. }
  66. // NewFileCredentialsCache returns a new file credentials cache.
  67. //
  68. // path is used for temporary files during save, and filename should be a relative filename
  69. // in the same directory where the cache is serialized and deserialized.
  70. //
  71. // cachePrefixKey is used for scoping credentials for a given credential cache (i.e. region and
  72. // accessKey).
  73. func NewFileCredentialsCache(path string, filename string, cachePrefixKey string) CredentialsCache {
  74. if _, err := os.Stat(path); err != nil {
  75. os.MkdirAll(path, 0700)
  76. }
  77. return &fileCredentialCache{path: path, filename: filename, cachePrefixKey: cachePrefixKey}
  78. }
  79. func (f *fileCredentialCache) Get(registry string) *AuthEntry {
  80. registryCache := f.init()
  81. return registryCache.Registries[f.cachePrefixKey+registry]
  82. }
  83. func (f *fileCredentialCache) Set(registry string, entry *AuthEntry) {
  84. registryCache := f.init()
  85. registryCache.Registries[f.cachePrefixKey+registry] = entry
  86. f.save(registryCache)
  87. }
  88. // List returns all of the available AuthEntries (regardless of prefix)
  89. func (f *fileCredentialCache) List() []*AuthEntry {
  90. registryCache := f.init()
  91. // optimize allocation for copy
  92. entries := make([]*AuthEntry, 0, len(registryCache.Registries))
  93. for _, entry := range registryCache.Registries {
  94. entries = append(entries, entry)
  95. }
  96. return entries
  97. }
  98. func (f *fileCredentialCache) Clear() {
  99. os.Remove(f.fullFilePath())
  100. }
  101. func (f *fileCredentialCache) fullFilePath() string {
  102. return filepath.Join(f.path, f.filename)
  103. }
  104. // Saves credential cache to disk. This writes to a temporary file first, then moves the file to the config location.
  105. // This eliminates from reading partially written credential files, and reduces (but does not eliminate) concurrent
  106. // file access. There is not guarantee here for handling multiple writes at once since there is no out of process locking.
  107. func (f *fileCredentialCache) save(registryCache *RegistryCache) error {
  108. file, err := ioutil.TempFile(f.path, ".config.json.tmp")
  109. if err != nil {
  110. return err
  111. }
  112. buff, err := json.MarshalIndent(registryCache, "", " ")
  113. if err != nil {
  114. file.Close()
  115. os.Remove(file.Name())
  116. return err
  117. }
  118. _, err = file.Write(buff)
  119. if err != nil {
  120. file.Close()
  121. os.Remove(file.Name())
  122. return err
  123. }
  124. file.Close()
  125. // note this is only atomic when relying on linux syscalls
  126. os.Rename(file.Name(), f.fullFilePath())
  127. return err
  128. }
  129. func (f *fileCredentialCache) init() *RegistryCache {
  130. registryCache, err := f.load()
  131. if err != nil {
  132. f.Clear()
  133. registryCache = newRegistryCache()
  134. }
  135. return registryCache
  136. }
  137. // Loading a cache from disk will return errors for malformed or incompatible cache files.
  138. func (f *fileCredentialCache) load() (*RegistryCache, error) {
  139. registryCache := newRegistryCache()
  140. file, err := os.Open(f.fullFilePath())
  141. if os.IsNotExist(err) {
  142. return registryCache, nil
  143. }
  144. if err != nil {
  145. return nil, err
  146. }
  147. defer file.Close()
  148. if err = json.NewDecoder(file).Decode(&registryCache); err != nil {
  149. return nil, err
  150. }
  151. if registryCache.Version != registryCacheVersion {
  152. return nil, fmt.Errorf("ecr: Registry cache version %#v is not compatible with %#v, ignoring existing cache",
  153. registryCache.Version,
  154. registryCacheVersion)
  155. }
  156. return registryCache, nil
  157. }