github.com/koderover/helm@v2.17.0+incompatible/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 "k8s.io/helm/pkg/lint/rules"
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/Masterminds/semver"
    27  
    28  	"github.com/asaskevich/govalidator"
    29  	"k8s.io/helm/pkg/chartutil"
    30  	"k8s.io/helm/pkg/lint/support"
    31  	"k8s.io/helm/pkg/proto/hapi/chart"
    32  )
    33  
    34  // Chartfile runs a set of linter rules related to Chart.yaml file
    35  func Chartfile(linter *support.Linter) {
    36  	chartFileName := "Chart.yaml"
    37  	chartPath := filepath.Join(linter.ChartDir, chartFileName)
    38  
    39  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
    40  
    41  	chartFile, err := chartutil.LoadChartfile(chartPath)
    42  	validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
    43  
    44  	// Guard clause. Following linter rules require a parseable ChartFile
    45  	if !validChartFile {
    46  		return
    47  	}
    48  
    49  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNamePresence(chartFile))
    50  	linter.RunLinterRule(support.WarningSev, chartFileName, validateChartNameFormat(chartFile))
    51  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNameDirMatch(linter.ChartDir, chartFile))
    52  
    53  	// Chart metadata
    54  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile))
    55  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
    56  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartEngine(chartFile))
    57  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
    58  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
    59  	linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile))
    60  	linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile))
    61  }
    62  
    63  func validateChartYamlNotDirectory(chartPath string) error {
    64  	fi, err := os.Stat(chartPath)
    65  
    66  	if err == nil && fi.IsDir() {
    67  		return errors.New("should be a file, not a directory")
    68  	}
    69  	return nil
    70  }
    71  
    72  func validateChartYamlFormat(chartFileError error) error {
    73  	if chartFileError != nil {
    74  		return fmt.Errorf("unable to parse YAML\n\t%s", chartFileError.Error())
    75  	}
    76  	return nil
    77  }
    78  
    79  func validateChartNamePresence(cf *chart.Metadata) error {
    80  	if cf.Name == "" {
    81  		return errors.New("name is required")
    82  	}
    83  	return nil
    84  }
    85  
    86  func validateChartNameFormat(cf *chart.Metadata) error {
    87  	if strings.Contains(cf.Name, ".") {
    88  		return errors.New("name should be lower case letters and numbers. Words may be separated with dashes")
    89  	}
    90  	return nil
    91  }
    92  
    93  func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) error {
    94  	if cf.Name != filepath.Base(chartDir) {
    95  		return fmt.Errorf("directory name (%s) and chart name (%s) must be the same", filepath.Base(chartDir), cf.Name)
    96  	}
    97  	return nil
    98  }
    99  
   100  func validateChartAPIVersion(cf *chart.Metadata) error {
   101  	if cf.ApiVersion == "" {
   102  		return errors.New("apiVersion is required")
   103  	}
   104  
   105  	if cf.ApiVersion != "v1" {
   106  		return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v1\"", cf.ApiVersion)
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  func validateChartVersion(cf *chart.Metadata) error {
   113  	if cf.Version == "" {
   114  		return errors.New("version is required")
   115  	}
   116  
   117  	version, err := semver.NewVersion(cf.Version)
   118  
   119  	if err != nil {
   120  		return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version)
   121  	}
   122  
   123  	c, err := semver.NewConstraint(">0.0.0-0")
   124  	if err != nil {
   125  		return err
   126  	}
   127  	valid, msg := c.Validate(version)
   128  
   129  	if !valid && len(msg) > 0 {
   130  		return fmt.Errorf("version %v", msg[0])
   131  	}
   132  
   133  	return nil
   134  }
   135  
   136  func validateChartEngine(cf *chart.Metadata) error {
   137  	if cf.Engine == "" {
   138  		return nil
   139  	}
   140  
   141  	keys := make([]string, 0, len(chart.Metadata_Engine_value))
   142  	for engine := range chart.Metadata_Engine_value {
   143  		str := strings.ToLower(engine)
   144  
   145  		if str == "unknown" {
   146  			continue
   147  		}
   148  
   149  		if str == cf.Engine {
   150  			return nil
   151  		}
   152  
   153  		keys = append(keys, str)
   154  	}
   155  
   156  	return fmt.Errorf("engine '%v' not valid. Valid options are %v", cf.Engine, keys)
   157  }
   158  
   159  func validateChartMaintainer(cf *chart.Metadata) error {
   160  	for _, maintainer := range cf.Maintainers {
   161  		if maintainer.Name == "" {
   162  			return errors.New("each maintainer requires a name")
   163  		} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
   164  			return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
   165  		} else if maintainer.Url != "" && !govalidator.IsURL(maintainer.Url) {
   166  			return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.Url, maintainer.Name)
   167  		}
   168  	}
   169  	return nil
   170  }
   171  
   172  func validateChartSources(cf *chart.Metadata) error {
   173  	for _, source := range cf.Sources {
   174  		if source == "" || !govalidator.IsRequestURL(source) {
   175  			return fmt.Errorf("invalid source URL '%s'", source)
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  func validateChartIconPresence(cf *chart.Metadata) error {
   182  	if cf.Icon == "" {
   183  		return errors.New("icon is recommended")
   184  	}
   185  	return nil
   186  }
   187  
   188  func validateChartIconURL(cf *chart.Metadata) error {
   189  	if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) {
   190  		return fmt.Errorf("invalid icon URL '%s'", cf.Icon)
   191  	}
   192  	return nil
   193  }