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