...

Source file src/helm.sh/helm/v3/pkg/repo/chartrepo.go

Documentation: helm.sh/helm/v3/pkg/repo

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

View as plain text