Skip to content

Commit d9ff283

Browse files
committed
⚠️ NewTypedUnmanaged: Stop requiring a manager
Currently, `NewUnmanaged` and `NewTypedUnmanaged` require a manager but the only properties they ever access from it are `GetControllerOptions` and `GetLogger`. It makes no sense to require a `Manager` for something that is unmanaged, so remove that. Doing so naively means that we require two options parameters for these constructors, one that has the actual options and another one to default from which again is confusing. Remove the options struct to default from and instead implement a `DefaultFromConfig` that defaults the controller options `config.Controller` which also clarifies what belongs where. This defaulting gets automatically called in the managed constructors and is exported so anyone can use it. This change is breaking only for users of `New[Typed]Unmanaged` in that: * The signature changes * The defaulting of the `controller.Options` from `config.Controller` doesn't happen anymore, but it is still possible to do that explicitly
1 parent 9024933 commit d9ff283

File tree

5 files changed

+111
-35
lines changed

5 files changed

+111
-35
lines changed

pkg/config/controller.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ limitations under the License.
1616

1717
package config
1818

19-
import "time"
19+
import (
20+
"time"
2021

21-
// Controller contains configuration options for a controller.
22+
"github.com/go-logr/logr"
23+
)
24+
25+
// Controller contains configuration options for controllers. It only includes options
26+
// that makes sense for a set of controllers and is used for defaulting the options
27+
// of multiple controllers.
2228
type Controller struct {
2329
// SkipNameValidation allows skipping the name validation that ensures that every controller name is unique.
2430
// Unique controller names are important to get unique metrics and logs for a controller.
@@ -59,4 +65,7 @@ type Controller struct {
5965
//
6066
// Note: This flag is disabled by default until a future version. It's currently in beta.
6167
UsePriorityQueue *bool
68+
69+
// Logger is the logger controllers should use.
70+
Logger logr.Logger
6271
}

pkg/controller/controller.go

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"k8s.io/klog/v2"
2727
"k8s.io/utils/ptr"
2828

29+
"sigs.k8s.io/controller-runtime/pkg/config"
2930
"sigs.k8s.io/controller-runtime/pkg/controller/priorityqueue"
3031
"sigs.k8s.io/controller-runtime/pkg/internal/controller"
3132
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -80,13 +81,53 @@ type TypedOptions[request comparable] struct {
8081
// Only use a custom NewQueue if you know what you are doing.
8182
NewQueue func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request]
8283

84+
// Logger will be used to build a default LogConstructor if unset.
85+
Logger logr.Logger
86+
8387
// LogConstructor is used to construct a logger used for this controller and passed
8488
// to each reconciliation via the context field.
8589
LogConstructor func(request *request) logr.Logger
90+
91+
// UsePriorityQueue configures the controllers queue to use the controller-runtime provided
92+
// priority queue.
93+
//
94+
// Note: This flag is disabled by default until a future version. It's currently in beta.
95+
UsePriorityQueue *bool
96+
}
97+
98+
// DefaultFromConfig defaults the config from a config.Controller
99+
func (options *TypedOptions[request]) DefaultFromConfig(config config.Controller) {
100+
if options.Logger.GetSink() == nil {
101+
options.Logger = config.Logger
102+
}
103+
104+
if options.SkipNameValidation == nil {
105+
options.SkipNameValidation = config.SkipNameValidation
106+
}
107+
108+
if options.MaxConcurrentReconciles <= 0 && config.MaxConcurrentReconciles > 0 {
109+
options.MaxConcurrentReconciles = config.MaxConcurrentReconciles
110+
}
111+
112+
if options.CacheSyncTimeout == 0 && config.CacheSyncTimeout > 0 {
113+
options.CacheSyncTimeout = config.CacheSyncTimeout
114+
}
115+
116+
if options.UsePriorityQueue == nil {
117+
options.UsePriorityQueue = config.UsePriorityQueue
118+
}
119+
120+
if options.RecoverPanic == nil {
121+
options.RecoverPanic = config.RecoverPanic
122+
}
123+
124+
if options.NeedLeaderElection == nil {
125+
options.NeedLeaderElection = config.NeedLeaderElection
126+
}
86127
}
87128

