From cbec9efaf1bc5600eedbdf3fd3c4188827b3ea80 Mon Sep 17 00:00:00 2001 From: Ben Vesel <10840174+bennerv@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:46:53 -0400 Subject: [PATCH 1/2] fix(mgmt-agent): correctly place podwatcher to always run regardless if ksm is enabled --- mgmt-agent/cmd/options.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mgmt-agent/cmd/options.go b/mgmt-agent/cmd/options.go index be82bd6cfa4..1d210c4ea8b 100644 --- a/mgmt-agent/cmd/options.go +++ b/mgmt-agent/cmd/options.go @@ -170,11 +170,15 @@ func (o *ValidatedControllerOptions) Complete(ctx context.Context) (*ControllerO resourceWatcher := controller.NewResourceWatcher(dynamicClient, discoveryClient) + clusterWideKubeInformers := kubeinformers.NewSharedInformerFactory(kubeClientset, 0) + podWatcher, err := controller.NewPodWatcher(clusterWideKubeInformers.Core().V1().Pods()) + if err != nil { + return nil, fmt.Errorf("failed to create pod watcher: %w", err) + } + var ksmCtrl *ksmhcp.KSMHCPController - var podWatcher *controller.PodWatcher var hsInformers hypershiftinformers.SharedInformerFactory var ksmKubeInformers kubeinformers.SharedInformerFactory - var clusterWideKubeInformers kubeinformers.SharedInformerFactory var dynInformers dynamicinformer.DynamicSharedInformerFactory if o.KSMImage != "" { hsClient, err := hypershiftclient.NewForConfig(kubeConfig) @@ -205,13 +209,6 @@ func (o *ValidatedControllerOptions) Complete(ctx context.Context) (*ControllerO if err != nil { return nil, fmt.Errorf("failed to create KSM HCP controller: %w", err) } - - clusterWideKubeInformers = kubeinformers.NewSharedInformerFactory(kubeClientset, 0) - - podWatcher, err = controller.NewPodWatcher(clusterWideKubeInformers.Core().V1().Pods()) - if err != nil { - return nil, fmt.Errorf("failed to create pod watcher: %w", err) - } } hostname, err := os.Hostname() From f5b6301ec29f3632af34d95a8a2b82330c858955 Mon Sep 17 00:00:00 2001 From: Ben Vesel <10840174+bennerv@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:14:49 -0400 Subject: [PATCH 2/2] feat(mgmt-agent): add configmap watcher for swift router cm --- mgmt-agent/cmd/options.go | 26 +++++ mgmt-agent/pkg/controller/cmwatcher.go | 102 ++++++++++++++++++++ mgmt-agent/pkg/controller/cmwatcher_test.go | 88 +++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 mgmt-agent/pkg/controller/cmwatcher.go create mode 100644 mgmt-agent/pkg/controller/cmwatcher_test.go diff --git a/mgmt-agent/cmd/options.go b/mgmt-agent/cmd/options.go index 1d210c4ea8b..761e2ad668d 100644 --- a/mgmt-agent/cmd/options.go +++ b/mgmt-agent/cmd/options.go @@ -99,9 +99,11 @@ type completedControllerOptions struct { ksmCtrl *ksmhcp.KSMHCPController resourceWatcher *controller.ResourceWatcher podWatcher *controller.PodWatcher + configMapWatcher *controller.ConfigMapWatcher kubeInformers kubeinformers.SharedInformerFactory ksmKubeInformers kubeinformers.SharedInformerFactory clusterWideKubeInformers kubeinformers.SharedInformerFactory + cmWatcherInformers kubeinformers.SharedInformerFactory hypershiftInformers hypershiftinformers.SharedInformerFactory dynamicInformers dynamicinformer.DynamicSharedInformerFactory workers int @@ -176,6 +178,16 @@ func (o *ValidatedControllerOptions) Complete(ctx context.Context) (*ControllerO return nil, fmt.Errorf("failed to create pod watcher: %w", err) } + cmWatcherInformers := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClientset, 0, + kubeinformers.WithTweakListOptions(func(opts *metav1.ListOptions) { + opts.FieldSelector = "metadata.name=" + controller.RouterConfigMapName + }), + ) + configMapWatcher, err := controller.NewConfigMapWatcher(cmWatcherInformers.Core().V1().ConfigMaps()) + if err != nil { + return nil, fmt.Errorf("failed to create ConfigMap watcher: %w", err) + } + var ksmCtrl *ksmhcp.KSMHCPController var hsInformers hypershiftinformers.SharedInformerFactory var ksmKubeInformers kubeinformers.SharedInformerFactory @@ -227,9 +239,11 @@ func (o *ValidatedControllerOptions) Complete(ctx context.Context) (*ControllerO ksmCtrl: ksmCtrl, resourceWatcher: resourceWatcher, podWatcher: podWatcher, + configMapWatcher: configMapWatcher, kubeInformers: kubeInformers, ksmKubeInformers: ksmKubeInformers, clusterWideKubeInformers: clusterWideKubeInformers, + cmWatcherInformers: cmWatcherInformers, hypershiftInformers: hsInformers, dynamicInformers: dynInformers, workers: o.Workers, @@ -342,6 +356,9 @@ func (o *ControllerOptions) runControllersUnderLeaderElection(ctx context.Contex if o.dynamicInformers != nil { o.dynamicInformers.Start(ctx.Done()) } + if o.cmWatcherInformers != nil { + o.cmWatcherInformers.Start(ctx.Done()) + } go func() { defer utilruntime.HandleCrash() @@ -349,12 +366,21 @@ func (o *ControllerOptions) runControllersUnderLeaderElection(ctx context.Contex logger.Error(err, "SwiftNIC controller failed") } }() + go func() { defer utilruntime.HandleCrash() if err := o.resourceWatcher.Run(ctx); err != nil { logger.Error(err, "resource watcher failed") } }() + + go func() { + defer utilruntime.HandleCrash() + if err := o.configMapWatcher.Run(ctx); err != nil { + logger.Error(err, "ConfigMap watcher failed") + } + }() + if o.ksmCtrl != nil { go func() { defer utilruntime.HandleCrash() diff --git a/mgmt-agent/pkg/controller/cmwatcher.go b/mgmt-agent/pkg/controller/cmwatcher.go new file mode 100644 index 00000000000..c93aecbbb23 --- /dev/null +++ b/mgmt-agent/pkg/controller/cmwatcher.go @@ -0,0 +1,102 @@ +// Copyright 2025 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + coreinformers "k8s.io/client-go/informers/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" +) + +// The HyperShift router ConfigMap has no distinguishing labels, so we select by name. +const RouterConfigMapName = "router" + +// ConfigMapWatcher watches ConfigMap resources using a typed informer and logs +// create, update, and delete events via structured logging. It is intended to +// be used with a field-selector-scoped informer factory so that only ConfigMaps +// with a specific name (e.g. "router") are watched. +type ConfigMapWatcher struct { + cmSynced cache.InformerSynced +} + +// NewConfigMapWatcher creates a new ConfigMapWatcher. It registers event handlers +// on the given ConfigMap informer to log ConfigMap lifecycle events. +func NewConfigMapWatcher(cmInformer coreinformers.ConfigMapInformer) (*ConfigMapWatcher, error) { + w := &ConfigMapWatcher{ + cmSynced: cmInformer.Informer().HasSynced, + } + + if _, err := cmInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + cm, ok := obj.(*corev1.ConfigMap) + if !ok { + return + } + logConfigMapEvent("Add", cm) + }, + UpdateFunc: func(_, newObj interface{}) { + cm, ok := newObj.(*corev1.ConfigMap) + if !ok { + return + } + logConfigMapEvent("Update", cm) + }, + DeleteFunc: func(obj interface{}) { + if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok { + obj = tombstone.Obj + } + cm, ok := obj.(*corev1.ConfigMap) + if !ok { + return + } + logConfigMapEvent("Delete", cm) + }, + }); err != nil { + return nil, fmt.Errorf("failed to add event handler: %w", err) + } + + return w, nil +} + +// Run waits for the ConfigMap informer cache to sync and blocks until the +// context is cancelled. +func (w *ConfigMapWatcher) Run(ctx context.Context) error { + logger := klog.FromContext(ctx) + logger.Info("Starting ConfigMap watcher") + + logger.Info("Waiting for ConfigMap informer cache to sync") + if ok := cache.WaitForCacheSync(ctx.Done(), w.cmSynced); !ok { + return fmt.Errorf("failed to wait for ConfigMap informer cache to sync") + } + + logger.Info("ConfigMap watcher informer synced and running") + <-ctx.Done() + logger.Info("Shutting down ConfigMap watcher") + return nil +} + +func logConfigMapEvent(eventType string, cm *corev1.ConfigMap) { + cm.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap")) + klog.InfoS("configmap event", + "event", eventType, + "namespace", cm.Namespace, + "name", cm.Name, + "object", cm, + ) +} diff --git a/mgmt-agent/pkg/controller/cmwatcher_test.go b/mgmt-agent/pkg/controller/cmwatcher_test.go new file mode 100644 index 00000000000..f1d982bc5b4 --- /dev/null +++ b/mgmt-agent/pkg/controller/cmwatcher_test.go @@ -0,0 +1,88 @@ +// Copyright 2025 Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controller + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubeinformers "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" +) + +func TestLogConfigMapEvent(t *testing.T) { + tests := []struct { + name string + eventType string + cm *corev1.ConfigMap + }{ + { + name: "Add event", + eventType: "Add", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: RouterConfigMapName, + Namespace: "ocm-hcp-test", + }, + Data: map[string]string{ + "haproxy.cfg": "global\n maxconn 4096", + }, + }, + }, + { + name: "Update event", + eventType: "Update", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: RouterConfigMapName, + Namespace: "ocm-hcp-test", + }, + }, + }, + { + name: "Delete event", + eventType: "Delete", + cm: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: RouterConfigMapName, + Namespace: "ocm-hcp-test", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logConfigMapEvent(tt.eventType, tt.cm) + }) + } +} + +func TestNewConfigMapWatcher(t *testing.T) { + clientset := fake.NewSimpleClientset() + factory := kubeinformers.NewSharedInformerFactory(clientset, 0) + + w, err := NewConfigMapWatcher(factory.Core().V1().ConfigMaps()) + if err != nil { + t.Fatalf("NewConfigMapWatcher() returned error: %v", err) + } + if w == nil { + t.Fatal("NewConfigMapWatcher() returned nil") + } + if w.cmSynced == nil { + t.Fatal("NewConfigMapWatcher() did not set cmSynced") + } +}