1/*
2Copyright The Helm Authors.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6
7http://www.apache.org/licenses/LICENSE-2.0
8
9Unless required by applicable law or agreed to in writing, software
10distributed under the License is distributed on an "AS IS" BASIS,
11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12See the License for the specific language governing permissions and
13limitations under the License.
14*/
15
16package resolver
17
18import (
19	"bytes"
20	"encoding/json"
21	"os"
22	"path/filepath"
23	"strings"
24	"time"
25
26	"github.com/Masterminds/semver/v3"
27	"github.com/pkg/errors"
28
29	"helm.sh/helm/v3/pkg/chart"
30	"helm.sh/helm/v3/pkg/chart/loader"
31	"helm.sh/helm/v3/pkg/gates"
32	"helm.sh/helm/v3/pkg/helmpath"
33	"helm.sh/helm/v3/pkg/provenance"
34	"helm.sh/helm/v3/pkg/repo"
35)
36
37const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI")
38
39// Resolver resolves dependencies from semantic version ranges to a particular version.
40type Resolver struct {
41	chartpath string
42	cachepath string
43}
44
45// New creates a new resolver for a given chart and a given helm home.
46func New(chartpath, cachepath string) *Resolver {
47	return &Resolver{
48		chartpath: chartpath,
49		cachepath: cachepath,
50	}
51}
52
53// Resolve resolves dependencies and returns a lock file with the resolution.
54func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
55
56	// Now we clone the dependencies, locking as we go.
57	locked := make([]*chart.Dependency, len(reqs))
58	missing := []string{}
59	for i, d := range reqs {
60		if d.Repository == "" {
61			// Local chart subfolder
62			if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil {
63				return nil, err
64			}
65
66			locked[i] = &chart.Dependency{
67				Name:       d.Name,
68				Repository: "",
69				Version:    d.Version,
70			}
71			continue
72		}
73		if strings.HasPrefix(d.Repository, "file://") {
74
75			chartpath, err := GetLocalPath(d.Repository, r.chartpath)
76			if err != nil {
77				return nil, err
78			}
79
80			// The version of the chart locked will be the version of the chart
81			// currently listed in the file system within the chart.
82			ch, err := loader.LoadDir(chartpath)
83			if err != nil {
84				return nil, err
85			}
86
87			locked[i] = &chart.Dependency{
88				Name:       d.Name,
89				Repository: d.Repository,
90				Version:    ch.Metadata.Version,
91			}
92			continue
93		}
94
95		constraint, err := semver.NewConstraint(d.Version)
96		if err != nil {
97			return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
98		}
99
100		repoName := repoNames[d.Name]
101		// if the repository was not defined, but the dependency defines a repository url, bypass the cache
102		if repoName == "" && d.Repository != "" {
103			locked[i] = &chart.Dependency{
104				Name:       d.Name,
105				Repository: d.Repository,
106				Version:    d.Version,
107			}
108			continue
109		}
110
111		var vs repo.ChartVersions
112		var version string
113		var ok bool
114		found := true
115		if !strings.HasPrefix(d.Repository, "oci://") {
116			repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
117			if err != nil {
118				return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)
119			}
120
121			vs, ok = repoIndex.Entries[d.Name]
122			if !ok {
123				return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository)
124			}
125			found = false
126		} else {
127			version = d.Version
128			if !FeatureGateOCI.IsEnabled() {
129				return nil, errors.Wrapf(FeatureGateOCI.Error(),
130					"repository %s is an OCI registry", d.Repository)
131			}
132		}
133
134		locked[i] = &chart.Dependency{
135			Name:       d.Name,
136			Repository: d.Repository,
137			Version:    version,
138		}
139		// The version are already sorted and hence the first one to satisfy the constraint is used
140		for _, ver := range vs {
141			v, err := semver.NewVersion(ver.Version)
142			if err != nil || len(ver.URLs) == 0 {
143				// Not a legit entry.
144				continue
145			}
146			if constraint.Check(v) {
147				found = true
148				locked[i].Version = v.Original()
149				break
150			}
151		}
152
153		if !found {
154			missing = append(missing, d.Name)
155		}
156	}
157	if len(missing) > 0 {
158		return nil, errors.Errorf("can't get a valid version for repositories %s. Try changing the version constraint in Chart.yaml", strings.Join(missing, ", "))
159	}
160
161	digest, err := HashReq(reqs, locked)
162	if err != nil {
163		return nil, err
164	}
165
166	return &chart.Lock{
167		Generated:    time.Now(),
168		Digest:       digest,
169		Dependencies: locked,
170	}, nil
171}
172
173// HashReq generates a hash of the dependencies.
174//
175// This should be used only to compare against another hash generated by this
176// function.
177func HashReq(req, lock []*chart.Dependency) (string, error) {
178	data, err := json.Marshal([2][]*chart.Dependency{req, lock})
179	if err != nil {
180		return "", err
181	}
182	s, err := provenance.Digest(bytes.NewBuffer(data))
183	return "sha256:" + s, err
184}
185
186// HashV2Req generates a hash of requirements generated in Helm v2.
187//
188// This should be used only to compare against another hash generated by the
189// Helm v2 hash function. It is to handle issue:
190// https://github.com/helm/helm/issues/7233
191func HashV2Req(req []*chart.Dependency) (string, error) {
192	dep := make(map[string][]*chart.Dependency)
193	dep["dependencies"] = req
194	data, err := json.Marshal(dep)
195	if err != nil {
196		return "", err
197	}
198	s, err := provenance.Digest(bytes.NewBuffer(data))
199	return "sha256:" + s, err
200}
201
202// GetLocalPath generates absolute local path when use
203// "file://" in repository of dependencies
204func GetLocalPath(repo, chartpath string) (string, error) {
205	var depPath string
206	var err error
207	p := strings.TrimPrefix(repo, "file://")
208
209	// root path is absolute
210	if strings.HasPrefix(p, "/") {
211		if depPath, err = filepath.Abs(p); err != nil {
212			return "", err
213		}
214	} else {
215		depPath = filepath.Join(chartpath, p)
216	}
217
218	if _, err = os.Stat(depPath); os.IsNotExist(err) {
219		return "", errors.Errorf("directory %s not found", depPath)
220	} else if err != nil {
221		return "", err
222	}
223
224	return depPath, nil
225}
226