Skip to content

Commit ceb6d37

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 ceb6d37

File tree

5 files changed

+112
-35
lines changed

5 files changed

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

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.
130+
// Controller implements an API. A Controller manages a work queue fed reconcile.Requests
131+
// from source.Sources. Work is performed through the reconcile.Reconciler for each enqueued item.
90132
// Work typically is reads and writes Kubernetes objects to make the system state match the state specified
91133
// in the object Spec.
92134
type Controller = TypedController[reconcile.Request]
@@ -119,7 +161,8 @@ func New(name string, mgr manager.Manager, options Options) (Controller, error)
119161
//
120162
// The name must be unique as it is used to identify the controller in metrics and logs.
121163
func NewTyped[request comparable](name string, mgr manager.Manager, options TypedOptions[request]) (TypedController[request], error) {
122-
c, err := NewTypedUnmanaged(name, mgr, options)
164+
options.DefaultFromConfig(mgr.GetControllerOptions())
165+
c, err := NewTypedUnmanaged(name, options)
123166
if err != nil {
124167
return nil, err
125168
}
@@ -132,14 +175,14 @@ func NewTyped[request comparable](name string, mgr manager.Manager, options Type
132175
// caller is responsible for starting the returned controller.
133176
//
134177
// 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)
178+
func NewUnmanaged(name string, options Options) (Controller, error) {
179+
return NewTypedUnmanaged(name, options)
137180
}
138181

139182
// NewTypedUnmanaged returns a new typed controller without adding it to the manager.
140183
//
141184
// 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) {
185+
func NewTypedUnmanaged[request comparable](name string, options TypedOptions[request]) (TypedController[request], error) {
143186
if options.Reconciler == nil {
144187
return nil, fmt.Errorf("must specify Reconciler")
145188
}
@@ -148,18 +191,14 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
148191
return nil, fmt.Errorf("must specify Name for Controller")
149192
}
150193

151-
if options.SkipNameValidation == nil {
152-
options.SkipNameValidation = mgr.GetControllerOptions().SkipNameValidation
153-
}
154-
155194
if options.SkipNameValidation == nil || !*options.SkipNameValidation {
156195
if err := checkName(name); err != nil {
157196
return nil, err
158197
}
159198
}
160199

161200
if options.LogConstructor == nil {
162-
log := mgr.GetLogger().WithValues(
201+
log := options.Logger.WithValues(
163202
"controller", name,
164203
)
165204
options.LogConstructor = func(in *request) logr.Logger {
@@ -175,23 +214,15 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
175214
}
176215

177216
if options.MaxConcurrentReconciles <= 0 {
178-
if mgr.GetControllerOptions().MaxConcurrentReconciles > 0 {
179-
options.MaxConcurrentReconciles = mgr.GetControllerOptions().MaxConcurrentReconciles
180-
} else {
181-
options.MaxConcurrentReconciles = 1
182-
}
217+
options.MaxConcurrentReconciles = 1
183218
}
184219

185220
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-
}
221+
options.CacheSyncTimeout = 2 * time.Minute
191222
}
192223

193224
if options.RateLimiter == nil {
194-
if ptr.Deref(mgr.GetControllerOptions().UsePriorityQueue, false) {
225+
if ptr.Deref(options.UsePriorityQueue, false) {
195226
options.RateLimiter = workqueue.NewTypedItemExponentialFailureRateLimiter[request](5*time.Millisecond, 1000*time.Second)
196227
} else {
197228
options.RateLimiter = workqueue.DefaultTypedControllerRateLimiter[request]()
@@ -200,9 +231,9 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
200231

201232
if options.NewQueue == nil {
202233
options.NewQueue = func(controllerName string, rateLimiter workqueue.TypedRateLimiter[request]) workqueue.TypedRateLimitingInterface[request] {
203-
if ptr.Deref(mgr.GetControllerOptions().UsePriorityQueue, false) {
234+
if ptr.Deref(options.UsePriorityQueue, false) {
204235
return priorityqueue.New(controllerName, func(o *priorityqueue.Opts[request]) {
205-
o.Log = mgr.GetLogger().WithValues("controller", controllerName)
236+
o.Log = options.Logger.WithValues("controller", controllerName)
206237
o.RateLimiter = rateLimiter
207238
})
208239
}
@@ -212,14 +243,6 @@ func NewTypedUnmanaged[request comparable](name string, mgr manager.Manager, opt
212243
}
213244
}
214245

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-
223246
// Create controller with dependencies set
224247
return &controller.Controller[request]{
225248
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)