Skip to content

Commit bca2baa

Browse files
authored
feat(multicluster): implement multicluster lifecycle management and reconcile logic (#21)
1 parent c7c942e commit bca2baa

25 files changed

+2123
-1465
lines changed

.testcoverage.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ exclude:
22
paths:
33
- ^controller/testSupport # exclude test support files
44
- mocks # exclude generated mock files
5-
- ^test/openfga
6-
5+
- ^test/
6+
- ^logger/testlogger
7+

config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ func BindConfigToFlags(v *viper.Viper, cmd *cobra.Command, config any) error {
212212
return nil
213213
}
214214

215+
// unmarshalIntoStruct returns a function that unmarshal viper config into cfg and panics on error.
215216
func unmarshalIntoStruct(v *viper.Viper, cfg any) func() {
216217
return func() {
217218
if err := v.Unmarshal(cfg); err != nil {

config/config_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,12 @@ func TestNewDefaultConfig(t *testing.T) {
168168
err = v.Unmarshal(&config.CommonServiceConfig{})
169169
assert.NoError(t, err)
170170
}
171+
172+
func TestGenerateFlagSetUnsupportedType(t *testing.T) {
173+
type test struct {
174+
UnsupportedField []string `mapstructure:"unsupported-field"`
175+
}
176+
testStruct := test{}
177+
err := config.BindConfigToFlags(viper.New(), &cobra.Command{}, &testStruct)
178+
assert.Error(t, err)
179+
}

controller/lifecycle/api/api.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@ type Config struct {
3030
}
3131

3232
type ConditionManager interface {
33-
MustToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) RuntimeObjectConditions
3433
SetInstanceConditionUnknownIfNotSet(conditions *[]metav1.Condition) bool
3534
SetSubroutineConditionToUnknownIfNotSet(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, isFinalize bool, log *logger.Logger) bool
3635
SetSubroutineCondition(conditions *[]metav1.Condition, subroutine subroutine.Subroutine, subroutineResult ctrl.Result, subroutineErr error, isFinalize bool, log *logger.Logger) bool
3736
SetInstanceConditionReady(conditions *[]metav1.Condition, status metav1.ConditionStatus) bool
38-
ToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (RuntimeObjectConditions, error)
3937
}
4038

4139
type RuntimeObjectConditions interface {
@@ -44,9 +42,8 @@ type RuntimeObjectConditions interface {
4442
}
4543

4644
type SpreadManager interface {
47-
ToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (RuntimeObjectSpreadReconcileStatus, error)
48-
MustToRuntimeObjectSpreadReconcileStatusInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) RuntimeObjectSpreadReconcileStatus
49-
OnNextReconcile(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger) (ctrl.Result, error)
45+
ReconcileRequired(instance runtimeobject.RuntimeObject, log *logger.Logger) bool
46+
OnNextReconcile(instance runtimeobject.RuntimeObject, log *logger.Logger) (ctrl.Result, error)
5047
RemoveRefreshLabelIfExists(instance runtimeobject.RuntimeObject) bool
5148
SetNextReconcileTime(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger)
5249
UpdateObservedGeneration(instanceStatusObj RuntimeObjectSpreadReconcileStatus, log *logger.Logger)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package builder
2+
3+
import (
4+
"sigs.k8s.io/controller-runtime/pkg/client"
5+
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
6+
7+
"github.com/platform-mesh/golang-commons/controller/lifecycle/controllerruntime"
8+
"github.com/platform-mesh/golang-commons/controller/lifecycle/multicluster"
9+
"github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine"
10+
"github.com/platform-mesh/golang-commons/logger"
11+
)
12+
13+
type Builder struct {
14+
operatorName string
15+
controllerName string
16+
withConditionManagement bool
17+
withSpreadingReconciles bool
18+
withReadOnly bool
19+
subroutines []subroutine.Subroutine
20+
log *logger.Logger
21+
}
22+
23+
func NewBuilder(operatorName, controllerName string, subroutines []subroutine.Subroutine, log *logger.Logger) *Builder {
24+
return &Builder{
25+
operatorName: operatorName,
26+
controllerName: controllerName,
27+
log: log,
28+
withConditionManagement: false,
29+
subroutines: subroutines,
30+
}
31+
}
32+
33+
func (b *Builder) WithConditionManagement() *Builder {
34+
b.withConditionManagement = true
35+
return b
36+
}
37+
38+
func (b *Builder) WithSpreadingReconciles() *Builder {
39+
b.withSpreadingReconciles = true
40+
return b
41+
}
42+
43+
func (b *Builder) WithReadOnly() *Builder {
44+
b.withReadOnly = true
45+
return b
46+
}
47+
48+
func (b *Builder) BuildControllerRuntime(cl client.Client) *controllerruntime.LifecycleManager {
49+
lm := controllerruntime.NewLifecycleManager(b.subroutines, b.operatorName, b.controllerName, cl, b.log)
50+
if b.withConditionManagement {
51+
lm.WithConditionManagement()
52+
}
53+
if b.withSpreadingReconciles {
54+
lm.WithSpreadingReconciles()
55+
}
56+
if b.withReadOnly {
57+
lm.WithReadOnly()
58+
}
59+
return lm
60+
}
61+
62+
func (b *Builder) BuildMultiCluster(mgr mcmanager.Manager) *multicluster.LifecycleManager {
63+
lm := multicluster.NewLifecycleManager(b.subroutines, b.operatorName, b.controllerName, mgr, b.log)
64+
if b.withConditionManagement {
65+
lm.WithConditionManagement()
66+
}
67+
if b.withSpreadingReconciles {
68+
lm.WithSpreadingReconciles()
69+
}
70+
if b.withReadOnly {
71+
lm.WithReadOnly()
72+
}
73+
return lm
74+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package builder
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"k8s.io/client-go/rest"
8+
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
9+
10+
pmtesting "github.com/platform-mesh/golang-commons/controller/testSupport"
11+
"github.com/platform-mesh/golang-commons/logger"
12+
)
13+
14+
func TestNewBuilder_Defaults(t *testing.T) {
15+
log := &logger.Logger{}
16+
b := NewBuilder("op", "ctrl", nil, log)
17+
if b.operatorName != "op" {
18+
t.Errorf("expected operatorName 'op', got %s", b.operatorName)
19+
}
20+
if b.controllerName != "ctrl" {
21+
t.Errorf("expected controllerName 'ctrl', got %s", b.controllerName)
22+
}
23+
if b.withConditionManagement {
24+
t.Error("expected withConditionManagement to be false")
25+
}
26+
if b.withSpreadingReconciles {
27+
t.Error("expected withSpreadingReconciles to be false")
28+
}
29+
if b.withReadOnly {
30+
t.Error("expected withReadOnly to be false")
31+
}
32+
if b.log != log {
33+
t.Error("expected log to be set")
34+
}
35+
}
36+
37+
func TestBuilder_WithConditionManagement(t *testing.T) {
38+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{})
39+
b.WithConditionManagement()
40+
if !b.withConditionManagement {
41+
t.Error("WithConditionManagement should set withConditionManagement to true")
42+
}
43+
}
44+
45+
func TestBuilder_WithSpreadingReconciles(t *testing.T) {
46+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{})
47+
b.WithSpreadingReconciles()
48+
if !b.withSpreadingReconciles {
49+
t.Error("WithSpreadingReconciles should set withSpreadingReconciles to true")
50+
}
51+
}
52+
53+
func TestBuilder_WithReadOnly(t *testing.T) {
54+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{})
55+
b.WithReadOnly()
56+
if !b.withReadOnly {
57+
t.Error("WithReadOnly should set withReadOnly to true")
58+
}
59+
}
60+
61+
func TestControllerRuntimeBuilder(t *testing.T) {
62+
t.Run("Minimal setup", func(t *testing.T) {
63+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{})
64+
fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{})
65+
lm := b.BuildControllerRuntime(fakeClient)
66+
assert.NotNil(t, lm)
67+
})
68+
t.Run("All Options", func(t *testing.T) {
69+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithConditionManagement().WithSpreadingReconciles()
70+
fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{})
71+
lm := b.BuildControllerRuntime(fakeClient)
72+
assert.NotNil(t, lm)
73+
})
74+
t.Run("ReadOnly", func(t *testing.T) {
75+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithReadOnly()
76+
fakeClient := pmtesting.CreateFakeClient(t, &pmtesting.TestApiObject{})
77+
lm := b.BuildControllerRuntime(fakeClient)
78+
assert.NotNil(t, lm)
79+
})
80+
}
81+
82+
func TestMulticontrollerRuntimeBuilder(t *testing.T) {
83+
t.Run("Minimal setup", func(t *testing.T) {
84+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{})
85+
cfg := &rest.Config{}
86+
provider := pmtesting.NewFakeProvider(cfg)
87+
mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{})
88+
assert.NoError(t, err)
89+
lm := b.BuildMultiCluster(mgr)
90+
assert.NotNil(t, lm)
91+
})
92+
t.Run("All Options", func(t *testing.T) {
93+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithConditionManagement().WithSpreadingReconciles()
94+
cfg := &rest.Config{}
95+
provider := pmtesting.NewFakeProvider(cfg)
96+
mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{})
97+
assert.NoError(t, err)
98+
lm := b.BuildMultiCluster(mgr)
99+
assert.NotNil(t, lm)
100+
})
101+
t.Run("ReadOnly", func(t *testing.T) {
102+
b := NewBuilder("op", "ctrl", nil, &logger.Logger{}).WithReadOnly()
103+
cfg := &rest.Config{}
104+
provider := pmtesting.NewFakeProvider(cfg)
105+
mgr, err := mcmanager.New(cfg, provider, mcmanager.Options{})
106+
assert.NoError(t, err)
107+
lm := b.BuildMultiCluster(mgr)
108+
assert.NotNil(t, lm)
109+
})
110+
}

