github.com/latiif/helm@v2.15.0+incompatible/cmd/helm/lint.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 main 18 19 import ( 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/ghodss/yaml" 29 "github.com/spf13/cobra" 30 31 "k8s.io/helm/pkg/chartutil" 32 "k8s.io/helm/pkg/lint" 33 "k8s.io/helm/pkg/lint/support" 34 "k8s.io/helm/pkg/strvals" 35 ) 36 37 var longLintHelp = ` 38 This command takes a path to a chart and runs a series of tests to verify that 39 the chart is well-formed. 40 41 If the linter encounters things that will cause the chart to fail installation, 42 it will emit [ERROR] messages. If it encounters issues that break with convention 43 or recommendation, it will emit [WARNING] messages. 44 ` 45 46 type lintCmd struct { 47 valueFiles valueFiles 48 values []string 49 sValues []string 50 fValues []string 51 namespace string 52 strict bool 53 paths []string 54 out io.Writer 55 } 56 57 func newLintCmd(out io.Writer) *cobra.Command { 58 l := &lintCmd{ 59 paths: []string{"."}, 60 out: out, 61 } 62 cmd := &cobra.Command{ 63 Use: "lint [flags] PATH", 64 Short: "Examines a chart for possible issues", 65 Long: longLintHelp, 66 RunE: func(cmd *cobra.Command, args []string) error { 67 if len(args) > 0 { 68 l.paths = args 69 } 70 return l.run() 71 }, 72 } 73 74 cmd.Flags().VarP(&l.valueFiles, "values", "f", "Specify values in a YAML file (can specify multiple)") 75 cmd.Flags().StringArrayVar(&l.values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 76 cmd.Flags().StringArrayVar(&l.sValues, "set-string", []string{}, "Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 77 cmd.Flags().StringArrayVar(&l.fValues, "set-file", []string{}, "Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") 78 cmd.Flags().StringVar(&l.namespace, "namespace", "default", "Namespace to put the release into") 79 cmd.Flags().BoolVar(&l.strict, "strict", false, "Fail on lint warnings") 80 81 return cmd 82 } 83 84 var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") 85 86 func (l *lintCmd) run() error { 87 var lowestTolerance int 88 if l.strict { 89 lowestTolerance = support.WarningSev 90 } else { 91 lowestTolerance = support.ErrorSev 92 } 93 94 // Get the raw values 95 rvals, err := l.vals() 96 if err != nil { 97 return err 98 } 99 100 var total int 101 var failures int 102 for _, path := range l.paths { 103 if linter, err := lintChart(path, rvals, l.namespace, l.strict); err != nil { 104 fmt.Println("==> Skipping", path) 105 fmt.Println(err) 106 if err == errLintNoChart { 107 failures = failures + 1 108 } 109 } else { 110 fmt.Println("==> Linting", path) 111 112 if len(linter.Messages) == 0 { 113 fmt.Println("Lint OK") 114 } 115 116 for _, msg := range linter.Messages { 117 fmt.Println(msg) 118 } 119 120 total = total + 1 121 if linter.HighestSeverity >= lowestTolerance { 122 failures = failures + 1 123 } 124 } 125 fmt.Println("") 126 } 127 128 msg := fmt.Sprintf("%d chart(s) linted", total) 129 if failures > 0 { 130 return fmt.Errorf("%s, %d chart(s) failed", msg, failures) 131 } 132 133 fmt.Fprintf(l.out, "%s, no failures\n", msg) 134 135 return nil 136 } 137 138 func lintChart(path string, vals []byte, namespace string, strict bool) (support.Linter, error) { 139 var chartPath string 140 linter := support.Linter{} 141 142 if strings.HasSuffix(path, ".tgz") { 143 tempDir, err := ioutil.TempDir("", "helm-lint") 144 if err != nil { 145 return linter, err 146 } 147 defer os.RemoveAll(tempDir) 148 149 file, err := os.Open(path) 150 if err != nil { 151 return linter, err 152 } 153 defer file.Close() 154 155 if err = chartutil.Expand(tempDir, file); err != nil { 156 return linter, err 157 } 158 159 files, err := ioutil.ReadDir(tempDir) 160 if err != nil { 161 return linter, fmt.Errorf("unable to read temporary output directory %s", tempDir) 162 } 163 if !files[0].IsDir() { 164 return linter, fmt.Errorf("unexpected file %s in temporary output directory %s", files[0].Name(), tempDir) 165 } 166 167 chartPath = filepath.Join(tempDir, files[0].Name()) 168 } else { 169 chartPath = path 170 } 171 172 // Guard: Error out if this is not a chart. 173 if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { 174 return linter, errLintNoChart 175 } 176 177 return lint.All(chartPath, vals, namespace, strict), nil 178 } 179 180 // vals merges values from files specified via -f/--values and 181 // directly via --set or --set-string or --set-file, marshaling them to YAML 182 // 183 // This func is implemented intentionally and separately from the `vals` func for the `install` and `upgrade` commands. 184 // Compared to the alternative func, this func lacks the parameters for tls opts - ca key, cert, and ca cert. 185 // That's because this command, `lint`, is explicitly forbidden from making server connections. 186 func (l *lintCmd) vals() ([]byte, error) { 187 base := map[string]interface{}{} 188 189 // User specified a values files via -f/--values 190 for _, filePath := range l.valueFiles { 191 currentMap := map[string]interface{}{} 192 bytes, err := ioutil.ReadFile(filePath) 193 if err != nil { 194 return []byte{}, err 195 } 196 197 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 198 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 199 } 200 // Merge with the previous map 201 base = mergeValues(base, currentMap) 202 } 203 204 // User specified a value via --set 205 for _, value := range l.values { 206 if err := strvals.ParseInto(value, base); err != nil { 207 return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 208 } 209 } 210 211 // User specified a value via --set-string 212 for _, value := range l.sValues { 213 if err := strvals.ParseIntoString(value, base); err != nil { 214 return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) 215 } 216 } 217 218 // User specified a value via --set-file 219 for _, value := range l.fValues { 220 reader := func(rs []rune) (interface{}, error) { 221 bytes, err := ioutil.ReadFile(string(rs)) 222 return string(bytes), err 223 } 224 if err := strvals.ParseIntoFile(value, base, reader); err != nil { 225 return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err) 226 } 227 } 228 229 return yaml.Marshal(base) 230 }