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