github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/action/dependency.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package action 18 19 import ( 20 "fmt" 21 "io" 22 "os" 23 "path/filepath" 24 "strings" 25 26 "github.com/Masterminds/semver/v3" 27 "github.com/gosuri/uitable" 28 29 "github.com/stefanmcshane/helm/pkg/chart" 30 "github.com/stefanmcshane/helm/pkg/chart/loader" 31 ) 32 33 // Dependency is the action for building a given chart's dependency tree. 34 // 35 // It provides the implementation of 'helm dependency' and its respective subcommands. 36 type Dependency struct { 37 Verify bool 38 Keyring string 39 SkipRefresh bool 40 ColumnWidth uint 41 } 42 43 // NewDependency creates a new Dependency object with the given configuration. 44 func NewDependency() *Dependency { 45 return &Dependency{ 46 ColumnWidth: 80, 47 } 48 } 49 50 // List executes 'helm dependency list'. 51 func (d *Dependency) List(chartpath string, out io.Writer) error { 52 c, err := loader.Load(chartpath) 53 if err != nil { 54 return err 55 } 56 57 if c.Metadata.Dependencies == nil { 58 fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(chartpath, "charts")) 59 return nil 60 } 61 62 d.printDependencies(chartpath, out, c) 63 fmt.Fprintln(out) 64 d.printMissing(chartpath, out, c.Metadata.Dependencies) 65 return nil 66 } 67 68 // dependencyStatus returns a string describing the status of a dependency viz a viz the parent chart. 69 func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string { 70 filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") 71 72 // If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs. 73 // Technically, this is COMPLETELY unnecessary, and should be removed in Helm 4. It is here 74 // to preserved backward compatibility. In Helm 2/3, there is a "difference" between 75 // the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outputs 76 // "unpacked"). Early in Helm 2's history, this would have made a difference. But it no 77 // longer does. However, since this code shipped with Helm 3, the output must remain stable 78 // until Helm 4. 79 switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); { 80 case err != nil: 81 return "bad pattern" 82 case len(archives) > 1: 83 // See if the second part is a SemVer 84 found := []string{} 85 for _, arc := range archives { 86 // we need to trip the prefix dirs and the extension off. 87 filename = strings.TrimSuffix(filepath.Base(arc), ".tgz") 88 maybeVersion := strings.TrimPrefix(filename, fmt.Sprintf("%s-", dep.Name)) 89 90 if _, err := semver.StrictNewVersion(maybeVersion); err == nil { 91 // If the version parsed without an error, it is possibly a valid 92 // version. 93 found = append(found, arc) 94 } 95 } 96 97 if l := len(found); l == 1 { 98 // If we get here, we do the same thing as in len(archives) == 1. 99 if r := statArchiveForStatus(found[0], dep); r != "" { 100 return r 101 } 102 103 // Fall through and look for directories 104 } else if l > 1 { 105 return "too many matches" 106 } 107 108 // The sanest thing to do here is to fall through and see if we have any directory 109 // matches. 110 111 case len(archives) == 1: 112 archive := archives[0] 113 if r := statArchiveForStatus(archive, dep); r != "" { 114 return r 115 } 116 117 } 118 // End unnecessary code. 119 120 var depChart *chart.Chart 121 for _, item := range parent.Dependencies() { 122 if item.Name() == dep.Name { 123 depChart = item 124 } 125 } 126 127 if depChart == nil { 128 return "missing" 129 } 130 131 if depChart.Metadata.Version != dep.Version { 132 constraint, err := semver.NewConstraint(dep.Version) 133 if err != nil { 134 return "invalid version" 135 } 136 137 v, err := semver.NewVersion(depChart.Metadata.Version) 138 if err != nil { 139 return "invalid version" 140 } 141 142 if !constraint.Check(v) { 143 return "wrong version" 144 } 145 } 146 147 return "unpacked" 148 } 149 150 // stat an archive and return a message if the stat is successful 151 // 152 // This is a refactor of the code originally in dependencyStatus. It is here to 153 // support legacy behavior, and should be removed in Helm 4. 154 func statArchiveForStatus(archive string, dep *chart.Dependency) string { 155 if _, err := os.Stat(archive); err == nil { 156 c, err := loader.Load(archive) 157 if err != nil { 158 return "corrupt" 159 } 160 if c.Name() != dep.Name { 161 return "misnamed" 162 } 163 164 if c.Metadata.Version != dep.Version { 165 constraint, err := semver.NewConstraint(dep.Version) 166 if err != nil { 167 return "invalid version" 168 } 169 170 v, err := semver.NewVersion(c.Metadata.Version) 171 if err != nil { 172 return "invalid version" 173 } 174 175 if !constraint.Check(v) { 176 return "wrong version" 177 } 178 } 179 return "ok" 180 } 181 return "" 182 } 183 184 // printDependencies prints all of the dependencies in the yaml file. 185 func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) { 186 table := uitable.New() 187 table.MaxColWidth = d.ColumnWidth 188 table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") 189 for _, row := range c.Metadata.Dependencies { 190 table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c)) 191 } 192 fmt.Fprintln(out, table) 193 } 194 195 // printMissing prints warnings about charts that are present on disk, but are 196 // not in Chart.yaml. 197 func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency) { 198 folder := filepath.Join(chartpath, "charts/*") 199 files, err := filepath.Glob(folder) 200 if err != nil { 201 fmt.Fprintln(out, err) 202 return 203 } 204 205 for _, f := range files { 206 fi, err := os.Stat(f) 207 if err != nil { 208 fmt.Fprintf(out, "Warning: %s\n", err) 209 } 210 // Skip anything that is not a directory and not a tgz file. 211 if !fi.IsDir() && filepath.Ext(f) != ".tgz" { 212 continue 213 } 214 c, err := loader.Load(f) 215 if err != nil { 216 fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) 217 continue 218 } 219 found := false 220 for _, d := range reqs { 221 if d.Name == c.Name() { 222 found = true 223 break 224 } 225 } 226 if !found { 227 fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f) 228 } 229 } 230 }