github.com/appscode/helm@v3.0.0-alpha.1+incompatible/pkg/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/pkg/registry"
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  	"sort"
    28  	"strings"
    29  	"time"
    30  
    31  	orascontent "github.com/deislabs/oras/pkg/content"
    32  	units "github.com/docker/go-units"
    33  	checksum "github.com/opencontainers/go-digest"
    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  var (
    43  	tableHeaders = []string{"name", "version", "digest", "size", "created"}
    44  )
    45  
    46  type (
    47  	filesystemCache struct {
    48  		out     io.Writer
    49  		rootDir string
    50  		store   *orascontent.Memorystore
    51  	}
    52  )
    53  
    54  func (cache *filesystemCache) LayersToChart(layers []ocispec.Descriptor) (*chart.Chart, error) {
    55  	metaLayer, contentLayer, err := extractLayers(layers)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	name, version, err := extractChartNameVersionFromLayer(contentLayer)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	// Obtain raw chart meta content (json)
    66  	_, metaJSONRaw, ok := cache.store.Get(metaLayer)
    67  	if !ok {
    68  		return nil, errors.New("error retrieving meta layer")
    69  	}
    70  
    71  	// Construct chart metadata object
    72  	metadata := chart.Metadata{}
    73  	err = json.Unmarshal(metaJSONRaw, &metadata)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	metadata.APIVersion = chart.APIVersionV1
    78  	metadata.Name = name
    79  	metadata.Version = version
    80  
    81  	// Obtain raw chart content
    82  	_, contentRaw, ok := cache.store.Get(contentLayer)
    83  	if !ok {
    84  		return nil, errors.New("error retrieving meta layer")
    85  	}
    86  
    87  	// Construct chart object and attach metadata
    88  	ch, err := loader.LoadArchive(bytes.NewBuffer(contentRaw))
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	ch.Metadata = &metadata
    93  
    94  	return ch, nil
    95  }
    96  
    97  func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descriptor, error) {
    98  
    99  	// extract/separate the name and version from other metadata
   100  	if err := ch.Validate(); err != nil {
   101  		return nil, err
   102  	}
   103  	name := ch.Metadata.Name
   104  	version := ch.Metadata.Version
   105  
   106  	// Create meta layer, clear name and version from Chart.yaml and convert to json
   107  	ch.Metadata.Name = ""
   108  	ch.Metadata.Version = ""
   109  	metaJSONRaw, err := json.Marshal(ch.Metadata)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaMediaType, metaJSONRaw)
   114  
   115  	// Create content layer
   116  	// TODO: something better than this hack. Currently needed for chartutil.Save()
   117  	// If metadata does not contain Name or Version, an error is returned
   118  	// such as "no chart name specified (Chart.yaml)"
   119  	ch.Metadata = &chart.Metadata{
   120  		APIVersion: chart.APIVersionV1,
   121  		Name:       "-",
   122  		Version:    "0.1.0",
   123  	}
   124  	destDir := mkdir(filepath.Join(cache.rootDir, "blobs", ".build"))
   125  	tmpFile, err := chartutil.Save(ch, destDir)
   126  	defer os.Remove(tmpFile)
   127  	if err != nil {
   128  		return nil, errors.Wrap(err, "failed to save")
   129  	}
   130  	contentRaw, err := ioutil.ReadFile(tmpFile)
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  	contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentMediaType, contentRaw)
   135  
   136  	// Set annotations
   137  	contentLayer.Annotations[HelmChartNameAnnotation] = name
   138  	contentLayer.Annotations[HelmChartVersionAnnotation] = version
   139  
   140  	layers := []ocispec.Descriptor{metaLayer, contentLayer}
   141  	return layers, nil
   142  }
   143  
   144  func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descriptor, error) {
   145  	tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tagOrDefault(ref.Tag))
   146  
   147  	// add meta layer
   148  	metaJSONRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "meta"))
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	metaLayer := cache.store.Add(HelmChartMetaFileName, HelmChartMetaMediaType, metaJSONRaw)
   153  
   154  	// add content layer
   155  	contentRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "content"))
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	contentLayer := cache.store.Add(HelmChartContentFileName, HelmChartContentMediaType, contentRaw)
   160  
   161  	// set annotations on content layer (chart name and version)
   162  	err = setLayerAnnotationsFromChartLink(contentLayer, filepath.Join(tagDir, "chart"))
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	printChartSummary(cache.out, metaLayer, contentLayer)
   168  	layers := []ocispec.Descriptor{metaLayer, contentLayer}
   169  	return layers, nil
   170  }
   171  
   172  func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.Descriptor) (bool, error) {
   173  	tag := tagOrDefault(ref.Tag)
   174  	tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tag))
   175  
   176  	// Retrieve just the meta and content layers
   177  	metaLayer, contentLayer, err := extractLayers(layers)
   178  	if err != nil {
   179  		return false, err
   180  	}
   181  
   182  	// Extract chart name and version
   183  	name, version, err := extractChartNameVersionFromLayer(contentLayer)
   184  	if err != nil {
   185  		return false, err
   186  	}
   187  
   188  	// Create chart file
   189  	chartPath, err := createChartFile(filepath.Join(cache.rootDir, "charts"), name, version)
   190  	if err != nil {
   191  		return false, err
   192  	}
   193  
   194  	// Create chart symlink
   195  	err = createSymlink(chartPath, filepath.Join(tagDir, "chart"))
   196  	if err != nil {
   197  		return false, err
   198  	}
   199  
   200  	// Save meta blob
   201  	metaExists, metaPath := digestPath(filepath.Join(cache.rootDir, "blobs"), metaLayer.Digest)
   202  	if !metaExists {
   203  		fmt.Fprintf(cache.out, "%s: Saving meta (%s)\n",
   204  			shortDigest(metaLayer.Digest.Hex()), byteCountBinary(metaLayer.Size))
   205  		_, metaJSONRaw, ok := cache.store.Get(metaLayer)
   206  		if !ok {
   207  			return false, errors.New("error retrieving meta layer")
   208  		}
   209  		err = writeFile(metaPath, metaJSONRaw)
   210  		if err != nil {
   211  			return false, err
   212  		}
   213  	}
   214  
   215  	// Create meta symlink
   216  	err = createSymlink(metaPath, filepath.Join(tagDir, "meta"))
   217  	if err != nil {
   218  		return false, err
   219  	}
   220  
   221  	// Save content blob
   222  	contentExists, contentPath := digestPath(filepath.Join(cache.rootDir, "blobs"), contentLayer.Digest)
   223  	if !contentExists {
   224  		fmt.Fprintf(cache.out, "%s: Saving content (%s)\n",
   225  			shortDigest(contentLayer.Digest.Hex()), byteCountBinary(contentLayer.Size))
   226  		_, contentRaw, ok := cache.store.Get(contentLayer)
   227  		if !ok {
   228  			return false, errors.New("error retrieving content layer")
   229  		}
   230  		err = writeFile(contentPath, contentRaw)
   231  		if err != nil {
   232  			return false, err
   233  		}
   234  	}
   235  
   236  	// Create content symlink
   237  	err = createSymlink(contentPath, filepath.Join(tagDir, "content"))
   238  	if err != nil {
   239  		return false, err
   240  	}
   241  
   242  	printChartSummary(cache.out, metaLayer, contentLayer)
   243  	return metaExists && contentExists, nil
   244  }
   245  
   246  func (cache *filesystemCache) DeleteReference(ref *Reference) error {
   247  	tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tagOrDefault(ref.Tag))
   248  	if _, err := os.Stat(tagDir); os.IsNotExist(err) {
   249  		return errors.New("ref not found")
   250  	}
   251  	return os.RemoveAll(tagDir)
   252  }
   253  
   254  func (cache *filesystemCache) TableRows() ([][]interface{}, error) {
   255  	return getRefsSorted(filepath.Join(cache.rootDir, "refs"))
   256  }
   257  
   258  // escape sanitizes a registry URL to remove characters such as ":"
   259  // which are illegal on windows
   260  func escape(s string) string {
   261  	return strings.ReplaceAll(s, ":", "_")
   262  }
   263  
   264  // escape reverses escape
   265  func unescape(s string) string {
   266  	return strings.ReplaceAll(s, "_", ":")
   267  }
   268  
   269  // printChartSummary prints details about a chart layers
   270  func printChartSummary(out io.Writer, metaLayer ocispec.Descriptor, contentLayer ocispec.Descriptor) {
   271  	fmt.Fprintf(out, "Name: %s\n", contentLayer.Annotations[HelmChartNameAnnotation])
   272  	fmt.Fprintf(out, "Version: %s\n", contentLayer.Annotations[HelmChartVersionAnnotation])
   273  	fmt.Fprintf(out, "Meta: %s\n", metaLayer.Digest)
   274  	fmt.Fprintf(out, "Content: %s\n", contentLayer.Digest)
   275  }
   276  
   277  // fileExists determines if a file exists
   278  func fileExists(path string) bool {
   279  	if _, err := os.Stat(path); os.IsNotExist(err) {
   280  		return false
   281  	}
   282  	return true
   283  }
   284  
   285  // mkdir will create a directory (no error check) and return the path
   286  func mkdir(dir string) string {
   287  	os.MkdirAll(dir, 0755)
   288  	return dir
   289  }
   290  
   291  // createSymlink creates a symbolic link, deleting existing one if exists
   292  func createSymlink(src string, dest string) error {
   293  	os.Remove(dest)
   294  	err := os.Symlink(src, dest)
   295  	return err
   296  }
   297  
   298  // getSymlinkDestContent returns the file contents of a symlink's destination
   299  func getSymlinkDestContent(linkPath string) ([]byte, error) {
   300  	src, err := os.Readlink(linkPath)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	return ioutil.ReadFile(src)
   305  }
   306  
   307  // setLayerAnnotationsFromChartLink will set chart name/version annotations on a layer
   308  // based on the path of the chart link destination
   309  func setLayerAnnotationsFromChartLink(layer ocispec.Descriptor, chartLinkPath string) error {
   310  	src, err := os.Readlink(chartLinkPath)
   311  	if err != nil {
   312  		return err
   313  	}
   314  	// example path: /some/path/charts/mychart/versions/1.2.0
   315  	chartName := filepath.Base(filepath.Dir(filepath.Dir(src)))
   316  	chartVersion := filepath.Base(src)
   317  	layer.Annotations[HelmChartNameAnnotation] = chartName
   318  	layer.Annotations[HelmChartVersionAnnotation] = chartVersion
   319  	return nil
   320  }
   321  
   322  // extractLayers obtains the meta and content layers from a list of layers
   323  func extractLayers(layers []ocispec.Descriptor) (ocispec.Descriptor, ocispec.Descriptor, error) {
   324  	var metaLayer, contentLayer ocispec.Descriptor
   325  
   326  	if len(layers) != 2 {
   327  		return metaLayer, contentLayer, errors.New("manifest does not contain exactly 2 layers")
   328  	}
   329  
   330  	for _, layer := range layers {
   331  		switch layer.MediaType {
   332  		case HelmChartMetaMediaType:
   333  			metaLayer = layer
   334  		case HelmChartContentMediaType:
   335  			contentLayer = layer
   336  		}
   337  	}
   338  
   339  	if metaLayer.Size == 0 {
   340  		return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart meta layer")
   341  	}
   342  
   343  	if contentLayer.Size == 0 {
   344  		return metaLayer, contentLayer, errors.New("manifest does not contain a Helm chart content layer")
   345  	}
   346  
   347  	return metaLayer, contentLayer, nil
   348  }
   349  
   350  // extractChartNameVersionFromLayer retrieves the chart name and version from layer annotations
   351  func extractChartNameVersionFromLayer(layer ocispec.Descriptor) (string, string, error) {
   352  	name, ok := layer.Annotations[HelmChartNameAnnotation]
   353  	if !ok {
   354  		return "", "", errors.New("could not find chart name in annotations")
   355  	}
   356  	version, ok := layer.Annotations[HelmChartVersionAnnotation]
   357  	if !ok {
   358  		return "", "", errors.New("could not find chart version in annotations")
   359  	}
   360  	return name, version, nil
   361  }
   362  
   363  // createChartFile creates a file under "<chartsdir>" dir which is linked to by ref
   364  func createChartFile(chartsRootDir string, name string, version string) (string, error) {
   365  	chartPathDir := filepath.Join(chartsRootDir, name, "versions")
   366  	chartPath := filepath.Join(chartPathDir, version)
   367  	if _, err := os.Stat(chartPath); err != nil && os.IsNotExist(err) {
   368  		os.MkdirAll(chartPathDir, 0755)
   369  		err := ioutil.WriteFile(chartPath, []byte("-"), 0644)
   370  		if err != nil {
   371  			return "", err
   372  		}
   373  	}
   374  	return chartPath, nil
   375  }
   376  
   377  // digestPath returns the path to addressable content, and whether the file exists
   378  func digestPath(rootDir string, digest checksum.Digest) (bool, string) {
   379  	path := filepath.Join(rootDir, "sha256", digest.Hex())
   380  	exists := fileExists(path)
   381  	return exists, path
   382  }
   383  
   384  // writeFile creates a path, ensuring parent directory
   385  func writeFile(path string, c []byte) error {
   386  	os.MkdirAll(filepath.Dir(path), 0755)
   387  	return ioutil.WriteFile(path, c, 0644)
   388  }
   389  
   390  // byteCountBinary produces a human-readable file size
   391  func byteCountBinary(b int64) string {
   392  	const unit = 1024
   393  	if b < unit {
   394  		return fmt.Sprintf("%d B", b)
   395  	}
   396  	div, exp := int64(unit), 0
   397  	for n := b / unit; n >= unit; n /= unit {
   398  		div *= unit
   399  		exp++
   400  	}
   401  	return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
   402  }
   403  
   404  // tagOrDefault returns the tag if present, if not the default tag
   405  func tagOrDefault(tag string) string {
   406  	if tag != "" {
   407  		return tag
   408  	}
   409  	return HelmChartDefaultTag
   410  }
   411  
   412  // shortDigest returns first 7 characters of a sha256 digest
   413  func shortDigest(digest string) string {
   414  	if len(digest) == 64 {
   415  		return digest[:7]
   416  	}
   417  	return digest
   418  }
   419  
   420  // getRefsSorted returns a map of all refs stored in a refsRootDir
   421  func getRefsSorted(refsRootDir string) ([][]interface{}, error) {
   422  	refsMap := map[string]map[string]string{}
   423  
   424  	// Walk the storage dir, check for symlinks under "refs" dir pointing to valid files in "blobs/" and "charts/"
   425  	err := filepath.Walk(refsRootDir, func(path string, fileInfo os.FileInfo, fileError error) error {
   426  
   427  		// Check if this file is a symlink
   428  		linkPath, err := os.Readlink(path)
   429  		if err == nil {
   430  			destFileInfo, err := os.Stat(linkPath)
   431  			if err == nil {
   432  				tagDir := filepath.Dir(path)
   433  
   434  				// Determine the ref
   435  				repo := unescape(strings.TrimLeft(
   436  					strings.TrimPrefix(filepath.Dir(filepath.Dir(tagDir)), refsRootDir), "/\\"))
   437  				tag := filepath.Base(tagDir)
   438  				ref := fmt.Sprintf("%s:%s", repo, tag)
   439  
   440  				// Init hashmap entry if does not exist
   441  				if _, ok := refsMap[ref]; !ok {
   442  					refsMap[ref] = map[string]string{}
   443  				}
   444  
   445  				// Add data to entry based on file name (symlink name)
   446  				base := filepath.Base(path)
   447  				switch base {
   448  				case "chart":
   449  					refsMap[ref]["name"] = filepath.Base(filepath.Dir(filepath.Dir(linkPath)))
   450  					refsMap[ref]["version"] = destFileInfo.Name()
   451  				case "content":
   452  
   453  					// Make sure the filename looks like a sha256 digest (64 chars)
   454  					digest := destFileInfo.Name()
   455  					if len(digest) == 64 {
   456  						refsMap[ref]["digest"] = shortDigest(digest)
   457  						refsMap[ref]["size"] = byteCountBinary(destFileInfo.Size())
   458  						refsMap[ref]["created"] = units.HumanDuration(time.Now().UTC().Sub(destFileInfo.ModTime()))
   459  					}
   460  				}
   461  			}
   462  		}
   463  
   464  		return nil
   465  	})
   466  
   467  	// Filter out any refs that are incomplete (do not have all required fields)
   468  	for k, ref := range refsMap {
   469  		allKeysFound := true
   470  		for _, v := range tableHeaders {
   471  			if _, ok := ref[v]; !ok {
   472  				allKeysFound = false
   473  				break
   474  			}
   475  		}
   476  		if !allKeysFound {
   477  			delete(refsMap, k)
   478  		}
   479  	}
   480  
   481  	// Sort and convert to format expected by uitable
   482  	refs := make([][]interface{}, len(refsMap))
   483  	keys := make([]string, 0, len(refsMap))
   484  	for key := range refsMap {
   485  		keys = append(keys, key)
   486  	}
   487  	sort.Strings(keys)
   488  	for i, key := range keys {
   489  		refs[i] = make([]interface{}, len(tableHeaders)+1)
   490  		refs[i][0] = key
   491  		ref := refsMap[key]
   492  		for j, k := range tableHeaders {
   493  			refs[i][j+1] = ref[k]
   494  		}
   495  	}
   496  
   497  	return refs, err
   498  }