fix(deps): drop client-go, apiextensions, cli-runtime and kustomize from chart-handling packages

pkg/getter, pkg/chart/common and internal/version each pulled a large
Kubernetes dependency subtree into every binary that only downloads and
loads charts:

- pkg/getter imported pkg/cli solely for getter.All's *cli.EnvSettings
  parameter, dragging in cli-runtime and kustomize. All and the plugin
  getter now accept an EnvProvider interface (EnvVars() map[string]string),
  which *cli.EnvSettings still satisfies, so callers are unchanged.
- pkg/chart/common built its default API version set by enumerating
  client-go's runtime scheme plus apiextensions at init time. The set is
  now generated (go generate) into zz_generated_known_versions.go, keeping
  client-go and apiextensions-apiserver out of the runtime graph.
- internal/version blank-imported client-go/kubernetes to pin the reported
  client-go version. The helm CLI already links client-go via cmd/helm, so
  the anchor was redundant there; chart-only consumers now get a graceful
  fallback instead of an init-time panic.

No behavioral change for the helm CLI. Chart-consuming libraries drop the
client-go/api/apiextensions/cli-runtime/kustomize trees entirely.

Signed-off-by: Jakob Möller <jakob.moeller@sap.com>
pull/32666/head
Jakob Möller 2 weeks ago
parent c378ef66c8
commit 8872b1d5cf
No known key found for this signature in database
GPG Key ID: 13025981F6A12D25

@ -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 {

@ -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)

@ -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)
}

@ -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",
}

@ -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)
}

@ -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
}

@ -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
}
}

Loading…
Cancel
Save