Skip to content

Commit 4a5b8c1

Browse files
Merge pull request openshift#102 from azych/feature-gates-lib
OPRUN-3663: Watch and reconcile feature gates changes
2 parents 382a0d3 + e6b2bc9 commit 4a5b8c1

35 files changed

+4526
-17
lines changed

cmd/cluster-olm-operator/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
goflag "flag"
67
"fmt"
78
"os"
@@ -192,6 +193,16 @@ func runOperator(ctx context.Context, cc *controllercmd.ControllerContext) error
192193

193194
cl.StartInformers(ctx)
194195

196+
log := klog.FromContext(ctx)
197+
select {
198+
case <-cl.FeatureGatesAccessor.InitialFeatureGatesObserved():
199+
featureGates, _ := cl.FeatureGatesAccessor.CurrentFeatureGates()
200+
log.Info("FeatureGates initialized", "knownFeatures", featureGates.KnownFeatures())
201+
case <-time.After(1 * time.Minute):
202+
log.Error(nil, "timed out waiting for FeatureGate detection")
203+
return errors.New("timed out waiting for FeatureGate detection")
204+
}
205+
195206
for _, c := range append(staticResourceControllerList, upgradeableConditionController, incompatibleOperatorController, clusterOperatorController, operatorLoggingController, proxyController) {
196207
go func(c factory.Controller) {
197208
defer runtime.HandleCrash()

internal/featuregates/mapper.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package featuregates
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"strings"
7+
8+
configv1 "github.com/openshift/api/config/v1"
9+
"github.com/openshift/api/features"
10+
)
11+
12+
// Add your new upstream feature gate here
13+
// const (
14+
// MyUpstreamFeature = "MyUpstreamFeature"
15+
// )
16+
17+
type MapperInterface interface {
18+
OperatorControllerUpstreamForDownstream(downstreamGate configv1.FeatureGateName) []string
19+
OperatorControllerDownstreamFeatureGates() []configv1.FeatureGateName
20+
CatalogdUpstreamForDownstream(downstreamGate configv1.FeatureGateName) []string
21+
CatalogdDownstreamFeatureGates() []configv1.FeatureGateName
22+
}
23+
24+
// Mapper knows the mapping between downstream and upstream feature gates for both OLM components
25+
type Mapper struct {
26+
operatorControllerGates map[configv1.FeatureGateName][]string
27+
catalogdGates map[configv1.FeatureGateName][]string
28+
}
29+
30+
func NewMapper() *Mapper {
31+
// Add your downstream to upstream mapping here
32+
operatorControllerGates := map[configv1.FeatureGateName][]string{
33+
// features.FeatureGateNewOLMMyDownstreamFeature: {MyUpstreamControllerOperatorFeature}
34+
}
35+
catalogdGates := map[configv1.FeatureGateName][]string{
36+
// features.FeatureGateNewOLMMyDownstreamFeature: {MyUpstreamCatalogdFeature}
37+
}
38+
39+
for _, m := range []map[configv1.FeatureGateName][]string{operatorControllerGates, catalogdGates} {
40+
for downstreamGate := range m {
41+
// features.FeatureGateNewOLM is a GA-enabled downstream feature gate.
42+
// If there is a need to enable upstream alpha/beta features in the downstream GA release
43+
// get approval via a merged openshift/enhancement describing the need, then carve out
44+
// an exception in this failsafe code
45+
if downstreamGate == features.FeatureGateNewOLM {
46+
panic(errors.New("FeatureGateNewOLM used in mappings"))
47+
}
48+
if !strings.HasPrefix(string(downstreamGate), string(features.FeatureGateNewOLM)) {
49+
panic(errors.New("all downstream feature gates must use NewOLM prefix by convention"))
50+
}
51+
}
52+
}
53+
54+
return &Mapper{operatorControllerGates: operatorControllerGates, catalogdGates: catalogdGates}
55+
}
56+
57+
// OperatorControllerDownstreamFeatureGates returns a list of all downstream feature gates
58+
// which have an upstream mapping configured for the operator-controller component
59+
func (m *Mapper) OperatorControllerDownstreamFeatureGates() []configv1.FeatureGateName {
60+
return getKeys(m.operatorControllerGates)
61+
}
62+
63+
// CatalogdDownstreamFeatureGates returns a list of all downstream feature gates
64+
// which have an upstream mapping configured for the catalogd component
65+
func (m *Mapper) CatalogdDownstreamFeatureGates() []configv1.FeatureGateName {
66+
return getKeys(m.catalogdGates)
67+
}
68+
69+
// OperatorControllerUpstreamForDownstream returns upstream feature gates which are configured
70+
// for a given downstream feature gate for the operator-controller component
71+
func (m *Mapper) OperatorControllerUpstreamForDownstream(downstreamGate configv1.FeatureGateName) []string {
72+
return m.operatorControllerGates[downstreamGate]
73+
}
74+
75+
// CatalogdUpstreamForDownstream returns upstream feature gates which are configured
76+
// for a given downstream feature gate for the catalogd component
77+
func (m *Mapper) CatalogdUpstreamForDownstream(downstreamGate configv1.FeatureGateName) []string {
78+
return m.catalogdGates[downstreamGate]
79+
}
80+
81+
// FormatAsEnabledArgs combines list of feature gate names into
82+
// an all-enabled arg format of <feature_gate_name1>=true,<feature_gate_name1>=true etc.
83+
func FormatAsEnabledArgs(enabledFeatureGates []string) string {
84+
buf := bytes.Buffer{}
85+
for _, gateName := range enabledFeatureGates {
86+
buf.WriteString(gateName)
87+
buf.WriteRune('=')
88+
buf.WriteString("true")
89+
buf.WriteRune(',')
90+
}
91+
if buf.Len() > 0 {
92+
// get rid of trailing ','
93+
buf.Truncate(buf.Len() - 1)
94+
}
95+
96+
return buf.String()
97+
}
98+
99+
func getKeys(m map[configv1.FeatureGateName][]string) []configv1.FeatureGateName {
100+
keys := make([]configv1.FeatureGateName, 0, len(m))
101+
for k := range m {
102+
keys = append(keys, k)
103+
}
104+
return keys
105+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package featuregates
2+
3+
import (
4+
"slices"
5+
"testing"
6+
7+
configv1 "github.com/openshift/api/config/v1"
8+
"github.com/openshift/api/features"
9+
)
10+
11+
func TestMapper_ControllerUpstreamForDownstream(t *testing.T) {
12+
t.Run("returns mapped upstream gates for operator-controller", func(t *testing.T) {
13+
expectedUpstreamGates := []string{"HelloGate", "WorldGate"}
14+
15+
mapper := NewMapper()
16+
mapper.operatorControllerGates = map[configv1.FeatureGateName][]string{
17+
features.FeatureGateNewOLM: expectedUpstreamGates,
18+
}
19+
upstream := mapper.OperatorControllerUpstreamForDownstream(features.FeatureGateNewOLM)
20+
if !slices.Equal(upstream, expectedUpstreamGates) {
21+
t.Fatalf("expected and returned upstream gates differ: upstream: %+v, expected: %+v",
22+
upstream, expectedUpstreamGates,
23+
)
24+
}
25+
})
26+
}
27+
28+
func TestMapper_CatalogdUpstreamForDownstream(t *testing.T) {
29+
t.Run("returns mapped upstream gates for catalogd", func(t *testing.T) {
30+
expectedUpstreamGates := []string{"HelloGate", "WorldGate"}
31+
32+
mapper := NewMapper()
33+
mapper.catalogdGates = map[configv1.FeatureGateName][]string{
34+
features.FeatureGateNewOLM: expectedUpstreamGates,
35+
}
36+
upstream := mapper.CatalogdUpstreamForDownstream(features.FeatureGateNewOLM)
37+
if !slices.Equal(upstream, expectedUpstreamGates) {
38+
t.Fatalf("expected and returned upstream gates differ: upstream: %+v, expected: %+v",
39+
upstream, expectedUpstreamGates,
40+
)
41+
}
42+
})
43+
}
44+
45+
func TestFormatAsEnabledArgs(t *testing.T) {
46+
testCases := []struct {
47+
name string
48+
in []string
49+
expected string
50+
}{
51+
{
52+
name: "empty",
53+
},
54+
{
55+
name: "single feature gate",
56+
in: []string{"testGate1"},
57+
expected: "testGate1=true",
58+
},
59+
{
60+
name: "multiple feature gates",
61+
in: []string{"testGate1", "testGate2", "testGate3"},
62+
expected: "testGate1=true,testGate2=true,testGate2=true",
63+
},
64+
}
65+
for _, testCase := range testCases {
66+
t.Run(testCase.name, func(t *testing.T) {
67+
result := FormatAsEnabledArgs(testCase.in)
68+
if result != testCase.expected {
69+
t.Fatalf("result and expected differ, expected: %q, got: %q", testCase.expected, result)
70+
}
71+
})
72+
}
73+
}

manifests/0000_51_olm_02_operator_clusterrole.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ rules:
1313
resources:
1414
- infrastructures
1515
- proxies
16+
- featuregates
17+
- clusterversions
1618
verbs:
1719
- get
1820
- list

pkg/clients/clients.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@ import (
2121
operatorv1apply "github.com/openshift/client-go/operator/applyconfigurations/operator/v1"
2222
operatorclient "github.com/openshift/client-go/operator/clientset/versioned"
2323
operatorinformers "github.com/openshift/client-go/operator/informers/externalversions"
24+
internalfeatures "github.com/openshift/cluster-olm-operator/internal/featuregates"
2425
"github.com/openshift/library-go/pkg/apiserver/jsonpatch"
2526
"github.com/openshift/library-go/pkg/controller/controllercmd"
27+
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"
28+
"github.com/openshift/library-go/pkg/operator/events"
2629
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
30+
"github.com/openshift/library-go/pkg/operator/status"
2731
"github.com/openshift/library-go/pkg/operator/v1helpers"
2832
ocv1 "github.com/operator-framework/operator-controller/api/v1"
33+
corev1 "k8s.io/api/core/v1"
2934
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
3035
"k8s.io/apimachinery/pkg/api/meta"
3136
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -62,6 +67,8 @@ type Clients struct {
6267
KubeInformerFactory informers.SharedInformerFactory
6368
ConfigInformerFactory configinformer.SharedInformerFactory
6469
KubeInformersForNamespaces v1helpers.KubeInformersForNamespaces
70+
FeatureGatesAccessor featuregates.FeatureGateAccess
71+
FeatureGateMapper internalfeatures.MapperInterface
6572
}
6673

6774
func New(cc *controllercmd.ControllerContext) (*Clients, error) {
@@ -122,10 +129,13 @@ func New(cc *controllercmd.ControllerContext) (*Clients, error) {
122129
ConfigClient: configClient,
123130
KubeInformerFactory: informers.NewSharedInformerFactory(kubeClient, defaultResyncPeriod),
124131
ConfigInformerFactory: configInformerFactory,
132+
FeatureGatesAccessor: setupFeatureGatesAccessor(kubeClient, configInformerFactory, cc.OperatorNamespace),
133+
FeatureGateMapper: internalfeatures.NewMapper(),
125134
}, nil
126135
}
127136

128137
func (c *Clients) StartInformers(ctx context.Context) {
138+
go c.FeatureGatesAccessor.Run(ctx)
129139
c.KubeInformerFactory.Start(ctx.Done())
130140
c.ConfigInformerFactory.Start(ctx.Done())
131141
c.OperatorInformers.Start(ctx.Done())
@@ -148,6 +158,38 @@ func (c *Clients) ClientHolder() *resourceapply.ClientHolder {
148158
return cl
149159
}
150160

161+
func setupFeatureGatesAccessor(
162+
kubeClient *kubernetes.Clientset,
163+
configInformerFactory configinformer.SharedInformerFactory,
164+
operatorNamespace string,
165+
) featuregates.FeatureGateAccess {
166+
eventRecorder := events.NewKubeRecorder(
167+
kubeClient.CoreV1().Events(operatorNamespace),
168+
"cluster-olm-operator",
169+
&corev1.ObjectReference{
170+
APIVersion: "apps/v1",
171+
Kind: "Deployment",
172+
Namespace: operatorNamespace,
173+
Name: "cluster-olm-operator",
174+
},
175+
)
176+
177+
operatorImageVersion := status.VersionForOperatorFromEnv()
178+
missingVersion := "0.0.1-snapshot"
179+
featureGateAccessor := featuregates.NewFeatureGateAccess(
180+
operatorImageVersion,
181+
missingVersion,
182+
configInformerFactory.Config().V1().ClusterVersions(), configInformerFactory.Config().V1().FeatureGates(),
183+
eventRecorder,
184+
)
185+
// modify the default behavior of calling exit(0) to noop whenever a FeatureGates set changes in cluster
186+
// reconsider this change if there ever comes a feature flag that affects the cluster-olm-operator directly
187+
// see: https://github.com/openshift/cluster-olm-operator/pull/102#discussion_r1926861888
188+
featureGateAccessor.SetChangeHandler(func(_ featuregates.FeatureChange) {})
189+
190+
return featureGateAccessor
191+
}
192+
151193
var _ v1helpers.OperatorClientWithFinalizers = &OperatorClient{}
152194

153195
const (

pkg/controller/builder.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io/fs"
99
"os"
1010
"path/filepath"
11+
"slices"
1112
"strconv"
1213
"strings"
1314

@@ -117,6 +118,7 @@ func (b *Builder) BuildControllers(subDirectories ...string) (map[string]factory
117118
replaceImageHook("${KUBE_RBAC_PROXY_IMAGE}", "KUBE_RBAC_PROXY_IMAGE"),
118119
},
119120
UpdateDeploymentProxyHook(b.Clients.ProxyClient),
121+
UpdateDeploymentFeatureGatesHook(b.Clients.FeatureGatesAccessor, b.Clients.FeatureGateMapper),
120122
)
121123
return nil
122124
}
@@ -219,19 +221,19 @@ func setContainerEnv(con *corev1.Container, envs []corev1.EnvVar) error {
219221

220222
func UpdateDeploymentProxyHook(pc clients.ProxyClientInterface) deploymentcontroller.DeploymentHookFunc {
221223
return func(_ *operatorv1.OperatorSpec, deployment *appsv1.Deployment) error {
222-
klog.FromContext(context.Background()).WithName("builder").V(0).Info("Updating environment", "deployment", deployment.Name)
224+
klog.FromContext(context.Background()).WithName("builder").V(0).Info("ProxyHook updating environment", "deployment", deployment.Name)
223225
proxyConfig, err := pc.Get("cluster")
224226
if err != nil {
225227
return fmt.Errorf("error getting proxies.config.openshift.io/cluster: %w", err)
226228
}
227229

228-
var errs []error
229230
vars := []corev1.EnvVar{
230231
{Name: HTTPSProxy, Value: proxyConfig.Status.HTTPSProxy},
231232
{Name: HTTPProxy, Value: proxyConfig.Status.HTTPProxy},
232233
{Name: NoProxy, Value: proxyConfig.Status.NoProxy},
233234
}
234235

236+
var errs []error
235237
for i := range deployment.Spec.Template.Spec.InitContainers {
236238
err = setContainerEnv(&deployment.Spec.Template.Spec.InitContainers[i], vars)
237239
if err != nil {
@@ -247,6 +249,22 @@ func UpdateDeploymentProxyHook(pc clients.ProxyClientInterface) deploymentcontro
247249
if len(errs) > 0 {
248250
return errors.Join(errs...)
249251
}
252+
253+
return nil
254+
}
255+
}
256+
257+
func setContainerArg(con *corev1.Container, arg, value string) error {
258+
if slices.ContainsFunc(con.Args, func(oldArg string) bool {
259+
return strings.HasPrefix(oldArg, arg)
260+
}) {
261+
return fmt.Errorf("unexpected argument %q in container %q while building manifests", arg, con.Name)
262+
}
263+
264+
if value == "" {
250265
return nil
251266
}
267+
klog.FromContext(context.Background()).WithName("builder").V(4).Info("Updated args", "container", con.Name, "arg", arg, "value", value)
268+
con.Args = append(con.Args, fmt.Sprintf("%s=%s", arg, value))
269+
return nil
252270
}

0 commit comments

Comments
 (0)