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 }