cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ocimem/registry.go (about)

     1  // Copyright 2023 CUE Labs AG
     2  //
     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  // Package ocimem provides a simple in-memory implementation of
    16  // an OCI registry.
    17  package ocimem
    18  
    19  import (
    20  	"fmt"
    21  	"sync"
    22  
    23  	"cuelabs.dev/go/oci/ociregistry"
    24  	"cuelabs.dev/go/oci/ociregistry/ociref"
    25  	"github.com/opencontainers/go-digest"
    26  )
    27  
    28  var _ ociregistry.Interface = (*Registry)(nil)
    29  
    30  type Registry struct {
    31  	*ociregistry.Funcs
    32  	cfg   Config
    33  	mu    sync.Mutex
    34  	repos map[string]*repository
    35  }
    36  
    37  type repository struct {
    38  	tags      map[string]ociregistry.Descriptor
    39  	manifests map[ociregistry.Digest]*blob
    40  	blobs     map[ociregistry.Digest]*blob
    41  	uploads   map[string]*Buffer
    42  }
    43  
    44  type blob struct {
    45  	mediaType string
    46  	data      []byte
    47  	subject   digest.Digest
    48  }
    49  
    50  func (b *blob) descriptor() ociregistry.Descriptor {
    51  	return ociregistry.Descriptor{
    52  		MediaType: b.mediaType,
    53  		Size:      int64(len(b.data)),
    54  		Digest:    digest.FromBytes(b.data),
    55  	}
    56  }
    57  
    58  // TODO (breaking API change) rename NewWithConfig to New
    59  // so we don't have two very similar entry points.
    60  
    61  // New is like NewWithConfig(nil).
    62  func New() *Registry {
    63  	return NewWithConfig(nil)
    64  }
    65  
    66  // NewWithConfig returns a new in-memory [ociregistry.Interface]
    67  // implementation using the given configuration. If
    68  // cfg is nil, it's treated the same as a pointer to the zero [Config] value.
    69  func NewWithConfig(cfg0 *Config) *Registry {
    70  	var cfg Config
    71  	if cfg0 != nil {
    72  		cfg = *cfg0
    73  	}
    74  	return &Registry{
    75  		cfg: cfg,
    76  	}
    77  }
    78  
    79  // Config holds configuration for the registry.
    80  type Config struct {
    81  	// ImmutableTags specifies that tags in the registry cannot
    82  	// be changed. Specifically the following restrictions are enforced:
    83  	// - no removal of tags from a manifest
    84  	// - no pushing of a tag if that tag already exists with a different
    85  	// digest or media type.
    86  	// - no deletion of directly tagged manifests
    87  	// - no deletion of any blob or manifest that a tagged manifest
    88  	// refers to (TODO: not implemented yet)
    89  	ImmutableTags bool
    90  }
    91  
    92  func (r *Registry) repo(repoName string) (*repository, error) {
    93  	if repo, ok := r.repos[repoName]; ok {
    94  		return repo, nil
    95  	}
    96  	return nil, ociregistry.ErrNameUnknown
    97  }
    98  
    99  func (r *Registry) manifestForDigest(repoName string, dig ociregistry.Digest) (*blob, error) {
   100  	repo, err := r.repo(repoName)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  	b := repo.manifests[dig]
   105  	if b == nil {
   106  		return nil, ociregistry.ErrManifestUnknown
   107  	}
   108  	return b, nil
   109  }
   110  
   111  func (r *Registry) blobForDigest(repoName string, dig ociregistry.Digest) (*blob, error) {
   112  	repo, err := r.repo(repoName)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	b := repo.blobs[dig]
   117  	if b == nil {
   118  		return nil, ociregistry.ErrBlobUnknown
   119  	}
   120  	return b, nil
   121  }
   122  
   123  func (r *Registry) makeRepo(repoName string) (*repository, error) {
   124  	if !ociref.IsValidRepository(repoName) {
   125  		return nil, ociregistry.ErrNameInvalid
   126  	}
   127  	if r.repos == nil {
   128  		r.repos = make(map[string]*repository)
   129  	}
   130  	if repo := r.repos[repoName]; repo != nil {
   131  		return repo, nil
   132  	}
   133  	repo := &repository{
   134  		tags:      make(map[string]ociregistry.Descriptor),
   135  		manifests: make(map[digest.Digest]*blob),
   136  		blobs:     make(map[digest.Digest]*blob),
   137  		uploads:   make(map[string]*Buffer),
   138  	}
   139  	r.repos[repoName] = repo
   140  	return repo, nil
   141  }
   142  
   143  // SHA256("")
   144  const emptyHash = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
   145  
   146  // CheckDescriptor checks that the given descriptor matches the given data or,
   147  // if data is nil, that the descriptor looks sane.
   148  func CheckDescriptor(desc ociregistry.Descriptor, data []byte) error {
   149  	if err := desc.Digest.Validate(); err != nil {
   150  		return fmt.Errorf("invalid digest: %v", err)
   151  	}
   152  	if data != nil {
   153  		if digest.FromBytes(data) != desc.Digest {
   154  			return fmt.Errorf("digest mismatch")
   155  		}
   156  		if desc.Size != int64(len(data)) {
   157  			return fmt.Errorf("size mismatch")
   158  		}
   159  	} else {
   160  		if desc.Size == 0 && desc.Digest != emptyHash {
   161  			return fmt.Errorf("zero sized content with mismatching digest")
   162  		}
   163  	}
   164  	if desc.MediaType == "" {
   165  		return fmt.Errorf("no media type in descriptor")
   166  	}
   167  	return nil
   168  }