diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index b36153ec6..af80a7261 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -160,7 +160,11 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string if yamlStruct != nil { // 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)) + // 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, renderedContent) { + linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) + } linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) @@ -235,6 +239,33 @@ func validateYamlContent(err error) error { return nil } +// isListResource returns true if the resource is a Kubernetes List type +// (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 // Kind, or if not set, defaults to the standard definition of a subdomain in // DNS (RFC 1123), used by most resources. diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index 787bd6e4b..17d37bd3a 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -438,3 +438,98 @@ items: t.Fatalf("List objects keep annotations should pass. got: %s", err) } } + +func TestIsListResource(t *testing.T) { + tests := []struct { + kind string + manifest string + expected bool + }{ + {"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, test.manifest) + if result != test.expected { + t.Errorf("isListResource(%q) = %v, expected %v", test.kind, result, test.expected) + } + }) + } +} + +func TestConfigMapListLinting(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/configmaplist-chart"} + Templates(&linter, values, namespace, strict) + + // The ConfigMapList should not generate any lint warnings about metadata.name + for _, msg := range linter.Messages { + if strings.Contains(msg.Err.Error(), "object name does not conform to Kubernetes naming requirements") { + t.Errorf("ConfigMapList should not generate metadata name validation errors, but got: %v", msg.Err) + } + } + + // Should have no errors or warnings + if len(linter.Messages) > 0 { + t.Errorf("Expected no lint messages for ConfigMapList, got %d messages:", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + } +} diff --git a/pkg/lint/rules/testdata/configmaplist-chart/Chart.yaml b/pkg/lint/rules/testdata/configmaplist-chart/Chart.yaml new file mode 100644 index 000000000..830ebea92 --- /dev/null +++ b/pkg/lint/rules/testdata/configmaplist-chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: configmaplist-chart +description: Test chart with ConfigMapList +type: application +version: "0.1.0" +appVersion: "1.0" diff --git a/pkg/lint/rules/testdata/configmaplist-chart/templates/configmap.yaml b/pkg/lint/rules/testdata/configmaplist-chart/templates/configmap.yaml new file mode 100644 index 000000000..605e363fc --- /dev/null +++ b/pkg/lint/rules/testdata/configmaplist-chart/templates/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-single-config + namespace: {{ .Release.Namespace }} +data: + setting: "value" \ No newline at end of file diff --git a/pkg/lint/rules/testdata/configmaplist-chart/templates/configmaplist.yaml b/pkg/lint/rules/testdata/configmaplist-chart/templates/configmaplist.yaml new file mode 100644 index 000000000..2227e0a45 --- /dev/null +++ b/pkg/lint/rules/testdata/configmaplist-chart/templates/configmaplist.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMapList +metadata: + # ConfigMapList objects don't require meaningful names + # This should not trigger lint warnings +items: + - apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .Release.Name }}-database-config + namespace: {{ .Release.Namespace }} + data: + host: {{ .Values.configs.database.host }} + port: "{{ .Values.configs.database.port }}" + - apiVersion: v1 + kind: ConfigMap + metadata: + name: {{ .Release.Name }}-cache-config + namespace: {{ .Release.Namespace }} + data: + redis-url: {{ .Values.configs.cache.redis }} \ No newline at end of file diff --git a/pkg/lint/rules/testdata/configmaplist-chart/values.yaml b/pkg/lint/rules/testdata/configmaplist-chart/values.yaml new file mode 100644 index 000000000..f7e69300e --- /dev/null +++ b/pkg/lint/rules/testdata/configmaplist-chart/values.yaml @@ -0,0 +1,7 @@ +# Default values for configmaplist-chart +configs: + database: + host: localhost + port: 5432 + cache: + redis: redis://localhost:6379 \ No newline at end of file