@@ -34,13 +34,121 @@ const (
3434 AnnStatus = "object-lease-controller.ullberg.io/lease-status"
3535)
3636
37+ // ParseParams holds runtime configuration parsed from flags and environment.
38+ type ParseParams struct {
39+ Group string
40+ Version string
41+ Kind string
42+ OptInLabelKey string
43+ OptInLabelValue string
44+ MetricsBindAddress string
45+ HealthProbeBindAddress string
46+ PprofBindAddress string
47+ LeaderElectionEnabled bool
48+ LeaderElectionNamespace string
49+ }
50+
3751var (
3852 setupLog = ctrl .Log .WithName ("setup" )
3953)
4054
55+ // Allow injection for testing
56+ var statFn = os .Stat
57+ var readFileFn = os .ReadFile
58+
4159func main () {
4260 ctrl .SetLogger (zap .New ())
4361
62+ params := parseParameters ()
63+
64+ enableLeaderElection , leaderElectionNamespace , errE := parseLeaderElectionConfig (params .LeaderElectionEnabled , params .LeaderElectionNamespace )
65+ if errE != nil {
66+ fmt .Printf ("%v\n " , errE )
67+ os .Exit (1 )
68+ }
69+
70+ if params .Version == "" || params .Kind == "" {
71+ fmt .Println ("Usage: lease-controller -group=GROUP -version=VERSION -kind=KIND [--leader-elect] [--leader-elect-namespace=NAMESPACE]" )
72+ fmt .Println ("Or set LEASE_GVK_GROUP, LEASE_GVK_VERSION, LEASE_GVK_KIND, LEASE_LEADER_ELECTION env vars" )
73+ os .Exit (1 )
74+ }
75+
76+ scheme := runtime .NewScheme ()
77+ _ = corev1 .AddToScheme (scheme )
78+
79+ gvk := schema.GroupVersionKind {
80+ Group : params .Group ,
81+ Version : params .Version ,
82+ Kind : params .Kind ,
83+ }
84+
85+ // Use a unique leader election ID per GVK in lower case
86+ leaderElectionID := strings .ToLower (fmt .Sprintf ("object-lease-controller-%s-%s-%s" , params .Group , params .Version , params .Kind ))
87+
88+ mgrOpts := buildManagerOptions (scheme , params .Group , params .Version , params .Kind , params .MetricsBindAddress , params .HealthProbeBindAddress , params .PprofBindAddress , enableLeaderElection , leaderElectionNamespace )
89+
90+ mgr , err := ctrl .NewManager (ctrl .GetConfigOrDie (), mgrOpts )
91+ if err != nil {
92+ setupLog .Error (err , "unable to start manager" )
93+ panic (err )
94+ }
95+
96+ // Create a LeaseWatcher for the specified GVK
97+ lw := newLeaseWatcher (mgr , gvk , leaderElectionID )
98+
99+ if tr , err := configureNamespaceReconciler (mgr , params .OptInLabelKey , params .OptInLabelValue , leaderElectionID ); err != nil {
100+ setupLog .Error (err , "unable to create controller" , "GVK" , gvk )
101+ panic (err )
102+ } else {
103+ lw .Tracker = tr
104+ }
105+
106+ // Register the LeaseWatcher with the manager
107+ if err := lw .SetupWithManager (mgr ); err != nil {
108+ setupLog .Error (err , "unable to create controller" , "GVK" , gvk )
109+ panic (err )
110+ }
111+
112+ // Add metrics server expvar handler
113+ if params .MetricsBindAddress != "" {
114+ setupLog .Info ("Adding /debug/vars to metrics" , "address" , params .MetricsBindAddress )
115+ if err := mgr .AddMetricsServerExtraHandler ("/debug/vars" , expvar .Handler ()); err != nil {
116+ setupLog .Error (err , "unable to set up metrics server extra handler" )
117+ os .Exit (1 )
118+ }
119+ }
120+
121+ healthCheck := func (req * http.Request ) error {
122+ return healthCheck (req , mgr , gvk )
123+ }
124+
125+ if err := mgr .AddHealthzCheck ("gvk" , healthCheck ); err != nil {
126+ setupLog .Error (err , "unable to set up health check" )
127+ os .Exit (1 )
128+ }
129+
130+ // Ready check: verify manager cache is synced
131+ readyCheck := func (req * http.Request ) error {
132+ if ! mgr .GetCache ().WaitForCacheSync (req .Context ()) {
133+ return fmt .Errorf ("cache not synced" )
134+ }
135+ return nil
136+ }
137+ if err := mgr .AddReadyzCheck ("readyz" , readyCheck ); err != nil {
138+ setupLog .Error (err , "unable to set up ready check" )
139+ os .Exit (1 )
140+ }
141+
142+ setupLog .Info ("Starting manager" , "group" , params .Group , "version" , params .Version , "kind" , params .Kind , "leaderElectionID" , leaderElectionID )
143+ if err := mgr .Start (ctrl .SetupSignalHandler ()); err != nil {
144+ setupLog .Error (err , "problem running manager" )
145+ panic (err )
146+ }
147+ }
148+
149+ // parseParameters returns a ParseParams struct instead of a tuple to make it
150+ // easier to extend and pass around in tests.
151+ func parseParameters () ParseParams {
44152 var group , version , kind string
45153 var optInLabelKey , optInLabelValue string
46154 flag .StringVar (& group , "group" , "" , "Kubernetes API group (e.g., \" apps\" )" )
@@ -84,56 +192,71 @@ func main() {
84192 optInLabelValue = os .Getenv ("LEASE_OPT_IN_LABEL_VALUE" )
85193 }
86194
195+ // Leader election may be enabled via env var when not set via flags
196+ if ! enableLeaderElection {
197+ if ele := os .Getenv ("LEASE_LEADER_ELECTION" ); ele != "" {
198+ if strings .EqualFold (ele , "true" ) || ele == "1" {
199+ enableLeaderElection = true
200+ }
201+ }
202+ }
203+
204+ // If no leader election namespace was provided via flags, allow env fallback.
205+ if leaderElectionNamespace == "" {
206+ leaderElectionNamespace = os .Getenv ("LEASE_LEADER_ELECTION_NAMESPACE" )
207+ }
208+
209+ return ParseParams {
210+ Group : group ,
211+ Version : version ,
212+ Kind : kind ,
213+ OptInLabelKey : optInLabelKey ,
214+ OptInLabelValue : optInLabelValue ,
215+ MetricsBindAddress : metricsAddr ,
216+ HealthProbeBindAddress : probeAddr ,
217+ PprofBindAddress : pprofAddr ,
218+ LeaderElectionEnabled : enableLeaderElection ,
219+ LeaderElectionNamespace : leaderElectionNamespace ,
220+ }
221+ }
222+
223+ // Parse leader election configuration from flags and environment.
224+ // Returns (enabled, namespace, error)
225+ func parseLeaderElectionConfig (enableLeaderElection bool , leaderElectionNamespace string ) (bool , string , error ) {
87226 if ! enableLeaderElection {
88227 if val := os .Getenv ("LEASE_LEADER_ELECTION" ); val != "" {
89228 var err error
90229 enableLeaderElection , err = strconv .ParseBool (val )
91230 if err != nil {
92- fmt .Printf ("Invalid LEASE_LEADER_ELECTION value: %v\n " , err )
93- os .Exit (1 )
231+ return false , "" , fmt .Errorf ("invalid LEASE_LEADER_ELECTION value: %v" , err )
94232 }
95233
96- // If leader election is enabled, check for namespace and fail if not set and not running in a cluster
97234 if enableLeaderElection && leaderElectionNamespace == "" {
98235 leaderElectionNamespace = os .Getenv ("LEASE_LEADER_ELECTION_NAMESPACE" )
99236 }
100- if leaderElectionNamespace == "" {
101- // If running outside a cluster, we need a namespace for leader election
102- if _ , err := os .Stat ("/var/run/secrets/kubernetes.io/serviceaccount/namespace" ); os .IsNotExist (err ) {
103- fmt .Println ("Leader election enabled but LEASE_LEADER_ELECTION_NAMESPACE is not set. Please set it to a valid namespace." )
104- os .Exit (1 )
105- } else {
106- // Default to the namespace file if running in a cluster
107- data , _ := os .ReadFile ("/var/run/secrets/kubernetes.io/serviceaccount/namespace" )
237+
238+ if enableLeaderElection && leaderElectionNamespace == "" {
239+ if _ , err := statFn ("/var/run/secrets/kubernetes.io/serviceaccount/namespace" ); os .IsNotExist (err ) {
240+ return false , "" , fmt .Errorf ("leader election enabled but LEASE_LEADER_ELECTION_NAMESPACE is not set; set it to a valid namespace" )
241+ }
242+ // we're in a cluster; default to serviceaccount namespace
243+ if data , err := readFileFn ("/var/run/secrets/kubernetes.io/serviceaccount/namespace" ); err == nil {
108244 leaderElectionNamespace = strings .TrimSpace (string (data ))
109245 }
110246 }
111247 }
112248 }
113249
114- if version == "" || kind == "" {
115- fmt .Println ("Usage: lease-controller -group=GROUP -version=VERSION -kind=KIND [--leader-elect] [--leader-elect-namespace=NAMESPACE]" )
116- fmt .Println ("Or set LEASE_GVK_GROUP, LEASE_GVK_VERSION, LEASE_GVK_KIND, LEASE_LEADER_ELECTION env vars" )
117- os .Exit (1 )
118- }
119-
120- scheme := runtime .NewScheme ()
121- _ = corev1 .AddToScheme (scheme )
122-
123- gvk := schema.GroupVersionKind {
124- Group : group ,
125- Version : version ,
126- Kind : kind ,
250+ if leaderElectionNamespace == "" {
251+ leaderElectionNamespace = os .Getenv ("LEASE_LEADER_ELECTION_NAMESPACE" )
127252 }
253+ return enableLeaderElection , leaderElectionNamespace , nil
254+ }
128255
129- // Use a unique leader election ID per GVK in lower case
256+ // Build manager options for a given GVK and flags; extracted for unit testing
257+ func buildManagerOptions (scheme * runtime.Scheme , group , version , kind string , metricsAddr , probeAddr , pprofAddr string , enableLeaderElection bool , leaderElectionNamespace string ) ctrl.Options {
130258 leaderElectionID := strings .ToLower (fmt .Sprintf ("object-lease-controller-%s-%s-%s" , group , version , kind ))
131-
132- // Set up metrics server options
133- metricsServerOptions := metricsserver.Options {
134- BindAddress : metricsAddr ,
135- }
136-
259+ metricsServerOptions := metricsserver.Options {BindAddress : metricsAddr }
137260 mgrOpts := ctrl.Options {
138261 Scheme : scheme ,
139262 LeaderElection : enableLeaderElection ,
@@ -143,24 +266,20 @@ func main() {
143266 Metrics : metricsServerOptions ,
144267 HealthProbeBindAddress : probeAddr ,
145268 Cache : cache.Options {
146- DefaultTransform : util .MinimalObjectTransform (
147- AnnTTL , AnnLeaseStart , AnnExpireAt , AnnStatus ,
148- ),
269+ DefaultTransform : util .MinimalObjectTransform (AnnTTL , AnnLeaseStart , AnnExpireAt , AnnStatus ),
149270 },
150271 }
151-
152272 if pprofAddr != "" {
153273 mgrOpts .PprofBindAddress = pprofAddr
154274 }
275+ return mgrOpts
276+ }
155277
156- mgr , err := ctrl .NewManager (ctrl .GetConfigOrDie (), mgrOpts )
157- if err != nil {
158- setupLog .Error (err , "unable to start manager" )
159- panic (err )
160- }
161-
162- // Create a LeaseWatcher for the specified GVK
163- lw := & controllers.LeaseWatcher {
278+ // Create a LeaseWatcher attached to the given manager. The LeaseWatcher is initialized
279+ // with default annotations and metrics for the provided GVK. The function does not
280+ // call SetupWithManager - this is left to the caller.
281+ func newLeaseWatcher (mgr ctrl.Manager , gvk schema.GroupVersionKind , leaderElectionID string ) * controllers.LeaseWatcher {
282+ return & controllers.LeaseWatcher {
164283 Client : mgr .GetClient (),
165284 GVK : gvk ,
166285 Recorder : mgr .GetEventRecorderFor (leaderElectionID ),
@@ -172,68 +291,27 @@ func main() {
172291 },
173292 Metrics : ometrics .NewLeaseMetrics (gvk ),
174293 }
294+ }
175295
176- if optInLabelKey != "" && optInLabelValue != "" {
177- tracker := util .NewNamespaceTracker ()
178-
179- nw := & controllers.NamespaceReconciler {
180- Client : mgr .GetClient (),
181- Recorder : mgr .GetEventRecorderFor (leaderElectionID ),
182- LabelKey : optInLabelKey ,
183- LabelValue : optInLabelValue ,
184- Tracker : tracker ,
185- }
186-
187- // Register NamespaceReconciler with the manager
188- if err := nw .SetupWithManager (mgr ); err != nil {
189- setupLog .Error (err , "unable to create controller" , "GVK" , gvk )
190- panic (err )
191- }
192-
193- lw .Tracker = tracker
194- }
195-
196- // Register the LeaseWatcher with the manager
197- if err := lw .SetupWithManager (mgr ); err != nil {
198- setupLog .Error (err , "unable to create controller" , "GVK" , gvk )
199- panic (err )
200- }
201-
202- // Add metrics server expvar handler
203- if metricsAddr != "" {
204- setupLog .Info ("Adding /debug/vars to metrics" , "address" , metricsAddr )
205- if err := mgr .AddMetricsServerExtraHandler ("/debug/vars" , expvar .Handler ()); err != nil {
206- setupLog .Error (err , "unable to set up metrics server extra handler" )
207- os .Exit (1 )
208- }
209- }
210-
211- healthCheck := func (req * http.Request ) error {
212- return healthCheck (req , mgr , gvk )
213- }
214-
215- if err := mgr .AddHealthzCheck ("gvk" , healthCheck ); err != nil {
216- setupLog .Error (err , "unable to set up health check" )
217- os .Exit (1 )
296+ // If optInLabelKey and optInLabelValue are provided, create a NamespaceReconciler and
297+ // register it with the manager. Returns the NamespaceTracker that was created if any,
298+ // or nil if opt-in was not requested.
299+ func configureNamespaceReconciler (mgr ctrl.Manager , optInLabelKey , optInLabelValue , leaderElectionID string ) (* util.NamespaceTracker , error ) {
300+ if optInLabelKey == "" || optInLabelValue == "" {
301+ return nil , nil
218302 }
219-
220- // Ready check: verify manager cache is synced
221- readyCheck := func (req * http.Request ) error {
222- if ! mgr .GetCache ().WaitForCacheSync (req .Context ()) {
223- return fmt .Errorf ("cache not synced" )
224- }
225- return nil
226- }
227- if err := mgr .AddReadyzCheck ("readyz" , readyCheck ); err != nil {
228- setupLog .Error (err , "unable to set up ready check" )
229- os .Exit (1 )
303+ tracker := util .NewNamespaceTracker ()
304+ nw := & controllers.NamespaceReconciler {
305+ Client : mgr .GetClient (),
306+ Recorder : mgr .GetEventRecorderFor (leaderElectionID ),
307+ LabelKey : optInLabelKey ,
308+ LabelValue : optInLabelValue ,
309+ Tracker : tracker ,
230310 }
231-
232- setupLog .Info ("Starting manager" , "group" , group , "version" , version , "kind" , kind , "leaderElectionID" , leaderElectionID )
233- if err := mgr .Start (ctrl .SetupSignalHandler ()); err != nil {
234- setupLog .Error (err , "problem running manager" )
235- panic (err )
311+ if err := nw .SetupWithManager (mgr ); err != nil {
312+ return nil , err
236313 }
314+ return tracker , nil
237315}
238316
239317// Health check: confirm GVK is discoverable and listable with minimal load
0 commit comments