1
15
16 package resolver
17
18 import (
19 "bytes"
20 "encoding/json"
21 "fmt"
22 "os"
23 "path/filepath"
24 "strings"
25 "time"
26
27 "github.com/Masterminds/semver/v3"
28 "github.com/pkg/errors"
29
30 "helm.sh/helm/v3/pkg/chart"
31 "helm.sh/helm/v3/pkg/chart/loader"
32 "helm.sh/helm/v3/pkg/helmpath"
33 "helm.sh/helm/v3/pkg/provenance"
34 "helm.sh/helm/v3/pkg/registry"
35 "helm.sh/helm/v3/pkg/repo"
36 )
37
38
39 type Resolver struct {
40 chartpath string
41 cachepath string
42 registryClient *registry.Client
43 }
44
45
46 func New(chartpath, cachepath string, registryClient *registry.Client) *Resolver {
47 return &Resolver{
48 chartpath: chartpath,
49 cachepath: cachepath,
50 registryClient: registryClient,
51 }
52 }
53
54
55 func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) {
56
57
58 locked := make([]*chart.Dependency, len(reqs))
59 missing := []string{}
60 for i, d := range reqs {
61 constraint, err := semver.NewConstraint(d.Version)
62 if err != nil {
63 return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
64 }
65
66 if d.Repository == "" {
67
68 if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil {
69 return nil, err
70 }
71
72 locked[i] = &chart.Dependency{
73 Name: d.Name,
74 Repository: "",
75 Version: d.Version,
76 }
77 continue
78 }
79 if strings.HasPrefix(d.Repository, "file://") {
80
81 chartpath, err := GetLocalPath(d.Repository, r.chartpath)
82 if err != nil {
83 return nil, err
84 }
85
86 ch, err := loader.LoadDir(chartpath)
87 if err != nil {
88 return nil, err
89 }
90
91 v, err := semver.NewVersion(ch.Metadata.Version)
92 if err != nil {
93
94 continue
95 }
96
97 if !constraint.Check(v) {
98 missing = append(missing, d.Name)
99 continue
100 }
101
102 locked[i] = &chart.Dependency{
103 Name: d.Name,
104 Repository: d.Repository,
105 Version: ch.Metadata.Version,
106 }
107 continue
108 }
109
110 repoName := repoNames[d.Name]
111
112 if repoName == "" && d.Repository != "" {
113 locked[i] = &chart.Dependency{
114 Name: d.Name,
115 Repository: d.Repository,
116 Version: d.Version,
117 }
118 continue
119 }
120
121 var vs repo.ChartVersions
122 var version string
123 var ok bool
124 found := true
125 if !registry.IsOCI(d.Repository) {
126 repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
127 if err != nil {
128 return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)
129 }
130
131 vs, ok = repoIndex.Entries[d.Name]
132 if !ok {
133 return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository)
134 }
135 found = false
136 } else {
137 version = d.Version
138
139
140 _, err := semver.NewVersion(version)
141
142
143 if err == nil {
144 vs = []*repo.ChartVersion{{
145 Metadata: &chart.Metadata{
146 Version: version,
147 },
148 }}
149
150 } else {
151
152 ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, fmt.Sprintf("%s://", registry.OCIScheme)), d.Name)
153 tags, err := r.registryClient.Tags(ref)
154 if err != nil {
155 return nil, errors.Wrapf(err, "could not retrieve list of tags for repository %s", d.Repository)
156 }
157
158 vs = make(repo.ChartVersions, len(tags))
159 for ti, t := range tags {
160
161 version := &repo.ChartVersion{
162 Metadata: &chart.Metadata{
163 Version: t,
164 },
165 }
166 vs[ti] = version
167 }
168 }
169 }
170
171 locked[i] = &chart.Dependency{
172 Name: d.Name,
173 Repository: d.Repository,
174 Version: version,
175 }
176
177 for _, ver := range vs {
178 v, err := semver.NewVersion(ver.Version)
179
180 if err != nil || (!registry.IsOCI(d.Repository) && len(ver.URLs) == 0) {
181
182 continue
183 }
184 if constraint.Check(v) {
185 found = true
186 locked[i].Version = v.Original()
187 break
188 }
189 }
190
191 if !found {
192 missing = append(missing, d.Name)
193 }
194 }
195 if len(missing) > 0 {
196 return nil, errors.Errorf("can't get a valid version for repositories %s. Try changing the version constraint in Chart.yaml", strings.Join(missing, ", "))
197 }
198
199 digest, err := HashReq(reqs, locked)
200 if err != nil {
201 return nil, err
202 }
203
204 return &chart.Lock{
205 Generated: time.Now(),
206 Digest: digest,
207 Dependencies: locked,
208 }, nil
209 }
210
211
212
213
214
215 func HashReq(req, lock []*chart.Dependency) (string, error) {
216 data, err := json.Marshal([2][]*chart.Dependency{req, lock})
217 if err != nil {
218 return "", err
219 }
220 s, err := provenance.Digest(bytes.NewBuffer(data))
221 return "sha256:" + s, err
222 }
223
224
225
226
227
228
229 func HashV2Req(req []*chart.Dependency) (string, error) {
230 dep := make(map[string][]*chart.Dependency)
231 dep["dependencies"] = req
232 data, err := json.Marshal(dep)
233 if err != nil {
234 return "", err
235 }
236 s, err := provenance.Digest(bytes.NewBuffer(data))
237 return "sha256:" + s, err
238 }
239
240
241
242 func GetLocalPath(repo, chartpath string) (string, error) {
243 var depPath string
244 var err error
245 p := strings.TrimPrefix(repo, "file://")
246
247
248 if strings.HasPrefix(p, "/") {
249 if depPath, err = filepath.Abs(p); err != nil {
250 return "", err
251 }
252 } else {
253 depPath = filepath.Join(chartpath, p)
254 }
255
256 if _, err = os.Stat(depPath); os.IsNotExist(err) {
257 return "", errors.Errorf("directory %s not found", depPath)
258 } else if err != nil {
259 return "", err
260 }
261
262 return depPath, nil
263 }
264
View as plain text