Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PersistentClient flag to allow control over Kubernetes client behavior #659

Merged
merged 3 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions api/v2beta1/helmrelease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ type HelmReleaseSpec struct {
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`

// PersistentClient tells the controller to use a persistent Kubernetes
// client for this release. When enabled, the client will be reused for the
// duration of the reconciliation, instead of being created and destroyed
// for each (step of a) Helm action.
//
// This can improve performance, but may cause issues with some Helm charts
// that for example do create Custom Resource Definitions during installation
// outside Helm's CRD lifecycle hooks, which are then not observed to be
// available by e.g. post-install hooks.
//
// If not set, it defaults to true.
//
makkes marked this conversation as resolved.
Show resolved Hide resolved
// +optional
PersistentClient *bool `json:"persistentClient,omitempty"`

// Install holds the configuration for Helm install actions for this HelmRelease.
// +optional
Install *Install `json:"install,omitempty"`
Expand Down Expand Up @@ -1039,6 +1054,15 @@ func (in HelmRelease) GetMaxHistory() int {
return *in.Spec.MaxHistory
}

// UsePersistentClient returns the configured PersistentClient, or the default
// of true.
func (in HelmRelease) UsePersistentClient() bool {
if in.Spec.PersistentClient == nil {
return true
}
return *in.Spec.PersistentClient
}

// GetDependsOn returns the list of dependencies across-namespaces.
func (in HelmRelease) GetDependsOn() []meta.NamespacedObjectReference {
return in.Spec.DependsOn
Expand Down
5 changes: 5 additions & 0 deletions api/v2beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ spec:
this HelmRelease. Use '0' for an unlimited number of revisions;
defaults to '10'.
type: integer
persistentClient:
description: "PersistentClient tells the controller to use a persistent
Kubernetes client for this release. When enabled, the client will
be reused for the duration of the reconciliation, instead of being
created and destroyed for each (step of a) Helm action. \n This
can improve performance, but may cause issues with some Helm charts
that for example do create Custom Resource Definitions during installation
outside Helm's CRD lifecycle hooks, which are then not observed
to be available by e.g. post-install hooks. \n If not set, it defaults
to true."
type: boolean
postRenderers:
description: PostRenderers holds an array of Helm PostRenderers, which
will be applied in order of their definition.
Expand Down
40 changes: 40 additions & 0 deletions docs/api/helmrelease.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,26 @@ when reconciling this HelmRelease.</p>
</tr>
<tr>
<td>
<code>persistentClient</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>PersistentClient tells the controller to use a persistent Kubernetes
client for this release. When enabled, the client will be reused for the
duration of the reconciliation, instead of being created and destroyed
for each (step of a) Helm action.</p>
<p>This can improve performance, but may cause issues with some Helm charts
that for example do create Custom Resource Definitions during installation
outside Helm&rsquo;s CRD lifecycle hooks, which are then not observed to be
available by e.g. post-install hooks.</p>
<p>If not set, it defaults to true.</p>
</td>
</tr>
<tr>
<td>
<code>install</code><br>
<em>
<a href="#helm.toolkit.fluxcd.io/v2beta1.Install">
Expand Down Expand Up @@ -1015,6 +1035,26 @@ when reconciling this HelmRelease.</p>
</tr>
<tr>
<td>
<code>persistentClient</code><br>
<em>
bool
</em>
</td>
<td>
<em>(Optional)</em>
<p>PersistentClient tells the controller to use a persistent Kubernetes
client for this release. When enabled, the client will be reused for the
duration of the reconciliation, instead of being created and destroyed
for each (step of a) Helm action.</p>
<p>This can improve performance, but may cause issues with some Helm charts
that for example do create Custom Resource Definitions during installation
outside Helm&rsquo;s CRD lifecycle hooks, which are then not observed to be
available by e.g. post-install hooks.</p>
<p>If not set, it defaults to true.</p>
</td>
</tr>
<tr>
<td>
<code>install</code><br>
<em>
<a href="#helm.toolkit.fluxcd.io/v2beta1.Install">
Expand Down
15 changes: 15 additions & 0 deletions docs/spec/v2beta1/helmreleases.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ type HelmReleaseSpec struct {
// +optional
MaxHistory *int `json:"maxHistory,omitempty"`

// PersistentClient tells the controller to use a persistent Kubernetes
// client for this release. When enabled, the client will be reused for the
// duration of the reconciliation, instead of being created and destroyed
// for each (step of a) Helm action.
//
// This can improve performance, but may cause issues with some Helm charts
// that for example do create Custom Resource Definitions during installation
// outside Helm's CRD lifecycle hooks, which are then not observed to be
// available by e.g. post-install hooks.
//
// If not set, it defaults to true.
//
// +optional
PersistentClient *bool `json:"persistentClient,omitempty"`

// Install holds the configuration for Helm install actions for this HelmRelease.
// +optional
Install *Install `json:"install,omitempty"`
Expand Down
1 change: 1 addition & 0 deletions internal/controllers/helmrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ func (r *HelmReleaseReconciler) buildRESTClientGetter(ctx context.Context, hr v2
// When ServiceAccountName is empty, it will fall back to the configured default.
// If this is not configured either, this option will result in a no-op.
kube.WithImpersonate(hr.Spec.ServiceAccountName, hr.GetNamespace()),
kube.WithPersistent(hr.UsePersistentClient()),
}
if hr.Spec.KubeConfig != nil {
secretName := types.NamespacedName{
Expand Down
107 changes: 80 additions & 27 deletions internal/kube/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,27 @@ func WithClientOptions(opts client.Options) Option {
}
}

// WithPersistent sets whether the client should persist the underlying client
// config, REST mapper, and discovery client.
func WithPersistent(persist bool) Option {
return func(c *MemoryRESTClientGetter) {
c.persistent = persist
}
}

// MemoryRESTClientGetter is a resource.RESTClientGetter that uses an
// in-memory REST config, REST mapper, and discovery client. The REST config,
// REST mapper, and discovery client are lazily initialized, and cached for
// subsequent calls.
// in-memory REST config, REST mapper, and discovery client.
// If configured, the client config, REST mapper, and discovery client are
// lazily initialized, and cached for subsequent calls.
type MemoryRESTClientGetter struct {
// namespace is the namespace to use for the client.
namespace string
// impersonate is the username to use for the client.
impersonate string
// persistent indicates whether the client should persist the restMapper,
// clientCfg, and discoveryClient. Rather than re-initializing them on
// every call, they will be cached and reused.
persistent bool

cfg *rest.Config

Expand Down Expand Up @@ -124,59 +136,100 @@ func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
// ToDiscoveryClient returns a memory cached discovery client. Calling it
// multiple times will return the same instance.
func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
c.clientCfgMu.Lock()
defer c.clientCfgMu.Unlock()
if c.persistent {
return c.toPersistentDiscoveryClient()
}
return c.toDiscoveryClient()
}

if c.discoveryClient == nil {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}
func (c *MemoryRESTClientGetter) toPersistentDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
c.discoveryMu.Lock()
defer c.discoveryMu.Unlock()

discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if c.discoveryClient == nil {
discoveryClient, err := c.toDiscoveryClient()
if err != nil {
return nil, err
}
c.discoveryClient = memory.NewMemCacheClient(discoveryClient)
c.discoveryClient = discoveryClient
}
return c.discoveryClient, nil
}

func (c *MemoryRESTClientGetter) toDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}

discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
return memory.NewMemCacheClient(discoveryClient), nil
}

// ToRESTMapper returns a meta.RESTMapper using the discovery client. Calling
// it multiple times will return the same instance.
func (c *MemoryRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
c.discoveryMu.Lock()
defer c.discoveryMu.Unlock()
if c.persistent {
return c.toPersistentRESTMapper()
}
return c.toRESTMapper()
}

func (c *MemoryRESTClientGetter) toPersistentRESTMapper() (meta.RESTMapper, error) {
c.restMapperMu.Lock()
defer c.restMapperMu.Unlock()

if c.restMapper == nil {
discoveryClient, err := c.ToDiscoveryClient()
restMapper, err := c.toRESTMapper()
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
c.restMapper = restmapper.NewShortcutExpander(mapper, discoveryClient)
c.restMapper = restMapper
}
return c.restMapper, nil
}

func (c *MemoryRESTClientGetter) toRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := c.ToDiscoveryClient()
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
return restmapper.NewShortcutExpander(mapper, discoveryClient), nil
}

// ToRawKubeConfigLoader returns a clientcmd.ClientConfig using
// clientcmd.DefaultClientConfig. With clientcmd.ClusterDefaults, namespace, and
// impersonate configured as overwrites.
func (c *MemoryRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
if c.persistent {
return c.toPersistentRawKubeConfigLoader()
}
return c.toRawKubeConfigLoader()
}

func (c *MemoryRESTClientGetter) toPersistentRawKubeConfigLoader() clientcmd.ClientConfig {
c.clientCfgMu.Lock()
defer c.clientCfgMu.Unlock()

if c.clientCfg == nil {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// use the standard defaults for this client command
// DEPRECATED: remove and replace with something more accurate
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig

overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
overrides.Context.Namespace = c.namespace
overrides.AuthInfo.Impersonate = c.impersonate

c.clientCfg = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
c.clientCfg = c.toRawKubeConfigLoader()
}
return c.clientCfg
}

func (c *MemoryRESTClientGetter) toRawKubeConfigLoader() clientcmd.ClientConfig {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// use the standard defaults for this client command
// DEPRECATED: remove and replace with something more accurate
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig

overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
overrides.Context.Namespace = c.namespace
overrides.AuthInfo.Impersonate = c.impersonate

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
}
Loading