controller/lifecycle/conditions/conditions.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@ import (
77
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
88
ctrl "sigs.k8s.io/controller-runtime"
99

10-
"github.com/platform-mesh/golang-commons/controller/lifecycle/api"
11-
"github.com/platform-mesh/golang-commons/controller/lifecycle/runtimeobject"
1210
"github.com/platform-mesh/golang-commons/controller/lifecycle/subroutine"
1311
"github.com/platform-mesh/golang-commons/logger"
14-
"github.com/platform-mesh/golang-commons/sentry"
1512
)
1613

1714
const (
@@ -118,22 +115,3 @@ func (c *ConditionManager) SetSubroutineCondition(conditions *[]metav1.Condition
118115
}
119116
return changed
120117
}
121-
122-
func (c *ConditionManager) ToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) (api.RuntimeObjectConditions, error) {
123-
if obj, ok := instance.(api.RuntimeObjectConditions); ok {
124-
return obj, nil
125-
}
126-
err := fmt.Errorf("ManageConditions is enabled, but instance does not implement RuntimeObjectConditions interface. This is a programming error")
127-
log.Error().Err(err).Msg("instance does not implement RuntimeObjectConditions interface")
128-
sentry.CaptureError(err, nil)
129-
return nil, err
130-
}
131-
132-
func (c *ConditionManager) MustToRuntimeObjectConditionsInterface(instance runtimeobject.RuntimeObject, log *logger.Logger) api.RuntimeObjectConditions {
133-
obj, err := c.ToRuntimeObjectConditionsInterface(instance, log)
134-
if err == nil {
135-
return obj
136-
}
137-
log.Panic().Err(err).Msg("instance does not implement RuntimeObjectConditions interface")
138-
return nil
139-
}

