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  }