github.com/cloudposse/helm@v2.2.3+incompatible/pkg/repo/chartrepo.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 "k8s.io/helm/pkg/repo" 18 19 import ( 20 "fmt" 21 "io/ioutil" 22 "net/http" 23 "os" 24 "path/filepath" 25 "strings" 26 27 "github.com/ghodss/yaml" 28 29 "k8s.io/helm/pkg/chartutil" 30 "k8s.io/helm/pkg/provenance" 31 "k8s.io/helm/pkg/tlsutil" 32 "k8s.io/helm/pkg/urlutil" 33 ) 34 35 // Entry represents a collection of parameters for chart repository 36 type Entry struct { 37 Name string `json:"name"` 38 Cache string `json:"cache"` 39 URL string `json:"url"` 40 CertFile string `json:"certFile"` 41 KeyFile string `json:"keyFile"` 42 CAFile string `json:"caFile"` 43 } 44 45 // ChartRepository represents a chart repository 46 type ChartRepository struct { 47 Config *Entry 48 ChartPaths []string 49 IndexFile *IndexFile 50 Client *http.Client 51 } 52 53 // Getter is an interface to support GET to the specified URL. 54 type Getter interface { 55 Get(url string) (*http.Response, error) 56 } 57 58 // NewChartRepository constructs ChartRepository 59 func NewChartRepository(cfg *Entry) (*ChartRepository, error) { 60 var client *http.Client 61 if cfg.CertFile != "" && cfg.KeyFile != "" && cfg.CAFile != "" { 62 tlsConf, err := tlsutil.NewClientTLS(cfg.CertFile, cfg.KeyFile, cfg.CAFile) 63 if err != nil { 64 return nil, fmt.Errorf("can't create TLS config for client: %s", err.Error()) 65 } 66 tlsConf.BuildNameToCertificate() 67 68 sni, err := urlutil.ExtractHostname(cfg.URL) 69 if err != nil { 70 return nil, err 71 } 72 tlsConf.ServerName = sni 73 74 client = &http.Client{ 75 Transport: &http.Transport{ 76 TLSClientConfig: tlsConf, 77 }, 78 } 79 } else { 80 client = http.DefaultClient 81 } 82 83 return &ChartRepository{ 84 Config: cfg, 85 IndexFile: NewIndexFile(), 86 Client: client, 87 }, nil 88 } 89 90 // Get issues a GET using configured client to the specified URL. 91 func (r *ChartRepository) Get(url string) (*http.Response, error) { 92 resp, err := r.Client.Get(url) 93 if err != nil { 94 return nil, err 95 } 96 return resp, nil 97 } 98 99 // Load loads a directory of charts as if it were a repository. 100 // 101 // It requires the presence of an index.yaml file in the directory. 102 func (r *ChartRepository) Load() error { 103 dirInfo, err := os.Stat(r.Config.Name) 104 if err != nil { 105 return err 106 } 107 if !dirInfo.IsDir() { 108 return fmt.Errorf("%q is not a directory", r.Config.Name) 109 } 110 111 // FIXME: Why are we recursively walking directories? 112 // FIXME: Why are we not reading the repositories.yaml to figure out 113 // what repos to use? 114 filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error { 115 if !f.IsDir() { 116 if strings.Contains(f.Name(), "-index.yaml") { 117 i, err := LoadIndexFile(path) 118 if err != nil { 119 return nil 120 } 121 r.IndexFile = i 122 } else if strings.HasSuffix(f.Name(), ".tgz") { 123 r.ChartPaths = append(r.ChartPaths, path) 124 } 125 } 126 return nil 127 }) 128 return nil 129 } 130 131 // DownloadIndexFile fetches the index from a repository. 132 // 133 // cachePath is prepended to any index that does not have an absolute path. This 134 // is for pre-2.2.0 repo files. 135 func (r *ChartRepository) DownloadIndexFile(cachePath string) error { 136 var indexURL string 137 138 indexURL = strings.TrimSuffix(r.Config.URL, "/") + "/index.yaml" 139 resp, err := r.Get(indexURL) 140 if err != nil { 141 return err 142 } 143 defer resp.Body.Close() 144 145 index, err := ioutil.ReadAll(resp.Body) 146 if err != nil { 147 return err 148 } 149 150 if _, err := loadIndex(index); err != nil { 151 return err 152 } 153 154 // In Helm 2.2.0 the config.cache was accidentally switched to an absolute 155 // path, which broke backward compatibility. This fixes it by prepending a 156 // global cache path to relative paths. 157 // 158 // It is changed on DownloadIndexFile because that was the method that 159 // originally carried the cache path. 160 cp := r.Config.Cache 161 if !filepath.IsAbs(cp) { 162 cp = filepath.Join(cachePath, cp) 163 } 164 println("Writing to", cp) 165 166 return ioutil.WriteFile(cp, index, 0644) 167 } 168 169 // Index generates an index for the chart repository and writes an index.yaml file. 170 func (r *ChartRepository) Index() error { 171 err := r.generateIndex() 172 if err != nil { 173 return err 174 } 175 return r.saveIndexFile() 176 } 177 178 func (r *ChartRepository) saveIndexFile() error { 179 index, err := yaml.Marshal(r.IndexFile) 180 if err != nil { 181 return err 182 } 183 return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644) 184 } 185 186 func (r *ChartRepository) generateIndex() error { 187 for _, path := range r.ChartPaths { 188 ch, err := chartutil.Load(path) 189 if err != nil { 190 return err 191 } 192 193 digest, err := provenance.DigestFile(path) 194 if err != nil { 195 return err 196 } 197 198 if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) { 199 r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest) 200 } 201 // TODO: If a chart exists, but has a different Digest, should we error? 202 } 203 r.IndexFile.SortEntries() 204 return nil 205 }