github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/registry/remote/auth/cache.go (about) 1 /* 2 Copyright The ORAS 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 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package auth 17 18 import ( 19 "context" 20 "strings" 21 "sync" 22 23 "github.com/opcr-io/oras-go/v2/errdef" 24 "github.com/opcr-io/oras-go/v2/internal/syncutil" 25 ) 26 27 // DefaultCache is the sharable cache used by DefaultClient. 28 var DefaultCache Cache = NewCache() 29 30 // Cache caches the auth-scheme and auth-token for the "Authorization" header in 31 // accessing the remote registry. 32 // Precisely, the header is `Authorization: auth-scheme auth-token`. 33 // The `auth-token` is a generic term as `token68` in RFC 7235 section 2.1. 34 type Cache interface { 35 // GetScheme returns the auth-scheme part cached for the given registry. 36 // A single registry is assumed to have a consistent scheme. 37 // If a registry has different schemes per path, the auth client is still 38 // workable. However, the cache may not be effective as the cache cannot 39 // correctly guess the scheme. 40 GetScheme(ctx context.Context, registry string) (Scheme, error) 41 42 // GetToken returns the auth-token part cached for the given registry of a 43 // given scheme. 44 // The underlying implementation MAY cache the token for all schemes for the 45 // given registry. 46 GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) 47 48 // Set fetches the token using the given fetch function and caches the token 49 // for the given scheme with the given key for the given registry. 50 // The return values of the fetch function is returned by this function. 51 // The underlying implementation MAY combine the fetch operation if the Set 52 // function is invoked multiple times at the same time. 53 Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) 54 } 55 56 // cacheEntry is a cache entry for a single registry. 57 type cacheEntry struct { 58 scheme Scheme 59 tokens sync.Map // map[string]string 60 } 61 62 // concurrentCache is a cache suitable for concurrent invocation. 63 type concurrentCache struct { 64 status sync.Map // map[string]*syncutil.Once 65 cache sync.Map // map[string]*cacheEntry 66 } 67 68 // NewCache creates a new go-routine safe cache instance. 69 func NewCache() Cache { 70 return &concurrentCache{} 71 } 72 73 // GetScheme returns the auth-scheme part cached for the given registry. 74 func (cc *concurrentCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { 75 entry, ok := cc.cache.Load(registry) 76 if !ok { 77 return SchemeUnknown, errdef.ErrNotFound 78 } 79 return entry.(*cacheEntry).scheme, nil 80 } 81 82 // GetToken returns the auth-token part cached for the given registry of a given 83 // scheme. 84 func (cc *concurrentCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { 85 entryValue, ok := cc.cache.Load(registry) 86 if !ok { 87 return "", errdef.ErrNotFound 88 } 89 entry := entryValue.(*cacheEntry) 90 if entry.scheme != scheme { 91 return "", errdef.ErrNotFound 92 } 93 if token, ok := entry.tokens.Load(key); ok { 94 return token.(string), nil 95 } 96 return "", errdef.ErrNotFound 97 } 98 99 // Set fetches the token using the given fetch function and caches the token 100 // for the given scheme with the given key for the given registry. 101 // Set combines the fetch operation if the Set is invoked multiple times at the 102 // same time. 103 func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { 104 // fetch token 105 statusKey := strings.Join([]string{ 106 registry, 107 scheme.String(), 108 key, 109 }, " ") 110 statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce()) 111 fetchOnce := statusValue.(*syncutil.Once) 112 fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) { 113 return fetch(ctx) 114 }) 115 if fetchedFirst { 116 cc.status.Delete(statusKey) 117 } 118 if err != nil { 119 return "", err 120 } 121 token := result.(string) 122 if !fetchedFirst { 123 return token, nil 124 } 125 126 // cache token 127 newEntry := &cacheEntry{ 128 scheme: scheme, 129 } 130 entryValue, exists := cc.cache.LoadOrStore(registry, newEntry) 131 entry := entryValue.(*cacheEntry) 132 if exists && entry.scheme != scheme { 133 // there is a scheme change, which is not expected in most scenarios. 134 // force invalidating all previous cache. 135 entry = newEntry 136 cc.cache.Store(registry, entry) 137 } 138 entry.tokens.Store(key, token) 139 140 return token, nil 141 } 142 143 // noCache is a cache implementation that does not do cache at all. 144 type noCache struct{} 145 146 // GetScheme always returns not found error as it has no cache. 147 func (noCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { 148 return SchemeUnknown, errdef.ErrNotFound 149 } 150 151 // GetToken always returns not found error as it has no cache. 152 func (noCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { 153 return "", errdef.ErrNotFound 154 } 155 156 // Set calls fetch directly without caching. 157 func (noCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { 158 return fetch(ctx) 159 }