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