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  }