diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index ced47ef4e..af80a7261 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -162,7 +162,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string // Refs https://github.com/helm/helm/issues/8596 // Skip metadata name validation for List resources as they don't require meaningful names // Refs https://github.com/helm/helm/issues/13192 - if !isListResource(yamlStruct) { + if !isListResource(yamlStruct, renderedContent) { linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) } linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) @@ -240,9 +240,30 @@ func validateYamlContent(err error) error { } // isListResource returns true if the resource is a Kubernetes List type -// (e.g. ConfigMapList, SecretList). -func isListResource(obj *k8sYamlStruct) bool { - return strings.HasSuffix(obj.Kind, "List") +// (e.g. ConfigMapList, SecretList). It checks both that the Kind ends with +// "List" and that the resource has an "items" field that is an array. +// This avoids incorrectly treating custom resources that happen to end with +// "List" but aren't actually list containers. +func isListResource(obj *k8sYamlStruct, manifest string) bool { + if !strings.HasSuffix(obj.Kind, "List") { + return false + } + + // For the special case of Kind == "List", we can be sure it's a list + if obj.Kind == "List" { + return true + } + + // For other kinds ending with "List", verify they have an items field + var listStruct struct { + Items []interface{} `json:"items"` + } + if err := yaml.Unmarshal([]byte(manifest), &listStruct); err != nil { + return false + } + + // Check if the items field exists and is an array (even if empty) + return listStruct.Items != nil } // validateMetadataName uses the correct validation function for the object diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index 695530155..17d37bd3a 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -442,21 +442,71 @@ items: func TestIsListResource(t *testing.T) { tests := []struct { kind string + manifest string expected bool }{ - {"ConfigMap", false}, - {"ConfigMapList", true}, - {"Secret", false}, - {"SecretList", true}, - {"List", true}, - {"", false}, - {"SomethingListExtra", false}, + {"ConfigMap", `apiVersion: v1 +kind: ConfigMap +metadata: + name: test +data: + key: value`, false}, + {"ConfigMapList", `apiVersion: v1 +kind: ConfigMapList +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: test1 +- apiVersion: v1 + kind: ConfigMap + metadata: + name: test2`, true}, + {"Secret", `apiVersion: v1 +kind: Secret +metadata: + name: test +data: + key: value`, false}, + {"SecretList", `apiVersion: v1 +kind: SecretList +items: +- apiVersion: v1 + kind: Secret + metadata: + name: test1 +- apiVersion: v1 + kind: Secret + metadata: + name: test2`, true}, + {"List", `apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: test`, true}, + {"", `apiVersion: v1 +kind: "" +metadata: + name: test`, false}, + {"SomethingListExtra", `apiVersion: v1 +kind: SomethingListExtra +metadata: + name: test`, false}, + // Test case for a custom resource ending in "List" but without items field (should be false) + {"AccessList", `apiVersion: example.com/v1 +kind: AccessList +metadata: + name: test +spec: + rules: []`, false}, } for _, test := range tests { t.Run(test.kind, func(t *testing.T) { obj := &k8sYamlStruct{Kind: test.kind} - result := isListResource(obj) + result := isListResource(obj, test.manifest) if result != test.expected { t.Errorf("isListResource(%q) = %v, expected %v", test.kind, result, test.expected) }