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  }