88-
// Controller implements a Kubernetes API. A Controller manages a work queue fed reconcile.Requests
89-
// from source.Sources. Work is performed through the reconcile.Reconciler for each enqueued item.
129+
// Controller implements an API. A Controller manages a work queue fed reconcile.Requests
130+
// from source.Sources. Work is performed through the reconcile.Reconciler for each enqueued item.
90131
// Work typically is reads and writes Kubernetes objects to make the system state match the state specified
91132
// in the object Spec.
92133
type Controller = TypedController[reconcile.Request]
@@ -119,7 +160,8 @@ func New(name string, mgr manager.Manager, options Options) (Controller, error)
119160
//
120161
// The name must be unique as it is used to identify the controller in metrics and logs.
121162
func NewTyped[request comparable](name string, mgr manager.Manager, options TypedOptions[request]) (TypedController[request], error) {
122-
c, err := NewTypedUnmanaged(name, mgr, options)
163+
options.DefaultFromConfig(mgr.GetControllerOptions())
164+
c, err := NewTypedUnmanaged(name, options)
123165
if err != nil {
124166
return nil, err
125167
}
@@ -132,14 +174,14 @@ func NewTyped[request comparable](name string, mgr manager.Manager, options Type
132174
// caller is responsible for starting the returned controller.
133175
//
134176
// The name must be unique as it is used to identify the controller in metrics and logs.
135-
func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller, error) {
136-
return NewTypedUnmanaged(name, mgr, options)
177+
func NewUnmanaged(name string, options Options) (Controller, error) {
178+
return NewTypedUnmanaged(name, options)
137179
}
138180

139181
// NewTypedUnmanaged returns a new typed controller without adding it to the manager.
140182
//
141183
// The name must be unique as it is used to identify the controller in metrics and logs.
142-
func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, options TypedOptions[request]) (TypedController[request], error) {
184+
func NewTypedUnmanaged[request comparable](name string, options TypedOptions[request]) (TypedController[request], error) {
143185
if options.Reconciler == nil {
144186
return nil, fmt.Errorf("must specify Reconciler")
145187
}
@@ -148,18 +190,14 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
148190
return nil, fmt.Errorf("must specify Name for Controller")
149191
}
150192

151-
if options.SkipNameValidation == nil {
152-
options.SkipNameValidation = mgr.GetControllerOptions().SkipNameValidation
153-
}
154-
155193
if options.SkipNameValidation == nil || !*options.SkipNameValidation {
156194
if err := checkName(name); err != nil {
157195
return nil, err
158196
}
159197
}
160198

161199
if options.LogConstructor == nil {
162-
log := mgr.GetLogger().WithValues(
200+
log := options.Logger.WithValues(
163201
"controller", name,
164202
)
165203
options.LogConstructor = func(in *request) logr.Logger {
@@ -175,23 +213,15 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
175213
}
176214

177215
if options.MaxConcurrentReconciles <= 0 {
178-
if mgr.GetControllerOptions().MaxConcurrentReconciles > 0 {
179-
options.MaxConcurrentReconciles = mgr.GetControllerOptions().MaxConcurrentReconciles
180-
} else {
181-
options.MaxConcurrentReconciles = 1
182-
}
216+
options.MaxConcurrentReconciles = 1
183217
}
184218

185219
if options.CacheSyncTimeout == 0 {
186-
if mgr.GetControllerOptions().CacheSyncTimeout != 0 {
187-
options.CacheSyncTimeout = mgr.GetControllerOptions().CacheSyncTimeout
188-
} else {
189-
options.CacheSyncTimeout = 2 * time.Minute
190-
}
220+
options.CacheSyncTimeout = 2 * time.Minute
191221
}
192222

193223
if options.RateLimiter == nil {
194-
if ptr.Deref(mgr.GetControllerOptions().UsePriorityQueue, false) {
224+
if ptr.Deref(options.UsePriorityQueue, false) {
195225
options.RateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[request](5*time.Millisecond, 1000*time.Second)
196226
} else {
197227
options.RateLimiter = workqueue.DefaultTypedControllerRateLimiter[request]()
@@ -200,9 +230,9 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
200230

201231
if options.NewQueue == nil {
202232
options.NewQueue = func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] {
203-
if ptr.Deref(mgr.GetControllerOptions().UsePriorityQueue, false) {
233+
if ptr.Deref(options.UsePriorityQueue, false) {
204234
return priorityqueue.New(controllerName, func(o *priorityqueue.Opts[request]) {
205-
o.Log = mgr.GetLogger().WithValues("controller", controllerName)
235+
o.Log = options.Logger.WithValues("controller", controllerName)
206236
o.RateLimiter = rateLimiter
207237
})
208238
}
@@ -212,14 +242,6 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
212242
}
213243
}
214244

215-
if options.RecoverPanic == nil {
216-
options.RecoverPanic = mgr.GetControllerOptions().RecoverPanic
217-
}
218-
219-
if options.NeedLeaderElection == nil {
220-
options.NeedLeaderElection = mgr.GetControllerOptions().NeedLeaderElection
221-
}
222-
223245
// Create controller with dependencies set
224246
return &controller.Controller[request]{
225247
Do: options.Reconciler,

pkg/controller/example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func ExampleNewUnmanaged() {
129129

130130
// Configure creates a new controller but does not add it to the supplied
131131
// manager.
132-
c, err := controller.NewUnmanaged("pod-controller", mgr, controller.Options{
132+
c, err := controller.NewUnmanaged("pod-controller", controller.Options{
133133
Reconciler: reconcile.Func(func(context.Context, reconcile.Request) (reconcile.Result, error) {
134134
return reconcile.Result{}, nil
135135
}),

pkg/manager/manager.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,10 @@ func setOptionsDefaults(options Options) Options {
544544
options.Logger = log.Log
545545
}
546546

547+
if options.Controller.Logger.GetSink() == nil {
548+
options.Controller.Logger = options.Logger
549+
}
550+
547551
if options.BaseContext == nil {
548552
options.BaseContext = defaultBaseContext
549553
}

pkg/manager/manager_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,47 @@ var _ = Describe("manger.Manager", func() {
10561056
)))
10571057
})
10581058

1059+
It("should default controller logger from manager logger", func() {
1060+
var lock sync.Mutex
1061+
var messages []string
1062+
options.Logger = funcr.NewJSON(func(object string) {
1063+
lock.Lock()
1064+
messages = append(messages, object)
1065+
lock.Unlock()
1066+
}, funcr.Options{})
1067+
options.LeaderElection = false
1068+
1069+
m, err := New(cfg, options)
1070+
Expect(err).NotTo(HaveOccurred())
1071+
for _, cb := range callbacks {
1072+
cb(m)
1073+
}
1074+
1075+
started := make(chan struct{})
1076+
Expect(m.Add(RunnableFunc(func(ctx context.Context) error {
1077+
close(started)
1078+
return nil
1079+
}))).To(Succeed())
1080+
1081+
stopped := make(chan error)
1082+
ctx, cancel := context.WithCancel(context.Background())
1083+
go func() {
1084+
stopped <- m.Start(ctx)
1085+
}()
1086+
1087+
// Wait for runnables to start as a proxy for the manager being fully started.
1088+
<-started
1089+
cancel()
1090+
Expect(<-stopped).To(Succeed())
1091+
1092+
msg := "controller log message"
1093+
m.GetControllerOptions().Logger.Info(msg)
1094+
1095+
Expect(messages).To(ContainElement(
1096+
ContainSubstring(msg),
1097+
))
1098+
})
1099+
10591100
It("should return both runnables and stop errors when both error", func() {
10601101
m, err := New(cfg, options)
10611102
Expect(err).NotTo(HaveOccurred())

0 commit comments

Comments
 (0)