github.com/x-helm/helm@v3.0.0-beta.3+incompatible/pkg/repo/chartrepo.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 repo // import "helm.sh/helm/pkg/repo"
    18  
    19  import (
    20  	"crypto/rand"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net/url"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/pkg/errors"
    30  	"sigs.k8s.io/yaml"
    31  
    32  	"helm.sh/helm/pkg/chart/loader"
    33  	"helm.sh/helm/pkg/getter"
    34  	"helm.sh/helm/pkg/helmpath"
    35  	"helm.sh/helm/pkg/provenance"
    36  )
    37  
    38  // Entry represents a collection of parameters for chart repository
    39  type Entry struct {
    40  	Name     string `json:"name"`
    41  	URL      string `json:"url"`
    42  	Username string `json:"username"`
    43  	Password string `json:"password"`
    44  	CertFile string `json:"certFile"`
    45  	KeyFile  string `json:"keyFile"`
    46  	CAFile   string `json:"caFile"`
    47  }
    48  
    49  // ChartRepository represents a chart repository
    50  type ChartRepository struct {
    51  	Config     *Entry
    52  	ChartPaths []string
    53  	IndexFile  *IndexFile
    54  	Client     getter.Getter
    55  	CachePath  string
    56  }
    57  
    58  // NewChartRepository constructs ChartRepository
    59  func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) {
    60  	u, err := url.Parse(cfg.URL)
    61  	if err != nil {
    62  		return nil, errors.Errorf("invalid chart URL format: %s", cfg.URL)
    63  	}
    64  
    65  	client, err := getters.ByScheme(u.Scheme)
    66  	if err != nil {
    67  		return nil, errors.Errorf("could not find protocol handler for: %s", u.Scheme)
    68  	}
    69  
    70  	return &ChartRepository{
    71  		Config:    cfg,
    72  		IndexFile: NewIndexFile(),
    73  		Client:    client,
    74  		CachePath: helmpath.CachePath("repository"),
    75  	}, nil
    76  }
    77  
    78  // Load loads a directory of charts as if it were a repository.
    79  //
    80  // It requires the presence of an index.yaml file in the directory.
    81  func (r *ChartRepository) Load() error {
    82  	dirInfo, err := os.Stat(r.Config.Name)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	if !dirInfo.IsDir() {
    87  		return errors.Errorf("%q is not a directory", r.Config.Name)
    88  	}
    89  
    90  	// FIXME: Why are we recursively walking directories?
    91  	// FIXME: Why are we not reading the repositories.yaml to figure out
    92  	// what repos to use?
    93  	filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error {
    94  		if !f.IsDir() {
    95  			if strings.Contains(f.Name(), "-index.yaml") {
    96  				i, err := LoadIndexFile(path)
    97  				if err != nil {
    98  					return nil
    99  				}
   100  				r.IndexFile = i
   101  			} else if strings.HasSuffix(f.Name(), ".tgz") {
   102  				r.ChartPaths = append(r.ChartPaths, path)
   103  			}
   104  		}
   105  		return nil
   106  	})
   107  	return nil
   108  }
   109  
   110  // DownloadIndexFile fetches the index from a repository.
   111  func (r *ChartRepository) DownloadIndexFile() (string, error) {
   112  	var indexURL string
   113  	parsedURL, err := url.Parse(r.Config.URL)
   114  	if err != nil {
   115  		return "", err
   116  	}
   117  	parsedURL.Path = strings.TrimSuffix(parsedURL.Path, "/") + "/index.yaml"
   118  
   119  	indexURL = parsedURL.String()
   120  	// TODO add user-agent
   121  	resp, err := r.Client.Get(indexURL,
   122  		getter.WithURL(r.Config.URL),
   123  		getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile),
   124  		getter.WithBasicAuth(r.Config.Username, r.Config.Password),
   125  	)
   126  	if err != nil {
   127  		return "", err
   128  	}
   129  
   130  	index, err := ioutil.ReadAll(resp)
   131  	if err != nil {
   132  		return "", err
   133  	}
   134  
   135  	if _, err := loadIndex(index); err != nil {
   136  		return "", err
   137  	}
   138  
   139  	fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))
   140  	os.MkdirAll(filepath.Dir(fname), 0755)
   141  	return fname, ioutil.WriteFile(fname, index, 0644)
   142  }
   143  
   144  // Index generates an index for the chart repository and writes an index.yaml file.
   145  func (r *ChartRepository) Index() error {
   146  	err := r.generateIndex()
   147  	if err != nil {
   148  		return err
   149  	}
   150  	return r.saveIndexFile()
   151  }
   152  
   153  func (r *ChartRepository) saveIndexFile() error {
   154  	index, err := yaml.Marshal(r.IndexFile)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
   159  }
   160  
   161  func (r *ChartRepository) generateIndex() error {
   162  	for _, path := range r.ChartPaths {
   163  		ch, err := loader.Load(path)
   164  		if err != nil {
   165  			return err
   166  		}
   167  
   168  		digest, err := provenance.DigestFile(path)
   169  		if err != nil {
   170  			return err
   171  		}
   172  
   173  		if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) {
   174  			r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
   175  		}
   176  		// TODO: If a chart exists, but has a different Digest, should we error?
   177  	}
   178  	r.IndexFile.SortEntries()
   179  	return nil
   180  }
   181  
   182  // FindChartInRepoURL finds chart in chart repository pointed by repoURL
   183  // without adding repo to repositories
   184  func FindChartInRepoURL(repoURL, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) {
   185  	return FindChartInAuthRepoURL(repoURL, "", "", chartName, chartVersion, certFile, keyFile, caFile, getters)
   186  }
   187  
   188  // FindChartInAuthRepoURL finds chart in chart repository pointed by repoURL
   189  // without adding repo to repositories, like FindChartInRepoURL,
   190  // but it also receives credentials for the chart repository.
   191  func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) {
   192  
   193  	// Download and write the index file to a temporary location
   194  	buf := make([]byte, 20)
   195  	rand.Read(buf)
   196  	name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-")
   197  
   198  	c := Entry{
   199  		URL:      repoURL,
   200  		Username: username,
   201  		Password: password,
   202  		CertFile: certFile,
   203  		KeyFile:  keyFile,
   204  		CAFile:   caFile,
   205  		Name:     name,
   206  	}
   207  	r, err := NewChartRepository(&c, getters)
   208  	if err != nil {
   209  		return "", err
   210  	}
   211  	idx, err := r.DownloadIndexFile()
   212  	if err != nil {
   213  		return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL)
   214  	}
   215  
   216  	// Read the index file for the repository to get chart information and return chart URL
   217  	repoIndex, err := LoadIndexFile(idx)
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  
   222  	errMsg := fmt.Sprintf("chart %q", chartName)
   223  	if chartVersion != "" {
   224  		errMsg = fmt.Sprintf("%s version %q", errMsg, chartVersion)
   225  	}
   226  	cv, err := repoIndex.Get(chartName, chartVersion)
   227  	if err != nil {
   228  		return "", errors.Errorf("%s not found in %s repository", errMsg, repoURL)
   229  	}
   230  
   231  	if len(cv.URLs) == 0 {
   232  		return "", errors.Errorf("%s has no downloadable URLs", errMsg)
   233  	}
   234  
   235  	chartURL := cv.URLs[0]
   236  
   237  	absoluteChartURL, err := ResolveReferenceURL(repoURL, chartURL)
   238  	if err != nil {
   239  		return "", errors.Wrap(err, "failed to make chart URL absolute")
   240  	}
   241  
   242  	return absoluteChartURL, nil
   243  }
   244  
   245  // ResolveReferenceURL resolves refURL relative to baseURL.
   246  // If refURL is absolute, it simply returns refURL.
   247  func ResolveReferenceURL(baseURL, refURL string) (string, error) {
   248  	parsedBaseURL, err := url.Parse(baseURL)
   249  	if err != nil {
   250  		return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL)
   251  	}
   252  
   253  	parsedRefURL, err := url.Parse(refURL)
   254  	if err != nil {
   255  		return "", errors.Wrapf(err, "failed to parse %s as URL", refURL)
   256  	}
   257  
   258  	// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
   259  	parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
   260  	return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil
   261  }