github.com/codefresh-io/kcfi@v0.0.0-20230301195427-c1578715cc46/pkg/helm-internal/experimental/registry/cache.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package registry // import "helm.sh/helm/v3/internal/experimental/registry" 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "time" 28 29 "github.com/containerd/containerd/content" 30 "github.com/containerd/containerd/errdefs" 31 orascontent "github.com/deislabs/oras/pkg/content" 32 digest "github.com/opencontainers/go-digest" 33 specs "github.com/opencontainers/image-spec/specs-go" 34 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 35 "github.com/pkg/errors" 36 37 "helm.sh/helm/v3/pkg/chart" 38 "helm.sh/helm/v3/pkg/chart/loader" 39 "helm.sh/helm/v3/pkg/chartutil" 40 ) 41 42 const ( 43 // CacheRootDir is the root directory for a cache 44 CacheRootDir = "cache" 45 ) 46 47 type ( 48 // Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout 49 Cache struct { 50 debug bool 51 out io.Writer 52 rootDir string 53 ociStore *orascontent.OCIStore 54 memoryStore *orascontent.Memorystore 55 } 56 57 // CacheRefSummary contains as much info as available describing a chart reference in cache 58 // Note: fields here are sorted by the order in which they are set in FetchReference method 59 CacheRefSummary struct { 60 Name string 61 Repo string 62 Tag string 63 Exists bool 64 Manifest *ocispec.Descriptor 65 Config *ocispec.Descriptor 66 ContentLayer *ocispec.Descriptor 67 Size int64 68 Digest digest.Digest 69 CreatedAt time.Time 70 Chart *chart.Chart 71 } 72 ) 73 74 // NewCache returns a new OCI Layout-compliant cache with config 75 func NewCache(opts ...CacheOption) (*Cache, error) { 76 cache := &Cache{ 77 out: ioutil.Discard, 78 } 79 for _, opt := range opts { 80 opt(cache) 81 } 82 // validate 83 if cache.rootDir == "" { 84 return nil, errors.New("must set cache root dir on initialization") 85 } 86 return cache, nil 87 } 88 89 // FetchReference retrieves a chart ref from cache 90 func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) { 91 if err := cache.init(); err != nil { 92 return nil, err 93 } 94 r := CacheRefSummary{ 95 Name: ref.FullName(), 96 Repo: ref.Repo, 97 Tag: ref.Tag, 98 } 99 for _, desc := range cache.ociStore.ListReferences() { 100 if desc.Annotations[ocispec.AnnotationRefName] == r.Name { 101 r.Exists = true 102 manifestBytes, err := cache.fetchBlob(&desc) 103 if err != nil { 104 return &r, err 105 } 106 var manifest ocispec.Manifest 107 err = json.Unmarshal(manifestBytes, &manifest) 108 if err != nil { 109 return &r, err 110 } 111 r.Manifest = &desc 112 r.Config = &manifest.Config 113 numLayers := len(manifest.Layers) 114 if numLayers != 1 { 115 return &r, errors.New( 116 fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers)) 117 } 118 var contentLayer *ocispec.Descriptor 119 for _, layer := range manifest.Layers { 120 switch layer.MediaType { 121 case HelmChartContentLayerMediaType: 122 contentLayer = &layer 123 } 124 } 125 if contentLayer == nil { 126 return &r, errors.New( 127 fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType)) 128 } 129 if contentLayer.Size == 0 { 130 return &r, errors.New( 131 fmt.Sprintf("manifest layer with mediatype %s is of size 0", HelmChartContentLayerMediaType)) 132 } 133 r.ContentLayer = contentLayer 134 info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) 135 if err != nil { 136 return &r, err 137 } 138 r.Size = info.Size 139 r.Digest = info.Digest 140 r.CreatedAt = info.CreatedAt 141 contentBytes, err := cache.fetchBlob(contentLayer) 142 if err != nil { 143 return &r, err 144 } 145 ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes)) 146 if err != nil { 147 return &r, err 148 } 149 r.Chart = ch 150 } 151 } 152 return &r, nil 153 } 154 155 // StoreReference stores a chart ref in cache 156 func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) { 157 if err := cache.init(); err != nil { 158 return nil, err 159 } 160 r := CacheRefSummary{ 161 Name: ref.FullName(), 162 Repo: ref.Repo, 163 Tag: ref.Tag, 164 Chart: ch, 165 } 166 existing, _ := cache.FetchReference(ref) 167 r.Exists = existing.Exists 168 config, _, err := cache.saveChartConfig(ch) 169 if err != nil { 170 return &r, err 171 } 172 r.Config = config 173 contentLayer, _, err := cache.saveChartContentLayer(ch) 174 if err != nil { 175 return &r, err 176 } 177 r.ContentLayer = contentLayer 178 info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) 179 if err != nil { 180 return &r, err 181 } 182 r.Size = info.Size 183 r.Digest = info.Digest 184 r.CreatedAt = info.CreatedAt 185 manifest, _, err := cache.saveChartManifest(config, contentLayer) 186 if err != nil { 187 return &r, err 188 } 189 r.Manifest = manifest 190 return &r, nil 191 } 192 193 // DeleteReference deletes a chart ref from cache 194 // TODO: garbage collection, only manifest removed 195 func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) { 196 if err := cache.init(); err != nil { 197 return nil, err 198 } 199 r, err := cache.FetchReference(ref) 200 if err != nil || !r.Exists { 201 return r, err 202 } 203 cache.ociStore.DeleteReference(r.Name) 204 err = cache.ociStore.SaveIndex() 205 return r, err 206 } 207 208 // ListReferences lists all chart refs in a cache 209 func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) { 210 if err := cache.init(); err != nil { 211 return nil, err 212 } 213 var rr []*CacheRefSummary 214 for _, desc := range cache.ociStore.ListReferences() { 215 name := desc.Annotations[ocispec.AnnotationRefName] 216 if name == "" { 217 if cache.debug { 218 fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex()) 219 } 220 continue 221 } 222 ref, err := ParseReference(name) 223 if err != nil { 224 return rr, err 225 } 226 r, err := cache.FetchReference(ref) 227 if err != nil { 228 return rr, err 229 } 230 rr = append(rr, r) 231 } 232 return rr, nil 233 } 234 235 // AddManifest provides a manifest to the cache index.json 236 func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error { 237 if err := cache.init(); err != nil { 238 return err 239 } 240 cache.ociStore.AddReference(ref.FullName(), *manifest) 241 err := cache.ociStore.SaveIndex() 242 return err 243 } 244 245 // Provider provides a valid containerd Provider 246 func (cache *Cache) Provider() content.Provider { 247 return content.Provider(cache.ociStore) 248 } 249 250 // Ingester provides a valid containerd Ingester 251 func (cache *Cache) Ingester() content.Ingester { 252 return content.Ingester(cache.ociStore) 253 } 254 255 // ProvideIngester provides a valid oras ProvideIngester 256 func (cache *Cache) ProvideIngester() orascontent.ProvideIngester { 257 return orascontent.ProvideIngester(cache.ociStore) 258 } 259 260 // init creates files needed necessary for OCI layout store 261 func (cache *Cache) init() error { 262 if cache.ociStore == nil { 263 ociStore, err := orascontent.NewOCIStore(cache.rootDir) 264 if err != nil { 265 return err 266 } 267 cache.ociStore = ociStore 268 cache.memoryStore = orascontent.NewMemoryStore() 269 } 270 return nil 271 } 272 273 // saveChartConfig stores the Chart.yaml as json blob and returns a descriptor 274 func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { 275 configBytes, err := json.Marshal(ch.Metadata) 276 if err != nil { 277 return nil, false, err 278 } 279 configExists, err := cache.storeBlob(configBytes) 280 if err != nil { 281 return nil, configExists, err 282 } 283 descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes) 284 return &descriptor, configExists, nil 285 } 286 287 // saveChartContentLayer stores the chart as tarball blob and returns a descriptor 288 func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { 289 destDir := filepath.Join(cache.rootDir, ".build") 290 os.MkdirAll(destDir, 0755) 291 tmpFile, err := chartutil.Save(ch, destDir) 292 defer os.Remove(tmpFile) 293 if err != nil { 294 return nil, false, errors.Wrap(err, "failed to save") 295 } 296 contentBytes, err := ioutil.ReadFile(tmpFile) 297 if err != nil { 298 return nil, false, err 299 } 300 contentExists, err := cache.storeBlob(contentBytes) 301 if err != nil { 302 return nil, contentExists, err 303 } 304 descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes) 305 return &descriptor, contentExists, nil 306 } 307 308 // saveChartManifest stores the chart manifest as json blob and returns a descriptor 309 func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) { 310 manifest := ocispec.Manifest{ 311 Versioned: specs.Versioned{SchemaVersion: 2}, 312 Config: *config, 313 Layers: []ocispec.Descriptor{*contentLayer}, 314 } 315 manifestBytes, err := json.Marshal(manifest) 316 if err != nil { 317 return nil, false, err 318 } 319 manifestExists, err := cache.storeBlob(manifestBytes) 320 if err != nil { 321 return nil, manifestExists, err 322 } 323 descriptor := ocispec.Descriptor{ 324 MediaType: ocispec.MediaTypeImageManifest, 325 Digest: digest.FromBytes(manifestBytes), 326 Size: int64(len(manifestBytes)), 327 } 328 return &descriptor, manifestExists, nil 329 } 330 331 // storeBlob stores a blob on filesystem 332 func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) { 333 var exists bool 334 writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug), 335 content.WithRef(digest.FromBytes(blobBytes).Hex())) 336 if err != nil { 337 return exists, err 338 } 339 _, err = writer.Write(blobBytes) 340 if err != nil { 341 return exists, err 342 } 343 err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest()) 344 if err != nil { 345 if !errdefs.IsAlreadyExists(err) { 346 return exists, err 347 } 348 exists = true 349 } 350 err = writer.Close() 351 return exists, err 352 } 353 354 // fetchBlob retrieves a blob from filesystem 355 func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) { 356 reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc) 357 if err != nil { 358 return nil, err 359 } 360 bytes := make([]byte, desc.Size) 361 _, err = reader.ReadAt(bytes, 0) 362 if err != nil { 363 return nil, err 364 } 365 return bytes, nil 366 }