github.com/Beeketing/helm@v2.12.1+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 lastHyphenIndex := strings.LastIndex(filepath.Base(path), "-") 160 if lastHyphenIndex <= 0 { 161 return linter, fmt.Errorf("unable to parse chart archive %q, missing '-'", filepath.Base(path)) 162 } 163 base := filepath.Base(path)[:lastHyphenIndex] 164 chartPath = filepath.Join(tempDir, base) 165 } else { 166 chartPath = path 167 } 168 169 // Guard: Error out of this is not a chart. 170 if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { 171 return linter, errLintNoChart 172 } 173 174 return lint.All(chartPath, vals, namespace, strict), nil 175 } 176 177 // vals merges values from files specified via -f/--values and 178 // directly via --set or --set-string or --set-file, marshaling them to YAML 179 // 180 // This func is implemented intentionally and separately from the `vals` func for the `install` and `upgrade` comammdsn. 181 // Compared to the alternative func, this func lacks the parameters for tls opts - ca key, cert, and ca cert. 182 // That's because this command, `lint`, is explicitly forbidden from making server connections. 183 func (l *lintCmd) vals() ([]byte, error) { 184 base := map[string]interface{}{} 185 186 // User specified a values files via -f/--values 187 for _, filePath := range l.valueFiles { 188 currentMap := map[string]interface{}{} 189 bytes, err := ioutil.ReadFile(filePath) 190 if err != nil { 191 return []byte{}, err 192 } 193 194 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 195 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 196 } 197 // Merge with the previous map 198 base = mergeValues(base, currentMap) 199 } 200 201 // User specified a value via --set 202 for _, value := range l.values { 203 if err := strvals.ParseInto(value, base); err != nil { 204 return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 205 } 206 } 207 208 // User specified a value via --set-string 209 for _, value := range l.sValues { 210 if err := strvals.ParseIntoString(value, base); err != nil { 211 return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) 212 } 213 } 214 215 // User specified a value via --set-file 216 for _, value := range l.fValues { 217 reader := func(rs []rune) (interface{}, error) { 218 bytes, err := ioutil.ReadFile(string(rs)) 219 return string(bytes), err 220 } 221 if err := strvals.ParseIntoFile(value, base, reader); err != nil { 222 return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err) 223 } 224 } 225 226 return yaml.Marshal(base) 227 }