Skip to content

Commit

Permalink
lint: Add --kube-version flag to set capabilities and deprecation rules
Browse files Browse the repository at this point in the history
Signed-off-by: Antoine Deschênes <antoine@antoinedeschenes.com>
  • Loading branch information
antoinedeschenes committed Feb 15, 2022
1 parent bc2f1e0 commit caee60c
Show file tree
Hide file tree
Showing 16 changed files with 107 additions and 25 deletions.
12 changes: 12 additions & 0 deletions cmd/helm/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/spf13/cobra"

"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/getter"
)
Expand All @@ -43,6 +44,7 @@ or recommendation, it will emit [WARNING] messages.
func newLintCmd(out io.Writer) *cobra.Command {
client := action.NewLint()
valueOpts := &values.Options{}
var kubeVersion string

cmd := &cobra.Command{
Use: "lint PATH",
Expand All @@ -53,6 +55,15 @@ func newLintCmd(out io.Writer) *cobra.Command {
if len(args) > 0 {
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 {
for _, p := range paths {
filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error {
Expand Down Expand Up @@ -120,6 +131,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for capabilities and deprecation checks")
addValueOptionsFlags(f, valueOpts)

return cmd
Expand Down
26 changes: 26 additions & 0 deletions cmd/helm/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,32 @@ func TestLintCmdWithSubchartsFlag(t *testing.T) {
runTestCmd(t, tests)
}

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) {
checkFileCompletion(t, "lint", true)
checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions cmd/helm/testdata/output/lint-chart-with-deprecated-api.txt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: deprecated
spec:
scaleTargetRef:
kind: Pod
name: pod
maxReplicas: 3
Empty file.
7 changes: 4 additions & 3 deletions pkg/action/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Lint struct {
Strict bool
Namespace string
WithSubcharts bool
KubeVersion *chartutil.KubeVersion
}

// LintResult is the result of Lint
Expand All @@ -58,7 +59,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
}
result := &LintResult{}
for _, path := range paths {
linter, err := lintChart(path, vals, l.Namespace, l.Strict)
linter, err := lintChart(path, vals, l.Namespace, l.Strict, l.KubeVersion)
if err != nil {
result.Errors = append(result.Errors, err)
continue
Expand All @@ -75,7 +76,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
return result
}

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, strict bool, kubeVersion *chartutil.KubeVersion) (support.Linter, error) {
var chartPath string
linter := support.Linter{}

Expand Down Expand Up @@ -114,5 +115,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 lint.All(chartPath, vals, namespace, strict), nil
return lint.All(chartPath, vals, namespace, strict, kubeVersion), nil
}
2 changes: 1 addition & 1 deletion pkg/action/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func TestLintChart(t *testing.T) {

for _, tt := range tests {
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, strict, nil)
switch {
case err != nil && !tt.err:
t.Errorf("%s", err)
Expand Down
5 changes: 3 additions & 2 deletions pkg/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@ package lint // import "helm.sh/helm/v3/pkg/lint"
import (
"path/filepath"

"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/rules"
"helm.sh/helm/v3/pkg/lint/support"
)

// 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, strict bool, kubeVersion *chartutil.KubeVersion) support.Linter {
// Using abs path to get directory context
chartDir, _ := filepath.Abs(basedir)

linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.ValuesWithOverrides(&linter, values)
rules.Templates(&linter, values, namespace, strict)
rules.Templates(&linter, values, namespace, strict, kubeVersion)
rules.Dependencies(&linter)
return linter
}
12 changes: 6 additions & 6 deletions pkg/lint/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const goodChartDir = "rules/testdata/goodone"
const subChartValuesDir = "rules/testdata/withsubchart"

func TestBadChart(t *testing.T) {
m := All(badChartDir, values, namespace, strict).Messages
m := All(badChartDir, values, namespace, strict, nil).Messages
if len(m) != 8 {
t.Errorf("Number of errors %v", len(m))
t.Errorf("All didn't fail with expected errors, got %#v", m)
Expand Down Expand Up @@ -87,7 +87,7 @@ func TestBadChart(t *testing.T) {
}

func TestInvalidYaml(t *testing.T) {
m := All(badYamlFileDir, values, namespace, strict).Messages
m := All(badYamlFileDir, values, namespace, strict, nil).Messages
if len(m) != 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
Expand All @@ -97,7 +97,7 @@ func TestInvalidYaml(t *testing.T) {
}

func TestBadValues(t *testing.T) {
m := All(badValuesFileDir, values, namespace, strict).Messages
m := All(badValuesFileDir, values, namespace, strict, nil).Messages
if len(m) < 1 {
t.Fatalf("All didn't fail with expected errors, got %#v", m)
}
Expand All @@ -107,7 +107,7 @@ func TestBadValues(t *testing.T) {
}

func TestGoodChart(t *testing.T) {
m := All(goodChartDir, values, namespace, strict).Messages
m := All(goodChartDir, values, namespace, strict, nil).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
for i, msg := range m {
Expand Down Expand Up @@ -135,7 +135,7 @@ func TestHelmCreateChart(t *testing.T) {

// Note: we test with strict=true here, even though others have
// strict = false.
m := All(createdChart, values, namespace, true).Messages
m := All(createdChart, values, namespace, true, nil).Messages
if ll := len(m); ll != 1 {
t.Errorf("All should have had exactly 1 error. Got %d", ll)
for i, msg := range m {
Expand All @@ -149,7 +149,7 @@ func TestHelmCreateChart(t *testing.T) {
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
func TestSubChartValuesChart(t *testing.T) {
m := All(subChartValuesDir, values, namespace, strict).Messages
m := All(subChartValuesDir, values, namespace, strict, nil).Messages
if len(m) != 0 {
t.Error("All returned linter messages when it shouldn't have")
for i, msg := range m {
Expand Down
9 changes: 8 additions & 1 deletion pkg/lint/rules/deprecations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package rules // import "helm.sh/helm/v3/pkg/lint/rules"

import (
"fmt"
"helm.sh/helm/v3/pkg/chartutil"
"strconv"

"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -45,7 +46,7 @@ func (e deprecatedAPIError) Error() string {
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.APIVersion == "" {
return nil
Expand All @@ -54,6 +55,11 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
return nil
}

if kubeVersion != nil {
k8sVersionMajor = kubeVersion.Major
k8sVersionMinor = kubeVersion.Minor
}

runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil {
// do not error for non-kubernetes resources
Expand All @@ -62,6 +68,7 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
}
return err
}

maj, err := strconv.Atoi(k8sVersionMajor)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions pkg/lint/rules/deprecations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestValidateNoDeprecations(t *testing.T) {
APIVersion: "extensions/v1beta1",
Kind: "Deployment",
}
err := validateNoDeprecations(deprecated)
err := validateNoDeprecations(deprecated, nil)
if err == nil {
t.Fatal("Expected deprecated extension to be flagged")
}
Expand All @@ -35,7 +35,7 @@ func TestValidateNoDeprecations(t *testing.T) {
if err := validateNoDeprecations(&K8sYamlStruct{
APIVersion: "v1",
Kind: "Pod",
}); err != nil {
}, nil); err != nil {
t.Errorf("Expected a v1 Pod to not be deprecated")
}
}
12 changes: 9 additions & 3 deletions pkg/lint/rules/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var (
)

// Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) {
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool, kubeVersion *chartutil.KubeVersion) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)

Expand All @@ -70,6 +70,11 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
Namespace: namespace,
}

caps := chartutil.DefaultCapabilities.Copy()
if kubeVersion != nil {
caps.KubeVersion = *kubeVersion
}

// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependencies(chart, values); err != nil {
Expand All @@ -80,7 +85,8 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
if err != nil {
return
}
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, nil)

valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, caps)
if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err)
return
Expand Down Expand Up @@ -149,7 +155,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
// NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596
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, validateListAnnotations(yamlStruct, renderedContent))
Expand Down
14 changes: 7 additions & 7 deletions pkg/lint/rules/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const strict = false

func TestTemplateParsing(t *testing.T) {
linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter, values, namespace, strict)
Templates(&linter, values, namespace, strict, nil)
res := linter.Messages

if len(res) != 1 {
Expand All @@ -78,7 +78,7 @@ func TestTemplateIntegrationHappyPath(t *testing.T) {
defer os.Rename(ignoredTemplatePath, wrongTemplatePath)

linter := support.Linter{ChartDir: templateTestBasedir}
Templates(&linter, values, namespace, strict)
Templates(&linter, values, namespace, strict, nil)
res := linter.Messages

if len(res) != 0 {
Expand All @@ -88,7 +88,7 @@ func TestTemplateIntegrationHappyPath(t *testing.T) {

func TestV3Fail(t *testing.T) {
linter := support.Linter{ChartDir: "./testdata/v3-fail"}
Templates(&linter, values, namespace, strict)
Templates(&linter, values, namespace, strict, nil)
res := linter.Messages

if len(res) != 3 {
Expand All @@ -108,7 +108,7 @@ func TestV3Fail(t *testing.T) {

func TestMultiTemplateFail(t *testing.T) {
linter := support.Linter{ChartDir: "./testdata/multi-template-fail"}
Templates(&linter, values, namespace, strict)
Templates(&linter, values, namespace, strict, nil)
res := linter.Messages

if len(res) != 1 {
Expand Down Expand Up @@ -229,7 +229,7 @@ func TestDeprecatedAPIFails(t *testing.T) {
}

linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
Templates(&linter, values, namespace, strict)
Templates(&linter, values, namespace, strict, nil)
if l := len(linter.Messages); l != 1 {
for i, msg := range linter.Messages {
t.Logf("Message %d: %s", i, msg)
Expand Down Expand Up @@ -286,7 +286,7 @@ func TestStrictTemplateParsingMapError(t *testing.T) {
linter := &support.Linter{
ChartDir: filepath.Join(dir, ch.Metadata.Name),
}
Templates(linter, ch.Values, namespace, strict)
Templates(linter, ch.Values, namespace, strict, nil)
if len(linter.Messages) != 0 {
t.Errorf("expected zero messages, got %d", len(linter.Messages))
for i, msg := range linter.Messages {
Expand Down Expand Up @@ -416,7 +416,7 @@ func TestEmptyWithCommentsManifests(t *testing.T) {
}

linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())}
Templates(&linter, values, namespace, strict)
Templates(&linter, values, namespace, strict, nil)
if l := len(linter.Messages); l > 0 {
for i, msg := range linter.Messages {
t.Logf("Message %d: %s", i, msg)
Expand Down

0 comments on commit caee60c

Please sign in to comment.