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