diff --git a/go.mod b/go.mod index e19d71e77..ed63d9a53 100644 --- a/go.mod +++ b/go.mod @@ -29,20 +29,20 @@ require ( github.com/rubenv/sql-migrate v1.8.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 + github.com/spf13/pflag v1.0.7 github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.40.0 golang.org/x/term v0.33.0 golang.org/x/text v0.27.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.2 - k8s.io/apiextensions-apiserver v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/apiserver v0.33.2 - k8s.io/cli-runtime v0.33.2 - k8s.io/client-go v0.33.2 + k8s.io/api v0.33.3 + k8s.io/apiextensions-apiserver v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/apiserver v0.33.3 + k8s.io/cli-runtime v0.33.3 + k8s.io/client-go v0.33.3 k8s.io/klog/v2 v2.130.1 - k8s.io/kubectl v0.33.2 + k8s.io/kubectl v0.33.3 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/kustomize/kyaml v0.20.0 @@ -171,7 +171,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/component-base v0.33.2 // indirect + k8s.io/component-base v0.33.3 // indirect k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/go.sum b/go.sum index 4fff82bcc..4742a54e7 100644 --- a/go.sum +++ b/go.sum @@ -303,8 +303,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -506,26 +507,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= -k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= -k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= -k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= -k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= -k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= -k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= -k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= +k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= +k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/cli-runtime v0.33.3 h1:Dgy4vPjNIu8LMJBSvs8W0LcdV0PX/8aGG1DA1W8lklA= +k8s.io/cli-runtime v0.33.3/go.mod h1:yklhLklD4vLS8HNGgC9wGiuHWze4g7x6XQZ+8edsKEo= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= +k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 h1:gAXU86Fmbr/ktY17lkHwSjw5aoThQvhnstGGIYKlKYc= k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911/go.mod h1:GLOk5B+hDbRROvt0X2+hqX64v/zO3vXN7J78OUmBSKw= -k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= -k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= +k8s.io/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac= +k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go index e760ae4d1..4cb77361a 100644 --- a/pkg/action/get_metadata.go +++ b/pkg/action/get_metadata.go @@ -34,11 +34,14 @@ type GetMetadata struct { } type Metadata struct { - Name string `json:"name" yaml:"name"` - Chart string `json:"chart" yaml:"chart"` - Version string `json:"version" yaml:"version"` - AppVersion string `json:"appVersion" yaml:"appVersion"` - Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + Name string `json:"name" yaml:"name"` + Chart string `json:"chart" yaml:"chart"` + Version string `json:"version" yaml:"version"` + AppVersion string `json:"appVersion" yaml:"appVersion"` + // Annotations are fetched from the Chart.yaml file + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` + // Labels of the release which are stored in driver metadata fields storage + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` Dependencies []*chart.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` Namespace string `json:"namespace" yaml:"namespace"` Revision int `json:"revision" yaml:"revision"` @@ -71,6 +74,7 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) { AppVersion: rel.Chart.Metadata.AppVersion, Dependencies: rel.Chart.Metadata.Dependencies, Annotations: rel.Chart.Metadata.Annotations, + Labels: rel.Labels, Namespace: rel.Namespace, Revision: rel.Version, Status: rel.Info.Status.String(), diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go new file mode 100644 index 000000000..08e99d8d6 --- /dev/null +++ b/pkg/action/get_metadata_test.go @@ -0,0 +1,42 @@ +/* +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 action + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + release "helm.sh/helm/v4/pkg/release/v1" +) + +func TestGetMetadata_Labels(t *testing.T) { + rel := releaseStub() + rel.Info.Status = release.StatusDeployed + customLabels := map[string]string{"key1": "value1", "key2": "value2"} + rel.Labels = customLabels + + metaGetter := NewGetMetadata(actionConfigFixture(t)) + err := metaGetter.cfg.Releases.Create(rel) + assert.NoError(t, err) + + metadata, err := metaGetter.Run(rel.Name) + assert.NoError(t, err) + + assert.Equal(t, metadata.Name, rel.Name) + assert.Equal(t, metadata.Labels, customLabels) +} diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go index a01580b0a..613149a4d 100644 --- a/pkg/action/lint_test.go +++ b/pkg/action/lint_test.go @@ -154,12 +154,12 @@ func TestLint_ChartWithWarnings(t *testing.T) { } }) - t.Run("should pass with no errors when strict", func(t *testing.T) { + t.Run("should fail with one error when strict", func(t *testing.T) { testCharts := []string{chartWithNoTemplatesDir} testLint := NewLint() testLint.Strict = true - if result := testLint.Run(testCharts, values); len(result.Errors) != 0 { - t.Error("expected no errors, but got", len(result.Errors)) + if result := testLint.Run(testCharts, values); len(result.Errors) != 1 { + t.Error("expected one error, but got", len(result.Errors)) } }) } diff --git a/pkg/cmd/get_metadata.go b/pkg/cmd/get_metadata.go index 9f58e0f4e..aea149f5e 100644 --- a/pkg/cmd/get_metadata.go +++ b/pkg/cmd/get_metadata.go @@ -80,6 +80,7 @@ func (w metadataWriter) WriteTable(out io.Writer) error { _, _ = fmt.Fprintf(out, "VERSION: %v\n", w.metadata.Version) _, _ = fmt.Fprintf(out, "APP_VERSION: %v\n", w.metadata.AppVersion) _, _ = fmt.Fprintf(out, "ANNOTATIONS: %v\n", k8sLabels.Set(w.metadata.Annotations).String()) + _, _ = fmt.Fprintf(out, "LABELS: %v\n", k8sLabels.Set(w.metadata.Labels).String()) _, _ = fmt.Fprintf(out, "DEPENDENCIES: %v\n", w.metadata.FormattedDepNames()) _, _ = fmt.Fprintf(out, "NAMESPACE: %v\n", w.metadata.Namespace) _, _ = fmt.Fprintf(out, "REVISION: %v\n", w.metadata.Revision) diff --git a/pkg/cmd/get_metadata_test.go b/pkg/cmd/get_metadata_test.go index a2ab2cba1..59fc3b82c 100644 --- a/pkg/cmd/get_metadata_test.go +++ b/pkg/cmd/get_metadata_test.go @@ -27,23 +27,23 @@ func TestGetMetadataCmd(t *testing.T) { name: "get metadata with a release", cmd: "get metadata thomas-guide", golden: "output/get-metadata.txt", - rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide", Labels: map[string]string{"key1": "value1"}})}, }, { name: "get metadata requires release name arg", cmd: "get metadata", golden: "output/get-metadata-args.txt", - rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide", Labels: map[string]string{"key1": "value1"}})}, wantError: true, }, { name: "get metadata to json", cmd: "get metadata thomas-guide --output json", golden: "output/get-metadata.json", - rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide", Labels: map[string]string{"key1": "value1"}})}, }, { name: "get metadata to yaml", cmd: "get metadata thomas-guide --output yaml", golden: "output/get-metadata.yaml", - rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide", Labels: map[string]string{"key1": "value1"}})}, }} runTestCmd(t, tests) } diff --git a/pkg/cmd/testdata/output/get-metadata.json b/pkg/cmd/testdata/output/get-metadata.json index 4c015b977..9166f87ac 100644 --- a/pkg/cmd/testdata/output/get-metadata.json +++ b/pkg/cmd/testdata/output/get-metadata.json @@ -1 +1 @@ -{"name":"thomas-guide","chart":"foo","version":"0.1.0-beta.1","appVersion":"1.0","annotations":{"category":"web-apps","supported":"true"},"dependencies":[{"name":"cool-plugin","version":"1.0.0","repository":"https://coolplugin.io/charts","condition":"coolPlugin.enabled","enabled":true},{"name":"crds","version":"2.7.1","repository":"","condition":"crds.enabled"}],"namespace":"default","revision":1,"status":"deployed","deployedAt":"1977-09-02T22:04:05Z"} +{"name":"thomas-guide","chart":"foo","version":"0.1.0-beta.1","appVersion":"1.0","annotations":{"category":"web-apps","supported":"true"},"labels":{"key1":"value1"},"dependencies":[{"name":"cool-plugin","version":"1.0.0","repository":"https://coolplugin.io/charts","condition":"coolPlugin.enabled","enabled":true},{"name":"crds","version":"2.7.1","repository":"","condition":"crds.enabled"}],"namespace":"default","revision":1,"status":"deployed","deployedAt":"1977-09-02T22:04:05Z"} diff --git a/pkg/cmd/testdata/output/get-metadata.txt b/pkg/cmd/testdata/output/get-metadata.txt index 01083b333..5744083dd 100644 --- a/pkg/cmd/testdata/output/get-metadata.txt +++ b/pkg/cmd/testdata/output/get-metadata.txt @@ -3,6 +3,7 @@ CHART: foo VERSION: 0.1.0-beta.1 APP_VERSION: 1.0 ANNOTATIONS: category=web-apps,supported=true +LABELS: key1=value1 DEPENDENCIES: cool-plugin,crds NAMESPACE: default REVISION: 1 diff --git a/pkg/cmd/testdata/output/get-metadata.yaml b/pkg/cmd/testdata/output/get-metadata.yaml index 6298436c9..98f567837 100644 --- a/pkg/cmd/testdata/output/get-metadata.yaml +++ b/pkg/cmd/testdata/output/get-metadata.yaml @@ -14,6 +14,8 @@ dependencies: repository: "" version: 2.7.1 deployedAt: "1977-09-02T22:04:05Z" +labels: + key1: value1 name: thomas-guide namespace: default revision: 1 diff --git a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt index 6e2efcecd..7b445a69a 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt @@ -1,6 +1,6 @@ ==> Linting testdata/testcharts/chart-with-bad-subcharts [INFO] Chart.yaml: icon is recommended -[ERROR] templates/: error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required +[WARNING] templates/: directory does not exist [ERROR] : unable to load chart error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required @@ -9,11 +9,12 @@ [ERROR] Chart.yaml: apiVersion is required. The value must be either "v1" or "v2" [ERROR] Chart.yaml: version is required [INFO] Chart.yaml: icon is recommended -[ERROR] templates/: validation: chart.metadata.name is required +[WARNING] templates/: directory does not exist [ERROR] : unable to load chart validation: chart.metadata.name is required ==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart [INFO] Chart.yaml: icon is recommended +[WARNING] templates/: directory does not exist Error: 3 chart(s) linted, 2 chart(s) failed diff --git a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt index af533797b..5a1c388bb 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts.txt @@ -1,6 +1,6 @@ ==> Linting testdata/testcharts/chart-with-bad-subcharts [INFO] Chart.yaml: icon is recommended -[ERROR] templates/: error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required +[WARNING] templates/: directory does not exist [ERROR] : unable to load chart error unpacking subchart bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required diff --git a/pkg/cmd/testdata/output/lint-quiet-with-error.txt b/pkg/cmd/testdata/output/lint-quiet-with-error.txt index e3d29a5a3..0731a07d1 100644 --- a/pkg/cmd/testdata/output/lint-quiet-with-error.txt +++ b/pkg/cmd/testdata/output/lint-quiet-with-error.txt @@ -1,7 +1,7 @@ ==> Linting testdata/testcharts/chart-bad-requirements [ERROR] Chart.yaml: unable to parse YAML error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator -[ERROR] templates/: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator +[WARNING] templates/: directory does not exist [ERROR] : unable to load chart cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator diff --git a/pkg/cmd/testdata/output/lint-quiet-with-warning.txt b/pkg/cmd/testdata/output/lint-quiet-with-warning.txt index e69de29bb..ebf6c1989 100644 --- a/pkg/cmd/testdata/output/lint-quiet-with-warning.txt +++ b/pkg/cmd/testdata/output/lint-quiet-with-warning.txt @@ -0,0 +1,4 @@ +==> Linting testdata/testcharts/chart-with-only-crds +[WARNING] templates/: directory does not exist + +1 chart(s) linted, 0 chart(s) failed 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..5b590c010 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/lint/support" ) @@ -32,6 +34,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" @@ -43,14 +46,19 @@ func TestBadChart(t *testing.T) { t.Errorf("Number of errors %v", len(m)) t.Errorf("All didn't fail with expected errors, got %#v", m) } - // There should be one INFO, and 2 ERROR messages, check for them - var i, e, e2, e3, e4, e5, e6 bool + // There should be one INFO, one WARNING, and 2 ERROR messages, check for them + var i, w, e, e2, e3, e4, e5, e6 bool for _, msg := range m { if msg.Severity == support.InfoSev { if strings.Contains(msg.Err.Error(), "icon is recommended") { i = true } } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "does not exist") { + w = true + } + } if msg.Severity == support.ErrorSev { if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { e = true @@ -76,7 +84,7 @@ func TestBadChart(t *testing.T) { } } } - if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 { + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w { t.Errorf("Didn't find all the expected errors, got %#v", m) } } @@ -93,7 +101,7 @@ func TestInvalidYaml(t *testing.T) { func TestInvalidChartYaml(t *testing.T) { m := RunAll(invalidChartFileDir, values, namespace).Messages - if len(m) != 1 { + if len(m) != 2 { t.Fatalf("All didn't fail with expected errors, got %#v", m) } if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") { @@ -111,6 +119,13 @@ func TestBadValues(t *testing.T) { } } +func TestBadCrdFile(t *testing.T) { + m := RunAll(badCrdFileDir, values, namespace).Messages + assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) + assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") + assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'") +} + 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..1b8a73139 --- /dev/null +++ b/pkg/lint/rules/crds.go @@ -0,0 +1,113 @@ +/* +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" + "io/fs" + "os" + "path/filepath" + "strings" + + "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) + + // crds directory is optional + if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) { + return + } + + crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath)) + if !crdsDirValid { + 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: + 1. It is a YAML file and not a template + 2. The API version is apiextensions.k8s.io + 3. 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 parsing fails here, it will always fail in the next block as well, so we should return here. + // This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct. + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct)) + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct)) + } + } +} + +// Validation functions +func validateCrdsDir(crdsPath string) error { + fi, err := os.Stat(crdsPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateCrdAPIVersion(obj *k8sYamlStruct) error { + if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") { + return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'") + } + 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..d497b29ba --- /dev/null +++ b/pkg/lint/rules/crds_test.go @@ -0,0 +1,36 @@ +/* +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 ( + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/pkg/lint/support" +) + +const invalidCrdsDir = "./testdata/invalidcrdsdir" + +func TestInvalidCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: invalidCrdsDir} + Crds(&linter) + res := linter.Messages + + assert.Len(t, res, 1) + assert.ErrorContains(t, res[0].Err, "not a directory") +} diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 463bd5341..b36153ec6 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -54,10 +54,14 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string fpath := "templates/" templatesPath := filepath.Join(linter.ChartDir, fpath) - templatesDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateTemplatesDir(templatesPath)) - // Templates directory is optional for now - if !templatesDirExist { + templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath)) + if !templatesDirExists { + return + } + + validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath)) + if !validTemplatesDir { return } @@ -194,11 +198,21 @@ func validateTopIndentLevel(content string) error { } // Validation functions +func templatesDirExists(templatesPath string) error { + _, err := os.Stat(templatesPath) + if errors.Is(err, os.ErrNotExist) { + return errors.New("directory does not exist") + } + return nil +} + func validateTemplatesDir(templatesPath string) error { - if fi, err := os.Stat(templatesPath); err == nil { - if !fi.IsDir() { - return errors.New("not a directory") - } + fi, err := os.Stat(templatesPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") } return nil } 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-apiversion.yaml b/pkg/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml new file mode 100644 index 000000000..468916053 --- /dev/null +++ b/pkg/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml @@ -0,0 +1,2 @@ +apiVersion: bad.k8s.io/v1beta1 +kind: CustomResourceDefinition 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/goodone/crds/test-crd.yaml b/pkg/lint/rules/testdata/goodone/crds/test-crd.yaml new file mode 100644 index 000000000..1d7350f1d --- /dev/null +++ b/pkg/lint/rules/testdata/goodone/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 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..6b1611a64 --- /dev/null +++ b/pkg/lint/rules/testdata/invalidcrdsdir/values.yaml @@ -0,0 +1 @@ +# Default values for invalidcrdsdir. diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 9ca57284c..3d3b0c2e2 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -46,6 +46,7 @@ type MockReleaseOptions struct { Chart *chart.Chart Status Status Namespace string + Labels map[string]string } // Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. @@ -66,6 +67,10 @@ func Mock(opts *MockReleaseOptions) *Release { if namespace == "" { namespace = "default" } + var labels map[string]string + if len(opts.Labels) > 0 { + labels = opts.Labels + } ch := opts.Chart if opts.Chart == nil { @@ -130,5 +135,6 @@ func Mock(opts *MockReleaseOptions) *Release { }, }, Manifest: MockManifest, + Labels: labels, } }