Skip to content

Commit 1f99680

Browse files
committed
MCO-1661: Add a centralized FeatureGate handler
This commit implements a wrapper around featuregates.FeatureGateAccess to centralize the way we handle FeatureGates checks. The handler exposes a few error-free methods to check if a FeatureGate is enabled or not or if it exist or not.
1 parent cd579c8 commit 1f99680

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed

pkg/controller/common/featuregates.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package common
33
import (
44
"context"
55
"fmt"
6+
"maps"
7+
"slices"
8+
"sync"
69
"time"
710

811
configv1 "github.com/openshift/api/config/v1"
@@ -12,6 +15,125 @@ import (
1215
"k8s.io/klog/v2"
1316
)
1417

18+
const featureGatesConnectionTimeout = 1 * time.Minute
19+
20+
// FeatureGatesHandler Represents a common easy to use entity to interact with FeatureGates
21+
type FeatureGatesHandler interface {
22+
// Connect Reached out the FeatureGates backed to fetch them.
23+
Connect(ctx context.Context) error
24+
25+
// Enabled Checks if the given FeatureGate is enabled or not.
26+
// If the feature does not exist it always returns false.
27+
Enabled(configv1.FeatureGateName) bool
28+
29+
// Exists Checks if the given FeatureGate is known or not.
30+
Exists(configv1.FeatureGateName) bool
31+
32+
// KnownFeatures Fetches a list of the known features.
33+
KnownFeatures() []configv1.FeatureGateName
34+
}
35+
36+
// FeatureGatesHandlerImpl Main implementation of the FeatureGatesHandler interface
37+
type FeatureGatesHandlerImpl struct {
38+
features map[configv1.FeatureGateName]bool
39+
fgAccess featuregates.FeatureGateAccess
40+
connectTimeout time.Duration
41+
connectionMutex sync.Mutex
42+
connectionDone bool
43+
}
44+
45+
// NewFeatureGatesAccessHandler Creates a FeatureGatesHandlerImpl from [featuregates.FeatureGateAccess]
46+
// The given [featuregates.FeatureGateAccess] should already have a Go routine for its Run method.
47+
func NewFeatureGatesAccessHandler(featureGateAccess featuregates.FeatureGateAccess) *FeatureGatesHandlerImpl {
48+
fgHandler := &FeatureGatesHandlerImpl{
49+
fgAccess: featureGateAccess,
50+
features: make(map[configv1.FeatureGateName]bool),
51+
connectTimeout: featureGatesConnectionTimeout,
52+
connectionMutex: sync.Mutex{},
53+
connectionDone: false,
54+
}
55+
return fgHandler
56+
}
57+
58+
// NewFeatureGatesHardcodedHandler Creates a FeatureGatesHandlerImpl from known enabled and
59+
// disable [configv1.FeatureGateName].
60+
func NewFeatureGatesHardcodedHandler(enabled, disabled []configv1.FeatureGateName) *FeatureGatesHandlerImpl {
61+
fgHandler := NewFeatureGatesAccessHandler(featuregates.NewHardcodedFeatureGateAccess(enabled, disabled))
62+
if err := fgHandler.Connect(context.Background()); err != nil {
63+
panic(fmt.Errorf("hardcoded feature gate impossible failure: %v", err))
64+
}
65+
return fgHandler
66+
}
67+
68+
// NewFeatureGatesCRHandlerImpl Creates a FeatureGatesHandlerImpl from a [configv1.FeatureGate] and a version.
69+
// The function may return an error if the given version is not found.
70+
func NewFeatureGatesCRHandlerImpl(featureGate *configv1.FeatureGate, desiredVersion string) (*FeatureGatesHandlerImpl, error) {
71+
fgAccess, err := featuregates.NewHardcodedFeatureGateAccessFromFeatureGate(featureGate, desiredVersion)
72+
if err != nil {
73+
return nil, err
74+
}
75+
fgHandler := NewFeatureGatesAccessHandler(fgAccess)
76+
if err = fgHandler.Connect(context.Background()); err != nil {
77+
panic(fmt.Errorf("hardcoded feature gate impossible failure: %v", err))
78+
}
79+
return fgHandler, nil
80+
}
81+
82+
func (h *FeatureGatesHandlerImpl) Enabled(featureGate configv1.FeatureGateName) bool {
83+
enabled, ok := h.features[featureGate]
84+
if !ok {
85+
klog.Infof("FeatureGate check request for unknown feature: %v", featureGate)
86+
return false
87+
}
88+
return enabled
89+
}
90+
91+
func (h *FeatureGatesHandlerImpl) Exists(featureGate configv1.FeatureGateName) bool {
92+
_, ok := h.features[featureGate]
93+
return ok
94+
}
95+
96+
func (h *FeatureGatesHandlerImpl) KnownFeatures() []configv1.FeatureGateName {
97+
return slices.Collect(maps.Keys(h.features))
98+
}
99+
100+
func (h *FeatureGatesHandlerImpl) registerFeatureGates(features featuregates.FeatureGate) {
101+
var enabled []string
102+
var disabled []string
103+
for _, feature := range features.KnownFeatures() {
104+
if features.Enabled(feature) {
105+
enabled = append(enabled, string(feature))
106+
h.features[feature] = true
107+
} else {
108+
disabled = append(disabled, string(feature))
109+
h.features[feature] = false
110+
}
111+
}
112+
klog.Infof("FeatureGates initialized: enabled=%v, disabled=%v", enabled, disabled)
113+
}
114+
115+
func (h *FeatureGatesHandlerImpl) Connect(ctx context.Context) error {
116+
h.connectionMutex.Lock()
117+
defer h.connectionMutex.Unlock()
118+
if h.connectionDone {
119+
return nil
120+
}
121+
select {
122+
case <-ctx.Done():
123+
return ctx.Err()
124+
case <-time.After(h.connectTimeout):
125+
return fmt.Errorf("timed out waiting for FeatureGates to be ready")
126+
case <-h.fgAccess.InitialFeatureGatesObserved():
127+
fgs, err := h.fgAccess.CurrentFeatureGates()
128+
if err != nil {
129+
return fmt.Errorf("unable to get initial features: %v", err)
130+
}
131+
h.registerFeatureGates(fgs)
132+
h.connectionDone = true
133+
return nil
134+
}
135+
}
136+
15137
func WaitForFeatureGatesReady(ctx context.Context, featureGateAccess featuregates.FeatureGateAccess) error {
16138
timeout := time.After(1 * time.Minute)
17139
for {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"errors"
6+
configv1 "github.com/openshift/api/config/v1"
7+
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
8+
"github.com/stretchr/testify/assert"
9+
"testing"
10+
"time"
11+
)
12+
13+
const (
14+
FeatureGatesTestExistingEnaFeatureGate1 configv1.FeatureGateName = "testExistingFeatureGate-enabled-1"
15+
FeatureGatesTestExistingEnaFeatureGate2 configv1.FeatureGateName = "testExistingFeatureGate-enabled-2"
16+
FeatureGatesTestExistingDisFeatureGate1 configv1.FeatureGateName = "testExistingFeatureGate-disabled-1"
17+
)
18+
19+
type FeatureGatesHandlerStub struct {
20+
testing *testing.T
21+
featureGates featuregates.FeatureGate
22+
featuresChan chan struct{}
23+
initTime time.Duration
24+
fetchError error
25+
}
26+
27+
func NewFeatureGatesHandlerStub(t *testing.T, initTime time.Duration, fetchError error) *FeatureGatesHandlerStub {
28+
return &FeatureGatesHandlerStub{
29+
testing: t,
30+
featureGates: featuregates.NewFeatureGate(
31+
[]configv1.FeatureGateName{
32+
FeatureGatesTestExistingEnaFeatureGate1,
33+
FeatureGatesTestExistingEnaFeatureGate2,
34+
},
35+
[]configv1.FeatureGateName{
36+
FeatureGatesTestExistingDisFeatureGate1,
37+
}),
38+
featuresChan: make(chan struct{}),
39+
initTime: initTime,
40+
fetchError: fetchError,
41+
}
42+
}
43+
44+
func (s *FeatureGatesHandlerStub) StartStub() {
45+
time.AfterFunc(s.initTime, func() {
46+
s.featuresChan <- struct{}{}
47+
})
48+
}
49+
50+
func (s *FeatureGatesHandlerStub) SetChangeHandler(_ featuregates.FeatureGateChangeHandlerFunc) {
51+
// The handler does not override the default FeatureGateChangeHandlerFunc
52+
s.testing.Fatal("SetChangeHandler should not be called by the feature gates handler")
53+
}
54+
55+
func (s *FeatureGatesHandlerStub) Run(_ context.Context) {
56+
// Run is never called from the handler. The entity that creates the FeatureGateAccess is responsible
57+
// for properly initializing it and creating the Go rutine.
58+
s.testing.Fatal("Run should not be called by the feature gates handler")
59+
}
60+
61+
func (s *FeatureGatesHandlerStub) InitialFeatureGatesObserved() <-chan struct{} {
62+
return s.featuresChan
63+
}
64+
65+
func (s *FeatureGatesHandlerStub) CurrentFeatureGates() (featuregates.FeatureGate, error) {
66+
return s.featureGates, s.fetchError
67+
}
68+
69+
func (s *FeatureGatesHandlerStub) AreInitialFeatureGatesObserved() bool {
70+
// The handler does not rely on AreInitialFeatureGatesObserved as it uses the channel
71+
// to get the notification of the FeatureGates ready.
72+
s.testing.Fatal("AreInitialFeatureGatesObserved should not be called by the feature gates handler")
73+
return false
74+
}
75+
76+
func TestFeatureGateHandlerAccess(t *testing.T) {
77+
fgStub := NewFeatureGatesHandlerStub(t, 100*time.Millisecond, nil)
78+
handler := NewFeatureGatesAccessHandler(fgStub)
79+
assert.NotNil(t, handler)
80+
assert.Empty(t, handler.KnownFeatures())
81+
fgStub.StartStub()
82+
assert.NoError(t, handler.Connect(context.Background()))
83+
84+
checkFeatureGates(t, handler)
85+
86+
// Check that a non-existing FG is reported as so and disabled by default
87+
const nonExistingFeatureGate = "test-non-existing"
88+
assert.False(t, handler.Exists(nonExistingFeatureGate))
89+
assert.False(t, handler.Enabled(nonExistingFeatureGate))
90+
}
91+
92+
func TestFeatureGateHandlerAccessTimeout(t *testing.T) {
93+
fgStub := NewFeatureGatesHandlerStub(t, 100*time.Millisecond, nil)
94+
handler := NewFeatureGatesAccessHandler(fgStub)
95+
assert.NotNil(t, handler)
96+
// Set a really low timeout to make connection fail by timeout
97+
handler.connectTimeout = 10 * time.Millisecond
98+
fgStub.StartStub()
99+
100+
err := handler.Connect(context.Background())
101+
assert.ErrorContains(t, err, "timed out")
102+
}
103+
104+
func TestFeatureGateHandlerAccessCancel(t *testing.T) {
105+
fgStub := NewFeatureGatesHandlerStub(t, 100*time.Millisecond, nil)
106+
handler := NewFeatureGatesAccessHandler(fgStub)
107+
assert.NotNil(t, handler)
108+
fgStub.StartStub()
109+
110+
ctx, cancel := context.WithCancel(context.Background())
111+
time.AfterFunc(10*time.Millisecond, func() { cancel() })
112+
err := handler.Connect(ctx)
113+
assert.ErrorContains(t, err, "context canceled")
114+
}
115+
116+
func TestFeatureGateHandlerAccessFetchError(t *testing.T) {
117+
fetchError := errors.New("fetch error")
118+
fgStub := NewFeatureGatesHandlerStub(t, 1*time.Millisecond, fetchError)
119+
handler := NewFeatureGatesAccessHandler(fgStub)
120+
assert.NotNil(t, handler)
121+
fgStub.StartStub()
122+
123+
err := handler.Connect(context.Background())
124+
assert.ErrorContains(t, err, fetchError.Error())
125+
}
126+
127+
func TestFeatureGateHandlerHardcoded(t *testing.T) {
128+
handler := NewFeatureGatesHardcodedHandler([]configv1.FeatureGateName{
129+
FeatureGatesTestExistingEnaFeatureGate1,
130+
FeatureGatesTestExistingEnaFeatureGate2,
131+
},
132+
[]configv1.FeatureGateName{
133+
FeatureGatesTestExistingDisFeatureGate1,
134+
})
135+
assert.NotNil(t, handler)
136+
checkFeatureGates(t, handler)
137+
}
138+
139+
func TestFeatureGateHandlerCR(t *testing.T) {
140+
fg := buildDummyFeatureGate()
141+
handler, err := NewFeatureGatesCRHandlerImpl(&fg, "v1")
142+
assert.NotNil(t, handler)
143+
assert.NoError(t, err)
144+
checkFeatureGates(t, handler)
145+
}
146+
147+
func TestFeatureGateHandlerCRParseError(t *testing.T) {
148+
fg := buildDummyFeatureGate()
149+
_, err := NewFeatureGatesCRHandlerImpl(&fg, "v2")
150+
assert.ErrorContains(t, err, "unable to determine features")
151+
}
152+
153+
func buildDummyFeatureGate() configv1.FeatureGate {
154+
fg := configv1.FeatureGate{
155+
Status: configv1.FeatureGateStatus{
156+
FeatureGates: []configv1.FeatureGateDetails{
157+
{
158+
Version: "v1",
159+
Enabled: []configv1.FeatureGateAttributes{
160+
{Name: FeatureGatesTestExistingEnaFeatureGate1},
161+
{Name: FeatureGatesTestExistingEnaFeatureGate2},
162+
},
163+
Disabled: []configv1.FeatureGateAttributes{
164+
{Name: FeatureGatesTestExistingDisFeatureGate1},
165+
},
166+
},
167+
},
168+
},
169+
}
170+
return fg
171+
}
172+
173+
func checkFeatureGates(t *testing.T, handler *FeatureGatesHandlerImpl) {
174+
// Check that all the known FGs are reported as known
175+
assert.True(t, handler.Exists(FeatureGatesTestExistingEnaFeatureGate1))
176+
assert.True(t, handler.Exists(FeatureGatesTestExistingEnaFeatureGate2))
177+
assert.True(t, handler.Exists(FeatureGatesTestExistingDisFeatureGate1))
178+
179+
// Check that all the known FGs are reported as enabled/disabled
180+
assert.True(t, handler.Enabled(FeatureGatesTestExistingEnaFeatureGate1))
181+
assert.True(t, handler.Enabled(FeatureGatesTestExistingEnaFeatureGate2))
182+
assert.False(t, handler.Enabled(FeatureGatesTestExistingDisFeatureGate1))
183+
}

0 commit comments

Comments
 (0)