1
16
17 package repo
18
19 import (
20 "crypto/rand"
21 "encoding/base64"
22 "encoding/json"
23 "fmt"
24 "io"
25 "log"
26 "net/url"
27 "os"
28 "path/filepath"
29 "strings"
30
31 "github.com/pkg/errors"
32 "sigs.k8s.io/yaml"
33
34 "helm.sh/helm/v3/pkg/chart/loader"
35 "helm.sh/helm/v3/pkg/getter"
36 "helm.sh/helm/v3/pkg/helmpath"
37 "helm.sh/helm/v3/pkg/provenance"
38 )
39
40
41 type Entry struct {
42 Name string `json:"name"`
43 URL string `json:"url"`
44 Username string `json:"username"`
45 Password string `json:"password"`
46 CertFile string `json:"certFile"`
47 KeyFile string `json:"keyFile"`
48 CAFile string `json:"caFile"`
49 InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"`
50 PassCredentialsAll bool `json:"pass_credentials_all"`
51 }
52
53
54 type ChartRepository struct {
55 Config *Entry
56 ChartPaths []string
57 IndexFile *IndexFile
58 Client getter.Getter
59 CachePath string
60 }
61
62
63 func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) {
64 u, err := url.Parse(cfg.URL)
65 if err != nil {
66 return nil, errors.Errorf("invalid chart URL format: %s", cfg.URL)
67 }
68
69 client, err := getters.ByScheme(u.Scheme)
70 if err != nil {
71 return nil, errors.Errorf("could not find protocol handler for: %s", u.Scheme)
72 }
73
74 return &ChartRepository{
75 Config: cfg,
76 IndexFile: NewIndexFile(),
77 Client: client,
78 CachePath: helmpath.CachePath("repository"),
79 }, nil
80 }
81
82
83
84
85
86
87 func (r *ChartRepository) Load() error {
88 dirInfo, err := os.Stat(r.Config.Name)
89 if err != nil {
90 return err
91 }
92 if !dirInfo.IsDir() {
93 return errors.Errorf("%q is not a directory", r.Config.Name)
94 }
95
96
97
98
99 filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, _ error) error {
100 if !f.IsDir() {
101 if strings.Contains(f.Name(), "-index.yaml") {
102 i, err := LoadIndexFile(path)
103 if err != nil {
104 return err
105 }
106 r.IndexFile = i
107 } else if strings.HasSuffix(f.Name(), ".tgz") {
108 r.ChartPaths = append(r.ChartPaths, path)
109 }
110 }
111 return nil
112 })
113 return nil
114 }
115
116
117 func (r *ChartRepository) DownloadIndexFile() (string, error) {
118 indexURL, err := ResolveReferenceURL(r.Config.URL, "index.yaml")
119 if err != nil {
120 return "", err
121 }
122
123 resp, err := r.Client.Get(indexURL,
124 getter.WithURL(r.Config.URL),
125 getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify),
126 getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile),
127 getter.WithBasicAuth(r.Config.Username, r.Config.Password),
128 getter.WithPassCredentialsAll(r.Config.PassCredentialsAll),
129 )
130 if err != nil {
131 return "", err
132 }
133
134 index, err := io.ReadAll(resp)
135 if err != nil {
136 return "", err
137 }
138
139 indexFile, err := loadIndex(index, r.Config.URL)
140 if err != nil {
141 return "", err
142 }
143
144
145 var charts strings.Builder
146 for name := range indexFile.Entries {
147 fmt.Fprintln(&charts, name)
148 }
149 chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))
150 os.MkdirAll(filepath.Dir(chartsFile), 0755)
151 os.WriteFile(chartsFile, []byte(charts.String()), 0644)
152
153
154 fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))
155 os.MkdirAll(filepath.Dir(fname), 0755)
156 return fname, os.WriteFile(fname, index, 0644)
157 }
158
159
160 func (r *ChartRepository) Index() error {
161 err := r.generateIndex()
162 if err != nil {
163 return err
164 }
165 return r.saveIndexFile()
166 }
167
168 func (r *ChartRepository) saveIndexFile() error {
169 index, err := yaml.Marshal(r.IndexFile)
170 if err != nil {
171 return err
172 }
173 return os.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
174 }
175
176 func (r *ChartRepository) generateIndex() error {
177 for _, path := range r.ChartPaths {
178 ch, err := loader.Load(path)
179 if err != nil {
180 return err
181 }
182
183 digest, err := provenance.DigestFile(path)
184 if err != nil {
185 return err
186 }
187
188 if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) {
189 if err := r.IndexFile.MustAdd(ch.Metadata, path, r.Config.URL, digest); err != nil {
190 return errors.Wrapf(err, "failed adding to %s to index", path)
191 }
192 }
193
194 }
195 r.IndexFile.SortEntries()
196 return nil
197 }
198
199
200
201 func FindChartInRepoURL(repoURL, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) {
202 return FindChartInAuthRepoURL(repoURL, "", "", chartName, chartVersion, certFile, keyFile, caFile, getters)
203 }
204
205
206
207
208 func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) {
209 return FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, getters)
210 }
211
212
213
214
215
216 func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) {
217 return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, insecureSkipTLSverify, false, getters)
218 }
219
220
221
222
223
224
225 func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify, passCredentialsAll bool, getters getter.Providers) (string, error) {
226
227
228 buf := make([]byte, 20)
229 rand.Read(buf)
230 name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-")
231
232 c := Entry{
233 URL: repoURL,
234 Username: username,
235 Password: password,
236 PassCredentialsAll: passCredentialsAll,
237 CertFile: certFile,
238 KeyFile: keyFile,
239 CAFile: caFile,
240 Name: name,
241 InsecureSkipTLSverify: insecureSkipTLSverify,
242 }
243 r, err := NewChartRepository(&c, getters)
244 if err != nil {
245 return "", err
246 }
247 idx, err := r.DownloadIndexFile()
248 if err != nil {
249 return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL)
250 }
251 defer func() {
252 os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)))
253 os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)))
254 }()
255
256
257 repoIndex, err := LoadIndexFile(idx)
258 if err != nil {
259 return "", err
260 }
261
262 errMsg := fmt.Sprintf("chart %q", chartName)
263 if chartVersion != "" {
264 errMsg = fmt.Sprintf("%s version %q", errMsg, chartVersion)
265 }
266 cv, err := repoIndex.Get(chartName, chartVersion)
267 if err != nil {
268 return "", errors.Errorf("%s not found in %s repository", errMsg, repoURL)
269 }
270
271 if len(cv.URLs) == 0 {
272 return "", errors.Errorf("%s has no downloadable URLs", errMsg)
273 }
274
275 chartURL := cv.URLs[0]
276
277 absoluteChartURL, err := ResolveReferenceURL(repoURL, chartURL)
278 if err != nil {
279 return "", errors.Wrap(err, "failed to make chart URL absolute")
280 }
281
282 return absoluteChartURL, nil
283 }
284
285
286
287 func ResolveReferenceURL(baseURL, refURL string) (string, error) {
288 parsedRefURL, err := url.Parse(refURL)
289 if err != nil {
290 return "", errors.Wrapf(err, "failed to parse %s as URL", refURL)
291 }
292
293 if parsedRefURL.IsAbs() {
294 return refURL, nil
295 }
296
297 parsedBaseURL, err := url.Parse(baseURL)
298 if err != nil {
299 return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL)
300 }
301
302
303 parsedBaseURL.RawPath = strings.TrimSuffix(parsedBaseURL.RawPath, "/") + "/"
304 parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
305
306 resolvedURL := parsedBaseURL.ResolveReference(parsedRefURL)
307 resolvedURL.RawQuery = parsedBaseURL.RawQuery
308 return resolvedURL.String(), nil
309 }
310
311 func (e *Entry) String() string {
312 buf, err := json.Marshal(e)
313 if err != nil {
314 log.Panic(err)
315 }
316 return string(buf)
317 }
318
View as plain text