github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/lint/rules/chartfile.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 rules // import "github.com/stefanmcshane/helm/pkg/lint/rules" 18 19 import ( 20 "fmt" 21 "io/ioutil" 22 "os" 23 "path/filepath" 24 25 "github.com/Masterminds/semver/v3" 26 "github.com/asaskevich/govalidator" 27 "github.com/pkg/errors" 28 "sigs.k8s.io/yaml" 29 30 "github.com/stefanmcshane/helm/pkg/chart" 31 "github.com/stefanmcshane/helm/pkg/chartutil" 32 "github.com/stefanmcshane/helm/pkg/lint/support" 33 ) 34 35 // Chartfile runs a set of linter rules related to Chart.yaml file 36 func Chartfile(linter *support.Linter) { 37 chartFileName := "Chart.yaml" 38 chartPath := filepath.Join(linter.ChartDir, chartFileName) 39 40 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) 41 42 chartFile, err := chartutil.LoadChartfile(chartPath) 43 validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) 44 45 // Guard clause. Following linter rules require a parsable ChartFile 46 if !validChartFile { 47 return 48 } 49 50 // type check for Chart.yaml . ignoring error as any parse 51 // errors would already be caught in the above load function 52 chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) 53 54 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) 55 56 // Chart metadata 57 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) 58 59 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) 60 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) 61 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) 62 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) 63 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) 64 linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) 65 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) 66 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) 67 linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) 68 } 69 70 func validateChartVersionType(data map[string]interface{}) error { 71 return isStringValue(data, "version") 72 } 73 74 func validateChartAppVersionType(data map[string]interface{}) error { 75 return isStringValue(data, "appVersion") 76 } 77 78 func isStringValue(data map[string]interface{}, key string) error { 79 value, ok := data[key] 80 if !ok { 81 return nil 82 } 83 valueType := fmt.Sprintf("%T", value) 84 if valueType != "string" { 85 return errors.Errorf("%s should be of type string but it's of type %s", key, valueType) 86 } 87 return nil 88 } 89 90 func validateChartYamlNotDirectory(chartPath string) error { 91 fi, err := os.Stat(chartPath) 92 93 if err == nil && fi.IsDir() { 94 return errors.New("should be a file, not a directory") 95 } 96 return nil 97 } 98 99 func validateChartYamlFormat(chartFileError error) error { 100 if chartFileError != nil { 101 return errors.Errorf("unable to parse YAML\n\t%s", chartFileError.Error()) 102 } 103 return nil 104 } 105 106 func validateChartName(cf *chart.Metadata) error { 107 if cf.Name == "" { 108 return errors.New("name is required") 109 } 110 return nil 111 } 112 113 func validateChartAPIVersion(cf *chart.Metadata) error { 114 if cf.APIVersion == "" { 115 return errors.New("apiVersion is required. The value must be either \"v1\" or \"v2\"") 116 } 117 118 if cf.APIVersion != chart.APIVersionV1 && cf.APIVersion != chart.APIVersionV2 { 119 return fmt.Errorf("apiVersion '%s' is not valid. The value must be either \"v1\" or \"v2\"", cf.APIVersion) 120 } 121 122 return nil 123 } 124 125 func validateChartVersion(cf *chart.Metadata) error { 126 if cf.Version == "" { 127 return errors.New("version is required") 128 } 129 130 version, err := semver.NewVersion(cf.Version) 131 132 if err != nil { 133 return errors.Errorf("version '%s' is not a valid SemVer", cf.Version) 134 } 135 136 c, err := semver.NewConstraint(">0.0.0-0") 137 if err != nil { 138 return err 139 } 140 valid, msg := c.Validate(version) 141 142 if !valid && len(msg) > 0 { 143 return errors.Errorf("version %v", msg[0]) 144 } 145 146 return nil 147 } 148 149 func validateChartMaintainer(cf *chart.Metadata) error { 150 for _, maintainer := range cf.Maintainers { 151 if maintainer.Name == "" { 152 return errors.New("each maintainer requires a name") 153 } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { 154 return errors.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) 155 } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { 156 return errors.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) 157 } 158 } 159 return nil 160 } 161 162 func validateChartSources(cf *chart.Metadata) error { 163 for _, source := range cf.Sources { 164 if source == "" || !govalidator.IsRequestURL(source) { 165 return errors.Errorf("invalid source URL '%s'", source) 166 } 167 } 168 return nil 169 } 170 171 func validateChartIconPresence(cf *chart.Metadata) error { 172 if cf.Icon == "" { 173 return errors.New("icon is recommended") 174 } 175 return nil 176 } 177 178 func validateChartIconURL(cf *chart.Metadata) error { 179 if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { 180 return errors.Errorf("invalid icon URL '%s'", cf.Icon) 181 } 182 return nil 183 } 184 185 func validateChartDependencies(cf *chart.Metadata) error { 186 if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV2 { 187 return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) 188 } 189 return nil 190 } 191 192 func validateChartType(cf *chart.Metadata) error { 193 if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV2 { 194 return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) 195 } 196 return nil 197 } 198 199 // loadChartFileForTypeCheck loads the Chart.yaml 200 // in a generic form of a map[string]interface{}, so that the type 201 // of the values can be checked 202 func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { 203 b, err := ioutil.ReadFile(filename) 204 if err != nil { 205 return nil, err 206 } 207 y := make(map[string]interface{}) 208 err = yaml.Unmarshal(b, &y) 209 return y, err 210 }