github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/internal/resolver/resolver.go (about) 1 /* 2 Copyright The Helm Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 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 "github.com/stefanmcshane/helm/pkg/chart" 31 "github.com/stefanmcshane/helm/pkg/chart/loader" 32 "github.com/stefanmcshane/helm/pkg/helmpath" 33 "github.com/stefanmcshane/helm/pkg/provenance" 34 "github.com/stefanmcshane/helm/pkg/registry" 35 "github.com/stefanmcshane/helm/pkg/repo" 36 ) 37 38 // Resolver resolves dependencies from semantic version ranges to a particular version. 39 type Resolver struct { 40 chartpath string 41 cachepath string 42 registryClient *registry.Client 43 } 44 45 // New creates a new resolver for a given chart, helm home and registry client. 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 // Resolve resolves dependencies and returns a lock file with the resolution. 55 func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { 56 57 // Now we clone the dependencies, locking as we go. 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 // Local chart subfolder 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 // Not a legit entry. 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 // if the repository was not defined, but the dependency defines a repository url, bypass the cache 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 // Check to see if an explicit version has been provided 140 _, err := semver.NewVersion(version) 141 142 // Use an explicit version, otherwise search for tags 143 if err == nil { 144 vs = []*repo.ChartVersion{{ 145 Metadata: &chart.Metadata{ 146 Version: version, 147 }, 148 }} 149 150 } else { 151 // Retrieve list of tags for repository 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 // Mock chart version objects 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 // The version are already sorted and hence the first one to satisfy the constraint is used 177 for _, ver := range vs { 178 v, err := semver.NewVersion(ver.Version) 179 // OCI does not need URLs 180 if err != nil || (!registry.IsOCI(d.Repository) && len(ver.URLs) == 0) { 181 // Not a legit entry. 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 // HashReq generates a hash of the dependencies. 212 // 213 // This should be used only to compare against another hash generated by this 214 // function. 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 // HashV2Req generates a hash of requirements generated in Helm v2. 225 // 226 // This should be used only to compare against another hash generated by the 227 // Helm v2 hash function. It is to handle issue: 228 // https://github.com/helm/helm/issues/7233 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 // GetLocalPath generates absolute local path when use 241 // "file://" in repository of dependencies 242 func GetLocalPath(repo, chartpath string) (string, error) { 243 var depPath string 244 var err error 245 p := strings.TrimPrefix(repo, "file://") 246 247 // root path is absolute 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 }