Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"
"helm.sh/helm/v3/pkg/lint/support"
Expand All @@ -44,6 +45,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 @@ -54,6 +56,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 @@ -137,6 +148,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
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)

return cmd
Expand Down
28 changes: 28 additions & 0 deletions cmd/helm/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,34 @@ 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,
}, {
// the test builds will use the default k8sVersionMinor const in deprecations.go and capabilities.go
// which is "20"
name: "lint chart with deprecated api version without kube version",
cmd: fmt.Sprintf("lint %s", testChart),
golden: "output/lint-chart-with-deprecated-api-old-k8s.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 {
Namespace string
WithSubcharts bool
Quiet 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.KubeVersion)
if err != nil {
result.Errors = append(result.Errors, err)
continue
Expand All @@ -85,7 +86,7 @@ func HasWarningsOrErrors(result *LintResult) bool {
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
linter := support.Linter{}

Expand Down Expand Up @@ -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 lint.All(chartPath, vals, namespace, strict), nil
return lint.AllWithKubeVersion(chartPath, vals, namespace, kubeVersion), nil
}
3 changes: 1 addition & 2 deletions pkg/action/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
var (
values = make(map[string]interface{})
namespace = "testNamespace"
strict = false
chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1"
chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2"
corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz"
Expand Down Expand Up @@ -78,7 +77,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, nil)
switch {
case err != nil && !tt.err:
t.Errorf("%s", err)
Expand Down
10 changes: 8 additions & 2 deletions pkg/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,25 @@ 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, _ 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
chartDir, _ := filepath.Abs(basedir)

linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.ValuesWithOverrides(&linter, values)
rules.Templates(&linter, values, namespace, strict)
rules.TemplatesWithKubeVersion(&linter, values, namespace, kubeVersion)
rules.Dependencies(&linter)
return linter
}
17 changes: 14 additions & 3 deletions pkg/lint/rules/deprecations.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/deprecation"
kscheme "k8s.io/client-go/kubernetes/scheme"

"helm.sh/helm/v3/pkg/chartutil"
)

var (
Expand All @@ -45,7 +47,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 +56,14 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
return nil
}

majorVersion := k8sVersionMajor
minorVersion := k8sVersionMinor

if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
}

runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil {
// do not error for non-kubernetes resources
Expand All @@ -62,11 +72,12 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
}
return err
}
maj, err := strconv.Atoi(k8sVersionMajor)

maj, err := strconv.Atoi(majorVersion)
if err != nil {
return err
}
min, err := strconv.Atoi(k8sVersionMinor)
min, err := strconv.Atoi(minorVersion)
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")
}
}
15 changes: 13 additions & 2 deletions pkg/lint/rules/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ var (

// Templates lints the templates in the Linter.
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/"
templatesPath := filepath.Join(linter.ChartDir, fpath)

Expand All @@ -70,6 +75,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.ProcessDependenciesWithMerge(chart, values); err != nil {
Expand All @@ -80,7 +90,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 @@ -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
// 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