...

Source file src/edge-infra.dev/pkg/f8n/warehouse/oci/cache/cache.go

Documentation: edge-infra.dev/pkg/f8n/warehouse/oci/cache

     1  package cache
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/google/go-containerregistry/pkg/name"
     9  	v1 "github.com/google/go-containerregistry/pkg/v1"
    10  	"github.com/google/go-containerregistry/pkg/v1/remote"
    11  
    12  	"edge-infra.dev/pkg/f8n/warehouse/oci"
    13  	"edge-infra.dev/pkg/f8n/warehouse/oci/cache/providers/memory"
    14  	"edge-infra.dev/pkg/f8n/warehouse/oci/layer"
    15  	whremote "edge-infra.dev/pkg/f8n/warehouse/oci/remote"
    16  )
    17  
    18  // [Cache] is the interface for a pull-through cache for Warehouse artifacts with support
    19  // for in-memory and disk caching layers.
    20  type Cache interface {
    21  	Get(name.Reference, ...GetOption) (oci.Artifact, error)
    22  	Exists(v1.Hash) bool
    23  	Add(oci.Artifact) (oci.Artifact, error)
    24  	Len() int
    25  }
    26  
    27  // [LazyCache] is a cache for Warehouse artifacts with support for lazily evaluating OCI Artifacts
    28  // by falling back from in-memory to network access
    29  type LazyCache struct {
    30  	tags     map[string]v1.Hash
    31  	puller   *remote.Puller
    32  	memory   *memory.ArcCache
    33  	recorder Recorder
    34  }
    35  
    36  // Recorder defines callbacks that are triggered by the cache.
    37  //
    38  // NOTE: This allows clean integration with metrics collectors without taking on
    39  // the massive dependency graph of the Prometheus package.
    40  type Recorder interface {
    41  	RecordGet(hit bool, provider string, objType string)
    42  	// TODO consider recording evictions
    43  }
    44  
    45  var (
    46  	// ErrNotFound error returned when the requested content does not exist in the cache.
    47  	ErrNotFound = errors.New("content not found")
    48  
    49  	// ErrInterfaceConversion
    50  	ErrInterfaceConversion = errors.New("failed to convert cached object to requested type")
    51  )
    52  
    53  // New return a new instance of the Warehouse [Cache].
    54  func New(opts ...Option) (*LazyCache, error) {
    55  	var (
    56  		mcache  *memory.ArcCache
    57  		options = makeOptions(opts...)
    58  	)
    59  
    60  	if options.memoryCacheSize > 0 {
    61  		memorycache, err := memory.New(options.memoryCacheSize)
    62  		if err != nil {
    63  			return nil, err
    64  		}
    65  		mcache = memorycache
    66  	}
    67  
    68  	return &LazyCache{
    69  		tags:     map[string]v1.Hash{},
    70  		memory:   mcache,
    71  		recorder: options.recorder,
    72  		puller:   options.puller,
    73  	}, nil
    74  }
    75  
    76  // Get attempts to retrieve an artifact by reference from the configured caches,
    77  // fetching from the remote registry if not found.
    78  func (w *LazyCache) Get(ref name.Reference, opts ...GetOption) (oci.Artifact, error) {
    79  	options := makeGetOptions(opts...)
    80  	if w.puller != nil {
    81  		options.remoteOpts = append(options.remoteOpts, remote.Reuse(w.puller))
    82  	}
    83  
    84  	h, err := w.resolveRef(ref, options.resolveTag, options.remoteOpts...)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	if a, err := w.getArtifact(h); err == nil {
    90  		return a, nil
    91  	} else if !errors.Is(err, ErrNotFound) {
    92  		return nil, fmt.Errorf("error getting cached artifact: %v", err)
    93  	}
    94  
    95  	return w.getRemote(ref, options.remoteOpts...)
    96  }
    97  
    98  // Exists checks if the artifact with the given hash exists in the cache
    99  func (w *LazyCache) Exists(h v1.Hash) bool {
   100  	return w.memory.Exists(h)
   101  }
   102  
   103  // Add inserts an artifact into the [Cache] and returns a version
   104  // wrapped with a caching accessor. Further operations should use the
   105  // returned Artifact
   106  func (w *LazyCache) Add(a oci.Artifact) (oci.Artifact, error) {
   107  	h, err := a.Digest()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	var cached oci.Artifact
   113  	switch a := a.(type) {
   114  	case oci.Unwrapper:
   115  		return w.Add(a.Unwrap())
   116  	case v1.ImageIndex:
   117  		cached = ImageIndex(a, w)
   118  	case v1.Image:
   119  		cached = Image(a, w)
   120  	default:
   121  		return nil, oci.ErrInvalidArtifact
   122  	}
   123  	w.memory.Add(h, cached)
   124  
   125  	return cached, nil
   126  }
   127  
   128  // Len returns the number of artifacts in the cache.
   129  func (w *LazyCache) Len() int {
   130  	if w.memory != nil {
   131  		return w.memory.Len()
   132  	}
   133  	return 0
   134  }
   135  
   136  // resolveRef converts ref into a digest. If ref contains a digest, it is returned. If ref contains a tag,
   137  // the digest is either fetched from the registry or retrieved from a local cache, depending on fetchTag.
   138  // The tag will always be fetched if there is no cached result
   139  func (w *LazyCache) resolveRef(ref name.Reference, fetchTag bool, remoteOpts ...remote.Option) (v1.Hash, error) {
   140  	var h v1.Hash
   141  	switch r := ref.(type) {
   142  	case name.Digest:
   143  		hash, err := v1.NewHash(r.DigestStr())
   144  		if err != nil {
   145  			return v1.Hash{}, err
   146  		}
   147  		h = hash
   148  	case name.Tag:
   149  		resolved, ok := w.tags[r.String()]
   150  		if fetchTag || !ok {
   151  			desc, err := remote.Head(r, remoteOpts...)
   152  			if err != nil {
   153  				return v1.Hash{}, err
   154  			}
   155  			h = desc.Digest
   156  		} else {
   157  			h = resolved
   158  		}
   159  		w.tags[r.String()] = h
   160  	default:
   161  		return v1.Hash{}, fmt.Errorf("invalid ref. ref must either be a Digest or Tag, but was %T", ref)
   162  	}
   163  	return h, nil
   164  }
   165  
   166  // getRemote fetches a reference and stores it in the cache
   167  func (w *LazyCache) getRemote(ref name.Reference, opts ...remote.Option) (oci.Artifact, error) {
   168  	a, err := whremote.Get(ref, opts...)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return w.Add(a)
   173  }
   174  
   175  // recordGet increments cache get metric via the record using the given dimensions
   176  // nolint:unparam // memory always passed here, kept open for compatibility with existing metrics
   177  func (w *LazyCache) recordGet(hit bool, provider string, objType string) {
   178  	if w.recorder != nil {
   179  		w.recorder.RecordGet(hit, provider, objType)
   180  	}
   181  }
   182  
   183  func (w *LazyCache) getArtifact(h v1.Hash) (oci.Artifact, error) {
   184  	var obj any
   185  	var hit bool
   186  	if w.memory != nil {
   187  		obj, hit = w.memory.Get(h)
   188  		if !hit {
   189  			w.recordGet(hit, "memory", "artifact")
   190  			return nil, ErrNotFound
   191  		}
   192  	}
   193  	var artifact oci.Artifact
   194  	switch a := obj.(type) {
   195  	case oci.Unwrapper:
   196  		artifact = a.Unwrap()
   197  	case v1.ImageIndex:
   198  		artifact = a
   199  	case v1.Image:
   200  		artifact = a
   201  	default:
   202  		return nil, ErrInterfaceConversion
   203  	}
   204  	w.recordGet(hit, "memory", "artifact")
   205  	return artifact, nil
   206  }
   207  
   208  func (w *LazyCache) putBlob(h v1.Hash, blob []byte) {
   209  	w.memory.Add(h, blob)
   210  }
   211  
   212  func (w *LazyCache) getLayer(h v1.Hash) (layer.Layer, error) {
   213  	var obj any
   214  	var hit bool
   215  	if w.memory != nil {
   216  		obj, hit = w.memory.Get(h)
   217  		if !hit {
   218  			w.recordGet(hit, "memory", "layer")
   219  			return nil, ErrNotFound
   220  		}
   221  	}
   222  
   223  	l, ok := obj.(layer.Layer)
   224  	if !ok {
   225  		return nil, ErrInterfaceConversion
   226  	}
   227  	w.recordGet(hit, "memory", "layer")
   228  
   229  	return l, nil
   230  }
   231  
   232  func (w *LazyCache) putLayer(l layer.Layer) (layer.Layer, error) {
   233  	h, err := l.Digest()
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	rc, err := l.Uncompressed()
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	data, err := io.ReadAll(rc)
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	cached := Layer(l, w, data)
   246  	w.memory.Add(h, cached)
   247  
   248  	return cached, nil
   249  }
   250  
   251  func (w *LazyCache) getImage(h v1.Hash) (v1.Image, error) {
   252  	var obj any
   253  	var hit bool
   254  	if w.memory != nil {
   255  		obj, hit = w.memory.Get(h)
   256  		if !hit {
   257  			w.recordGet(hit, "memory", "image")
   258  			return nil, ErrNotFound
   259  		}
   260  	}
   261  	img, ok := obj.(v1.Image)
   262  	if !ok {
   263  		return nil, ErrInterfaceConversion
   264  	}
   265  	w.recordGet(hit, "memory", "image")
   266  
   267  	return img, nil
   268  }
   269  
   270  func (w *LazyCache) putImage(img v1.Image) (v1.Image, error) {
   271  	h, err := img.Digest()
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	cached := Image(img, w)
   276  	w.memory.Add(h, cached)
   277  
   278  	return cached, nil
   279  }
   280  
   281  func (w *LazyCache) getIndex(h v1.Hash) (v1.ImageIndex, error) {
   282  	var obj any
   283  	var hit bool
   284  	if w.memory != nil {
   285  		obj, hit = w.memory.Get(h)
   286  		if !hit {
   287  			w.recordGet(hit, "memory", "index")
   288  			return nil, ErrNotFound
   289  		}
   290  	}
   291  	ii, ok := obj.(v1.ImageIndex)
   292  	if !ok {
   293  		return nil, ErrInterfaceConversion
   294  	}
   295  	w.recordGet(hit, "memory", "index")
   296  
   297  	return ii, nil
   298  }
   299  
   300  func (w *LazyCache) putIndex(ii v1.ImageIndex) (v1.ImageIndex, error) {
   301  	h, err := ii.Digest()
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	cached := ImageIndex(ii, w)
   306  	w.memory.Add(h, cached)
   307  
   308  	return cached, nil
   309  }
   310  

View as plain text