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 }