github.com/diafour/helm@v3.0.0-beta.3+incompatible/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/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/pkg/chart"
    38  	"helm.sh/helm/pkg/chart/loader"
    39  	"helm.sh/helm/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.Size == 0 {
   126  				return &r, errors.New(
   127  					fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType))
   128  			}
   129  			r.ContentLayer = contentLayer
   130  			info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
   131  			if err != nil {
   132  				return &r, err
   133  			}
   134  			r.Size = info.Size
   135  			r.Digest = info.Digest
   136  			r.CreatedAt = info.CreatedAt
   137  			contentBytes, err := cache.fetchBlob(contentLayer)
   138  			if err != nil {
   139  				return &r, err
   140  			}
   141  			ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes))
   142  			if err != nil {
   143  				return &r, err
   144  			}
   145  			r.Chart = ch
   146  		}
   147  	}
   148  	return &r, nil
   149  }
   150  
   151  // StoreReference stores a chart ref in cache
   152  func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) {
   153  	if err := cache.init(); err != nil {
   154  		return nil, err
   155  	}
   156  	r := CacheRefSummary{
   157  		Name:  ref.FullName(),
   158  		Repo:  ref.Repo,
   159  		Tag:   ref.Tag,
   160  		Chart: ch,
   161  	}
   162  	existing, _ := cache.FetchReference(ref)
   163  	r.Exists = existing.Exists
   164  	config, _, err := cache.saveChartConfig(ch)
   165  	if err != nil {
   166  		return &r, err
   167  	}
   168  	r.Config = config
   169  	contentLayer, _, err := cache.saveChartContentLayer(ch)
   170  	if err != nil {
   171  		return &r, err
   172  	}
   173  	r.ContentLayer = contentLayer
   174  	info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
   175  	if err != nil {
   176  		return &r, err
   177  	}
   178  	r.Size = info.Size
   179  	r.Digest = info.Digest
   180  	r.CreatedAt = info.CreatedAt
   181  	manifest, _, err := cache.saveChartManifest(config, contentLayer)
   182  	if err != nil {
   183  		return &r, err
   184  	}
   185  	r.Manifest = manifest
   186  	return &r, nil
   187  }
   188  
   189  // DeleteReference deletes a chart ref from cache
   190  // TODO: garbage collection, only manifest removed
   191  func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) {
   192  	if err := cache.init(); err != nil {
   193  		return nil, err
   194  	}
   195  	r, err := cache.FetchReference(ref)
   196  	if err != nil || !r.Exists {
   197  		return r, err
   198  	}
   199  	cache.ociStore.DeleteReference(r.Name)
   200  	err = cache.ociStore.SaveIndex()
   201  	return r, err
   202  }
   203  
   204  // ListReferences lists all chart refs in a cache
   205  func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) {
   206  	if err := cache.init(); err != nil {
   207  		return nil, err
   208  	}
   209  	var rr []*CacheRefSummary
   210  	for _, desc := range cache.ociStore.ListReferences() {
   211  		name := desc.Annotations[ocispec.AnnotationRefName]
   212  		if name == "" {
   213  			if cache.debug {
   214  				fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex())
   215  			}
   216  			continue
   217  		}
   218  		ref, err := ParseReference(name)
   219  		if err != nil {
   220  			return rr, err
   221  		}
   222  		r, err := cache.FetchReference(ref)
   223  		if err != nil {
   224  			return rr, err
   225  		}
   226  		rr = append(rr, r)
   227  	}
   228  	return rr, nil
   229  }
   230  
   231  // AddManifest provides a manifest to the cache index.json
   232  func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error {
   233  	if err := cache.init(); err != nil {
   234  		return err
   235  	}
   236  	cache.ociStore.AddReference(ref.FullName(), *manifest)
   237  	err := cache.ociStore.SaveIndex()
   238  	return err
   239  }
   240  
   241  // Provider provides a valid containerd Provider
   242  func (cache *Cache) Provider() content.Provider {
   243  	return content.Provider(cache.ociStore)
   244  }
   245  
   246  // Ingester provides a valid containerd Ingester
   247  func (cache *Cache) Ingester() content.Ingester {
   248  	return content.Ingester(cache.ociStore)
   249  }
   250  
   251  // ProvideIngester provides a valid oras ProvideIngester
   252  func (cache *Cache) ProvideIngester() orascontent.ProvideIngester {
   253  	return orascontent.ProvideIngester(cache.ociStore)
   254  }
   255  
   256  // init creates files needed necessary for OCI layout store
   257  func (cache *Cache) init() error {
   258  	if cache.ociStore == nil {
   259  		ociStore, err := orascontent.NewOCIStore(cache.rootDir)
   260  		if err != nil {
   261  			return err
   262  		}
   263  		cache.ociStore = ociStore
   264  		cache.memoryStore = orascontent.NewMemoryStore()
   265  	}
   266  	return nil
   267  }
   268  
   269  // saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
   270  func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
   271  	configBytes, err := json.Marshal(ch.Metadata)
   272  	if err != nil {
   273  		return nil, false, err
   274  	}
   275  	configExists, err := cache.storeBlob(configBytes)
   276  	if err != nil {
   277  		return nil, configExists, err
   278  	}
   279  	descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes)
   280  	return &descriptor, configExists, nil
   281  }
   282  
   283  // saveChartContentLayer stores the chart as tarball blob and returns a descriptor
   284  func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
   285  	destDir := filepath.Join(cache.rootDir, ".build")
   286  	os.MkdirAll(destDir, 0755)
   287  	tmpFile, err := chartutil.Save(ch, destDir)
   288  	defer os.Remove(tmpFile)
   289  	if err != nil {
   290  		return nil, false, errors.Wrap(err, "failed to save")
   291  	}
   292  	contentBytes, err := ioutil.ReadFile(tmpFile)
   293  	if err != nil {
   294  		return nil, false, err
   295  	}
   296  	contentExists, err := cache.storeBlob(contentBytes)
   297  	if err != nil {
   298  		return nil, contentExists, err
   299  	}
   300  	descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes)
   301  	return &descriptor, contentExists, nil
   302  }
   303  
   304  // saveChartManifest stores the chart manifest as json blob and returns a descriptor
   305  func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) {
   306  	manifest := ocispec.Manifest{
   307  		Versioned: specs.Versioned{SchemaVersion: 2},
   308  		Config:    *config,
   309  		Layers:    []ocispec.Descriptor{*contentLayer},
   310  	}
   311  	manifestBytes, err := json.Marshal(manifest)
   312  	if err != nil {
   313  		return nil, false, err
   314  	}
   315  	manifestExists, err := cache.storeBlob(manifestBytes)
   316  	if err != nil {
   317  		return nil, manifestExists, err
   318  	}
   319  	descriptor := ocispec.Descriptor{
   320  		MediaType: ocispec.MediaTypeImageManifest,
   321  		Digest:    digest.FromBytes(manifestBytes),
   322  		Size:      int64(len(manifestBytes)),
   323  	}
   324  	return &descriptor, manifestExists, nil
   325  }
   326  
   327  // storeBlob stores a blob on filesystem
   328  func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) {
   329  	var exists bool
   330  	writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug),
   331  		content.WithRef(digest.FromBytes(blobBytes).Hex()))
   332  	if err != nil {
   333  		return exists, err
   334  	}
   335  	_, err = writer.Write(blobBytes)
   336  	if err != nil {
   337  		return exists, err
   338  	}
   339  	err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest())
   340  	if err != nil {
   341  		if !errdefs.IsAlreadyExists(err) {
   342  			return exists, err
   343  		}
   344  		exists = true
   345  	}
   346  	err = writer.Close()
   347  	return exists, err
   348  }
   349  
   350  // fetchBlob retrieves a blob from filesystem
   351  func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) {
   352  	reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	bytes := make([]byte, desc.Size)
   357  	_, err = reader.ReadAt(bytes, 0)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  	return bytes, nil
   362  }