1
16
17 package repo
18
19 import (
20 "bytes"
21 "encoding/json"
22 "log"
23 "os"
24 "path"
25 "path/filepath"
26 "sort"
27 "strings"
28 "time"
29
30 "github.com/Masterminds/semver/v3"
31 "github.com/pkg/errors"
32 "sigs.k8s.io/yaml"
33
34 "helm.sh/helm/v3/internal/fileutil"
35 "helm.sh/helm/v3/internal/urlutil"
36 "helm.sh/helm/v3/pkg/chart"
37 "helm.sh/helm/v3/pkg/chart/loader"
38 "helm.sh/helm/v3/pkg/provenance"
39 )
40
41 var indexPath = "index.yaml"
42
43
44 const APIVersionV1 = "v1"
45
46 var (
47
48 ErrNoAPIVersion = errors.New("no API version specified")
49
50 ErrNoChartVersion = errors.New("no chart version found")
51
52 ErrNoChartName = errors.New("no chart name found")
53
54 ErrEmptyIndexYaml = errors.New("empty index.yaml file")
55 )
56
57
58
59 type ChartVersions []*ChartVersion
60
61
62 func (c ChartVersions) Len() int { return len(c) }
63
64
65 func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
66
67
68 func (c ChartVersions) Less(a, b int) bool {
69
70 i, err := semver.NewVersion(c[a].Version)
71 if err != nil {
72 return true
73 }
74 j, err := semver.NewVersion(c[b].Version)
75 if err != nil {
76 return false
77 }
78 return i.LessThan(j)
79 }
80
81
82 type IndexFile struct {
83
84 ServerInfo map[string]interface{} `json:"serverInfo,omitempty"`
85 APIVersion string `json:"apiVersion"`
86 Generated time.Time `json:"generated"`
87 Entries map[string]ChartVersions `json:"entries"`
88 PublicKeys []string `json:"publicKeys,omitempty"`
89
90
91
92 Annotations map[string]string `json:"annotations,omitempty"`
93 }
94
95
96 func NewIndexFile() *IndexFile {
97 return &IndexFile{
98 APIVersion: APIVersionV1,
99 Generated: time.Now(),
100 Entries: map[string]ChartVersions{},
101 PublicKeys: []string{},
102 }
103 }
104
105
106 func LoadIndexFile(path string) (*IndexFile, error) {
107 b, err := os.ReadFile(path)
108 if err != nil {
109 return nil, err
110 }
111 i, err := loadIndex(b, path)
112 if err != nil {
113 return nil, errors.Wrapf(err, "error loading %s", path)
114 }
115 return i, nil
116 }
117
118
119
120 func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error {
121 if i.Entries == nil {
122 return errors.New("entries not initialized")
123 }
124
125 if md.APIVersion == "" {
126 md.APIVersion = chart.APIVersionV1
127 }
128 if err := md.Validate(); err != nil {
129 return errors.Wrapf(err, "validate failed for %s", filename)
130 }
131
132 u := filename
133 if baseURL != "" {
134 _, file := filepath.Split(filename)
135 var err error
136 u, err = urlutil.URLJoin(baseURL, file)
137 if err != nil {
138 u = path.Join(baseURL, file)
139 }
140 }
141 cr := &ChartVersion{
142 URLs: []string{u},
143 Metadata: md,
144 Digest: digest,
145 Created: time.Now(),
146 }
147 ee := i.Entries[md.Name]
148 i.Entries[md.Name] = append(ee, cr)
149 return nil
150 }
151
152
153
154
155 func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
156 if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
157 log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err)
158 }
159 }
160
161
162 func (i IndexFile) Has(name, version string) bool {
163 _, err := i.Get(name, version)
164 return err == nil
165 }
166
167
168
169
170
171
172
173 func (i IndexFile) SortEntries() {
174 for _, versions := range i.Entries {
175 sort.Sort(sort.Reverse(versions))
176 }
177 }
178
179
180
181
182
183 func (i IndexFile) Get(name, version string) (*ChartVersion, error) {
184 vs, ok := i.Entries[name]
185 if !ok {
186 return nil, ErrNoChartName
187 }
188 if len(vs) == 0 {
189 return nil, ErrNoChartVersion
190 }
191
192 var constraint *semver.Constraints
193 if version == "" {
194 constraint, _ = semver.NewConstraint("*")
195 } else {
196 var err error
197 constraint, err = semver.NewConstraint(version)
198 if err != nil {
199 return nil, err
200 }
201 }
202
203
204 if len(version) != 0 {
205 for _, ver := range vs {
206 if version == ver.Version {
207 return ver, nil
208 }
209 }
210 }
211
212 for _, ver := range vs {
213 test, err := semver.NewVersion(ver.Version)
214 if err != nil {
215 continue
216 }
217
218 if constraint.Check(test) {
219 return ver, nil
220 }
221 }
222 return nil, errors.Errorf("no chart version found for %s-%s", name, version)
223 }
224
225
226
227
228 func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
229 b, err := yaml.Marshal(i)
230 if err != nil {
231 return err
232 }
233 return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
234 }
235
236
237
238
239
240 func (i IndexFile) WriteJSONFile(dest string, mode os.FileMode) error {
241 b, err := json.MarshalIndent(i, "", " ")
242 if err != nil {
243 return err
244 }
245 return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
246 }
247
248
249
250
251
252
253
254
255
256 func (i *IndexFile) Merge(f *IndexFile) {
257 for _, cvs := range f.Entries {
258 for _, cv := range cvs {
259 if !i.Has(cv.Name, cv.Version) {
260 e := i.Entries[cv.Name]
261 i.Entries[cv.Name] = append(e, cv)
262 }
263 }
264 }
265 }
266
267
268 type ChartVersion struct {
269 *chart.Metadata
270 URLs []string `json:"urls"`
271 Created time.Time `json:"created,omitempty"`
272 Removed bool `json:"removed,omitempty"`
273 Digest string `json:"digest,omitempty"`
274
275
276
277
278 ChecksumDeprecated string `json:"checksum,omitempty"`
279
280
281
282 EngineDeprecated string `json:"engine,omitempty"`
283
284
285
286 TillerVersionDeprecated string `json:"tillerVersion,omitempty"`
287
288
289
290 URLDeprecated string `json:"url,omitempty"`
291 }
292
293
294
295
296
297
298 func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
299 archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
300 if err != nil {
301 return nil, err
302 }
303 moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz"))
304 if err != nil {
305 return nil, err
306 }
307 archives = append(archives, moreArchives...)
308
309 index := NewIndexFile()
310 for _, arch := range archives {
311 fname, err := filepath.Rel(dir, arch)
312 if err != nil {
313 return index, err
314 }
315
316 var parentDir string
317 parentDir, fname = filepath.Split(fname)
318
319 parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator))
320 parentURL, err := urlutil.URLJoin(baseURL, parentDir)
321 if err != nil {
322 parentURL = path.Join(baseURL, parentDir)
323 }
324
325 c, err := loader.Load(arch)
326 if err != nil {
327
328 continue
329 }
330 hash, err := provenance.DigestFile(arch)
331 if err != nil {
332 return index, err
333 }
334 if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
335 return index, errors.Wrapf(err, "failed adding to %s to index", fname)
336 }
337 }
338 return index, nil
339 }
340
341
342
343
344
345 func loadIndex(data []byte, source string) (*IndexFile, error) {
346 i := &IndexFile{}
347
348 if len(data) == 0 {
349 return i, ErrEmptyIndexYaml
350 }
351
352 if err := jsonOrYamlUnmarshal(data, i); err != nil {
353 return i, err
354 }
355
356 for name, cvs := range i.Entries {
357 for idx := len(cvs) - 1; idx >= 0; idx-- {
358 if cvs[idx] == nil {
359 log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)
360 continue
361 }
362
363 if cvs[idx].Metadata == nil {
364 cvs[idx].Metadata = &chart.Metadata{}
365 }
366 if cvs[idx].APIVersion == "" {
367 cvs[idx].APIVersion = chart.APIVersionV1
368 }
369 if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil {
370 log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)
371 cvs = append(cvs[:idx], cvs[idx+1:]...)
372 }
373 }
374 }
375 i.SortEntries()
376 if i.APIVersion == "" {
377 return i, ErrNoAPIVersion
378 }
379 return i, nil
380 }
381
382
383
384
385
386
387
388
389 func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
390 if json.Valid(b) {
391 return json.Unmarshal(b, i)
392 }
393 return yaml.UnmarshalStrict(b, i)
394 }
395
396
397
398
399
400
401
402 func ignoreSkippableChartValidationError(err error) error {
403 verr, ok := err.(chart.ValidationError)
404 if !ok {
405 return err
406 }
407
408
409 if strings.HasPrefix(verr.Error(), "validation: more than one dependency with name or alias") {
410 return nil
411 }
412
413 return err
414 }
415
View as plain text