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 }