lint: Add --kube-version flag to set capabilities and deprecation rules

Signed-off-by: Antoine Deschênes <antoine@antoinedeschenes.com>
pull/10677/head
Antoine Deschênes 3 years ago committed by Antoine Deschênes
parent 77d54d7dbe
commit 869c1d2560

@ -104,7 +104,7 @@ test: test-unit
test-unit: test-unit:
@echo @echo
@echo "==> Running unit tests <==" @echo "==> Running unit tests <=="
GO111MODULE=on go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) GO111MODULE=on go test $(GOFLAGS) -ldflags '$(LDFLAGS)' -run $(TESTS) $(PKG) $(TESTFLAGS)
.PHONY: test-coverage .PHONY: test-coverage
test-coverage: test-coverage:

@ -27,6 +27,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/lint/support" "helm.sh/helm/v3/pkg/lint/support"
@ -44,6 +45,7 @@ or recommendation, it will emit [WARNING] messages.
func newLintCmd(out io.Writer) *cobra.Command { func newLintCmd(out io.Writer) *cobra.Command {
client := action.NewLint() client := action.NewLint()
valueOpts := &values.Options{} valueOpts := &values.Options{}
var kubeVersion string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "lint PATH", Use: "lint PATH",
@ -54,6 +56,15 @@ func newLintCmd(out io.Writer) *cobra.Command {
if len(args) > 0 { if len(args) > 0 {
paths = args paths = args
} }
if kubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}
if client.WithSubcharts { if client.WithSubcharts {
for _, p := range paths { for _, p := range paths {
filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error { filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error {
@ -137,6 +148,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings") f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts") f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors") f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for capabilities and deprecation checks")
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
return cmd return cmd

@ -63,6 +63,32 @@ func TestLintCmdWithQuietFlag(t *testing.T) {
} }
func TestLintCmdWithKubeVersionFlag(t *testing.T) {
testChart := "testdata/testcharts/chart-with-deprecated-api"
tests := []cmdTestCase{{
name: "lint chart with deprecated api version using kube version flag",
cmd: fmt.Sprintf("lint --kube-version 1.22.0 %s", testChart),
golden: "output/lint-chart-with-deprecated-api.txt",
wantError: false,
}, {
name: "lint chart with deprecated api version using kube version and strict flag",
cmd: fmt.Sprintf("lint --kube-version 1.22.0 --strict %s", testChart),
golden: "output/lint-chart-with-deprecated-api-strict.txt",
wantError: true,
}, {
name: "lint chart with deprecated api version without kube version",
cmd: fmt.Sprintf("lint %s", testChart),
golden: "output/lint-chart-with-deprecated-api.txt",
wantError: false,
}, {
name: "lint chart with deprecated api version with older kube version",
cmd: fmt.Sprintf("lint --kube-version 1.21.0 --strict %s", testChart),
golden: "output/lint-chart-with-deprecated-api-old-k8s.txt",
wantError: false,
}}
runTestCmd(t, tests)
}
func TestLintFileCompletion(t *testing.T) { func TestLintFileCompletion(t *testing.T) {
checkFileCompletion(t, "lint", true) checkFileCompletion(t, "lint", true)
checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given

@ -0,0 +1,4 @@
==> Linting testdata/testcharts/chart-with-deprecated-api
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed

@ -0,0 +1,5 @@
==> Linting testdata/testcharts/chart-with-deprecated-api
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler
Error: 1 chart(s) linted, 1 chart(s) failed

@ -0,0 +1,5 @@
==> Linting testdata/testcharts/chart-with-deprecated-api
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler
1 chart(s) linted, 0 chart(s) failed

@ -0,0 +1,6 @@
apiVersion: v2
appVersion: "1.0.0"
description: A Helm chart for Kubernetes
name: chart-with-deprecated-api
type: application
version: 1.0.0

@ -0,0 +1,9 @@
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: deprecated
spec:
scaleTargetRef:
kind: Pod
name: pod
maxReplicas: 3

@ -36,6 +36,7 @@ type Lint struct {
Namespace string Namespace string
WithSubcharts bool WithSubcharts bool
Quiet bool Quiet bool
KubeVersion *chartutil.KubeVersion
} }
// LintResult is the result of Lint // LintResult is the result of Lint
@ -58,7 +59,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
} }
result := &LintResult{} result := &LintResult{}
for _, path := range paths { for _, path := range paths {
linter, err := lintChart(path, vals, l.Namespace, l.Strict) linter, err := lintChart(path, vals, l.Namespace, l.KubeVersion)
if err != nil { if err != nil {
result.Errors = append(result.Errors, err) result.Errors = append(result.Errors, err)
continue continue
@ -85,7 +86,7 @@ func HasWarningsOrErrors(result *LintResult) bool {
return len(result.Errors) > 0 return len(result.Errors) > 0
} }
func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) { func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) (support.Linter, error) {
var chartPath string var chartPath string
linter := support.Linter{} linter := support.Linter{}
@ -124,5 +125,5 @@ func lintChart(path string, vals map[string]interface{}, namespace string, stric
return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart") return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart")
} }
return lint.All(chartPath, vals, namespace, strict), nil return lint.AllWithKubeVersion(chartPath, vals, namespace, kubeVersion), nil
} }

@ -23,7 +23,6 @@ import (
var ( var (
values = make(map[string]interface{}) values = make(map[string]interface{})
namespace = "testNamespace" namespace = "testNamespace"
strict = false
chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1" chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1"
chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2" chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2"
corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz" corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz"
@ -78,7 +77,7 @@ func TestLintChart(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, strict) _, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, nil)
switch { switch {
case err != nil && !tt.err: case err != nil && !tt.err:
t.Errorf("%s", err) t.Errorf("%s", err)

@ -19,19 +19,25 @@ package lint // import "helm.sh/helm/v3/pkg/lint"
import ( import (
"path/filepath" "path/filepath"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/rules" "helm.sh/helm/v3/pkg/lint/rules"
"helm.sh/helm/v3/pkg/lint/support" "helm.sh/helm/v3/pkg/lint/support"
) )
// All runs all of the available linters on the given base directory. // All runs all of the available linters on the given base directory.
func All(basedir string, values map[string]interface{}, namespace string, strict bool) support.Linter { func All(basedir string, values map[string]interface{}, namespace string, _ bool) support.Linter {
return AllWithKubeVersion(basedir, values, namespace, nil)
}
// AllWithKubeVersion runs all the available linters on the given base directory, allowing to specify the kubernetes version.
func AllWithKubeVersion(basedir string, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) support.Linter {
// Using abs path to get directory context // Using abs path to get directory context
chartDir, _ := filepath.Abs(basedir) chartDir, _ := filepath.Abs(basedir)
linter := support.Linter{ChartDir: chartDir} linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter) rules.Chartfile(&linter)
rules.ValuesWithOverrides(&linter, values) rules.ValuesWithOverrides(&linter, values)
rules.Templates(&linter, values, namespace, strict) rules.TemplatesWithKubeVersion(&linter, values, namespace, kubeVersion)
rules.Dependencies(&linter) rules.Dependencies(&linter)
return linter return linter
} }

@ -24,6 +24,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/deprecation" "k8s.io/apiserver/pkg/endpoints/deprecation"
kscheme "k8s.io/client-go/kubernetes/scheme" kscheme "k8s.io/client-go/kubernetes/scheme"
"helm.sh/helm/v3/pkg/chartutil"
) )
var ( var (
@ -45,7 +47,7 @@ func (e deprecatedAPIError) Error() string {
return msg return msg
} }
func validateNoDeprecations(resource *K8sYamlStruct) error { func validateNoDeprecations(resource *K8sYamlStruct, kubeVersion *chartutil.KubeVersion) error {
// if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation
if resource.APIVersion == "" { if resource.APIVersion == "" {
return nil return nil
@ -54,6 +56,14 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
return nil return nil
} }
majorVersion := k8sVersionMajor
minorVersion := k8sVersionMinor
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
}
runtimeObject, err := resourceToRuntimeObject(resource) runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil { if err != nil {
// do not error for non-kubernetes resources // do not error for non-kubernetes resources
@ -62,11 +72,12 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
} }
return err return err
} }
maj, err := strconv.Atoi(k8sVersionMajor)
maj, err := strconv.Atoi(majorVersion)
if err != nil { if err != nil {
return err return err
} }
min, err := strconv.Atoi(k8sVersionMinor) min, err := strconv.Atoi(minorVersion)
if err != nil { if err != nil {
return err return err
} }

@ -23,7 +23,7 @@ func TestValidateNoDeprecations(t *testing.T) {
APIVersion: "extensions/v1beta1", APIVersion: "extensions/v1beta1",
Kind: "Deployment", Kind: "Deployment",
} }
err := validateNoDeprecations(deprecated) err := validateNoDeprecations(deprecated, nil)
if err == nil { if err == nil {
t.Fatal("Expected deprecated extension to be flagged") t.Fatal("Expected deprecated extension to be flagged")
} }
@ -35,7 +35,7 @@ func TestValidateNoDeprecations(t *testing.T) {
if err := validateNoDeprecations(&K8sYamlStruct{ if err := validateNoDeprecations(&K8sYamlStruct{
APIVersion: "v1", APIVersion: "v1",
Kind: "Pod", Kind: "Pod",
}); err != nil { }, nil); err != nil {
t.Errorf("Expected a v1 Pod to not be deprecated") t.Errorf("Expected a v1 Pod to not be deprecated")
} }
} }

@ -46,6 +46,11 @@ var (
// Templates lints the templates in the Linter. // Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
TemplatesWithKubeVersion(linter, values, namespace, nil)
}
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) {
fpath := "templates/" fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath) templatesPath := filepath.Join(linter.ChartDir, fpath)
@ -70,6 +75,11 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
Namespace: namespace, Namespace: namespace,
} }
caps := chartutil.DefaultCapabilities.Copy()
if kubeVersion != nil {
caps.KubeVersion = *kubeVersion
}
// lint ignores import-values // lint ignores import-values
// See https://github.com/helm/helm/issues/9658 // See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil { if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil {
@ -80,7 +90,8 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
if err != nil { if err != nil {
return return
} }
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, nil)
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, caps)
if err != nil { if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err) linter.RunLinterRule(support.ErrorSev, fpath, err)
return return
@ -150,7 +161,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
// NOTE: set to warnings to allow users to support out-of-date kubernetes // NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596 // Refs https://github.com/helm/helm/issues/8596
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct)) linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))

Loading…
Cancel
Save