Files
Reloader/internal/pkg/controller/manager.go
T

255 lines
8.4 KiB
Go

package controller
import (
"context"
"fmt"
argorolloutsv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
"github.com/go-logr/logr"
openshiftv1 "github.com/openshift/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/healthz"
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics/server"
csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1"
"github.com/stakater/Reloader/internal/pkg/alerting"
"github.com/stakater/Reloader/internal/pkg/config"
"github.com/stakater/Reloader/internal/pkg/events"
"github.com/stakater/Reloader/internal/pkg/metrics"
"github.com/stakater/Reloader/internal/pkg/reload"
"github.com/stakater/Reloader/internal/pkg/webhook"
"github.com/stakater/Reloader/internal/pkg/workload"
)
var runtimeScheme = runtime.NewScheme()
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(runtimeScheme))
}
// AddOptionalSchemes adds optional workload/resource type schemes if enabled.
func AddOptionalSchemes(argoRolloutsEnabled, deploymentConfigEnabled, csiEnabled bool) {
if argoRolloutsEnabled {
utilruntime.Must(argorolloutsv1alpha1.AddToScheme(runtimeScheme))
}
if deploymentConfigEnabled {
utilruntime.Must(openshiftv1.AddToScheme(runtimeScheme))
}
if csiEnabled {
utilruntime.Must(csiv1.AddToScheme(runtimeScheme))
}
}
// buildDefaultNamespaces returns the controller-runtime cache namespace scoping
// for the given watched namespaces. An empty input yields nil, meaning the cache
// watches all namespaces.
func buildDefaultNamespaces(namespaces []string) map[string]cache.Config {
if len(namespaces) == 0 {
return nil
}
out := make(map[string]cache.Config, len(namespaces))
for _, ns := range namespaces {
out[ns] = cache.Config{}
}
return out
}
// ManagerOptions contains options for creating a new Manager.
type ManagerOptions struct {
Config *config.Config
Log logr.Logger
Collectors *metrics.Collectors
}
// NewManager creates a new controller-runtime manager using the ambient cluster
// configuration (in-cluster or kubeconfig). It follows controller-runtime and
// operator-sdk conventions for leader election.
func NewManager(opts ManagerOptions) (ctrl.Manager, error) {
return NewManagerWithRestConfig(opts, ctrl.GetConfigOrDie())
}
// NewManagerWithRestConfig creates a new controller-runtime manager with the
// given rest.Config. NewManager delegates here; tests can call it directly with
// a pre-existing cluster configuration (set cfg.MetricsAddr/HealthAddr to "0" to
// disable those servers and avoid port conflicts).
func NewManagerWithRestConfig(opts ManagerOptions, restConfig *rest.Config) (ctrl.Manager, error) {
cfg := opts.Config
le := cfg.LeaderElection
mgrOpts := ctrl.Options{
Scheme: runtimeScheme,
Metrics: ctrlmetrics.Options{
BindAddress: cfg.MetricsAddr,
},
HealthProbeBindAddress: cfg.HealthAddr,
// Leader election configuration following operator-sdk best practices:
// - LeaderElection enables/disables leader election
// - LeaderElectionID is the name of the lease resource
// - LeaderElectionNamespace where the lease is created (defaults to pod namespace)
// - LeaderElectionReleaseOnCancel allows faster failover by releasing the lock on shutdown
LeaderElection: cfg.EnableHA,
LeaderElectionID: le.LockName,
LeaderElectionNamespace: le.Namespace,
LeaderElectionReleaseOnCancel: le.ReleaseOnCancel,
LeaseDuration: &le.LeaseDuration,
RenewDeadline: &le.RenewDeadline,
RetryPeriod: &le.RetryPeriod,
}
if nsScope := buildDefaultNamespaces(cfg.WatchedNamespaces); nsScope != nil {
mgrOpts.Cache = cache.Options{
DefaultNamespaces: nsScope,
}
opts.Log.Info("namespace filtering enabled", "namespaces", cfg.WatchedNamespaces)
}
mgr, err := ctrl.NewManager(restConfig, mgrOpts)
if err != nil {
return nil, fmt.Errorf("creating manager: %w", err)
}
// Add health and readiness probes.
// The healthz probe reports whether the manager is running.
// The readyz probe reports whether the manager is ready to serve requests.
// When leader election is enabled, readyz will fail until this instance becomes leader.
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
return nil, fmt.Errorf("setting up health check: %w", err)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
return nil, fmt.Errorf("setting up ready check: %w", err)
}
return mgr, nil
}
// SetupReconcilers sets up all reconcilers with the manager.
func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, collectors *metrics.Collectors) error {
registry := workload.NewRegistry(
workload.RegistryOptions{
ArgoRolloutsEnabled: cfg.ArgoRolloutsEnabled,
DeploymentConfigEnabled: cfg.DeploymentConfigEnabled,
RolloutStrategyAnnotation: cfg.Annotations.RolloutStrategy,
},
)
reloadService := reload.NewService(cfg, log.WithName("reload"))
eventRecorder := events.NewRecorder(mgr.GetEventRecorder("reloader"))
pauseHandler := reload.NewPauseHandler(cfg)
// Create alerter based on configuration
alerter := alerting.NewAlerter(cfg)
if cfg.Alerting.Enabled {
log.Info("alerting enabled", "sink", cfg.Alerting.Sink)
}
// Create webhook client if URL is configured
var webhookClient *webhook.Client
if cfg.WebhookURL != "" {
webhookClient = webhook.NewClient(cfg.WebhookURL, log.WithName("webhook"))
log.Info("webhook mode enabled", "url", cfg.WebhookURL)
}
// Create namespace cache if namespace selectors are configured.
// This cache is shared between the namespace reconciler and resource reconcilers.
var nsCache *NamespaceCache
if len(cfg.NamespaceSelectors) > 0 {
nsCache = NewNamespaceCache(true)
if err := (&NamespaceReconciler{
Client: mgr.GetClient(),
Log: log.WithName("namespace-reconciler"),
Config: cfg,
Cache: nsCache,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setting up namespace reconciler: %w", err)
}
log.Info("namespace reconciler enabled for label selector filtering")
}
// Setup ConfigMap reconciler
if !cfg.IsResourceIgnored("configmaps") {
cmReconciler := NewConfigMapReconciler(
mgr.GetClient(),
log.WithName("configmap-reconciler"),
cfg,
reloadService,
registry,
collectors,
eventRecorder,
webhookClient,
alerter,
pauseHandler,
nsCache,
)
if err := SetupConfigMapReconciler(mgr, cmReconciler); err != nil {
return fmt.Errorf("setting up configmap reconciler: %w", err)
}
}
// Setup Secret reconciler
if !cfg.IsResourceIgnored("secrets") {
secretReconciler := NewSecretReconciler(
mgr.GetClient(),
log.WithName("secret-reconciler"),
cfg,
reloadService,
registry,
collectors,
eventRecorder,
webhookClient,
alerter,
pauseHandler,
nsCache,
)
if err := SetupSecretReconciler(mgr, secretReconciler); err != nil {
return fmt.Errorf("setting up secret reconciler: %w", err)
}
}
// Setup SecretProviderClass reconciler (CSI integration)
if cfg.CSIIntegrationEnabled {
spcReconciler := NewSecretProviderClassReconciler(
ResourceReconcilerDeps{
Client: mgr.GetClient(),
Log: log.WithName("secretproviderclass-reconciler"),
Config: cfg,
ReloadService: reloadService,
Registry: registry,
Collectors: collectors,
EventRecorder: eventRecorder,
WebhookClient: webhookClient,
Alerter: alerter,
PauseHandler: pauseHandler,
NamespaceCache: nsCache,
},
mgr.GetAPIReader(),
)
if err := SetupSecretProviderClassReconciler(mgr, spcReconciler); err != nil {
return fmt.Errorf("setting up secretproviderclass reconciler: %w", err)
}
log.Info("CSI SecretProviderClass reconciler enabled")
}
// Setup Deployment reconciler for pause handling
if err := (&DeploymentReconciler{
Client: mgr.GetClient(),
Log: log.WithName("deployment-reconciler"),
Config: cfg,
PauseHandler: pauseHandler,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("setting up deployment reconciler: %w", err)
}
return nil
}
// RunManager starts the manager and blocks until it stops.
func RunManager(ctx context.Context, mgr ctrl.Manager, log logr.Logger) error {
log.Info("starting manager")
return mgr.Start(ctx)
}