diff --git a/go.mod b/go.mod index 36f63a023..87f1f255d 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/google/cel-go v0.26.1 github.com/google/go-cmp v0.7.0 github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/lib/pq v1.10.9 github.com/mitchellh/copystructure v1.2.0 github.com/onsi/gomega v1.39.1 github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98 @@ -157,7 +158,6 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/internal/action/config.go b/internal/action/config.go index 61a42e201..b5a07a9c6 100644 --- a/internal/action/config.go +++ b/internal/action/config.go @@ -20,6 +20,8 @@ import ( "context" "fmt" "log/slog" + "strings" + "sync" helmaction "helm.sh/helm/v4/pkg/action" helmstorage "helm.sh/helm/v4/pkg/storage" @@ -31,10 +33,88 @@ import ( "github.com/fluxcd/helm-controller/internal/storage" ) -const ( - // DefaultStorageDriver is the default Helm storage driver. - DefaultStorageDriver = helmdriver.SecretsDriverName -) +// DefaultStorageDriver is the default Helm storage driver. +const DefaultStorageDriver = helmdriver.SecretsDriverName + +// NormalizeStorageDriverName maps a user-supplied storage driver name to its +// canonical Helm constant. The accepted forms mirror the Helm CLI / SDK +// behaviour (see helm.sh/helm/v4/pkg/action.(*Configuration).Init), allowing +// both "secret"/"secrets" and "configmap"/"configmaps" interchangeably and +// matching case-insensitively. An empty name maps to DefaultStorageDriver. +// The boolean return is false when name is not a recognised driver. +func NormalizeStorageDriverName(name string) (string, bool) { + switch strings.ToLower(name) { + case "": + return DefaultStorageDriver, true + case strings.ToLower(helmdriver.SecretsDriverName), "secrets": + return helmdriver.SecretsDriverName, true + case strings.ToLower(helmdriver.ConfigMapsDriverName), "configmaps": + return helmdriver.ConfigMapsDriverName, true + case strings.ToLower(helmdriver.MemoryDriverName): + return helmdriver.MemoryDriverName, true + case strings.ToLower(helmdriver.SQLDriverName): + return helmdriver.SQLDriverName, true + } + return "", false +} + +// IsSupportedStorageDriver reports whether name is a recognised storage driver +// name. The accepted forms match those of the Helm CLI's HELM_DRIVER variable, +// matched case-insensitively. An empty name is treated as the default driver. +func IsSupportedStorageDriver(name string) bool { + _, ok := NormalizeStorageDriverName(name) + return ok +} + +// SQLDriverPool returns Helm SQL storage drivers keyed by storage namespace, +// reusing the underlying database handle across reconciliations to avoid +// opening a fresh connection pool on every action. +// +// Helm v4 does not expose a Close method on storage.Driver, so the connection +// pools opened by helmdriver.NewSQL are released only when the controller +// process exits. The pool therefore caches drivers per storage namespace, +// bounding the resource usage to the set of namespaces actually in use. +type SQLDriverPool struct { + connectionString string + factory func(connection, namespace string) (helmdriver.Driver, error) + + mu sync.Mutex + drivers map[string]helmdriver.Driver +} + +// NewSQLDriverPool returns an SQLDriverPool that lazily opens a Helm SQL driver +// per namespace using the given connection string. +func NewSQLDriverPool(connectionString string) *SQLDriverPool { + return &SQLDriverPool{ + connectionString: connectionString, + factory: func(connection, namespace string) (helmdriver.Driver, error) { + return helmdriver.NewSQL(connection, namespace) + }, + drivers: make(map[string]helmdriver.Driver), + } +} + +// SetFactory replaces the driver constructor; intended for tests. It is not +// safe to call concurrently with DriverFor. +func (p *SQLDriverPool) SetFactory(f func(connection, namespace string) (helmdriver.Driver, error)) { + p.factory = f +} + +// DriverFor returns the cached SQL driver for the given storage namespace, +// constructing a new one on first use. +func (p *SQLDriverPool) DriverFor(namespace string) (helmdriver.Driver, error) { + p.mu.Lock() + defer p.mu.Unlock() + if d, ok := p.drivers[namespace]; ok { + return d, nil + } + d, err := p.factory(p.connectionString, namespace) + if err != nil { + return nil, err + } + p.drivers[namespace] = d + return d, nil +} // ConfigFactory is a factory for the Helm action configuration of a (series // of) Helm action(s). It allows for sharing Kubernetes client(s) and the @@ -51,6 +131,9 @@ type ConfigFactory struct { KubeClient *Client // Driver to use for the Helm action. Driver helmdriver.Driver + // SQLDriverPool, when set, supplies SQL storage drivers per namespace and + // is consulted by WithStorage when the SQL driver is requested. + SQLDriverPool *SQLDriverPool // StorageLog is the logger to use for the Helm storage driver. StorageLog slog.Handler // NewResourceManager is the resource manager used to evaluate custom health checks. @@ -84,38 +167,56 @@ func NewConfigFactory(getter genericclioptions.RESTClientGetter, opts ...ConfigF // WithStorage configures the ConfigFactory.Driver by constructing a new Helm // driver.Driver using the provided driver name and namespace. -// It supports driver.ConfigMapsDriverName, driver.SecretsDriverName and -// driver.MemoryDriverName. -// It returns an error when the driver name is not supported, or the client -// configuration for the storage fails. +// +// Driver names are matched case-insensitively and accept the same aliases as +// the Helm CLI: "secret"/"secrets", "configmap"/"configmaps", "memory", "sql". +// An empty driver falls back to DefaultStorageDriver. +// +// The SQL driver requires a SQLDriverPool to have been set on the +// ConfigFactory via WithSQLDriverPool. The Memory driver creates a fresh +// store on every call and is therefore only suitable for tests or +// short-lived processes. +// +// It returns an error when the driver name is not supported, the namespace +// is empty, or the underlying client/storage configuration fails. func WithStorage(driver, namespace string) ConfigFactoryOption { - if driver == "" { - driver = DefaultStorageDriver - } + canonical, ok := NormalizeStorageDriverName(driver) return func(f *ConfigFactory) error { + if !ok { + return fmt.Errorf("unsupported Helm storage driver '%s'", driver) + } if namespace == "" { - return fmt.Errorf("no namespace provided for '%s' storage driver", driver) + return fmt.Errorf("no namespace provided for Helm storage driver '%s'", canonical) } - switch driver { - case helmdriver.SecretsDriverName, helmdriver.ConfigMapsDriverName, "": + switch canonical { + case helmdriver.SecretsDriverName, helmdriver.ConfigMapsDriverName: clientSet, err := f.KubeClient.Factory.KubernetesClientSet() if err != nil { - return fmt.Errorf("could not get client set for '%s' storage driver: %w", driver, err) + return fmt.Errorf("could not get client set for '%s' storage driver: %w", canonical, err) } - if driver == helmdriver.ConfigMapsDriverName { + if canonical == helmdriver.ConfigMapsDriverName { f.Driver = helmdriver.NewConfigMaps(clientSet.CoreV1().ConfigMaps(namespace)) - } - if driver == helmdriver.SecretsDriverName { + } else { f.Driver = helmdriver.NewSecrets(clientSet.CoreV1().Secrets(namespace)) } case helmdriver.MemoryDriverName: - driver := helmdriver.NewMemory() - driver.SetNamespace(namespace) - f.Driver = driver - default: - return fmt.Errorf("unsupported Helm storage driver '%s'", driver) + d := helmdriver.NewMemory() + d.SetNamespace(namespace) + f.Driver = d + case helmdriver.SQLDriverName: + if f.SQLDriverPool == nil { + return fmt.Errorf("Helm storage driver '%s' requires a SQL driver pool", canonical) + } + sqlDriver, err := f.SQLDriverPool.DriverFor(namespace) + if err != nil { + // Underlying errors from helmdriver.NewSQL / sqlx.Connect can + // echo the connection string; surface a generic message and + // keep the detail off the HelmRelease status condition. + return fmt.Errorf("could not initialize Helm SQL storage driver: connection failed") + } + f.Driver = sqlDriver } return nil } @@ -129,6 +230,15 @@ func WithDriver(driver helmdriver.Driver) ConfigFactoryOption { } } +// WithSQLDriverPool sets the ConfigFactory.SQLDriverPool. The pool is consulted +// by WithStorage when the SQL driver is requested. +func WithSQLDriverPool(pool *SQLDriverPool) ConfigFactoryOption { + return func(f *ConfigFactory) error { + f.SQLDriverPool = pool + return nil + } +} + // WithStorageLog sets the ConfigFactory.StorageLog. func WithStorageLog(log slog.Handler) ConfigFactoryOption { return func(f *ConfigFactory) error { diff --git a/internal/action/config_test.go b/internal/action/config_test.go index 1c82b6cbf..d54af9628 100644 --- a/internal/action/config_test.go +++ b/internal/action/config_test.go @@ -19,6 +19,7 @@ package action import ( "errors" "log/slog" + "strings" "testing" . "github.com/onsi/gomega" @@ -33,6 +34,26 @@ import ( "github.com/fluxcd/helm-controller/internal/testutil" ) +// fakeSQLDriverPool returns a pool whose factory yields a memory-backed +// driver that reports its Name as SQL. +func fakeSQLDriverPool() *SQLDriverPool { + pool := NewSQLDriverPool("postgres://user@example.com:5432/helm?sslmode=disable") + pool.SetFactory(func(_ string, namespace string) (helmdriver.Driver, error) { + d := helmdriver.NewMemory() + d.SetNamespace(namespace) + return fakeSQLDriver{Driver: d}, nil + }) + return pool +} + +type fakeSQLDriver struct { + helmdriver.Driver +} + +func (f fakeSQLDriver) Name() string { + return helmdriver.SQLDriverName +} + func TestNewConfigFactory(t *testing.T) { tests := []struct { name string @@ -80,6 +101,68 @@ func TestNewConfigFactory(t *testing.T) { } } +func TestIsSupportedStorageDriver(t *testing.T) { + g := NewWithT(t) + + g.Expect(IsSupportedStorageDriver("")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("secret")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("Secret")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("SECRET")).To(BeTrue()) + // Plural aliases match Helm CLI's HELM_DRIVER behaviour. + g.Expect(IsSupportedStorageDriver("secrets")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("Secrets")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("configmap")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("ConfigMap")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("configmaps")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("memory")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("Memory")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("sql")).To(BeTrue()) + g.Expect(IsSupportedStorageDriver("SQL")).To(BeTrue()) + + g.Expect(IsSupportedStorageDriver("postgres")).To(BeFalse()) + g.Expect(IsSupportedStorageDriver(" secret ")).To(BeFalse()) +} + +func TestSQLDriverPool(t *testing.T) { + t.Run("caches one driver per namespace", func(t *testing.T) { + g := NewWithT(t) + + var calls int + pool := NewSQLDriverPool("ignored") + pool.SetFactory(func(_ string, ns string) (helmdriver.Driver, error) { + calls++ + d := helmdriver.NewMemory() + d.SetNamespace(ns) + return fakeSQLDriver{Driver: d}, nil + }) + + first, err := pool.DriverFor("foo") + g.Expect(err).ToNot(HaveOccurred()) + again, err := pool.DriverFor("foo") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(again).To(BeIdenticalTo(first)) + g.Expect(calls).To(Equal(1)) + + other, err := pool.DriverFor("bar") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(other).ToNot(BeIdenticalTo(first)) + g.Expect(calls).To(Equal(2)) + }) + + t.Run("propagates factory errors", func(t *testing.T) { + g := NewWithT(t) + + pool := NewSQLDriverPool("ignored") + pool.SetFactory(func(_ string, _ string) (helmdriver.Driver, error) { + return nil, errors.New("boom") + }) + + d, err := pool.DriverFor("foo") + g.Expect(err).To(MatchError("boom")) + g.Expect(d).To(BeNil()) + }) +} + func TestWithStorage(t *testing.T) { tests := []struct { name string @@ -106,6 +189,24 @@ func TestWithStorage(t *testing.T) { }, wantDriver: helmdriver.SecretsDriverName, }, + { + name: "lowercase secret", + driverName: strings.ToLower(helmdriver.SecretsDriverName), + namespace: "default", + factory: ConfigFactory{ + KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())}, + }, + wantDriver: helmdriver.SecretsDriverName, + }, + { + name: "secrets alias", + driverName: "secrets", + namespace: "default", + factory: ConfigFactory{ + KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())}, + }, + wantDriver: helmdriver.SecretsDriverName, + }, { name: helmdriver.ConfigMapsDriverName, driverName: helmdriver.ConfigMapsDriverName, @@ -115,6 +216,24 @@ func TestWithStorage(t *testing.T) { }, wantDriver: helmdriver.ConfigMapsDriverName, }, + { + name: "lowercase configmap", + driverName: strings.ToLower(helmdriver.ConfigMapsDriverName), + namespace: "default", + factory: ConfigFactory{ + KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())}, + }, + wantDriver: helmdriver.ConfigMapsDriverName, + }, + { + name: "configmaps alias", + driverName: "configmaps", + namespace: "default", + factory: ConfigFactory{ + KubeClient: &Client{Client: helmkube.New(cmdtest.NewTestFactory())}, + }, + wantDriver: helmdriver.ConfigMapsDriverName, + }, { name: helmdriver.MemoryDriverName, driverName: helmdriver.MemoryDriverName, @@ -122,12 +241,47 @@ func TestWithStorage(t *testing.T) { factory: ConfigFactory{}, wantDriver: helmdriver.MemoryDriverName, }, + { + name: "uppercase sql", + driverName: helmdriver.SQLDriverName, + namespace: "default", + factory: ConfigFactory{ + SQLDriverPool: fakeSQLDriverPool(), + }, + wantDriver: helmdriver.SQLDriverName, + }, + { + name: "lowercase sql", + driverName: strings.ToLower(helmdriver.SQLDriverName), + namespace: "default", + factory: ConfigFactory{ + SQLDriverPool: fakeSQLDriverPool(), + }, + wantDriver: helmdriver.SQLDriverName, + }, { name: "invalid namespace", driverName: helmdriver.SecretsDriverName, namespace: "", factory: ConfigFactory{}, - wantErr: errors.New("no namespace provided for Helm storage driver 'secrets'"), + wantErr: errors.New("no namespace provided for Helm storage driver '" + helmdriver.SecretsDriverName + "'"), + }, + { + name: "invalid namespace via alias", + driverName: "secrets", + namespace: "", + factory: ConfigFactory{}, + // Error message is normalised to the canonical Helm constant. + wantErr: errors.New("no namespace provided for Helm storage driver '" + helmdriver.SecretsDriverName + "'"), + }, + { + name: "sql with empty namespace", + driverName: helmdriver.SQLDriverName, + namespace: "", + factory: ConfigFactory{ + SQLDriverPool: fakeSQLDriverPool(), + }, + wantErr: errors.New("no namespace provided for Helm storage driver 'SQL'"), }, { name: "invalid driver", @@ -136,6 +290,29 @@ func TestWithStorage(t *testing.T) { factory: ConfigFactory{}, wantErr: errors.New("unsupported Helm storage driver 'invalid'"), }, + { + name: "sql without pool", + driverName: helmdriver.SQLDriverName, + namespace: "default", + factory: ConfigFactory{}, + wantErr: errors.New("Helm storage driver 'SQL' requires a SQL driver pool"), + }, + { + name: "sql with failing pool", + driverName: helmdriver.SQLDriverName, + namespace: "default", + factory: ConfigFactory{ + SQLDriverPool: func() *SQLDriverPool { + p := NewSQLDriverPool("postgres://user:secret@host:5432/db") + p.SetFactory(func(_ string, _ string) (helmdriver.Driver, error) { + return nil, errors.New("dial error: postgres://user:secret@host:5432/db: refused") + }) + return p + }(), + }, + // Underlying error must NOT be surfaced (it can echo the DSN). + wantErr: errors.New("could not initialize Helm SQL storage driver: connection failed"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -144,7 +321,7 @@ func TestWithStorage(t *testing.T) { factory := tt.factory err := WithStorage(tt.driverName, tt.namespace)(&factory) if tt.wantErr != nil { - g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(tt.wantErr.Error())) g.Expect(factory.Driver).To(BeNil()) return } @@ -165,6 +342,15 @@ func TestWithDriver(t *testing.T) { g.Expect(factory.Driver).To(Equal(driver)) } +func TestWithSQLDriverPool(t *testing.T) { + g := NewWithT(t) + + factory := &ConfigFactory{} + pool := NewSQLDriverPool("ignored") + g.Expect(WithSQLDriverPool(pool)(factory)).NotTo(HaveOccurred()) + g.Expect(factory.SQLDriverPool).To(BeIdenticalTo(pool)) +} + func TestStorageLog(t *testing.T) { g := NewWithT(t) diff --git a/internal/controller/helmrelease_controller.go b/internal/controller/helmrelease_controller.go index e0c3315da..96788eaed 100644 --- a/internal/controller/helmrelease_controller.go +++ b/internal/controller/helmrelease_controller.go @@ -100,6 +100,11 @@ type HelmReleaseReconciler struct { APIReader client.Reader TokenCache *cache.TokenCache + // Helm storage configuration + + HelmStorageDriver string + HelmStorageSQLPool *action.SQLDriverPool + // Retry and requeue configuration DependencyRequeueInterval time.Duration @@ -402,7 +407,8 @@ func (r *HelmReleaseReconciler) reconcileRelease(ctx context.Context, // Construct config factory for any further Helm actions. cfg, err := action.NewConfigFactory(getter, - action.WithStorage(action.DefaultStorageDriver, obj.Status.StorageNamespace), + action.WithSQLDriverPool(r.HelmStorageSQLPool), + action.WithStorage(r.HelmStorageDriver, obj.Status.StorageNamespace), action.WithStorageLog(action.NewTraceLogger(ctx)), action.WithResourceManager(resourceManager), action.WithWaitContext(ctx), @@ -581,7 +587,8 @@ func (r *HelmReleaseReconciler) reconcileUninstall(ctx context.Context, getter g // Construct config factory for current release first to validate // storage configuration before building the resource manager. cfg, err := action.NewConfigFactory(getter, - action.WithStorage(action.DefaultStorageDriver, obj.Status.StorageNamespace), + action.WithSQLDriverPool(r.HelmStorageSQLPool), + action.WithStorage(r.HelmStorageDriver, obj.Status.StorageNamespace), action.WithStorageLog(action.NewTraceLogger(ctx)), action.WithWaitContext(ctx), ) diff --git a/main.go b/main.go index a2153982e..6da0a3d9b 100644 --- a/main.go +++ b/main.go @@ -17,12 +17,16 @@ limitations under the License. package main import ( + "context" + "database/sql" "fmt" "os" "time" + _ "github.com/lib/pq" flag "github.com/spf13/pflag" "helm.sh/helm/v4/pkg/kube" + helmdriver "helm.sh/helm/v4/pkg/storage/driver" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -63,7 +67,13 @@ import ( "github.com/fluxcd/helm-controller/internal/oomwatch" ) -const controllerName = "helm-controller" +const ( + controllerName = "helm-controller" + + // helmDriverSQLConnectionStringEnv names the environment variable from which + // the connection string for the Helm SQL storage driver is read. + helmDriverSQLConnectionStringEnv = "HELM_DRIVER_SQL_CONNECTION_STRING" +) var ( scheme = runtime.NewScheme() @@ -109,6 +119,7 @@ func main() { disallowedFieldManagers []string tokenCacheOptions cache.TokenFlags defaultKubeConfigServiceAccount string + helmStorageDriver string ) flag.StringVar(&metricsAddr, "metrics-addr", ":8080", @@ -143,6 +154,11 @@ func main() { "The algorithm to use to calculate the digest of Helm release storage snapshots.") flag.StringArrayVar(&disallowedFieldManagers, "override-manager", []string{}, "List of field managers to override during drift detection.") + flag.StringVar(&helmStorageDriver, "helm-storage-driver", "", + "The Helm storage driver used to store release information. One of: secret, configmap, memory, sql "+ + "(case-insensitive). If unset, the HELM_DRIVER environment variable is consulted, and if also unset "+ + "the controller defaults to secret. The 'memory' driver is not persistent across reconciles and is "+ + "intended for tests only. The 'sql' driver requires HELM_DRIVER_SQL_CONNECTION_STRING.") clientOptions.BindFlags(flag.CommandLine) logOptions.BindFlags(flag.CommandLine) @@ -213,6 +229,58 @@ func main() { watchNamespace = os.Getenv("RUNTIME_NAMESPACE") } + // Resolve --helm-storage-driver, falling back to HELM_DRIVER and finally the + // built-in default. Validation is intentionally up-front so an invalid value + // fails the controller process rather than every HelmRelease reconcile. + if helmStorageDriver == "" { + if driverEnv, ok := os.LookupEnv("HELM_DRIVER"); ok && driverEnv != "" { + helmStorageDriver = driverEnv + } else { + helmStorageDriver = action.DefaultStorageDriver + } + } + canonicalDriver, ok := action.NormalizeStorageDriverName(helmStorageDriver) + if !ok { + setupLog.Error(fmt.Errorf("unsupported Helm storage driver %q", helmStorageDriver), + "valid values are secret, secrets, configmap, configmaps, memory, sql") + os.Exit(1) + } + helmStorageDriver = canonicalDriver + + var helmStorageSQLPool *action.SQLDriverPool + if helmStorageDriver == helmdriver.SQLDriverName { + connectionString := os.Getenv(helmDriverSQLConnectionStringEnv) + if connectionString == "" { + setupLog.Error(fmt.Errorf("%s must be set when --helm-storage-driver=sql", helmDriverSQLConnectionStringEnv), + "missing Helm SQL connection string") + os.Exit(1) + } + // Eagerly verify the DSN and database reachability at startup, rather + // than letting every HelmRelease reconcile fail. Use database/sql + // directly so we can Close() the probe connection — Helm v4 does not + // expose Close on storage.Driver, so going through the pool would leak + // a permanent connection. + probe, err := sql.Open("postgres", connectionString) + if err != nil { + setupLog.Error(fmt.Errorf("invalid Helm SQL connection string"), + "check HELM_DRIVER_SQL_CONNECTION_STRING") + os.Exit(1) + } + probeCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + if pingErr := probe.PingContext(probeCtx); pingErr != nil { + cancel() + _ = probe.Close() + setupLog.Error(fmt.Errorf("failed to reach Helm SQL storage backend"), + "check HELM_DRIVER_SQL_CONNECTION_STRING and database availability") + os.Exit(1) + } + cancel() + _ = probe.Close() + + helmStorageSQLPool = action.NewSQLDriverPool(connectionString) + } + setupLog.Info("Helm storage driver configured", "driver", helmStorageDriver) + watchSelector, err := helper.GetWatchSelector(watchOptions) if err != nil { setupLog.Error(err, "unable to configure watch label selector for manager") @@ -398,6 +466,8 @@ func main() { ArtifactFetchTimeout: httpTimeout, AllowExternalArtifact: allowExternalArtifact, DisallowedFieldManagers: disallowedFieldManagers, + HelmStorageDriver: helmStorageDriver, + HelmStorageSQLPool: helmStorageSQLPool, }).SetupWithManager(ctx, mgr, controller.HelmReleaseReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), WatchConfigs: watchConfigs,