From 439a7b5d02a059ac29ab8dc735ad0d4c4e64bb9b Mon Sep 17 00:00:00 2001 From: Thomas Jungblut Date: Wed, 15 Oct 2025 11:15:44 +0200 Subject: [PATCH] CNTRLPLANE-1616: add event-ttl config observer This updates the API to latest main and adds the logic to set the event-ttl accoringly to the newly introduced API field. Note to the reviewer, this has been largely AI generated by Cursor. Signed-off-by: Thomas Jungblut --- .../apiserver/observe_event_ttl.go | 70 +++++++ .../apiserver/observe_event_ttl_test.go | 177 ++++++++++++++++++ .../observe_config_controller.go | 2 + 3 files changed, 249 insertions(+) create mode 100644 pkg/operator/configobservation/apiserver/observe_event_ttl.go create mode 100644 pkg/operator/configobservation/apiserver/observe_event_ttl_test.go diff --git a/pkg/operator/configobservation/apiserver/observe_event_ttl.go b/pkg/operator/configobservation/apiserver/observe_event_ttl.go new file mode 100644 index 0000000000..18136aa5d0 --- /dev/null +++ b/pkg/operator/configobservation/apiserver/observe_event_ttl.go @@ -0,0 +1,70 @@ +package apiserver + +import ( + "fmt" + + "github.com/openshift/api/features" + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation" + "github.com/openshift/library-go/pkg/operator/configobserver" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + "github.com/openshift/library-go/pkg/operator/events" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var eventTTLPath = []string{"apiServerArguments", "event-ttl"} + +// NewObserveEventTTL returns a config observation function that observes +// the EventTTLMinutes field from the KubeAPIServer operator CRD +func NewObserveEventTTL(featureGateAccessor featuregates.FeatureGateAccess) configobserver.ObserveConfigFunc { + return (&eventTTLObserver{ + featureGateAccessor: featureGateAccessor, + }).ObserveEventTTL +} + +type eventTTLObserver struct { + featureGateAccessor featuregates.FeatureGateAccess +} + +// ObserveEventTTL reads the eventTTLMinutes from the KubeAPIServer operator CRD +func (o *eventTTLObserver) ObserveEventTTL(genericListers configobserver.Listers, recorder events.Recorder, existingConfig map[string]interface{}) (ret map[string]interface{}, errs []error) { + defer func() { + // Prune the observed config to only include the event-ttl path + ret = configobserver.Pruned(ret, eventTTLPath) + }() + + if !o.featureGateAccessor.AreInitialFeatureGatesObserved() { + // if we haven't observed featuregates yet, return the existing + return existingConfig, nil + } + + featureGates, err := o.featureGateAccessor.CurrentFeatureGates() + if err != nil { + return existingConfig, []error{err} + } + + if !featureGates.Enabled(features.FeatureEventTTL) { + // Feature disabled: return no opinion so any previously observed value is removed. + // Pruning in defer will ensure only the relevant path is considered. + return map[string]interface{}{}, nil + } + + kubeAPIServer, err := genericListers.(configobservation.Listers).KubeAPIServerOperatorLister().Get("cluster") + if err != nil { + return existingConfig, []error{err} + } + + // Determine the event TTL value to use + var eventTTLValue string + if kubeAPIServer.Spec.EventTTLMinutes > 0 { + observedConfig := map[string]interface{}{} + // Use the specified value, convert minutes to duration string (e.g., "180m" for 180 minutes) + eventTTLValue = fmt.Sprintf("%dm", kubeAPIServer.Spec.EventTTLMinutes) + if err := unstructured.SetNestedStringSlice(observedConfig, []string{eventTTLValue}, eventTTLPath...); err != nil { + return existingConfig, []error{err} + } + return observedConfig, nil + } + + // Use default value from the defaultconfig.yaml when EventTTLMinutes is 0 or not set + return map[string]interface{}{}, nil +} diff --git a/pkg/operator/configobservation/apiserver/observe_event_ttl_test.go b/pkg/operator/configobservation/apiserver/observe_event_ttl_test.go new file mode 100644 index 0000000000..f38c36cbc5 --- /dev/null +++ b/pkg/operator/configobservation/apiserver/observe_event_ttl_test.go @@ -0,0 +1,177 @@ +package apiserver + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/api/features" + operatorv1 "github.com/openshift/api/operator/v1" + operatorlistersv1 "github.com/openshift/client-go/operator/listers/operator/v1" + "github.com/openshift/cluster-kube-apiserver-operator/pkg/operator/configobservation" + "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + "github.com/openshift/library-go/pkg/operator/events" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + clocktesting "k8s.io/utils/clock/testing" +) + +func TestObserveEventTTL(t *testing.T) { + scenarios := []struct { + name string + existingKubeAPIConfig map[string]interface{} + expectedKubeAPIConfig map[string]interface{} + eventTTLMinutes int32 + featureOn bool + }{ + { + name: "feature gate disabled", + existingKubeAPIConfig: map[string]interface{}{}, + expectedKubeAPIConfig: map[string]interface{}{}, + eventTTLMinutes: 120, + featureOn: false, + }, + { + name: "feature gate disabled clears existing event-ttl", + existingKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"120m"}, + }, + }, + expectedKubeAPIConfig: map[string]interface{}{}, + eventTTLMinutes: 0, + featureOn: false, + }, + { + name: "feature gate enabled, no event TTL set - use default from defaultconfig.yaml", + existingKubeAPIConfig: map[string]interface{}{}, + expectedKubeAPIConfig: map[string]interface{}{}, + eventTTLMinutes: 0, + featureOn: true, + }, + { + name: "feature gate enabled, event TTL set to 60 minutes", + existingKubeAPIConfig: map[string]interface{}{}, + expectedKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"60m"}, + }, + }, + eventTTLMinutes: 60, + featureOn: true, + }, + { + name: "feature gate enabled, event TTL set to 180 minutes (maximum)", + existingKubeAPIConfig: map[string]interface{}{}, + expectedKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"180m"}, + }, + }, + eventTTLMinutes: 180, + featureOn: true, + }, + { + name: "feature gate enabled, event TTL set to 5 minutes (minimum)", + existingKubeAPIConfig: map[string]interface{}{}, + expectedKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"5m"}, + }, + }, + eventTTLMinutes: 5, + featureOn: true, + }, + { + name: "feature gate enabled, update existing config", + existingKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"120m"}, + }, + }, + expectedKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"90m"}, + }, + }, + eventTTLMinutes: 90, + featureOn: true, + }, + { + name: "feature gate enabled, no change needed", + existingKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"120m"}, + }, + }, + expectedKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"120m"}, + }, + }, + eventTTLMinutes: 120, + featureOn: true, + }, + { + name: "feature gate enabled, set default event-ttl when set to 0", + existingKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"120m"}, + }, + }, + expectedKubeAPIConfig: map[string]interface{}{}, + eventTTLMinutes: 0, + featureOn: true, + }, + { + name: "feature gate enabled, no change needed when already at default, returning empty", + existingKubeAPIConfig: map[string]interface{}{ + "apiServerArguments": map[string]interface{}{ + "event-ttl": []interface{}{"3h"}, + }, + }, + expectedKubeAPIConfig: map[string]interface{}{}, + eventTTLMinutes: 0, + featureOn: true, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + // test data + eventRecorder := events.NewInMemoryRecorder("", clocktesting.NewFakePassiveClock(time.Now())) + kubeAPIServerIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + + // Add KubeAPIServer resource + _ = kubeAPIServerIndexer.Add(&operatorv1.KubeAPIServer{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Spec: operatorv1.KubeAPIServerSpec{ + EventTTLMinutes: scenario.eventTTLMinutes, + }, + }) + + listers := configobservation.Listers{ + KubeAPIServerOperatorLister_: operatorlistersv1.NewKubeAPIServerLister(kubeAPIServerIndexer), + } + + // Set up feature gate accessor + var fg featuregates.FeatureGateAccess + if scenario.featureOn { + fg = featuregates.NewHardcodedFeatureGateAccess([]configv1.FeatureGateName{features.FeatureEventTTL}, []configv1.FeatureGateName{}) + } else { + fg = featuregates.NewHardcodedFeatureGateAccess([]configv1.FeatureGateName{}, []configv1.FeatureGateName{features.FeatureEventTTL}) + } + + observer := NewObserveEventTTL(fg) + observedKubeAPIConfig, errs := observer(listers, eventRecorder, scenario.existingKubeAPIConfig) + + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + if diff := cmp.Diff(scenario.expectedKubeAPIConfig, observedKubeAPIConfig); diff != "" { + t.Fatalf("unexpected configuration, diff = %s", diff) + } + }) + } +} diff --git a/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go b/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go index 9cf66a9689..d8ab496987 100644 --- a/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go +++ b/pkg/operator/configobservation/configobservercontroller/observe_config_controller.go @@ -97,6 +97,7 @@ func NewConfigObserver(operatorClient v1helpers.StaticPodOperatorClient, kubeInf ResourceSync: resourceSyncer, PreRunCachesSynced: append(preRunCacheSynced, operatorClient.Informer().HasSynced, + operatorInformer.Operator().V1().KubeAPIServers().Informer().HasSynced, kubeInformersForNamespaces.InformersFor("openshift-etcd").Core().V1().ConfigMaps().Informer().HasSynced, kubeInformersForNamespaces.InformersFor(operatorclient.TargetNamespace).Core().V1().Secrets().Informer().HasSynced, @@ -125,6 +126,7 @@ func NewConfigObserver(operatorClient v1helpers.StaticPodOperatorClient, kubeInf apiserver.ObserveSendRetryAfterWhileNotReadyOnce, apiserver.ObserveGoawayChance, apiserver.ObserveAdmissionPlugins, + apiserver.NewObserveEventTTL(featureGateAccessor), libgoapiserver.ObserveTLSSecurityProfile, auth.ObserveAuthMetadata, auth.ObserveServiceAccountIssuer,