github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/cmd/helm/repo_add.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 main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/gofrs/flock"
    30  	"github.com/pkg/errors"
    31  	"github.com/spf13/cobra"
    32  	"golang.org/x/term"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	"github.com/stefanmcshane/helm/cmd/helm/require"
    36  	"github.com/stefanmcshane/helm/pkg/getter"
    37  	"github.com/stefanmcshane/helm/pkg/repo"
    38  )
    39  
    40  // Repositories that have been permanently deleted and no longer work
    41  var deprecatedRepos = map[string]string{
    42  	"//kubernetes-charts.storage.googleapis.com":           "https://charts.helm.sh/stable",
    43  	"//kubernetes-charts-incubator.storage.googleapis.com": "https://charts.helm.sh/incubator",
    44  }
    45  
    46  type repoAddOptions struct {
    47  	name                 string
    48  	url                  string
    49  	username             string
    50  	password             string
    51  	passwordFromStdinOpt bool
    52  	passCredentialsAll   bool
    53  	forceUpdate          bool
    54  	allowDeprecatedRepos bool
    55  
    56  	certFile              string
    57  	keyFile               string
    58  	caFile                string
    59  	insecureSkipTLSverify bool
    60  
    61  	repoFile  string
    62  	repoCache string
    63  
    64  	// Deprecated, but cannot be removed until Helm 4
    65  	deprecatedNoUpdate bool
    66  }
    67  
    68  func newRepoAddCmd(out io.Writer) *cobra.Command {
    69  	o := &repoAddOptions{}
    70  
    71  	cmd := &cobra.Command{
    72  		Use:               "add [NAME] [URL]",
    73  		Short:             "add a chart repository",
    74  		Args:              require.ExactArgs(2),
    75  		ValidArgsFunction: noCompletions,
    76  		RunE: func(cmd *cobra.Command, args []string) error {
    77  			o.name = args[0]
    78  			o.url = args[1]
    79  			o.repoFile = settings.RepositoryConfig
    80  			o.repoCache = settings.RepositoryCache
    81  
    82  			return o.run(out)
    83  		},
    84  	}
    85  
    86  	f := cmd.Flags()
    87  	f.StringVar(&o.username, "username", "", "chart repository username")
    88  	f.StringVar(&o.password, "password", "", "chart repository password")
    89  	f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
    90  	f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
    91  	f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
    92  	f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
    93  	f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
    94  	f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
    95  	f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
    96  	f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior")
    97  	f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
    98  
    99  	return cmd
   100  }
   101  
   102  func (o *repoAddOptions) run(out io.Writer) error {
   103  	// Block deprecated repos
   104  	if !o.allowDeprecatedRepos {
   105  		for oldURL, newURL := range deprecatedRepos {
   106  			if strings.Contains(o.url, oldURL) {
   107  				return fmt.Errorf("repo %q is no longer available; try %q instead", o.url, newURL)
   108  			}
   109  		}
   110  	}
   111  
   112  	// Ensure the file directory exists as it is required for file locking
   113  	err := os.MkdirAll(filepath.Dir(o.repoFile), os.ModePerm)
   114  	if err != nil && !os.IsExist(err) {
   115  		return err
   116  	}
   117  
   118  	// Acquire a file lock for process synchronization
   119  	repoFileExt := filepath.Ext(o.repoFile)
   120  	var lockPath string
   121  	if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) {
   122  		lockPath = strings.TrimSuffix(o.repoFile, repoFileExt) + ".lock"
   123  	} else {
   124  		lockPath = o.repoFile + ".lock"
   125  	}
   126  	fileLock := flock.New(lockPath)
   127  	lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
   128  	defer cancel()
   129  	locked, err := fileLock.TryLockContext(lockCtx, time.Second)
   130  	if err == nil && locked {
   131  		defer fileLock.Unlock()
   132  	}
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	b, err := ioutil.ReadFile(o.repoFile)
   138  	if err != nil && !os.IsNotExist(err) {
   139  		return err
   140  	}
   141  
   142  	var f repo.File
   143  	if err := yaml.Unmarshal(b, &f); err != nil {
   144  		return err
   145  	}
   146  
   147  	if o.username != "" && o.password == "" {
   148  		if o.passwordFromStdinOpt {
   149  			passwordFromStdin, err := io.ReadAll(os.Stdin)
   150  			if err != nil {
   151  				return err
   152  			}
   153  			password := strings.TrimSuffix(string(passwordFromStdin), "\n")
   154  			password = strings.TrimSuffix(password, "\r")
   155  			o.password = password
   156  		} else {
   157  			fd := int(os.Stdin.Fd())
   158  			fmt.Fprint(out, "Password: ")
   159  			password, err := term.ReadPassword(fd)
   160  			fmt.Fprintln(out)
   161  			if err != nil {
   162  				return err
   163  			}
   164  			o.password = string(password)
   165  		}
   166  	}
   167  
   168  	c := repo.Entry{
   169  		Name:                  o.name,
   170  		URL:                   o.url,
   171  		Username:              o.username,
   172  		Password:              o.password,
   173  		PassCredentialsAll:    o.passCredentialsAll,
   174  		CertFile:              o.certFile,
   175  		KeyFile:               o.keyFile,
   176  		CAFile:                o.caFile,
   177  		InsecureSkipTLSverify: o.insecureSkipTLSverify,
   178  	}
   179  
   180  	// Check if the repo name is legal
   181  	if strings.Contains(o.name, "/") {
   182  		return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name)
   183  	}
   184  
   185  	// If the repo exists do one of two things:
   186  	// 1. If the configuration for the name is the same continue without error
   187  	// 2. When the config is different require --force-update
   188  	if !o.forceUpdate && f.Has(o.name) {
   189  		existing := f.Get(o.name)
   190  		if c != *existing {
   191  
   192  			// The input coming in for the name is different from what is already
   193  			// configured. Return an error.
   194  			return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name)
   195  		}
   196  
   197  		// The add is idempotent so do nothing
   198  		fmt.Fprintf(out, "%q already exists with the same configuration, skipping\n", o.name)
   199  		return nil
   200  	}
   201  
   202  	r, err := repo.NewChartRepository(&c, getter.All(settings))
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	if o.repoCache != "" {
   208  		r.CachePath = o.repoCache
   209  	}
   210  	if _, err := r.DownloadIndexFile(); err != nil {
   211  		return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", o.url)
   212  	}
   213  
   214  	f.Update(&c)
   215  
   216  	if err := f.WriteFile(o.repoFile, 0644); err != nil {
   217  		return err
   218  	}
   219  	fmt.Fprintf(out, "%q has been added to your repositories\n", o.name)
   220  	return nil
   221  }