Skip to content

Commit d170c4c

Browse files
authored
Merge pull request #32 from 3scale-sre/feat/runtimeconfig
feat: centralize scheme configuration via runtimeconfig
2 parents 4e57157 + cd198cc commit d170c4c

File tree

8 files changed

+110
-67
lines changed

8 files changed

+110
-67
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,13 @@ template := resource.NewTemplateFromObjectFunction(func() *appsv1.Deployment {
207207
Set a global default scheme to avoid passing it to every constructor:
208208

209209
```go
210-
import myscheme "github.com/myorg/myoperator/pkg/scheme"
210+
import (
211+
myscheme "github.com/myorg/myoperator/pkg/scheme"
212+
"github.com/3scale-sre/basereconciler/runtimeconfig"
213+
)
211214

212215
func init() {
213-
resource.Scheme = myscheme.Scheme // Now used by default for all templates
216+
runtimeconfig.SetDefaultScheme(myscheme.Scheme) // Now used by default for all templates
214217
}
215218
```
216219

reconciler/reconciler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/3scale-sre/basereconciler/config"
99
"github.com/3scale-sre/basereconciler/resource"
10+
"github.com/3scale-sre/basereconciler/runtimeconfig"
1011
"github.com/3scale-sre/basereconciler/util"
1112
"github.com/go-logr/logr"
1213
corev1 "k8s.io/api/core/v1"
@@ -136,6 +137,7 @@ type Reconciler struct {
136137

137138
// NewFromManager returns a new Reconciler from a controller-runtime manager.Manager
138139
func NewFromManager(mgr manager.Manager) *Reconciler {
140+
runtimeconfig.EnsureDefaultScheme(mgr.GetScheme())
139141
return &Reconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Log: logr.Discard(), mgr: mgr}
140142
}
141143

resource/doc.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,17 @@
165165
//
166166
// # Scheme Management
167167
//
168-
// The package provides a convenient default scheme (resource.Scheme) that is used for GVK inference
169-
// when no explicit scheme is provided to template constructors. This defaults to the standard
170-
// Kubernetes scheme but can be overridden globally:
168+
// The package uses a convenient shared default scheme, managed by the runtimeconfig package, for
169+
// GVK inference when no explicit scheme is provided to template constructors. This defaults to the
170+
// standard Kubernetes scheme but can be overridden globally:
171171
//
172-
// import myscheme "github.com/myorg/myoperator/pkg/scheme"
172+
// import (
173+
// myscheme "github.com/myorg/myoperator/pkg/scheme"
174+
// "github.com/3scale-sre/basereconciler/runtimeconfig"
175+
// )
173176
//
174177
// func init() {
175-
// resource.Scheme = myscheme.Scheme
178+
// runtimeconfig.SetDefaultScheme(myscheme.Scheme)
176179
// }
177180
//
178181
// This eliminates the need to pass scheme parameters to every template constructor while still
@@ -204,7 +207,7 @@
204207
// import myscheme "github.com/myorg/myoperator/pkg/scheme"
205208
//
206209
// func init() {
207-
// resource.Scheme = myscheme.Scheme // Now all templates use this by default
210+
// runtimeconfig.SetDefaultScheme(myscheme.Scheme) // Now all templates use this by default
208211
// }
209212
//
210213
// Reconcile Resource (operation type determined by template configuration):
@@ -213,8 +216,8 @@
213216
//
214217
// # Advanced Features
215218
//
216-
// Default Scheme Management: Global default scheme (resource.Scheme) eliminates the need
217-
// to pass scheme parameters to every constructor while still allowing per-call overrides.
219+
// Default Scheme Management: Global default scheme via runtimeconfig eliminates the need to pass
220+
// scheme parameters to every constructor while still allowing per-call overrides.
218221
//
219222
// Automatic GVK Inference: Templates automatically determine their resource type from
220223
// the generic type parameter, reducing boilerplate and preventing mismatches.

resource/scheme.go

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,13 @@
11
package resource
22

33
import (
4+
"github.com/3scale-sre/basereconciler/runtimeconfig"
45
"k8s.io/apimachinery/pkg/runtime"
5-
"k8s.io/client-go/kubernetes/scheme"
66
)
77

8-
// Scheme is the default runtime scheme used for GVK inference when no explicit scheme is provided
9-
// to template constructors (NewTemplate, NewTemplateFromObjectFunction) or other functions that
10-
// require scheme information.
11-
//
12-
// By default, this is set to the standard Kubernetes scheme (scheme.Scheme from client-go) which
13-
// includes all core Kubernetes types (Pod, Service, Deployment, etc.).
14-
//
15-
// Users can override this globally to include custom resources or use a different scheme:
16-
//
17-
// import myscheme "github.com/myorg/myoperator/pkg/scheme"
18-
//
19-
// func init() {
20-
// resource.Scheme = myscheme.Scheme
21-
// }
22-
//
23-
// This provides a convenient way to avoid passing the scheme parameter to every template constructor
24-
// while still allowing per-call overrides when needed.
25-
var Scheme *runtime.Scheme = scheme.Scheme
26-
278
// GetScheme returns the appropriate runtime scheme to use for GVK inference and other operations.
28-
// If explicit schemes are provided, it returns the first one. Otherwise, it returns the package
29-
// default scheme (resource.Scheme).
30-
//
31-
// This function is used internally by template constructors and other functions that need scheme
32-
// access, providing a consistent way to handle both explicit and default scheme scenarios.
33-
//
34-
// Examples:
35-
//
36-
// // Uses the default scheme (resource.Scheme)
37-
// scheme := GetScheme()
38-
//
39-
// // Uses the provided scheme, ignoring the default
40-
// scheme := GetScheme(myCustomScheme)
41-
//
42-
// // Multiple schemes provided, uses the first one
43-
// scheme := GetScheme(scheme1, scheme2) // Returns scheme1
9+
// If explicit schemes are provided, it returns the first one. Otherwise, it returns the shared
10+
// default scheme managed by the runtimeconfig package.
4411
func GetScheme(s ...*runtime.Scheme) *runtime.Scheme {
45-
if len(s) > 0 {
46-
return s[0]
47-
}
48-
return Scheme
12+
return runtimeconfig.SelectScheme(s...)
4913
}

resource/template.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"reflect"
77

88
"github.com/3scale-sre/basereconciler/config"
9+
"github.com/3scale-sre/basereconciler/runtimeconfig"
910
"github.com/3scale-sre/basereconciler/util"
1011
"k8s.io/apimachinery/pkg/runtime"
1112
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -54,7 +55,7 @@ type TemplateInterface interface {
5455
// GetGVK returns the GVK of the resource this template describes.
5556
// If the template's GVK field is already set, it returns that value.
5657
// Otherwise, it infers the GVK from the generic type T using the provided scheme,
57-
// or the package default scheme (resource.Scheme) if no scheme is provided.
58+
// or the package default scheme if no scheme is provided.
5859
GetGVK(s ...*runtime.Scheme) schema.GroupVersionKind
5960
}
6061

@@ -156,15 +157,15 @@ type Template[T client.Object] struct {
156157
// The template is enabled by default and automatically infers its GVK from the generic type T.
157158
//
158159
// Scheme handling:
159-
// - If no scheme is provided, uses the package default (resource.Scheme)
160+
// - If no scheme is provided, uses the shared default scheme managed by runtimeconfig
160161
// - If a scheme is provided, uses that specific scheme for GVK inference
161162
// - This allows for both convenient defaults and explicit overrides when needed
162163
func NewTemplate[T client.Object](tb TemplateBuilderFunction[T], scheme ...*runtime.Scheme) *Template[T] {
163164
return &Template[T]{
164165
TemplateBuilder: tb,
165166
// default to true - resources should exist unless explicitly disabled
166167
IsEnabled: true,
167-
GVK: inferGVKFromType[T](GetScheme(scheme...)),
168+
GVK: inferGVKFromType[T](runtimeconfig.SelectScheme(scheme...)),
168169
}
169170
}
170171

@@ -174,7 +175,7 @@ func NewTemplate[T client.Object](tb TemplateBuilderFunction[T], scheme ...*runt
174175
// from the generic type T.
175176
//
176177
// Scheme handling:
177-
// - If no scheme is provided, uses the package default (resource.Scheme)
178+
// - If no scheme is provided, uses the shared default scheme managed by runtimeconfig
178179
// - If a scheme is provided, uses that specific scheme for GVK inference
179180
// - This allows for both convenient defaults and explicit overrides when needed
180181
//
@@ -188,7 +189,7 @@ func NewTemplateFromObjectFunction[T client.Object](fn func() T, scheme ...*runt
188189
TemplateBuilder: func(client.Object) (T, error) { return fn(), nil },
189190
// default to true - resources should exist unless explicitly disabled
190191
IsEnabled: true,
191-
GVK: inferGVKFromType[T](GetScheme(scheme...)),
192+
GVK: inferGVKFromType[T](runtimeconfig.SelectScheme(scheme...)),
192193
}
193194
}
194195

@@ -263,7 +264,7 @@ func (t *Template[T]) GetGVK(s ...*runtime.Scheme) schema.GroupVersionKind {
263264
if t.GVK != (schema.GroupVersionKind{}) {
264265
return t.GVK
265266
}
266-
gvk := inferGVKFromType[T](GetScheme(s...))
267+
gvk := inferGVKFromType[T](runtimeconfig.SelectScheme(s...))
267268
return gvk
268269
}
269270

runtimeconfig/scheme.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package runtimeconfig
2+
3+
import (
4+
"sync"
5+
6+
"k8s.io/apimachinery/pkg/runtime"
7+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
8+
)
9+
10+
var (
11+
schemeMu sync.RWMutex
12+
defaultScheme *runtime.Scheme = clientgoscheme.Scheme
13+
schemeFrozen bool
14+
)
15+
16+
// DefaultScheme returns the current shared runtime scheme used across the basereconciler
17+
// packages. It is safe for concurrent use.
18+
func DefaultScheme() *runtime.Scheme {
19+
schemeMu.RLock()
20+
defer schemeMu.RUnlock()
21+
return defaultScheme
22+
}
23+
24+
// SetDefaultScheme overrides the shared runtime scheme. Once set, the scheme becomes frozen
25+
// and subsequent calls to EnsureDefaultScheme will not replace it. Passing a nil scheme is a no-op.
26+
func SetDefaultScheme(s *runtime.Scheme) {
27+
if s == nil {
28+
return
29+
}
30+
schemeMu.Lock()
31+
defaultScheme = s
32+
schemeFrozen = true
33+
schemeMu.Unlock()
34+
}
35+
36+
// EnsureDefaultScheme sets the shared runtime scheme only if it has not been customized yet.
37+
// This provides a first-one-wins behaviour that is useful when wiring a reconciler from a
38+
// controller-runtime manager. Passing a nil scheme is a no-op.
39+
func EnsureDefaultScheme(s *runtime.Scheme) {
40+
if s == nil {
41+
return
42+
}
43+
schemeMu.Lock()
44+
if !schemeFrozen {
45+
defaultScheme = s
46+
schemeFrozen = true
47+
}
48+
schemeMu.Unlock()
49+
}
50+
51+
// SelectScheme returns the first non-nil scheme from the provided list. If none are supplied,
52+
// it falls back to the shared default scheme.
53+
func SelectScheme(explicit ...*runtime.Scheme) *runtime.Scheme {
54+
if len(explicit) > 0 && explicit[0] != nil {
55+
return explicit[0]
56+
}
57+
return DefaultScheme()
58+
}

test/api/v1alpha1/example.com_tests.yaml

Lines changed: 16 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/suite_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"time"
2626

2727
"github.com/3scale-sre/basereconciler/reconciler"
28+
"github.com/3scale-sre/basereconciler/runtimeconfig"
2829
"github.com/goombaio/namegenerator"
2930
. "github.com/onsi/ginkgo/v2"
3031
. "github.com/onsi/gomega"
@@ -61,6 +62,11 @@ func TestAPIs(t *testing.T) {
6162
RunSpecs(t, "Controller Suite")
6263
}
6364

65+
func init() {
66+
utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
67+
runtimeconfig.SetDefaultScheme(scheme.Scheme)
68+
}
69+
6470
var _ = BeforeSuite(func() {
6571
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
6672

@@ -80,10 +86,8 @@ var _ = BeforeSuite(func() {
8086
Expect(err).NotTo(HaveOccurred())
8187
Expect(cfg).NotTo(BeNil())
8288

83-
utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
84-
8589
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
86-
Scheme: scheme.Scheme,
90+
Scheme: runtimeconfig.DefaultScheme(),
8791
// Disable the metrics port to allow running the
8892
// test suite in parallel
8993
Metrics: metricsserver.Options{BindAddress: "0"},

0 commit comments

Comments
 (0)