controller/lifecycle/conditions/conditions_test.go

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -240,44 +240,3 @@ func TestSubroutineCondition(t *testing.T) {
240240
assert.Equal(t, metav1.ConditionFalse, condition[0].Status)
241241
})
242242
}
243-
244-
// Dummy types for testing interface conversion
245-
246-
func TestToRuntimeObjectConditionsInterface(t *testing.T) {
247-
log, err := logger.New(logger.DefaultConfig())
248-
require.NoError(t, err)
249-
cm := NewConditionManager()
250-
251-
t.Run("Implements interface", func(t *testing.T) {
252-
obj := pmtesting.DummyRuntimeObjectWithConditions{}
253-
res, err := cm.ToRuntimeObjectConditionsInterface(obj, log)
254-
assert.NoError(t, err)
255-
assert.NotNil(t, res)
256-
})
257-
258-
t.Run("Does not implement interface", func(t *testing.T) {
259-
obj := pmtesting.DummyRuntimeObject{}
260-
res, err := cm.ToRuntimeObjectConditionsInterface(obj, log)
261-
assert.Error(t, err)
262-
assert.Nil(t, res)
263-
})
264-
}
265-
266-
func TestMustToRuntimeObjectConditionsInterface(t *testing.T) {
267-
log, err := logger.New(logger.DefaultConfig())
268-
require.NoError(t, err)
269-
cm := NewConditionManager()
270-
271-
t.Run("Implements interface", func(t *testing.T) {
272-
obj := pmtesting.DummyRuntimeObjectWithConditions{}
273-
res := cm.MustToRuntimeObjectConditionsInterface(obj, log)
274-
assert.NotNil(t, res)
275-
})
276-
277-
t.Run("Does not implement interface panics", func(t *testing.T) {
278-
obj := pmtesting.DummyRuntimeObject{}
279-
assert.Panics(t, func() {
280-
cm.MustToRuntimeObjectConditionsInterface(obj, log)
281-
})
282-
})
283-
}

