/* Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package registry // import "helm.sh/helm/v3/internal/experimental/registry" import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "os" "path/filepath" "time" "github.com/containerd/containerd/content" "github.com/containerd/containerd/errdefs" orascontent "github.com/deislabs/oras/pkg/content" digest "github.com/opencontainers/go-digest" specs "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" ) const ( // CacheRootDir is the root directory for a cache CacheRootDir = "cache" ) type ( // Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout Cache struct { debug bool out io.Writer rootDir string ociStore *orascontent.OCIStore memoryStore *orascontent.Memorystore } // CacheRefSummary contains as much info as available describing a chart reference in cache // Note: fields here are sorted by the order in which they are set in FetchReference method CacheRefSummary struct { Name string Repo string Tag string Exists bool Manifest *ocispec.Descriptor Config *ocispec.Descriptor ContentLayer *ocispec.Descriptor Size int64 Digest digest.Digest CreatedAt time.Time Chart *chart.Chart } ) // NewCache returns a new OCI Layout-compliant cache with config func NewCache(opts ...CacheOption) (*Cache, error) { cache := &Cache{ out: ioutil.Discard, } for _, opt := range opts { opt(cache) } // validate if cache.rootDir == "" { return nil, errors.New("must set cache root dir on initialization") } return cache, nil } // FetchReference retrieves a chart ref from cache func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) { if err := cache.init(); err != nil { return nil, err } r := CacheRefSummary{ Name: ref.FullName(), Repo: ref.Repo, Tag: ref.Tag, } for _, desc := range cache.ociStore.ListReferences() { if desc.Annotations[ocispec.AnnotationRefName] == r.Name { r.Exists = true manifestBytes, err := cache.fetchBlob(&desc) if err != nil { return &r, err } var manifest ocispec.Manifest err = json.Unmarshal(manifestBytes, &manifest) if err != nil { return &r, err } r.Manifest = &desc r.Config = &manifest.Config numLayers := len(manifest.Layers) if numLayers != 1 { return &r, errors.New( fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers)) } var contentLayer *ocispec.Descriptor for _, layer := range manifest.Layers { switch layer.MediaType { case HelmChartContentLayerMediaType: contentLayer = &layer } } if contentLayer == nil { return &r, errors.New( fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType)) } if contentLayer.Size == 0 { return &r, errors.New( fmt.Sprintf("manifest layer with mediatype %s is of size 0", HelmChartContentLayerMediaType)) } r.ContentLayer = contentLayer info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) if err != nil { return &r, err } r.Size = info.Size r.Digest = info.Digest r.CreatedAt = info.CreatedAt contentBytes, err := cache.fetchBlob(contentLayer) if err != nil { return &r, err } ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes)) if err != nil { return &r, err } r.Chart = ch } } return &r, nil } // StoreReference stores a chart ref in cache func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) { if err := cache.init(); err != nil { return nil, err } r := CacheRefSummary{ Name: ref.FullName(), Repo: ref.Repo, Tag: ref.Tag, Chart: ch, } existing, _ := cache.FetchReference(ref) r.Exists = existing.Exists config, _, err := cache.saveChartConfig(ch) if err != nil { return &r, err } r.Config = config contentLayer, _, err := cache.saveChartContentLayer(ch) if err != nil { return &r, err } r.ContentLayer = contentLayer info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) if err != nil { return &r, err } r.Size = info.Size r.Digest = info.Digest r.CreatedAt = info.CreatedAt manifest, _, err := cache.saveChartManifest(config, contentLayer) if err != nil { return &r, err } r.Manifest = manifest return &r, nil } // DeleteReference deletes a chart ref from cache // TODO: garbage collection, only manifest removed func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) { if err := cache.init(); err != nil { return nil, err } r, err := cache.FetchReference(ref) if err != nil || !r.Exists { return r, err } cache.ociStore.DeleteReference(r.Name) err = cache.ociStore.SaveIndex() return r, err } // ListReferences lists all chart refs in a cache func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) { if err := cache.init(); err != nil { return nil, err } var rr []*CacheRefSummary for _, desc := range cache.ociStore.ListReferences() { name := desc.Annotations[ocispec.AnnotationRefName] if name == "" { if cache.debug { fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex()) } continue } ref, err := ParseReference(name) if err != nil { return rr, err } r, err := cache.FetchReference(ref) if err != nil { return rr, err } rr = append(rr, r) } return rr, nil } // AddManifest provides a manifest to the cache index.json func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error { if err := cache.init(); err != nil { return err } cache.ociStore.AddReference(ref.FullName(), *manifest) err := cache.ociStore.SaveIndex() return err } // Provider provides a valid containerd Provider func (cache *Cache) Provider() content.Provider { return content.Provider(cache.ociStore) } // Ingester provides a valid containerd Ingester func (cache *Cache) Ingester() content.Ingester { return content.Ingester(cache.ociStore) } // ProvideIngester provides a valid oras ProvideIngester func (cache *Cache) ProvideIngester() orascontent.ProvideIngester { return orascontent.ProvideIngester(cache.ociStore) } // init creates files needed necessary for OCI layout store func (cache *Cache) init() error { if cache.ociStore == nil { ociStore, err := orascontent.NewOCIStore(cache.rootDir) if err != nil { return err } cache.ociStore = ociStore cache.memoryStore = orascontent.NewMemoryStore() } return nil } // saveChartConfig stores the Chart.yaml as json blob and returns a descriptor func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { configBytes, err := json.Marshal(ch.Metadata) if err != nil { return nil, false, err } configExists, err := cache.storeBlob(configBytes) if err != nil { return nil, configExists, err } descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes) return &descriptor, configExists, nil } // saveChartContentLayer stores the chart as tarball blob and returns a descriptor func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { destDir := filepath.Join(cache.rootDir, ".build") os.MkdirAll(destDir, 0755) tmpFile, err := chartutil.Save(ch, destDir) defer os.Remove(tmpFile) if err != nil { return nil, false, errors.Wrap(err, "failed to save") } contentBytes, err := ioutil.ReadFile(tmpFile) if err != nil { return nil, false, err } contentExists, err := cache.storeBlob(contentBytes) if err != nil { return nil, contentExists, err } descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes) return &descriptor, contentExists, nil } // saveChartManifest stores the chart manifest as json blob and returns a descriptor func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) { manifest := ocispec.Manifest{ Versioned: specs.Versioned{SchemaVersion: 2}, Config: *config, Layers: []ocispec.Descriptor{*contentLayer}, } manifestBytes, err := json.Marshal(manifest) if err != nil { return nil, false, err } manifestExists, err := cache.storeBlob(manifestBytes) if err != nil { return nil, manifestExists, err } descriptor := ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageManifest, Digest: digest.FromBytes(manifestBytes), Size: int64(len(manifestBytes)), } return &descriptor, manifestExists, nil } // storeBlob stores a blob on filesystem func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) { var exists bool writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug), content.WithRef(digest.FromBytes(blobBytes).Hex())) if err != nil { return exists, err } _, err = writer.Write(blobBytes) if err != nil { return exists, err } err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest()) if err != nil { if !errdefs.IsAlreadyExists(err) { return exists, err } exists = true } err = writer.Close() return exists, err } // fetchBlob retrieves a blob from filesystem func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) { reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc) if err != nil { return nil, err } bytes := make([]byte, desc.Size) _, err = reader.ReadAt(bytes, 0) if err != nil { return nil, err } return bytes, nil }