From 21b9aa3d942bc96df9be7cd8996b5730c49b0ccb Mon Sep 17 00:00:00 2001 From: Zach Burgess Date: Thu, 26 Jun 2025 14:07:54 -0700 Subject: [PATCH] Lint the `crds/` directory. This checks that the `crds/` dir only contains YAML files that define K8s resources with `kind: CustomResourceDefinition`. Checking that the YAML files are not templates will be done in a separate commit. Signed-off-by: Zach Burgess --- pkg/lint/lint.go | 1 + pkg/lint/lint_test.go | 11 +++ pkg/lint/rules/crds.go | 96 +++++++++++++++++++ pkg/lint/rules/crds_test.go | 51 ++++++++++ pkg/lint/rules/testdata/badcrdfile/Chart.yaml | 6 ++ .../testdata/badcrdfile/crds/bad-crd.yaml | 2 + .../rules/testdata/badcrdfile/values.yaml | 1 + .../rules/testdata/invalidcrdsdir/Chart.yaml | 6 ++ pkg/lint/rules/testdata/invalidcrdsdir/crds | 0 .../rules/testdata/invalidcrdsdir/values.yaml | 1 + pkg/lint/rules/testdata/withcrd/Chart.yaml | 5 + .../rules/testdata/withcrd/crds/test-crd.yaml | 19 ++++ 12 files changed, 199 insertions(+) create mode 100644 pkg/lint/rules/crds.go create mode 100644 pkg/lint/rules/crds_test.go create mode 100644 pkg/lint/rules/testdata/badcrdfile/Chart.yaml create mode 100644 pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml create mode 100644 pkg/lint/rules/testdata/badcrdfile/values.yaml create mode 100644 pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml create mode 100644 pkg/lint/rules/testdata/invalidcrdsdir/crds create mode 100644 pkg/lint/rules/testdata/invalidcrdsdir/values.yaml create mode 100644 pkg/lint/rules/testdata/withcrd/Chart.yaml create mode 100644 pkg/lint/rules/testdata/withcrd/crds/test-crd.yaml diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index a61d5e43f..64b2a6057 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -60,6 +60,7 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt rules.ValuesWithOverrides(&result, values) rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) rules.Dependencies(&result) + rules.Crds(&result) return result } diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index 888d3dfe6..45e24f533 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -32,6 +32,7 @@ const namespace = "testNamespace" const badChartDir = "rules/testdata/badchartfile" const badValuesFileDir = "rules/testdata/badvaluesfile" const badYamlFileDir = "rules/testdata/albatross" +const badCrdFileDir = "rules/testdata/badcrdfile" const goodChartDir = "rules/testdata/goodone" const subChartValuesDir = "rules/testdata/withsubchart" const malformedTemplate = "rules/testdata/malformed-template" @@ -111,6 +112,16 @@ func TestBadValues(t *testing.T) { } } +func TestBadCrdFile(t *testing.T) { + m := RunAll(badCrdFileDir, values, namespace).Messages + if len(m) < 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "object kind is not 'CustomResourceDefinition'") { + t.Errorf("All didn't have the error for invalid CRD: %s", m[0].Err) + } +} + func TestGoodChart(t *testing.T) { m := RunAll(goodChartDir, values, namespace).Messages if len(m) != 0 { diff --git a/pkg/lint/rules/crds.go b/pkg/lint/rules/crds.go new file mode 100644 index 000000000..bba06ddc3 --- /dev/null +++ b/pkg/lint/rules/crds.go @@ -0,0 +1,96 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "k8s.io/apimachinery/pkg/util/yaml" + + "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v4/pkg/lint/support" +) + +// Crds lints the CRDs in the Linter. +func Crds(linter *support.Linter) { + fpath := "crds/" + crdsPath := filepath.Join(linter.ChartDir, fpath) + + crdsDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateCrdsDir(crdsPath)) + + // crds directory is optional + if !crdsDirExist { + return + } + + // Load chart and parse CRDs + chart, err := loader.Load(linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + /* Iterate over all the CRDs to check: + - It is a YAML file + - The kind is CustomResourceDefinition + */ + for _, crd := range chart.CRDObjects() { + fileName := crd.Name + fpath = fileName + + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096) + for { + var yamlStruct *K8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if err == io.EOF { + break + } + + // If YAML linting fails here, it will always fail in the next block as well, so we should return here. + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct)) + } + } +} + +// Validation functions +func validateCrdsDir(crdsPath string) error { + if fi, err := os.Stat(crdsPath); err == nil { + if !fi.IsDir() { + return errors.New("not a directory") + } + } + return nil +} + +func validateCrdKind(obj *K8sYamlStruct) error { + if obj.Kind != "CustomResourceDefinition" { + return fmt.Errorf("object kind is not 'CustomResourceDefinition'") + } + return nil +} diff --git a/pkg/lint/rules/crds_test.go b/pkg/lint/rules/crds_test.go new file mode 100644 index 000000000..52432a130 --- /dev/null +++ b/pkg/lint/rules/crds_test.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "strings" + "testing" + + "helm.sh/helm/v4/pkg/lint/support" +) + +const crdsTestBasedir = "./testdata/withcrd" +const invalidCrdsDir = "./testdata/invalidcrdsdir" + +func TestCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: crdsTestBasedir} + Crds(&linter) + res := linter.Messages + + if len(res) > 0 { + t.Fatalf("Expected no errors, got %d, %v", len(res), res) + } +} + +func TestInvalidCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: invalidCrdsDir} + Crds(&linter) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected one error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "not a directory") { + t.Errorf("Unexpected error: %s", res[0]) + } +} diff --git a/pkg/lint/rules/testdata/badcrdfile/Chart.yaml b/pkg/lint/rules/testdata/badcrdfile/Chart.yaml new file mode 100644 index 000000000..08c4b61ac --- /dev/null +++ b/pkg/lint/rules/testdata/badcrdfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: badcrdfile +type: application +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml new file mode 100644 index 000000000..523b97f85 --- /dev/null +++ b/pkg/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml @@ -0,0 +1,2 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: NotACustomResourceDefinition diff --git a/pkg/lint/rules/testdata/badcrdfile/values.yaml b/pkg/lint/rules/testdata/badcrdfile/values.yaml new file mode 100644 index 000000000..2fffc7715 --- /dev/null +++ b/pkg/lint/rules/testdata/badcrdfile/values.yaml @@ -0,0 +1 @@ +# Default values for badcrdfile. diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml new file mode 100644 index 000000000..18e30f70f --- /dev/null +++ b/pkg/lint/rules/testdata/invalidcrdsdir/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: invalidcrdsdir +type: application +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/crds b/pkg/lint/rules/testdata/invalidcrdsdir/crds new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/lint/rules/testdata/invalidcrdsdir/values.yaml b/pkg/lint/rules/testdata/invalidcrdsdir/values.yaml new file mode 100644 index 000000000..2fffc7715 --- /dev/null +++ b/pkg/lint/rules/testdata/invalidcrdsdir/values.yaml @@ -0,0 +1 @@ +# Default values for badcrdfile. diff --git a/pkg/lint/rules/testdata/withcrd/Chart.yaml b/pkg/lint/rules/testdata/withcrd/Chart.yaml new file mode 100644 index 000000000..58e3a0c27 --- /dev/null +++ b/pkg/lint/rules/testdata/withcrd/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: withcrd +description: testing chart with a CRD +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/lint/rules/testdata/withcrd/crds/test-crd.yaml b/pkg/lint/rules/testdata/withcrd/crds/test-crd.yaml new file mode 100644 index 000000000..1d7350f1d --- /dev/null +++ b/pkg/lint/rules/testdata/withcrd/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false