controller/lifecycle/controllerruntime/lifecycle.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type LifecycleManager struct {
3131
prepareContextFunc api.PrepareContextFunc
3232
}
3333

34-
func NewLifecycleManager(log *logger.Logger, operatorName string, controllerName string, client client.Client, subroutines []subroutine.Subroutine) *LifecycleManager {
34+
func NewLifecycleManager(subroutines []subroutine.Subroutine, operatorName string, controllerName string, client client.Client, log *logger.Logger) *LifecycleManager {
3535
log = log.MustChildLoggerWithAttributes("operator", operatorName, "controller", controllerName)
3636
return &LifecycleManager{
3737
log: log,
@@ -63,37 +63,18 @@ func (l *LifecycleManager) ConditionsManager() api.ConditionManager {
6363
}
6464
return l.conditionsManager
6565
}
66-
6766
func (l *LifecycleManager) Spreader() api.SpreadManager {
68-
// it is important to return nil unsted of a nil pointer to the interface to avoid misbehaving nil checks
67+
// it is important to return nil instead of a nil pointer to the interface to avoid misbehaving nil checks
6968
if l.spreader == nil {
7069
return nil
7170
}
7271
return l.spreader
7372
}
74-
7573
func (l *LifecycleManager) Reconcile(ctx context.Context, req ctrl.Request, instance runtimeobject.RuntimeObject) (ctrl.Result, error) {
76-
return lifecycle.Reconcile(ctx, req, instance, l.client, l)
74+
return lifecycle.Reconcile(ctx, req.NamespacedName, instance, l.client, l)
7775
}
78-
79-
func (l *LifecycleManager) validateInterfaces(instance runtimeobject.RuntimeObject, log *logger.Logger) error {
80-
if l.Spreader() != nil {
81-
_, err := l.Spreader().ToRuntimeObjectSpreadReconcileStatusInterface(instance, log)
82-
if err != nil {
83-
return err
84-
}
85-
}
86-
if l.ConditionsManager() != nil {
87-
_, err := l.ConditionsManager().ToRuntimeObjectConditionsInterface(instance, log)
88-
if err != nil {
89-
return err
90-
}
91-
}
92-
return nil
93-
}
94-
9576
func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, log *logger.Logger, eventPredicates ...predicate.Predicate) (*builder.Builder, error) {
96-
if err := l.validateInterfaces(instance, log); err != nil {
77+
if err := lifecycle.ValidateInterfaces(instance, log, l); err != nil {
9778
return nil, err
9879
}
9980

@@ -108,7 +89,6 @@ func (l *LifecycleManager) SetupWithManagerBuilder(mgr ctrl.Manager, maxReconcil
10889
WithOptions(controller.Options{MaxConcurrentReconciles: maxReconciles}).
10990
WithEventFilter(predicate.And(eventPredicates...)), nil
11091
}
111-
11292
func (l *LifecycleManager) SetupWithManager(mgr ctrl.Manager, maxReconciles int, reconcilerName string, instance runtimeobject.RuntimeObject, debugLabelValue string, r reconcile.Reconciler, log *logger.Logger, eventPredicates ...predicate.Predicate) error {
11393
b, err := l.SetupWithManagerBuilder(mgr, maxReconciles, reconcilerName, instance, debugLabelValue, log, eventPredicates...)
11494
if err != nil {

0 commit comments

Comments
 (0)