diff --git a/internal/version/clientgo.go b/internal/version/clientgo.go index 50a4fd5cb..e331bad72 100644 --- a/internal/version/clientgo.go +++ b/internal/version/clientgo.go @@ -20,10 +20,16 @@ import ( "errors" "runtime/debug" "slices" - - _ "k8s.io/client-go/kubernetes" // Force k8s.io/client-go to be included in the build ) +// K8sIOClientGoModVersion reports the version of k8s.io/client-go this binary +// was built against, read from the module build info. +// +// client-go is included in the build graph by whatever links a Kubernetes +// client (for the helm CLI, that is cmd/helm). When a consumer links Helm's +// chart libraries WITHOUT a client-go dependency, build info will not list it +// and this returns an error; callers must tolerate that (see version.Get and +// chart/common capabilities). func K8sIOClientGoModVersion() (string, error) { info, ok := debug.ReadBuildInfo() if !ok { diff --git a/pkg/chart/common/capabilities.go b/pkg/chart/common/capabilities.go index c7217a503..c5e76eb57 100644 --- a/pkg/chart/common/capabilities.go +++ b/pkg/chart/common/capabilities.go @@ -23,10 +23,6 @@ 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" @@ -128,19 +124,19 @@ func (v VersionSet) Has(apiVersion string) bool { return slices.Contains(v, apiVersion) } +// allKnownVersions returns the built-in Kubernetes API group/versions that seed +// the default capability version set. +// +// The list is generated (see gen_known_versions.go and +// zz_generated_known_versions.go) by enumerating client-go's runtime scheme +// plus the apiextensions groups. Returning the pre-computed slice here keeps +// this package — and therefore every chart-handling binary — free of the +// client-go, k8s.io/api and apiextensions-apiserver dependency trees, which +// are only needed to build the list, not to consult it. +// +//go:generate go run gen_known_versions.go 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 + return slices.Clone(defaultKnownVersions) } func makeDefaultCapabilities() (*Capabilities, error) { @@ -151,9 +147,13 @@ func makeDefaultCapabilities() (*Capabilities, error) { return newCapabilities(kubeVersionMajorTesting, kubeVersionMinorTesting) } + // K8sIOClientGoModVersion reads the client-go version from build info. When a + // consumer links Helm's chart libraries without client-go, that lookup fails; + // fall back to the built-in default kube version rather than erroring, since + // this default is advisory (it only seeds Capabilities.KubeVersion). vstr, err := helmversion.K8sIOClientGoModVersion() if err != nil { - return nil, fmt.Errorf("failed to retrieve k8s.io/client-go version: %w", err) + return newCapabilities(kubeVersionMajorTesting, kubeVersionMinorTesting) } v, err := semver.NewVersion(vstr) diff --git a/pkg/chart/common/gen_known_versions.go b/pkg/chart/common/gen_known_versions.go new file mode 100644 index 000000000..b274696f3 --- /dev/null +++ b/pkg/chart/common/gen_known_versions.go @@ -0,0 +1,98 @@ +//go:build ignore + +/* +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. +*/ + +// This program regenerates zz_generated_known_versions.go, the built-in +// Kubernetes API group/versions that seed the default capability version set. +// +// It enumerates client-go's runtime scheme plus the apiextensions groups — +// exactly as pkg/chart/common used to do at runtime — and writes the result as +// a static slice. Doing this at generate time keeps client-go, k8s.io/api and +// apiextensions-apiserver out of the runtime dependency graph of every binary +// that links Helm's chart packages. +// +// Run via `go generate ./pkg/chart/common/...`. +package main + +import ( + "bytes" + "fmt" + "go/format" + "os" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/client-go/kubernetes/scheme" +) + +const target = "zz_generated_known_versions.go" + +func main() { + // 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. + if err := apiextensionsv1beta1.AddToScheme(scheme.Scheme); err != nil { + fatal(err) + } + if err := apiextensionsv1.AddToScheme(scheme.Scheme); err != nil { + fatal(err) + } + + var buf bytes.Buffer + buf.WriteString(`/* +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. +*/ + +// Code generated by gen_known_versions.go; DO NOT EDIT. + +package common + +// defaultKnownVersions is the set of built-in Kubernetes API group/versions +// used to seed the default capability version set. Regenerate with +// go generate ./pkg/chart/common/... after a client-go bump. +var defaultKnownVersions = VersionSet{ +`) + for _, gv := range scheme.Scheme.PrioritizedVersionsAllGroups() { + fmt.Fprintf(&buf, "\t%q,\n", gv.String()) + } + buf.WriteString("}\n") + + out, err := format.Source(buf.Bytes()) + if err != nil { + fatal(err) + } + if err := os.WriteFile(target, out, 0o644); err != nil { + fatal(err) + } +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, "gen_known_versions:", err) + os.Exit(1) +} diff --git a/pkg/chart/common/zz_generated_known_versions.go b/pkg/chart/common/zz_generated_known_versions.go new file mode 100644 index 000000000..90ec76e22 --- /dev/null +++ b/pkg/chart/common/zz_generated_known_versions.go @@ -0,0 +1,82 @@ +/* +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. +*/ + +// Code generated by gen_known_versions.go; DO NOT EDIT. + +package common + +// defaultKnownVersions is the set of built-in Kubernetes API group/versions +// used to seed the default capability version set. Regenerate with +// go generate ./pkg/chart/common/... after a client-go bump. +var defaultKnownVersions = 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", + "lifecycle.k8s.io/v1alpha1", + "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/v1alpha3", + "scheduling.k8s.io/v1beta1", + "scheduling.k8s.io/v1", + "storage.k8s.io/v1beta1", + "storage.k8s.io/v1", + "storage.k8s.io/v1alpha1", + "storagemigration.k8s.io/v1", + "storagemigration.k8s.io/v1beta1", + "apiextensions.k8s.io/v1beta1", + "apiextensions.k8s.io/v1", +} diff --git a/pkg/cmd/version_test.go b/pkg/cmd/version_test.go index 9551de767..6cbe17e26 100644 --- a/pkg/cmd/version_test.go +++ b/pkg/cmd/version_test.go @@ -16,7 +16,13 @@ limitations under the License. package cmd import ( + "runtime/debug" + "strings" "testing" + + "github.com/stretchr/testify/require" + + "helm.sh/helm/v4/internal/version" ) func TestVersion(t *testing.T) { @@ -39,3 +45,16 @@ func TestVersion(t *testing.T) { func TestVersionFileCompletion(t *testing.T) { checkFileCompletion(t, "version", false) } + +// TestVersionReportsKubeClient ensures the helm binary keeps client-go linked so +// `helm version` still reports KubeClientVersion. clientgo.go no longer forces +// the dependency, so guard the CLI's own linkage here (this test binary mirrors +// it) to catch a future change that unlinks client-go from the command surface. +func TestVersionReportsKubeClient(t *testing.T) { + if _, ok := debug.ReadBuildInfo(); !ok { + t.Skip("build info unavailable (Go < 1.27)") + } + v, err := version.K8sIOClientGoModVersion() + require.NoError(t, err) + require.True(t, strings.HasPrefix(v, "v"), "expected client-go version, got %q", v) +} diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index a2d0f0ee2..b15afa119 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -23,7 +23,6 @@ import ( "slices" "time" - "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/registry" ) @@ -221,12 +220,23 @@ func Getters(extraOpts ...Option) Providers { } } +// EnvProvider supplies the Helm environment used to discover getter plugins. +// *cli.EnvSettings satisfies it. Accepting an interface here keeps pkg/getter +// free of a dependency on pkg/cli, which would otherwise pull the Kubernetes +// client-go, cli-runtime and kustomize trees into every binary that links a +// getter. +type EnvProvider interface { + // EnvVars returns the Helm environment variables, including HELM_PLUGINS, + // which locates the plugin directory scanned for getter plugins. + EnvVars() map[string]string +} + // All finds all of the registered getters as a list of Provider instances. // Currently, the built-in getters and the discovered plugins with downloader // notations are collected. -func All(settings *cli.EnvSettings, opts ...Option) Providers { +func All(env EnvProvider, opts ...Option) Providers { result := Getters(opts...) - pluginDownloaders, _ := collectGetterPlugins(settings) + pluginDownloaders, _ := collectGetterPlugins(env) result = append(result, pluginDownloaders...) return result } diff --git a/pkg/getter/plugingetter.go b/pkg/getter/plugingetter.go index 0e048ccb3..1d43d849f 100644 --- a/pkg/getter/plugingetter.go +++ b/pkg/getter/plugingetter.go @@ -24,26 +24,26 @@ import ( "helm.sh/helm/v4/internal/plugin" "helm.sh/helm/v4/internal/plugin/schema" - "helm.sh/helm/v4/pkg/cli" ) -// collectGetterPlugins scans for getter plugins. -// This will load plugins according to the cli. -func collectGetterPlugins(settings *cli.EnvSettings) (Providers, error) { +// collectGetterPlugins scans for getter plugins in the plugin directory named +// by the HELM_PLUGINS entry of the provided environment. +func collectGetterPlugins(env EnvProvider) (Providers, error) { + envVars := env.EnvVars() d := plugin.Descriptor{ Type: "getter/v1", } - plgs, err := plugin.FindPlugins([]string{settings.PluginsDirectory}, d) + plgs, err := plugin.FindPlugins([]string{envVars["HELM_PLUGINS"]}, d) if err != nil { return nil, err } - env := plugin.FormatEnv(settings.EnvVars()) + pluginEnv := plugin.FormatEnv(envVars) pluginConstructorBuilder := func(plg plugin.Plugin) Constructor { return func(option ...Option) (Getter, error) { return &getterPlugin{ options: append([]Option{}, option...), plg: plg, - env: env, + env: pluginEnv, }, nil } }