Files
open-cluster-management/pkg/operator/operators/klusterlet/controllers/addontlsconfigcontroller/controller.go
Jian Zhu a5375afe6e 🌱 Copy TLS ConfigMap to addon namespaces in klusterlet operator (#1480)
* 🌱 Copy TLS ConfigMap to addon namespaces in klusterlet operator

Add AddonTLSConfigController that copies the ocm-tls-profile ConfigMap
from the klusterlet operator namespace to addon namespaces (labeled with
addon.open-cluster-management.io/namespace: "true"). This allows addon
agents to read TLS profile settings without cross-namespace RBAC.

The controller mirrors the existing addonsecretcontroller pattern:
- Watches namespaces with the addon label via filtered informer
- Copies ConfigMap data on namespace creation/update
- Deletes target ConfigMap when source is removed
- Skips update when target is already up-to-date

Assisted by Claude

Signed-off-by: zhujian <jiazhu@redhat.com>

* 🌱 Fix ConfigMap update to preserve ResourceVersion and add stale-target test

- Reuse existing ConfigMap object on update to preserve ResourceVersion,
  preventing optimistic concurrency conflicts
- Add test case for stale target ConfigMap being updated

Assisted by Claude

Signed-off-by: zhujian <jiazhu@redhat.com>

---------

Signed-off-by: zhujian <jiazhu@redhat.com>
2026-04-13 06:14:02 +00:00

112 lines
3.3 KiB
Go

package addontlsconfigcontroller
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
coreinformer "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
tlslib "open-cluster-management.io/sdk-go/pkg/tls"
"open-cluster-management.io/ocm/pkg/common/queue"
)
const (
addonInstallNamespaceLabelKey = "addon.open-cluster-management.io/namespace"
)
// addonTLSConfigController copies the ocm-tls-profile ConfigMap from the operator namespace
// to addon namespaces (labeled with "addon.open-cluster-management.io/namespace":"true").
// This allows addon agents to read TLS profile settings without needing cross-namespace access.
type addonTLSConfigController struct {
operatorNamespace string
kubeClient kubernetes.Interface
}
func NewAddonTLSConfigController(kubeClient kubernetes.Interface, operatorNamespace string,
namespaceInformer coreinformer.NamespaceInformer) factory.Controller {
c := &addonTLSConfigController{
operatorNamespace: operatorNamespace,
kubeClient: kubeClient,
}
return factory.New().WithFilteredEventsInformersQueueKeysFunc(
queue.QueueKeyByMetaName,
queue.FileterByLabelKeyValue(addonInstallNamespaceLabelKey, "true"),
namespaceInformer.Informer()).WithSync(c.sync).ToController("AddonTLSConfigController")
}
func (c *addonTLSConfigController) sync(ctx context.Context, _ factory.SyncContext, namespace string) error {
if namespace == "" {
return nil
}
ns, err := c.kubeClient.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
if !ns.DeletionTimestamp.IsZero() {
return nil
}
if ns.Labels[addonInstallNamespaceLabelKey] != "true" {
return nil
}
return c.syncConfigMap(ctx, namespace)
}
func (c *addonTLSConfigController) syncConfigMap(ctx context.Context, targetNamespace string) error {
name := tlslib.ConfigMapName
source, err := c.kubeClient.CoreV1().ConfigMaps(c.operatorNamespace).Get(ctx, name, metav1.GetOptions{})
if errors.IsNotFound(err) {
// Source doesn't exist — clean up target if it exists
if delErr := c.kubeClient.CoreV1().ConfigMaps(targetNamespace).Delete(
ctx, name, metav1.DeleteOptions{}); delErr != nil && !errors.IsNotFound(delErr) {
return delErr
}
return nil
}
if err != nil {
return err
}
existing, err := c.kubeClient.CoreV1().ConfigMaps(targetNamespace).Get(ctx, name, metav1.GetOptions{})
if err != nil && !errors.IsNotFound(err) {
return err
}
if err == nil {
// Target exists — update only if data changed
if equality.Semantic.DeepEqual(existing.Data, source.Data) {
return nil
}
existing.Data = source.Data
_, err = c.kubeClient.CoreV1().ConfigMaps(targetNamespace).Update(ctx, existing, metav1.UpdateOptions{})
} else {
// Target doesn't exist — create it
target := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: targetNamespace,
},
Data: source.Data,
}
_, err = c.kubeClient.CoreV1().ConfigMaps(targetNamespace).Create(ctx, target, metav1.CreateOptions{})
}
if err != nil {
return err
}
klog.Infof("Synced ConfigMap %s from %s to %s", name, c.operatorNamespace, targetNamespace)
return nil
}