From 6bf75723034671b3fa56286f324029d56db96eb1 Mon Sep 17 00:00:00 2001 From: Yugo Kobayashi Date: Fri, 24 Jul 2026 22:07:41 +0900 Subject: [PATCH 1/3] refactor(chart): avoid runtime scheme initialization for capabilities Signed-off-by: Yugo Kobayashi --- pkg/chart/common/capabilities.go | 21 ------ pkg/chart/common/default_versions.go | 80 +++++++++++++++++++++++ pkg/chart/common/default_versions_test.go | 42 ++++++++++++ 3 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 pkg/chart/common/default_versions.go create mode 100644 pkg/chart/common/default_versions_test.go diff --git a/pkg/chart/common/capabilities.go b/pkg/chart/common/capabilities.go index 16910acaa..faae42ef0 100644 --- a/pkg/chart/common/capabilities.go +++ b/pkg/chart/common/capabilities.go @@ -23,10 +23,7 @@ import ( "testing" "github.com/Masterminds/semver/v3" - "k8s.io/client-go/kubernetes/scheme" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" k8sversion "k8s.io/apimachinery/pkg/util/version" helmversion "helm.sh/helm/v4/internal/version" @@ -38,9 +35,6 @@ const ( ) var ( - // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). - DefaultVersionSet = allKnownVersions() - DefaultCapabilities = func() *Capabilities { caps, err := makeDefaultCapabilities() if err != nil { @@ -127,21 +121,6 @@ func (v VersionSet) Has(apiVersion string) bool { return slices.Contains(v, apiVersion) } -func allKnownVersions() VersionSet { - // We should register the built in extension APIs as well so CRDs are - // supported in the default version set. This has caused problems with `helm - // template` in the past, so let's be safe - apiextensionsv1beta1.AddToScheme(scheme.Scheme) - apiextensionsv1.AddToScheme(scheme.Scheme) - - groups := scheme.Scheme.PrioritizedVersionsAllGroups() - vs := make(VersionSet, 0, len(groups)) - for _, gv := range groups { - vs = append(vs, gv.String()) - } - return vs -} - func makeDefaultCapabilities() (*Capabilities, error) { // Test builds don't include debug info / module info // (And even if they did, we probably want stable capabilities for tests anyway) diff --git a/pkg/chart/common/default_versions.go b/pkg/chart/common/default_versions.go new file mode 100644 index 000000000..b6048bf0a --- /dev/null +++ b/pkg/chart/common/default_versions.go @@ -0,0 +1,80 @@ +/* +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 common + +// DefaultVersionSet is the default set of Kubernetes API versions. +// +// This static snapshot avoids initializing the Kubernetes runtime scheme in +// binaries that only need chart capabilities. TestDefaultVersionSetMatchesScheme +// verifies that it remains in sync with the Kubernetes dependencies used by +// Helm. +var DefaultVersionSet = VersionSet{ + "v1", + "admissionregistration.k8s.io/v1", + "admissionregistration.k8s.io/v1alpha1", + "admissionregistration.k8s.io/v1beta1", + "internal.apiserver.k8s.io/v1alpha1", + "apps/v1", + "apps/v1beta1", + "apps/v1beta2", + "authentication.k8s.io/v1", + "authentication.k8s.io/v1alpha1", + "authentication.k8s.io/v1beta1", + "authorization.k8s.io/v1", + "authorization.k8s.io/v1beta1", + "autoscaling/v1", + "autoscaling/v2", + "batch/v1", + "batch/v1beta1", + "certificates.k8s.io/v1", + "certificates.k8s.io/v1beta1", + "certificates.k8s.io/v1alpha1", + "coordination.k8s.io/v1alpha2", + "coordination.k8s.io/v1beta1", + "coordination.k8s.io/v1", + "discovery.k8s.io/v1", + "discovery.k8s.io/v1beta1", + "events.k8s.io/v1", + "events.k8s.io/v1beta1", + "extensions/v1beta1", + "flowcontrol.apiserver.k8s.io/v1", + "flowcontrol.apiserver.k8s.io/v1beta1", + "flowcontrol.apiserver.k8s.io/v1beta2", + "flowcontrol.apiserver.k8s.io/v1beta3", + "networking.k8s.io/v1", + "networking.k8s.io/v1beta1", + "node.k8s.io/v1", + "node.k8s.io/v1alpha1", + "node.k8s.io/v1beta1", + "policy/v1", + "policy/v1beta1", + "rbac.authorization.k8s.io/v1", + "rbac.authorization.k8s.io/v1beta1", + "rbac.authorization.k8s.io/v1alpha1", + "resource.k8s.io/v1", + "resource.k8s.io/v1beta2", + "resource.k8s.io/v1beta1", + "resource.k8s.io/v1alpha3", + "scheduling.k8s.io/v1alpha2", + "scheduling.k8s.io/v1beta1", + "scheduling.k8s.io/v1", + "storage.k8s.io/v1beta1", + "storage.k8s.io/v1", + "storage.k8s.io/v1alpha1", + "storagemigration.k8s.io/v1beta1", + "apiextensions.k8s.io/v1beta1", + "apiextensions.k8s.io/v1", +} diff --git a/pkg/chart/common/default_versions_test.go b/pkg/chart/common/default_versions_test.go new file mode 100644 index 000000000..fc0e5b56e --- /dev/null +++ b/pkg/chart/common/default_versions_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 common + +import ( + "testing" + + "github.com/stretchr/testify/require" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" +) + +func TestDefaultVersionSetMatchesScheme(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, clientgoscheme.AddToScheme(scheme)) + require.NoError(t, apiextensionsv1beta1.AddToScheme(scheme)) + require.NoError(t, apiextensionsv1.AddToScheme(scheme)) + + groups := scheme.PrioritizedVersionsAllGroups() + schemeVersions := make(VersionSet, 0, len(groups)) + for _, group := range groups { + schemeVersions = append(schemeVersions, group.String()) + } + + require.ElementsMatch(t, DefaultVersionSet, schemeVersions, "DefaultVersionSet must stay in sync with the Kubernetes scheme") +} From 87dfbebdf5076b9905ee12b4743020cd43b7e767 Mon Sep 17 00:00:00 2001 From: Yugo Kobayashi Date: Fri, 24 Jul 2026 22:51:07 +0900 Subject: [PATCH 2/3] test(chart): verify per-group version priority Signed-off-by: Yugo Kobayashi --- pkg/chart/common/default_versions_test.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/chart/common/default_versions_test.go b/pkg/chart/common/default_versions_test.go index fc0e5b56e..a2959cbc7 100644 --- a/pkg/chart/common/default_versions_test.go +++ b/pkg/chart/common/default_versions_test.go @@ -23,6 +23,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ) @@ -32,11 +33,17 @@ func TestDefaultVersionSetMatchesScheme(t *testing.T) { require.NoError(t, apiextensionsv1beta1.AddToScheme(scheme)) require.NoError(t, apiextensionsv1.AddToScheme(scheme)) - groups := scheme.PrioritizedVersionsAllGroups() - schemeVersions := make(VersionSet, 0, len(groups)) - for _, group := range groups { - schemeVersions = append(schemeVersions, group.String()) + defaultVersionsByGroup := make(map[string][]string) + for _, apiVersion := range DefaultVersionSet { + groupVersion, err := schema.ParseGroupVersion(apiVersion) + require.NoErrorf(t, err, "invalid API version %q in DefaultVersionSet", apiVersion) + defaultVersionsByGroup[groupVersion.Group] = append(defaultVersionsByGroup[groupVersion.Group], groupVersion.Version) } - require.ElementsMatch(t, DefaultVersionSet, schemeVersions, "DefaultVersionSet must stay in sync with the Kubernetes scheme") + schemeVersionsByGroup := make(map[string][]string) + for _, groupVersion := range scheme.PrioritizedVersionsAllGroups() { + schemeVersionsByGroup[groupVersion.Group] = append(schemeVersionsByGroup[groupVersion.Group], groupVersion.Version) + } + + require.Equal(t, schemeVersionsByGroup, defaultVersionsByGroup, "DefaultVersionSet must stay in sync with the Kubernetes scheme") } From 1d294b981d3880a19c8b0d87273339939f2a57a7 Mon Sep 17 00:00:00 2001 From: Yugo Kobayashi Date: Fri, 7 Aug 2026 06:42:50 +0900 Subject: [PATCH 3/3] refactor(chart): generate default API versions Signed-off-by: Yugo Kobayashi --- internal/cmd/gendefaultversions/main.go | 125 ++++++++++++++++++++++ pkg/chart/common/capabilities.go | 2 + pkg/chart/common/default_versions.go | 28 +++-- pkg/chart/common/default_versions_test.go | 26 +++-- 4 files changed, 162 insertions(+), 19 deletions(-) create mode 100644 internal/cmd/gendefaultversions/main.go diff --git a/internal/cmd/gendefaultversions/main.go b/internal/cmd/gendefaultversions/main.go new file mode 100644 index 000000000..565d80c12 --- /dev/null +++ b/internal/cmd/gendefaultversions/main.go @@ -0,0 +1,125 @@ +/* +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 main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "os" + "slices" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" +) + +const licenseHeader = `/* +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. +*/` + +func main() { + output := flag.String("output", "default_versions.go", "path to the generated Go file") + flag.Parse() + + versions, err := knownVersions() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + var source bytes.Buffer + fmt.Fprintln(&source, licenseHeader) + fmt.Fprintln(&source) + fmt.Fprintln(&source, "// Code generated by gendefaultversions; DO NOT EDIT.") + fmt.Fprintln(&source) + fmt.Fprintln(&source, "package common") + fmt.Fprintln(&source) + fmt.Fprintln(&source, "// DefaultVersionSet is the default set of Kubernetes API versions used") + fmt.Fprintln(&source, "// when Helm renders without live cluster discovery.") + fmt.Fprintln(&source, "//") + fmt.Fprintln(&source, "// This is a snapshot of the API versions registered by Helm's Kubernetes") + fmt.Fprintln(&source, "// dependencies, not a list of APIs served by every supported cluster. Live") + fmt.Fprintln(&source, "// cluster operations build Capabilities from server discovery instead.") + fmt.Fprintln(&source, "//") + fmt.Fprintln(&source, "// Review API additions and removals against the Kubernetes versions supported") + fmt.Fprintln(&source, "// by this Helm release before committing a regenerated snapshot.") + fmt.Fprintln(&source, "//") + fmt.Fprintln(&source, "// Regenerate after updating Kubernetes dependencies with:") + fmt.Fprintln(&source, "//") + fmt.Fprintln(&source, "//\tgo generate ./pkg/chart/common") + fmt.Fprintln(&source, "var DefaultVersionSet = VersionSet{") + for _, version := range versions { + fmt.Fprintf(&source, "\t%q,\n", version) + } + fmt.Fprintln(&source, "}") + + formatted, err := format.Source(source.Bytes()) + if err != nil { + fmt.Fprintf(os.Stderr, "format generated source: %v\n", err) + os.Exit(1) + } + if err := os.WriteFile(*output, formatted, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "write generated source: %v\n", err) + os.Exit(1) + } +} + +func knownVersions() ([]string, error) { + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("register client-go scheme: %w", err) + } + if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("register apiextensions v1beta1 scheme: %w", err) + } + if err := apiextensionsv1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("register apiextensions v1 scheme: %w", err) + } + + groupSet := make(map[string]struct{}) + for _, groupVersion := range scheme.PrioritizedVersionsAllGroups() { + groupSet[groupVersion.Group] = struct{}{} + } + groups := make([]string, 0, len(groupSet)) + for group := range groupSet { + groups = append(groups, group) + } + slices.Sort(groups) + + versions := make([]string, 0) + for _, group := range groups { + for _, groupVersion := range scheme.PrioritizedVersionsForGroup(group) { + versions = append(versions, groupVersion.String()) + } + } + return versions, nil +} diff --git a/pkg/chart/common/capabilities.go b/pkg/chart/common/capabilities.go index faae42ef0..b2ab6e53f 100644 --- a/pkg/chart/common/capabilities.go +++ b/pkg/chart/common/capabilities.go @@ -34,6 +34,8 @@ const ( kubeVersionMinorTesting = 20 ) +//go:generate go run helm.sh/helm/v4/internal/cmd/gendefaultversions -output default_versions.go + var ( DefaultCapabilities = func() *Capabilities { caps, err := makeDefaultCapabilities() diff --git a/pkg/chart/common/default_versions.go b/pkg/chart/common/default_versions.go index b6048bf0a..3392c12db 100644 --- a/pkg/chart/common/default_versions.go +++ b/pkg/chart/common/default_versions.go @@ -1,10 +1,11 @@ /* 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 + 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, @@ -13,20 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Code generated by gendefaultversions; DO NOT EDIT. + package common -// DefaultVersionSet is the default set of Kubernetes API versions. +// DefaultVersionSet is the default set of Kubernetes API versions used +// when Helm renders without live cluster discovery. +// +// This is a snapshot of the API versions registered by Helm's Kubernetes +// dependencies, not a list of APIs served by every supported cluster. Live +// cluster operations build Capabilities from server discovery instead. // -// This static snapshot avoids initializing the Kubernetes runtime scheme in -// binaries that only need chart capabilities. TestDefaultVersionSetMatchesScheme -// verifies that it remains in sync with the Kubernetes dependencies used by -// Helm. +// Review API additions and removals against the Kubernetes versions supported +// by this Helm release before committing a regenerated snapshot. +// +// Regenerate after updating Kubernetes dependencies with: +// +// go generate ./pkg/chart/common var DefaultVersionSet = VersionSet{ "v1", "admissionregistration.k8s.io/v1", "admissionregistration.k8s.io/v1alpha1", "admissionregistration.k8s.io/v1beta1", - "internal.apiserver.k8s.io/v1alpha1", + "apiextensions.k8s.io/v1beta1", + "apiextensions.k8s.io/v1", "apps/v1", "apps/v1beta1", "apps/v1beta2", @@ -54,6 +65,7 @@ var DefaultVersionSet = VersionSet{ "flowcontrol.apiserver.k8s.io/v1beta1", "flowcontrol.apiserver.k8s.io/v1beta2", "flowcontrol.apiserver.k8s.io/v1beta3", + "internal.apiserver.k8s.io/v1alpha1", "networking.k8s.io/v1", "networking.k8s.io/v1beta1", "node.k8s.io/v1", @@ -75,6 +87,4 @@ var DefaultVersionSet = VersionSet{ "storage.k8s.io/v1", "storage.k8s.io/v1alpha1", "storagemigration.k8s.io/v1beta1", - "apiextensions.k8s.io/v1beta1", - "apiextensions.k8s.io/v1", } diff --git a/pkg/chart/common/default_versions_test.go b/pkg/chart/common/default_versions_test.go index a2959cbc7..090668229 100644 --- a/pkg/chart/common/default_versions_test.go +++ b/pkg/chart/common/default_versions_test.go @@ -16,6 +16,7 @@ limitations under the License. package common import ( + "slices" "testing" "github.com/stretchr/testify/require" @@ -23,7 +24,6 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ) @@ -33,17 +33,23 @@ func TestDefaultVersionSetMatchesScheme(t *testing.T) { require.NoError(t, apiextensionsv1beta1.AddToScheme(scheme)) require.NoError(t, apiextensionsv1.AddToScheme(scheme)) - defaultVersionsByGroup := make(map[string][]string) - for _, apiVersion := range DefaultVersionSet { - groupVersion, err := schema.ParseGroupVersion(apiVersion) - require.NoErrorf(t, err, "invalid API version %q in DefaultVersionSet", apiVersion) - defaultVersionsByGroup[groupVersion.Group] = append(defaultVersionsByGroup[groupVersion.Group], groupVersion.Version) + groupSet := make(map[string]struct{}) + for _, groupVersion := range scheme.PrioritizedVersionsAllGroups() { + groupSet[groupVersion.Group] = struct{}{} + } + groups := make([]string, 0, len(groupSet)) + for group := range groupSet { + groups = append(groups, group) } + slices.Sort(groups) - schemeVersionsByGroup := make(map[string][]string) - for _, groupVersion := range scheme.PrioritizedVersionsAllGroups() { - schemeVersionsByGroup[groupVersion.Group] = append(schemeVersionsByGroup[groupVersion.Group], groupVersion.Version) + schemeVersions := make(VersionSet, 0) + for _, group := range groups { + for _, groupVersion := range scheme.PrioritizedVersionsForGroup(group) { + schemeVersions = append(schemeVersions, groupVersion.String()) + } } - require.Equal(t, schemeVersionsByGroup, defaultVersionsByGroup, "DefaultVersionSet must stay in sync with the Kubernetes scheme") + require.Equal(t, schemeVersions, DefaultVersionSet, + "DefaultVersionSet must stay in sync with the Kubernetes scheme; run `go generate ./pkg/chart/common`") }