Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions mgmt-agent/cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -170,11 +172,25 @@ 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)
}

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 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)
Expand Down Expand Up @@ -205,13 +221,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()
Expand All @@ -230,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,
Expand Down Expand Up @@ -345,19 +356,31 @@ 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()
if err := o.ctrl.Run(ctx, o.workers); err != nil {
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()
Expand Down
102 changes: 102 additions & 0 deletions mgmt-agent/pkg/controller/cmwatcher.go
Original file line number Diff line number Diff line change
@@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add

cm.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))

by default the serialization of cm here won't have apiVersion * kind fields

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks fixed.

{"time":"2026-07-12T19:58:35.944790175Z","level":"INFO","source":{"function":"github.com/Azure/ARO-HCP/mgmt-agent/pkg/controller.logConfigMapEvent","file":"/app/mgmt-agent/pkg/controller/cmwatcher.go","line":96},"msg":"configmap event","event":"Delete","namespace":"ocm-arohcppers-2rfhh895c2l3eaed01asv15rimfo5dm9-e3m2t3d4r2c2m2n","name":"router","object":{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"router","namespace":"ocm-arohcppers-2rfhh895c2l3eaed01asv15rimfo5dm9-e3m2t3d4r2c2m2n","uid":"8dcddc7a-6424-4190-becd-6ba26ec157a1","resourceVersion":"4243052","creationTimestamp":"2026-07-10T22:19:19Z","ownerReferences":[{"apiVersion":"hypershift.openshift.io/v1beta1","kind":"HostedControlPlane","name":"e3m2t3d4r2c2m2n","uid":"a30063b0-5e8f-4c04-bedd-75fe79c6fc80","controller":true,"blockOwnerDeletion":true}],"managedFields":[{"manager":"hypershift-controlplane-manager","operation":"Update","apiVersion":"v1","time":"2026-07-10T22:19:19Z","fieldsType":"FieldsV1","fieldsV1":{"f:data":{".":{},"f:haproxy.cfg":{}},"f:metadata":{"f:ownerReferences":{".":{},"k:{\"uid\":\"a30063b0-5e8f-4c04-bedd-75fe79c6fc80\"}":{}}}}}]},"data":{"haproxy.cfg":"global\n  log stdout local0\n  maxconn 20000\n\ndefaults\n  mode tcp\n  log global\n  log-format \"%ci:%cp -> %fi:%fp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq\"\n  option  dontlognull\n  retries 3\n  timeout http-request 30s\n  timeout queue        1m\n  timeout connect      10s\n  timeout client       86400s\n  timeout server       86400s\n  timeout tunnel       86400s\n\nresolvers azure_dns\n  nameserver azure 168.63.129.16:53\n\nfrontend main\n  bind :::8443 v4v6\n  tcp-request inspect-delay 5s\n  tcp-request content accept if { req_ssl_hello_type 1 }\n  acl is_ignition req_ssl_sni -i ignition-server.apps.e3m2t3d4r2c2m2n.hypershift.local\n  acl is_konnectivity req_ssl_sni -i konnectivity-server.apps.e3m2t3d4r2c2m2n.hypershift.local\n  acl is_keyvault req_ssl_sni -i cust-kv-ucdeewzzrgy6c.vault.azure.net\n  use_backend ignition if is_ignition\n  use_backend konnectivity if is_konnectivity\n  use_backend keyvault if is_keyvault\n  default_backend kube_api\n\nlisten health_check_http_url\n  bind :::9444 v4v6\n  mode http\n  monitor-uri /haproxy_ready\n\nbackend ignition\n  server ignition 10.130.206.111:443\n\nbackend konnectivity\n  server konnectivity 10.130.218.133:8091\n\nbackend kube_api\n  server kube_api 10.130.201.218:6443\n\nbackend keyvault\n  mode tcp\n  server kv cust-kv-ucdeewzzrgy6c.vault.azure.net:443 resolvers azure_dns\n"}}}

cm.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("ConfigMap"))
klog.InfoS("configmap event",
"event", eventType,
"namespace", cm.Namespace,
"name", cm.Name,
"object", cm,
)
}
Comment on lines +94 to +102
88 changes: 88 additions & 0 deletions mgmt-agent/pkg/controller/cmwatcher_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Comment on lines +67 to +71
}

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")
}
}
Comment on lines +74 to +88