🌱 sync clusterprofile based on managedclusterset and managedclustersetbinding (#1351)
Some checks failed
Scorecard supply-chain security / Scorecard analysis (push) Failing after 25s
Post / images (amd64, placement) (push) Failing after 47s
Post / images (amd64, registration) (push) Failing after 44s
Post / images (amd64, registration-operator) (push) Failing after 44s
Post / images (amd64, work) (push) Failing after 43s
Post / images (arm64, addon-manager) (push) Failing after 42s
Post / images (arm64, placement) (push) Failing after 41s
Post / images (arm64, registration) (push) Failing after 43s
Post / images (arm64, registration-operator) (push) Failing after 41s
Post / images (arm64, work) (push) Failing after 41s
Post / images (amd64, addon-manager) (push) Failing after 7m45s
Post / image manifest (addon-manager) (push) Has been skipped
Post / image manifest (placement) (push) Has been skipped
Post / image manifest (registration) (push) Has been skipped
Post / image manifest (registration-operator) (push) Has been skipped
Post / image manifest (work) (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Post / coverage (push) Failing after 38m55s
Close stale issues and PRs / stale (push) Successful in 50s

* sync clusterprofile based on managedclusterset and managedclustersetbinding

Co-authored-by: Claude <claude@anthropic.com>

Signed-off-by: Morven Cao <lcao@redhat.com>

* Refactor ClusterProfile controller into two separate controllers.

Signed-off-by: Morven Cao <lcao@redhat.com>

* address comments.

Signed-off-by: Morven Cao <lcao@redhat.com>

* fix lint issues.

Signed-off-by: Morven Cao <lcao@redhat.com>

* address comments.

Signed-off-by: Morven Cao <lcao@redhat.com>

* address comments.

Signed-off-by: Morven Cao <lcao@redhat.com>

---------

Signed-off-by: Morven Cao <lcao@redhat.com>
This commit is contained in:
Morven Cao
2026-01-28 23:37:46 +08:00
committed by GitHub
parent 9d1a993e2c
commit d1221c4a79
11 changed files with 4647 additions and 435 deletions

View File

@@ -1,170 +0,0 @@
package clusterprofile
import (
"context"
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
cpv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
cpclientset "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned"
cpinformerv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions/apis/v1alpha1"
cplisterv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/listers/apis/v1alpha1"
informerv1 "open-cluster-management.io/api/client/cluster/informers/externalversions/cluster/v1"
listerv1 "open-cluster-management.io/api/client/cluster/listers/cluster/v1"
v1 "open-cluster-management.io/api/cluster/v1"
v1beta2 "open-cluster-management.io/api/cluster/v1beta2"
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
"open-cluster-management.io/sdk-go/pkg/patcher"
"open-cluster-management.io/ocm/pkg/common/queue"
)
const (
ClusterProfileManagerName = "open-cluster-management"
ClusterProfileNamespace = "open-cluster-management"
)
// clusterProfileController reconciles instances of ClusterProfile on the hub.
type clusterProfileController struct {
clusterLister listerv1.ManagedClusterLister
clusterProfileClient cpclientset.Interface
clusterProfileLister cplisterv1alpha1.ClusterProfileLister
patcher patcher.Patcher[*cpv1alpha1.ClusterProfile, cpv1alpha1.ClusterProfileSpec, cpv1alpha1.ClusterProfileStatus]
}
// NewClusterProfileController creates a new managed cluster controller
func NewClusterProfileController(
clusterInformer informerv1.ManagedClusterInformer,
clusterProfileClient cpclientset.Interface,
clusterProfileInformer cpinformerv1alpha1.ClusterProfileInformer) factory.Controller {
c := &clusterProfileController{
clusterLister: clusterInformer.Lister(),
clusterProfileClient: clusterProfileClient,
clusterProfileLister: clusterProfileInformer.Lister(),
patcher: patcher.NewPatcher[
*cpv1alpha1.ClusterProfile, cpv1alpha1.ClusterProfileSpec, cpv1alpha1.ClusterProfileStatus](
clusterProfileClient.ApisV1alpha1().ClusterProfiles(ClusterProfileNamespace)),
}
return factory.New().
WithInformersQueueKeysFunc(queue.QueueKeyByMetaName, clusterInformer.Informer(), clusterProfileInformer.Informer()).
WithSync(c.sync).
ToController("ClusterProfileController")
}
func (c *clusterProfileController) sync(ctx context.Context, syncCtx factory.SyncContext, managedClusterName string) error {
logger := klog.FromContext(ctx).WithValues("managedClusterName", managedClusterName)
logger.V(4).Info("Reconciling Cluster")
managedCluster, err := c.clusterLister.Get(managedClusterName)
if errors.IsNotFound(err) {
// Spoke cluster not found, could have been deleted, do nothing.
return nil
}
if err != nil {
return err
}
clusterProfile, err := c.clusterProfileLister.ClusterProfiles(ClusterProfileNamespace).Get(managedClusterName)
// if the managed cluster is deleting, delete the clusterprofile as well.
if !managedCluster.DeletionTimestamp.IsZero() {
if errors.IsNotFound(err) {
return nil
}
err = c.clusterProfileClient.ApisV1alpha1().ClusterProfiles(ClusterProfileNamespace).Delete(ctx, managedClusterName, metav1.DeleteOptions{})
return err
}
// create cluster profile if not found
if errors.IsNotFound(err) {
clusterProfile = &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: managedClusterName,
Labels: map[string]string{cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: managedClusterName,
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
_, err = c.clusterProfileClient.ApisV1alpha1().ClusterProfiles(ClusterProfileNamespace).Create(ctx, clusterProfile, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}
// return if not managed by ocm
if clusterProfile.Spec.ClusterManager.Name != ClusterProfileManagerName {
logger.Info("Not managed by open-cluster-management, skipping", "ClusterName", managedClusterName)
return nil
}
newClusterProfile := clusterProfile.DeepCopy()
// sync required labels
mclLabels := managedCluster.GetLabels()
mclSetLabel := mclLabels[v1beta2.ClusterSetLabel]
// The value of label "x-k8s.io/cluster-manager" MUST be the same as the name of the cluster manager.
requiredLabels := map[string]string{
cpv1alpha1.LabelClusterManagerKey: newClusterProfile.Spec.ClusterManager.Name,
cpv1alpha1.LabelClusterSetKey: mclSetLabel,
}
modified := false
resourcemerge.MergeMap(&modified, &newClusterProfile.Labels, requiredLabels)
// patch labels
if modified {
_, err := c.patcher.PatchLabelAnnotations(ctx, newClusterProfile, newClusterProfile.ObjectMeta, clusterProfile.ObjectMeta)
return err
}
// sync status.version.kubernetes
newClusterProfile.Status.Version.Kubernetes = managedCluster.Status.Version.Kubernetes
// sync status.properties
cpProperties := []cpv1alpha1.Property{}
for _, v := range managedCluster.Status.ClusterClaims {
cpProperties = append(cpProperties, cpv1alpha1.Property{Name: v.Name, Value: v.Value})
}
newClusterProfile.Status.Properties = cpProperties
// sync status.conditions
managedClusterAvailableCondition := meta.FindStatusCondition(managedCluster.Status.Conditions, v1.ManagedClusterConditionAvailable)
if managedClusterAvailableCondition != nil {
c := metav1.Condition{
Type: cpv1alpha1.ClusterConditionControlPlaneHealthy,
Status: managedClusterAvailableCondition.Status,
Reason: managedClusterAvailableCondition.Reason,
Message: managedClusterAvailableCondition.Message,
}
meta.SetStatusCondition(&newClusterProfile.Status.Conditions, c)
}
managedClusterJoinedCondition := meta.FindStatusCondition(managedCluster.Status.Conditions, v1.ManagedClusterConditionJoined)
if managedClusterJoinedCondition != nil {
c := metav1.Condition{
Type: "Joined",
Status: managedClusterJoinedCondition.Status,
Reason: managedClusterJoinedCondition.Reason,
Message: managedClusterJoinedCondition.Message,
}
meta.SetStatusCondition(&newClusterProfile.Status.Conditions, c)
}
// patch status
updated, err := c.patcher.PatchStatus(ctx, newClusterProfile, newClusterProfile.Status, clusterProfile.Status)
if err != nil {
return err
}
if updated {
syncCtx.Recorder().Eventf(ctx, "ClusterProfileSynced", "cluster profile %s is synced from open cluster management", managedClusterName)
}
return nil
}

View File

@@ -1,261 +0,0 @@
package clusterprofile
import (
"context"
"encoding/json"
"reflect"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clienttesting "k8s.io/client-go/testing"
cpv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
cpfake "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned/fake"
cpinformers "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions"
clusterfake "open-cluster-management.io/api/client/cluster/clientset/versioned/fake"
clusterinformers "open-cluster-management.io/api/client/cluster/informers/externalversions"
v1 "open-cluster-management.io/api/cluster/v1"
v1beta2 "open-cluster-management.io/api/cluster/v1beta2"
"open-cluster-management.io/sdk-go/pkg/patcher"
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
)
func TestSyncClusterProfile(t *testing.T) {
managedCluster := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: testinghelpers.TestManagedClusterName,
Labels: map[string]string{v1beta2.ClusterSetLabel: "default"},
},
Status: v1.ManagedClusterStatus{
Version: v1.ManagedClusterVersion{
Kubernetes: "v1.25.3",
},
ClusterClaims: []v1.ManagedClusterClaim{
{Name: "claim1", Value: "value1"},
},
Conditions: []metav1.Condition{
{
Type: v1.ManagedClusterConditionAvailable,
Status: metav1.ConditionTrue,
},
{
Type: v1.ManagedClusterConditionJoined,
Status: metav1.ConditionTrue,
},
},
},
}
expectedCreatedClusterProfile := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: testinghelpers.TestManagedClusterName,
Namespace: ClusterProfileNamespace,
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: testinghelpers.TestManagedClusterName,
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
expectedPatchedClusterProfileLabels := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: testinghelpers.TestManagedClusterName,
Namespace: ClusterProfileNamespace,
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "default",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: testinghelpers.TestManagedClusterName,
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
expectedPatchedClusterProfileStatus := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: testinghelpers.TestManagedClusterName,
Namespace: ClusterProfileNamespace,
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "default",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: testinghelpers.TestManagedClusterName,
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
Status: cpv1alpha1.ClusterProfileStatus{
Version: cpv1alpha1.ClusterVersion{
Kubernetes: "v1.25.3",
},
Properties: []cpv1alpha1.Property{
{Name: "claim1", Value: "value1"},
},
Conditions: []metav1.Condition{
{
Type: v1.ManagedClusterConditionAvailable,
Status: metav1.ConditionTrue,
},
{
Type: v1.ManagedClusterConditionJoined,
Status: metav1.ConditionTrue,
},
},
},
}
cases := []struct {
name string
autoApprovalEnabled bool
mc []runtime.Object
cp []runtime.Object
validateActions func(t *testing.T, actions []clienttesting.Action)
}{
{
name: "create clusterprofile",
mc: []runtime.Object{managedCluster},
cp: []runtime.Object{},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "create")
clusterprofile := actions[0].(clienttesting.CreateAction).GetObject().(*cpv1alpha1.ClusterProfile)
if !reflect.DeepEqual(clusterprofile.Labels, expectedCreatedClusterProfile.Labels) {
t.Errorf("expect clusterprofile labels %v but get %v", expectedCreatedClusterProfile.Labels, clusterprofile.Labels)
}
if !reflect.DeepEqual(clusterprofile.Spec, expectedCreatedClusterProfile.Spec) {
t.Errorf("expect clusterprofile spec %v but get %v", expectedCreatedClusterProfile.Spec, clusterprofile.Spec)
}
},
},
{
name: "patch clusterprofile clusterset labels",
mc: []runtime.Object{managedCluster},
cp: []runtime.Object{expectedCreatedClusterProfile},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
patch := actions[0].(clienttesting.PatchAction).GetPatch()
clusterprofile := &cpv1alpha1.ClusterProfile{}
err := json.Unmarshal(patch, clusterprofile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(clusterprofile.Labels[cpv1alpha1.LabelClusterSetKey], expectedPatchedClusterProfileLabels.Labels[cpv1alpha1.LabelClusterSetKey]) {
t.Errorf("expect clusterprofile labels %v but get %v", expectedPatchedClusterProfileLabels.Labels, clusterprofile.Labels)
}
},
},
{
name: "patch clusterprofile status",
mc: []runtime.Object{managedCluster},
cp: []runtime.Object{expectedPatchedClusterProfileLabels},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "patch")
patch := actions[0].(clienttesting.PatchAction).GetPatch()
clusterprofile := &cpv1alpha1.ClusterProfile{}
err := json.Unmarshal(patch, clusterprofile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(clusterprofile.Status.Version, expectedPatchedClusterProfileStatus.Status.Version) ||
!reflect.DeepEqual(clusterprofile.Status.Properties, expectedPatchedClusterProfileStatus.Status.Properties) ||
len(expectedPatchedClusterProfileStatus.Status.Conditions) != 2 {
t.Errorf("expect clusterprofile status %v but get %v", expectedPatchedClusterProfileStatus.Status, clusterprofile.Status)
}
},
},
{
name: "deleting clusterprofile",
mc: []runtime.Object{testinghelpers.NewDeletingManagedCluster()},
cp: []runtime.Object{expectedPatchedClusterProfileStatus},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertActions(t, actions, "delete")
},
},
{
name: "deleted clusterprofile",
mc: []runtime.Object{testinghelpers.NewDeletingManagedCluster()},
cp: []runtime.Object{},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertNoActions(t, actions)
},
},
{
name: "no managed cluster",
mc: []runtime.Object{},
cp: []runtime.Object{},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertNoActions(t, actions)
},
},
{
name: "clusterprofile not managed by ocm",
mc: []runtime.Object{managedCluster},
cp: []runtime.Object{&cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: testinghelpers.TestManagedClusterName,
Namespace: ClusterProfileNamespace,
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: "not-open-cluster-management",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
ClusterManager: cpv1alpha1.ClusterManager{
Name: "not-open-cluster-management",
},
},
}},
validateActions: func(t *testing.T, actions []clienttesting.Action) {
testingcommon.AssertNoActions(t, actions)
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
clusterClient := clusterfake.NewSimpleClientset(c.mc...)
clusterProfileClient := cpfake.NewSimpleClientset(c.cp...)
clusterInformerFactory := clusterinformers.NewSharedInformerFactory(clusterClient, time.Minute*10)
clusterProfileInformerFactory := cpinformers.NewSharedInformerFactory(clusterProfileClient, time.Minute*10)
clusterStore := clusterInformerFactory.Cluster().V1().ManagedClusters().Informer().GetStore()
for _, cluster := range c.mc {
if err := clusterStore.Add(cluster); err != nil {
t.Fatal(err)
}
}
clusterProfileStore := clusterProfileInformerFactory.Apis().V1alpha1().ClusterProfiles().Informer().GetStore()
for _, clusterprofile := range c.cp {
if err := clusterProfileStore.Add(clusterprofile); err != nil {
t.Fatal(err)
}
}
ctrl := clusterProfileController{
clusterInformerFactory.Cluster().V1().ManagedClusters().Lister(),
clusterProfileClient,
clusterProfileInformerFactory.Apis().V1alpha1().ClusterProfiles().Lister(),
patcher.NewPatcher[
*cpv1alpha1.ClusterProfile, cpv1alpha1.ClusterProfileSpec, cpv1alpha1.ClusterProfileStatus](
clusterProfileClient.ApisV1alpha1().ClusterProfiles(ClusterProfileNamespace)),
}
syncErr := ctrl.sync(context.TODO(), testingcommon.NewFakeSyncContext(t, testinghelpers.TestManagedClusterName), testinghelpers.TestManagedClusterName)
if syncErr != nil {
t.Errorf("unexpected err: %v", syncErr)
}
c.validateActions(t, clusterProfileClient.Actions())
})
}
}

View File

@@ -0,0 +1,438 @@
package clusterprofile
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
cpv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
cpclientset "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned"
cpinformerv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions/apis/v1alpha1"
cplisterv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/listers/apis/v1alpha1"
informerv1 "open-cluster-management.io/api/client/cluster/informers/externalversions/cluster/v1"
clusterinformerv1beta2 "open-cluster-management.io/api/client/cluster/informers/externalversions/cluster/v1beta2"
listerv1 "open-cluster-management.io/api/client/cluster/listers/cluster/v1"
clusterlisterv1beta2 "open-cluster-management.io/api/client/cluster/listers/cluster/v1beta2"
v1 "open-cluster-management.io/api/cluster/v1"
v1beta2 "open-cluster-management.io/api/cluster/v1beta2"
clustersdkv1beta2 "open-cluster-management.io/sdk-go/pkg/apis/cluster/v1beta2"
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
"open-cluster-management.io/ocm/pkg/registration/hub/managedclustersetbinding"
)
const (
ClusterProfileManagerName = "open-cluster-management"
)
// clusterProfileLifecycleController manages ClusterProfile creation and deletion
// based on ManagedClusterSetBindings.
//
// Queue key: namespace (e.g., "kueue-system")
//
// This controller reconciles ALL ClusterProfiles in a namespace based on ALL
// ManagedClusterSetBindings in that namespace.
//
// Key constraint: ManagedClusterSetBinding.Name MUST equal ManagedClusterSetBinding.Spec.ClusterSet
type clusterProfileLifecycleController struct {
clusterLister listerv1.ManagedClusterLister
clusterSetLister clusterlisterv1beta2.ManagedClusterSetLister
clusterSetBindingLister clusterlisterv1beta2.ManagedClusterSetBindingLister
clusterSetBindingIndexer cache.Indexer
clusterProfileClient cpclientset.Interface
clusterProfileLister cplisterv1alpha1.ClusterProfileLister
}
// NewClusterProfileLifecycleController creates a controller that manages ClusterProfile lifecycle
func NewClusterProfileLifecycleController(
clusterInformer informerv1.ManagedClusterInformer,
clusterSetInformer clusterinformerv1beta2.ManagedClusterSetInformer,
clusterSetBindingInformer clusterinformerv1beta2.ManagedClusterSetBindingInformer,
clusterProfileClient cpclientset.Interface,
clusterProfileInformer cpinformerv1alpha1.ClusterProfileInformer) factory.Controller {
// Note: ByClusterSetIndex indexer is already added by managedclustersetbinding controller,
// so we don't need to add it again here. Informers are shared across controllers.
c := &clusterProfileLifecycleController{
clusterLister: clusterInformer.Lister(),
clusterSetLister: clusterSetInformer.Lister(),
clusterSetBindingLister: clusterSetBindingInformer.Lister(),
clusterSetBindingIndexer: clusterSetBindingInformer.Informer().GetIndexer(),
clusterProfileClient: clusterProfileClient,
clusterProfileLister: clusterProfileInformer.Lister(),
}
controller := factory.New().
WithBareInformers(clusterInformer.Informer(), clusterProfileInformer.Informer()).
WithInformersQueueKeysFunc(c.clusterSetToQueueKeys, clusterSetInformer.Informer()).
WithInformersQueueKeysFunc(c.bindingToQueueKey, clusterSetBindingInformer.Informer()).
WithSync(c.sync).
ToController("ClusterProfileLifecycleController")
// Add custom event handlers that only handle create and delete (not update) to avoid noisy events
c.registerClusterEventHandler(clusterInformer, controller)
c.registerProfileEventHandler(clusterProfileInformer, controller)
return controller
}
// registerClusterEventHandler adds a custom event handler to cluster informer that only processes
// create and delete events, skipping updates to avoid noisy events.
func (c *clusterProfileLifecycleController) registerClusterEventHandler(
clusterInformer informerv1.ManagedClusterInformer,
controller factory.Controller) {
queue := controller.SyncContext().Queue()
_, err := clusterInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
cluster, ok := obj.(*v1.ManagedCluster)
if !ok {
return
}
keys := c.clusterToQueueKeys(cluster)
for _, key := range keys {
queue.Add(key)
}
},
// UpdateFunc intentionally omitted - we don't care about managedcluster updates
DeleteFunc: func(obj interface{}) {
cluster, ok := obj.(*v1.ManagedCluster)
if !ok {
// Handle tombstone
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
cluster, ok = tombstone.Obj.(*v1.ManagedCluster)
if !ok {
return
}
} else {
return
}
}
keys := c.clusterToQueueKeys(cluster)
for _, key := range keys {
queue.Add(key)
}
},
})
if err != nil {
utilruntime.HandleError(err)
}
}
// registerProfileEventHandler adds a custom event handler to profile informer that only processes
// create and delete events, skipping updates to avoid noisy events.
func (c *clusterProfileLifecycleController) registerProfileEventHandler(
clusterProfileInformer cpinformerv1alpha1.ClusterProfileInformer,
controller factory.Controller) {
queue := controller.SyncContext().Queue()
_, err := clusterProfileInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
profile, ok := obj.(*cpv1alpha1.ClusterProfile)
if !ok {
return
}
keys := c.profileToQueueKey(profile)
for _, key := range keys {
queue.Add(key)
}
},
// UpdateFunc intentionally omitted - we don't care about clusterprofile updates
DeleteFunc: func(obj interface{}) {
profile, ok := obj.(*cpv1alpha1.ClusterProfile)
if !ok {
// Handle tombstone
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
profile, ok = tombstone.Obj.(*cpv1alpha1.ClusterProfile)
if !ok {
return
}
} else {
return
}
}
keys := c.profileToQueueKey(profile)
for _, key := range keys {
queue.Add(key)
}
},
})
if err != nil {
utilruntime.HandleError(err)
}
}
// getBindingsByClusterSet efficiently retrieves all bindings for a given clusterset using the indexer
func (c *clusterProfileLifecycleController) getBindingsByClusterSet(clusterSetName string) ([]*v1beta2.ManagedClusterSetBinding, error) {
objs, err := c.clusterSetBindingIndexer.ByIndex(managedclustersetbinding.ByClusterSetIndex, clusterSetName)
if err != nil {
return nil, err
}
bindings := make([]*v1beta2.ManagedClusterSetBinding, 0, len(objs))
for _, obj := range objs {
binding, ok := obj.(*v1beta2.ManagedClusterSetBinding)
if !ok {
continue
}
bindings = append(bindings, binding)
}
return bindings, nil
}
// bindingToQueueKey maps a ManagedClusterSetBinding to its namespace
func (c *clusterProfileLifecycleController) bindingToQueueKey(obj runtime.Object) []string {
binding, ok := obj.(*v1beta2.ManagedClusterSetBinding)
if !ok {
return nil
}
// Queue the namespace where the binding exists
return []string{binding.Namespace}
}
// clusterSetToQueueKeys maps a ManagedClusterSet to all namespaces that have bindings to it
func (c *clusterProfileLifecycleController) clusterSetToQueueKeys(obj runtime.Object) []string {
clusterSet, ok := obj.(*v1beta2.ManagedClusterSet)
if !ok {
return nil
}
// Use indexer to efficiently find all bindings that reference this clusterset
bindings, err := c.getBindingsByClusterSet(clusterSet.Name)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get bindings for clusterset %s: %w", clusterSet.Name, err))
return nil
}
// Collect unique namespaces that have bindings to this clusterset
namespaces := make(map[string]bool)
for _, binding := range bindings {
namespaces[binding.Namespace] = true
}
keys := make([]string, 0, len(namespaces))
for ns := range namespaces {
keys = append(keys, ns)
}
return keys
}
// clusterToQueueKeys maps a ManagedCluster to all namespaces that should have its profile
func (c *clusterProfileLifecycleController) clusterToQueueKeys(obj runtime.Object) []string {
cluster, ok := obj.(*v1.ManagedCluster)
if !ok {
return nil
}
// Find all clustersets containing this cluster
clusterSets, err := clustersdkv1beta2.GetClusterSetsOfCluster(cluster, c.clusterSetLister)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get clustersets for cluster %s: %w", cluster.Name, err))
return nil
}
// For each clusterset, use indexer to efficiently find namespaces with bindings to it
namespaces := make(map[string]bool)
for _, clusterSet := range clusterSets {
bindings, err := c.getBindingsByClusterSet(clusterSet.Name)
if err != nil {
utilruntime.HandleError(fmt.Errorf("failed to get bindings for clusterset %s: %w", clusterSet.Name, err))
continue
}
for _, binding := range bindings {
namespaces[binding.Namespace] = true
}
}
keys := make([]string, 0, len(namespaces))
for ns := range namespaces {
keys = append(keys, ns)
}
return keys
}
// profileToQueueKey maps a ClusterProfile to its namespace
func (c *clusterProfileLifecycleController) profileToQueueKey(obj runtime.Object) []string {
profile, ok := obj.(*cpv1alpha1.ClusterProfile)
if !ok {
return nil
}
return []string{profile.Namespace}
}
// sync reconciles all ClusterProfiles in a given namespace
//
// 1. List all ManagedClusterSetBindings in the namespace
// 2. For each bound binding, get its clusterset and all clusters in it
// 3. Build desired state: set of clusters that should have profiles
// 4. Compare with existing profiles and create/delete as needed
func (c *clusterProfileLifecycleController) sync(ctx context.Context, syncCtx factory.SyncContext, key string) error {
namespace := key // Queue key is the namespace
logger := klog.FromContext(ctx).WithValues("namespace", namespace)
logger.V(4).Info("Reconciling ClusterProfiles in namespace")
// 1. Get all bindings in this namespace
allBindings, err := c.clusterSetBindingLister.ManagedClusterSetBindings(namespace).List(labels.Everything())
if err != nil {
return err
}
logger.V(4).Info("Found bindings", "count", len(allBindings))
// 2. Build the desired state: which clusters should have profiles in this namespace
desiredClusters := sets.New[string]()
for _, binding := range allBindings {
// Check if binding is bound
isBound := meta.IsStatusConditionTrue(binding.Status.Conditions, v1beta2.ClusterSetBindingBoundType)
if !isBound {
logger.V(4).Info("Binding not bound, skipping", "binding", binding.Name)
continue
}
// Get the clusterset
// Note: binding.Name should equal binding.Spec.ClusterSet (constraint)
clusterSet, err := c.clusterSetLister.Get(binding.Spec.ClusterSet)
if errors.IsNotFound(err) {
logger.V(4).Info("ClusterSet not found for binding", "binding", binding.Name, "clusterset", binding.Spec.ClusterSet)
continue
}
if err != nil {
logger.Error(err, "Failed to get clusterset", "clusterset", binding.Spec.ClusterSet)
continue
}
// Get all clusters in this clusterset
clusters, err := clustersdkv1beta2.GetClustersFromClusterSet(clusterSet, c.clusterLister)
if err != nil {
logger.Error(err, "Failed to get clusters from clusterset", "clusterset", clusterSet.Name)
continue
}
logger.V(4).Info("Processing clusterset",
"binding", binding.Name,
"clusterset", clusterSet.Name,
"clusterCount", len(clusters))
// Add clusters to desired set
for _, cluster := range clusters {
// Skip clusters that are being deleted
if !cluster.DeletionTimestamp.IsZero() {
continue
}
desiredClusters.Insert(cluster.Name)
}
}
logger.V(4).Info("Calculated desired state", "desiredClusterCount", desiredClusters.Len())
// 3. Get all existing profiles in this namespace managed by us
existingProfiles, err := c.clusterProfileLister.ClusterProfiles(namespace).List(
labels.SelectorFromSet(labels.Set{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
}))
if err != nil {
return err
}
// Build set of existing cluster names
existingClusters := sets.New[string]()
for _, profile := range existingProfiles {
existingClusters.Insert(profile.Name)
}
logger.V(4).Info("Found existing profiles", "count", existingClusters.Len())
// 4. Reconcile using set difference operations
// Clusters to create = desired - existing
clustersToCreate := desiredClusters.Difference(existingClusters)
// Clusters to delete = existing - desired
clustersToDelete := existingClusters.Difference(desiredClusters)
profilesCreated := 0
profilesDeleted := 0
var errs []error
// Create missing profiles
for clusterName := range clustersToCreate {
err := c.createClusterProfile(ctx, namespace, clusterName)
if err != nil {
logger.Error(err, "Failed to create ClusterProfile", "cluster", clusterName)
errs = append(errs, fmt.Errorf("failed to create ClusterProfile %s/%s: %w", namespace, clusterName, err))
} else {
profilesCreated++
}
}
// Delete extra profiles
for clusterName := range clustersToDelete {
// ClusterProfile.Name equals clusterName, so we can delete directly
err := c.clusterProfileClient.ApisV1alpha1().ClusterProfiles(namespace).Delete(
ctx, clusterName, metav1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
logger.Error(err, "Failed to delete ClusterProfile", "cluster", clusterName)
errs = append(errs, fmt.Errorf("failed to delete ClusterProfile %s/%s: %w", namespace, clusterName, err))
} else if err == nil {
profilesDeleted++
logger.V(2).Info("Deleted ClusterProfile", "namespace", namespace, "name", clusterName)
}
}
if profilesCreated > 0 || profilesDeleted > 0 {
logger.Info("Namespace reconciliation complete",
"profilesCreated", profilesCreated,
"profilesDeleted", profilesDeleted,
"totalDesired", desiredClusters.Len())
syncCtx.Recorder().Eventf(ctx, "ClusterProfilesReconciled",
"reconciled namespace %s: created %d, deleted %d profiles",
namespace, profilesCreated, profilesDeleted)
}
return utilerrors.NewAggregate(errs)
}
// createClusterProfile creates a new ClusterProfile in the specified namespace
func (c *clusterProfileLifecycleController) createClusterProfile(ctx context.Context, namespace, clusterName string) error {
logger := klog.FromContext(ctx)
clusterProfile := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: clusterName,
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: clusterName,
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
// Note: ClusterProfile Status will be handled by the status controller
}
_, err := c.clusterProfileClient.ApisV1alpha1().ClusterProfiles(namespace).Create(ctx, clusterProfile, metav1.CreateOptions{})
if err != nil {
return err
}
logger.V(2).Info("Created ClusterProfile", "namespace", namespace, "name", clusterName)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,796 @@
package clusterprofile
import (
"context"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clienttesting "k8s.io/client-go/testing"
cpv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
cpfake "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned/fake"
cpinformers "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions"
clusterfake "open-cluster-management.io/api/client/cluster/clientset/versioned/fake"
clusterinformers "open-cluster-management.io/api/client/cluster/informers/externalversions"
v1 "open-cluster-management.io/api/cluster/v1"
v1beta2 "open-cluster-management.io/api/cluster/v1beta2"
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
)
func TestLifecycleControllerSync(t *testing.T) {
// ========== Clusters ==========
// Clusters with ExclusiveClusterSetLabel
cluster1 := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Labels: map[string]string{v1beta2.ClusterSetLabel: "default"},
},
}
cluster2 := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster2",
Labels: map[string]string{v1beta2.ClusterSetLabel: "default"},
},
}
clusterDeleting := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-deleting",
Labels: map[string]string{v1beta2.ClusterSetLabel: "default"},
DeletionTimestamp: &metav1.Time{Time: metav1.Now().Time},
},
}
clusterBoundSet := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-bound-set",
Labels: map[string]string{v1beta2.ClusterSetLabel: "set-bound"},
},
}
clusterUnboundSet := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-unbound-set",
Labels: map[string]string{v1beta2.ClusterSetLabel: "set-unbound"},
},
}
// Clusters with custom labels for LabelSelector
clusterProdUSWest := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-prod-uswest",
Labels: map[string]string{
"environment": "production",
"region": "us-west",
},
},
}
clusterDevUSWest := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-dev-uswest",
Labels: map[string]string{
"environment": "development",
"region": "us-west",
},
},
}
clusterMixed := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-mixed",
Labels: map[string]string{
v1beta2.ClusterSetLabel: "default", // Also in default via ExclusiveClusterSetLabel
"environment": "production",
"region": "eu-west",
},
},
}
clusterProdEUWest := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-prod-eu-west",
Labels: map[string]string{
"environment": "production",
"region": "eu-west",
},
},
}
clusterNoLabels := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-no-labels",
Labels: map[string]string{},
},
}
// ========== ManagedClusterSets ==========
// ManagedClusterSet with ExclusiveClusterSetLabel selector
defaultClusterSet := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.ExclusiveClusterSetLabel,
},
},
}
clusterSetbound := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set-bound",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.ExclusiveClusterSetLabel,
},
},
}
clusterSetUnbound := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set-unbound",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.ExclusiveClusterSetLabel,
},
},
}
// ManagedClusterSet with LabelSelector environment: production
clusterSetProdLabelSelector := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set-prod-label-selector",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.LabelSelector,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"environment": "production",
},
},
},
},
}
// ManagedClusterSet with LabelSelector region: us-west
clusterSetUSWestLabelSelector := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set-uswest-label-selector",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.LabelSelector,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"region": "us-west",
},
},
},
},
}
clusterSetEnvMatchExpressions := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set-env-match-expressions",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.LabelSelector,
LabelSelector: &metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "environment",
Operator: metav1.LabelSelectorOpIn,
Values: []string{"production", "development"},
},
},
},
},
},
}
// Global clusterset with empty selector matches ALL clusters
globalClusterSet := &v1beta2.ManagedClusterSet{
ObjectMeta: metav1.ObjectMeta{
Name: "global",
},
Spec: v1beta2.ManagedClusterSetSpec{
ClusterSelector: v1beta2.ManagedClusterSelector{
SelectorType: v1beta2.LabelSelector,
LabelSelector: &metav1.LabelSelector{}, // empty selector
},
},
}
// ========== ManagedClusterSetBindings ==========
boundBindingDefault := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "ns1",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "default",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
unboundBindingDefault := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "ns2",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "default",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionFalse,
},
},
},
}
boundBindingSet := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-bound",
Namespace: "ns1",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-bound",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
unboundBindingSetUnbound := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-unbound",
Namespace: "ns1",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-unbound",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionFalse, // NOT bound
},
},
},
}
boundBindingProdLabelSelector := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-prod-label-selector",
Namespace: "ns-label",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-prod-label-selector",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingEnvMatchExpr := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-env-match-expressions",
Namespace: "ns-expr",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-env-match-expressions",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingDefaultOverlap := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "ns-overlap",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "default",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingProdLabelSelectorOverlap := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-prod-label-selector",
Namespace: "ns-overlap",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-prod-label-selector",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingProdLabelSelectorLabelOverlap := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-prod-label-selector",
Namespace: "ns-label-overlap",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-prod-label-selector",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingUSWestLabelSelectorLabelOverlap := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-uswest-label-selector",
Namespace: "ns-label-overlap",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-uswest-label-selector",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingGlobal := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "global",
Namespace: "ns-global",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "global",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingDefaultComplex := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "ns-complex",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "default",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingProdLabelSelectorComplex := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-prod-label-selector",
Namespace: "ns-complex",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-prod-label-selector",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingEnvMatchExprComplex := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "set-env-match-expressions",
Namespace: "ns-complex",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "set-env-match-expressions",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingGlobalOverlap := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "global",
Namespace: "ns-global-overlap",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "global",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
boundBindingDefaultGlobalOverlap := &v1beta2.ManagedClusterSetBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "ns-global-overlap",
},
Spec: v1beta2.ManagedClusterSetBindingSpec{
ClusterSet: "default",
},
Status: v1beta2.ManagedClusterSetBindingStatus{
Conditions: []metav1.Condition{
{
Type: v1beta2.ClusterSetBindingBoundType,
Status: metav1.ConditionTrue,
},
},
},
}
// ========== ClusterProfiles ==========
existingProfile := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster1",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
staleProfile := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "stale-cluster",
Namespace: "ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "stale-cluster",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
// ========== Test Cases ==========
cases := []struct {
name string
key string
clusters []runtime.Object
clusterSets []runtime.Object
bindings []runtime.Object
existingProfiles []runtime.Object
expectedCreates []string // cluster names that should be created
expectedDeletes []string // cluster names that should be deleted
expectedNumActions int
}{
{
name: "create profiles for bound binding",
key: "ns1",
clusters: []runtime.Object{cluster1, cluster2},
clusterSets: []runtime.Object{defaultClusterSet},
bindings: []runtime.Object{boundBindingDefault},
expectedCreates: []string{"cluster1", "cluster2"},
expectedNumActions: 2, // 2 creates
},
{
name: "no action for unbound binding",
key: "ns2",
clusters: []runtime.Object{cluster1, cluster2},
clusterSets: []runtime.Object{defaultClusterSet},
bindings: []runtime.Object{unboundBindingDefault},
expectedCreates: nil,
expectedDeletes: nil,
expectedNumActions: 0,
},
{
name: "update existing profile (no creates)",
key: "ns1",
clusters: []runtime.Object{cluster1, cluster2},
clusterSets: []runtime.Object{defaultClusterSet},
bindings: []runtime.Object{boundBindingDefault},
existingProfiles: []runtime.Object{existingProfile},
expectedCreates: []string{"cluster2"}, // only cluster2 needs to be created
expectedNumActions: 1, // 1 create
},
{
name: "delete stale profile",
key: "ns1",
clusters: []runtime.Object{cluster1, cluster2},
clusterSets: []runtime.Object{defaultClusterSet},
bindings: []runtime.Object{boundBindingDefault},
existingProfiles: []runtime.Object{existingProfile, staleProfile},
expectedCreates: []string{"cluster2"},
expectedDeletes: []string{"stale-cluster"},
expectedNumActions: 2, // 1 create + 1 delete
},
{
name: "namespace with no bindings",
key: "empty-ns",
clusters: []runtime.Object{cluster1},
clusterSets: []runtime.Object{defaultClusterSet},
bindings: []runtime.Object{},
expectedCreates: nil,
expectedNumActions: 0,
},
{
name: "multiple bindings in same namespace - non-overlapping",
key: "ns1",
clusters: []runtime.Object{cluster1, cluster2, clusterBoundSet},
clusterSets: []runtime.Object{defaultClusterSet, clusterSetbound},
bindings: []runtime.Object{boundBindingDefault, boundBindingSet},
expectedCreates: []string{"cluster1", "cluster2", "cluster-bound-set"},
expectedNumActions: 3, // 3 creates
},
{
name: "multiple bindings with clusters in deletion state",
key: "ns1",
clusters: []runtime.Object{cluster1, clusterDeleting, cluster2},
clusterSets: []runtime.Object{defaultClusterSet},
bindings: []runtime.Object{boundBindingDefault},
expectedCreates: []string{"cluster1", "cluster2"}, // cluster-deleting should be skipped
expectedNumActions: 2, // 2 creates (no profile for deleting cluster)
},
{
name: "multiple bindings, one unbound",
key: "ns1",
clusters: []runtime.Object{cluster1, cluster2, clusterUnboundSet},
clusterSets: []runtime.Object{defaultClusterSet, clusterSetUnbound},
bindings: []runtime.Object{boundBindingDefault, unboundBindingSetUnbound},
expectedCreates: []string{"cluster1", "cluster2"}, // only clusters from default
expectedNumActions: 2, // cluster-unbound-set should not have a profile
},
// LabelSelector test cases
{
name: "LabelSelector - create profiles for clusters matching label selector",
key: "ns-label",
clusters: []runtime.Object{clusterProdUSWest, clusterDevUSWest, clusterMixed},
clusterSets: []runtime.Object{clusterSetProdLabelSelector},
bindings: []runtime.Object{boundBindingProdLabelSelector},
expectedCreates: []string{"cluster-prod-uswest", "cluster-mixed"}, // only production clusters
expectedNumActions: 2,
},
{
name: "LabelSelector with MatchExpressions - select multiple environments",
key: "ns-expr",
clusters: []runtime.Object{clusterProdUSWest, clusterDevUSWest, cluster1},
clusterSets: []runtime.Object{clusterSetEnvMatchExpressions},
bindings: []runtime.Object{boundBindingEnvMatchExpr},
expectedCreates: []string{"cluster-prod-uswest", "cluster-dev-uswest"},
expectedNumActions: 2,
},
{
name: "overlap - ExclusiveClusterSetLabel and LabelSelector selecting same cluster",
key: "ns-overlap",
clusters: []runtime.Object{cluster1, clusterMixed, clusterProdUSWest},
clusterSets: []runtime.Object{defaultClusterSet, clusterSetProdLabelSelector},
bindings: []runtime.Object{boundBindingDefaultOverlap, boundBindingProdLabelSelectorOverlap},
expectedCreates: []string{"cluster1", "cluster-mixed", "cluster-prod-uswest"}, // cluster-mixed should only be created once
expectedNumActions: 3, // Should deduplicate cluster-mixed
},
{
name: "overlap - multiple LabelSelectors selecting overlapping clusters",
key: "ns-label-overlap",
clusters: []runtime.Object{clusterProdUSWest, clusterProdEUWest},
clusterSets: []runtime.Object{clusterSetProdLabelSelector, clusterSetUSWestLabelSelector},
bindings: []runtime.Object{boundBindingProdLabelSelectorLabelOverlap, boundBindingUSWestLabelSelectorLabelOverlap},
expectedCreates: []string{"cluster-prod-uswest", "cluster-prod-eu-west"}, // cluster-prod should only be created once
expectedNumActions: 2, // Should deduplicate cluster-prod
},
{
name: "global clusterset - matches all clusters",
key: "ns-global",
clusters: []runtime.Object{cluster1, clusterProdUSWest, clusterDevUSWest, clusterNoLabels},
clusterSets: []runtime.Object{globalClusterSet},
bindings: []runtime.Object{boundBindingGlobal},
expectedCreates: []string{"cluster1", "cluster-prod-uswest", "cluster-dev-uswest", "cluster-no-labels"}, // all clusters including no labels
expectedNumActions: 4,
},
{
name: "global clusterset - skips clusters in deletion",
key: "ns-global",
clusters: []runtime.Object{cluster1, clusterProdUSWest, clusterDeleting},
clusterSets: []runtime.Object{globalClusterSet},
bindings: []runtime.Object{boundBindingGlobal},
expectedCreates: []string{"cluster1", "cluster-prod-uswest"}, // should skip cluster-deleting
expectedNumActions: 2,
},
{
name: "global clusterset - overlap with default clusterset",
key: "ns-global-overlap",
clusters: []runtime.Object{cluster1, cluster2, clusterProdUSWest},
clusterSets: []runtime.Object{globalClusterSet, defaultClusterSet},
bindings: []runtime.Object{boundBindingGlobalOverlap, boundBindingDefaultGlobalOverlap},
expectedCreates: []string{"cluster1", "cluster2", "cluster-prod-uswest"}, // all unique clusters, deduplicate cluster1 and cluster2
expectedNumActions: 3,
},
{
name: "complex overlap - ExclusiveClusterSetLabel, LabelSelector with MatchLabels, and MatchExpressions",
key: "ns-complex",
clusters: []runtime.Object{cluster1, clusterMixed, clusterProdUSWest, clusterDevUSWest},
clusterSets: []runtime.Object{defaultClusterSet, clusterSetProdLabelSelector, clusterSetEnvMatchExpressions},
bindings: []runtime.Object{boundBindingDefaultComplex, boundBindingProdLabelSelectorComplex, boundBindingEnvMatchExprComplex},
expectedCreates: []string{"cluster1", "cluster-mixed", "cluster-prod-uswest", "cluster-dev-uswest"}, // all unique clusters
expectedNumActions: 4, // Should deduplicate all overlaps
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
clusterObjects := append(c.clusters, c.clusterSets...)
clusterObjects = append(clusterObjects, c.bindings...)
clusterClient := clusterfake.NewSimpleClientset(clusterObjects...)
clusterInformers := clusterinformers.NewSharedInformerFactory(clusterClient, 0)
cpClient := cpfake.NewSimpleClientset(c.existingProfiles...)
cpInformers := cpinformers.NewSharedInformerFactory(cpClient, 0)
// Populate informers
for _, cluster := range c.clusters {
clusterInformers.Cluster().V1().ManagedClusters().Informer().GetStore().Add(cluster)
}
for _, set := range c.clusterSets {
clusterInformers.Cluster().V1beta2().ManagedClusterSets().Informer().GetStore().Add(set)
}
for _, binding := range c.bindings {
clusterInformers.Cluster().V1beta2().ManagedClusterSetBindings().Informer().GetStore().Add(binding)
}
for _, profile := range c.existingProfiles {
cpInformers.Apis().V1alpha1().ClusterProfiles().Informer().GetStore().Add(profile)
}
ctrl := &clusterProfileLifecycleController{
clusterLister: clusterInformers.Cluster().V1().ManagedClusters().Lister(),
clusterSetLister: clusterInformers.Cluster().V1beta2().ManagedClusterSets().Lister(),
clusterSetBindingLister: clusterInformers.Cluster().V1beta2().ManagedClusterSetBindings().Lister(),
clusterProfileClient: cpClient,
clusterProfileLister: cpInformers.Apis().V1alpha1().ClusterProfiles().Lister(),
}
syncCtx := testingcommon.NewFakeSyncContext(t, c.key)
err := ctrl.sync(context.TODO(), syncCtx, c.key)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Verify actions
actions := cpClient.Actions()
createActions := []clienttesting.CreateAction{}
deleteActions := []clienttesting.DeleteAction{}
for _, action := range actions {
if action.GetVerb() == "create" {
createActions = append(createActions, action.(clienttesting.CreateAction))
}
if action.GetVerb() == "delete" {
deleteActions = append(deleteActions, action.(clienttesting.DeleteAction))
}
}
if len(actions) != c.expectedNumActions {
t.Errorf("expected %d actions, got %d: %v", c.expectedNumActions, len(actions), actions)
}
// Verify creates
if len(createActions) != len(c.expectedCreates) {
t.Errorf("expected %d creates, got %d", len(c.expectedCreates), len(createActions))
}
for _, expectedName := range c.expectedCreates {
found := false
for _, action := range createActions {
profile := action.GetObject().(*cpv1alpha1.ClusterProfile)
if profile.Name == expectedName {
found = true
// Verify labels
if profile.Labels[cpv1alpha1.LabelClusterManagerKey] != ClusterProfileManagerName {
t.Errorf("expected label %s, got %s", ClusterProfileManagerName, profile.Labels[cpv1alpha1.LabelClusterManagerKey])
}
if profile.Labels[v1.ClusterNameLabelKey] != expectedName {
t.Errorf("expected cluster-name label %s, got %s", expectedName, profile.Labels[v1.ClusterNameLabelKey])
}
break
}
}
if !found {
t.Errorf("expected profile %s to be created, but it wasn't", expectedName)
}
}
// Verify deletes
if len(deleteActions) != len(c.expectedDeletes) {
t.Errorf("expected %d deletes, got %d", len(c.expectedDeletes), len(deleteActions))
}
for _, expectedName := range c.expectedDeletes {
found := false
for _, action := range deleteActions {
if action.GetName() == expectedName {
found = true
break
}
}
if !found {
t.Errorf("expected profile %s to be deleted, but it wasn't", expectedName)
}
}
})
}
}

View File

@@ -0,0 +1,259 @@
package clusterprofile
import (
"context"
"fmt"
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
cpv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
cpclientset "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned"
cpinformerv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions/apis/v1alpha1"
cplisterv1alpha1 "sigs.k8s.io/cluster-inventory-api/client/listers/apis/v1alpha1"
informerv1 "open-cluster-management.io/api/client/cluster/informers/externalversions/cluster/v1"
listerv1 "open-cluster-management.io/api/client/cluster/listers/cluster/v1"
v1 "open-cluster-management.io/api/cluster/v1"
v1beta2 "open-cluster-management.io/api/cluster/v1beta2"
"open-cluster-management.io/sdk-go/pkg/basecontroller/factory"
"open-cluster-management.io/sdk-go/pkg/patcher"
"open-cluster-management.io/ocm/pkg/common/queue"
)
const (
byClusterName = "by-cluster-name"
)
// clusterProfileStatusController updates ClusterProfile status and labels from ManagedCluster
// Queue key: cluster name (e.g., "cluster1")
type clusterProfileStatusController struct {
clusterLister listerv1.ManagedClusterLister
clusterProfileClient cpclientset.Interface
clusterProfileLister cplisterv1alpha1.ClusterProfileLister
clusterProfileIndexer cache.Indexer
}
// indexByClusterName is the indexer function for ClusterProfile by cluster name
func indexByClusterName(obj interface{}) ([]string, error) {
profile, ok := obj.(*cpv1alpha1.ClusterProfile)
if !ok {
return []string{}, fmt.Errorf("obj is supposed to be a ClusterProfile, but is %T", obj)
}
// Index by the cluster-name label
if clusterName, ok := profile.Labels[v1.ClusterNameLabelKey]; ok {
return []string{clusterName}, nil
}
// Fallback: use profile name as cluster name (lifecycle controller sets Name = cluster name)
return []string{profile.Name}, nil
}
func NewClusterProfileStatusController(
clusterInformer informerv1.ManagedClusterInformer,
clusterProfileClient cpclientset.Interface,
clusterProfileInformer cpinformerv1alpha1.ClusterProfileInformer) factory.Controller {
// Add indexer for efficient lookup of profiles by cluster name
err := clusterProfileInformer.Informer().AddIndexers(cache.Indexers{
byClusterName: indexByClusterName,
})
if err != nil {
utilruntime.HandleError(err)
}
c := &clusterProfileStatusController{
clusterLister: clusterInformer.Lister(),
clusterProfileClient: clusterProfileClient,
clusterProfileLister: clusterProfileInformer.Lister(),
clusterProfileIndexer: clusterProfileInformer.Informer().GetIndexer(),
}
return factory.New().
WithInformersQueueKeysFunc(c.clusterToQueueKey, clusterInformer.Informer()).
WithFilteredEventsInformersQueueKeysFunc(
c.profileToQueueKey,
queue.UnionFilter(
queue.FileterByLabel(v1.ClusterNameLabelKey),
queue.FileterByLabelKeyValue(cpv1alpha1.LabelClusterManagerKey, ClusterProfileManagerName),
),
clusterProfileInformer.Informer()).
WithSync(c.sync).
ToController("ClusterProfileStatusController")
}
func (c *clusterProfileStatusController) clusterToQueueKey(obj runtime.Object) []string {
cluster, ok := obj.(*v1.ManagedCluster)
if !ok {
return nil
}
return []string{cluster.Name}
}
func (c *clusterProfileStatusController) profileToQueueKey(obj runtime.Object) []string {
profile, ok := obj.(*cpv1alpha1.ClusterProfile)
if !ok {
return nil
}
// Use cluster-name label (filtering already done by WithFilteredEventsInformersQueueKeysFunc)
if clusterName, ok := profile.Labels[v1.ClusterNameLabelKey]; ok {
return []string{clusterName}
}
// Fallback: profile name = cluster name
return []string{profile.Name}
}
func (c *clusterProfileStatusController) sync(ctx context.Context, syncCtx factory.SyncContext, key string) error {
clusterName := key
logger := klog.FromContext(ctx).WithValues("managedCluster", clusterName)
logger.V(4).Info("Updating ClusterProfile status")
cluster, err := c.clusterLister.Get(clusterName)
if errors.IsNotFound(err) {
logger.V(4).Info("Cluster not found, skipping status update")
return nil
}
if err != nil {
return err
}
// Use indexer for efficient lookup instead of listing all profiles across all namespaces
allProfiles, err := c.getProfilesByClusterName(clusterName)
if err != nil {
return err
}
logger.V(4).Info("Found profiles to update", "count", len(allProfiles))
updatedCount := 0
var errs []error
for _, profile := range allProfiles {
if profile.Spec.ClusterManager.Name != ClusterProfileManagerName {
continue
}
err := c.updateClusterProfile(ctx, profile, cluster)
if err != nil {
logger.Error(err, "Failed to update profile",
"namespace", profile.Namespace, "name", profile.Name)
errs = append(errs, fmt.Errorf("failed to update ClusterProfile %s/%s: %w", profile.Namespace, profile.Name, err))
continue
}
updatedCount++
}
if updatedCount > 0 {
logger.V(2).Info("Updated profiles", "count", updatedCount)
syncCtx.Recorder().Eventf(ctx, "ClusterProfileStatusUpdated",
"updated %d cluster profiles for cluster %s", updatedCount, clusterName)
}
return utilerrors.NewAggregate(errs)
}
// getProfilesByClusterName efficiently retrieves all profiles for a given cluster using the indexer
func (c *clusterProfileStatusController) getProfilesByClusterName(clusterName string) ([]*cpv1alpha1.ClusterProfile, error) {
objs, err := c.clusterProfileIndexer.ByIndex(byClusterName, clusterName)
if err != nil {
return nil, err
}
profiles := make([]*cpv1alpha1.ClusterProfile, 0, len(objs))
for _, obj := range objs {
profile, ok := obj.(*cpv1alpha1.ClusterProfile)
if !ok {
continue
}
profiles = append(profiles, profile)
}
return profiles, nil
}
func (c *clusterProfileStatusController) updateClusterProfile(
ctx context.Context,
existing *cpv1alpha1.ClusterProfile,
cluster *v1.ManagedCluster) error {
// Create a patcher for this specific namespace
profilePatcher := patcher.NewPatcher[
*cpv1alpha1.ClusterProfile, cpv1alpha1.ClusterProfileSpec, cpv1alpha1.ClusterProfileStatus](
c.clusterProfileClient.ApisV1alpha1().ClusterProfiles(existing.Namespace))
newProfile := existing.DeepCopy()
// Sync status
syncStatusFromCluster(newProfile, cluster)
// Patch status first to avoid ResourceVersion conflict
// If status has been updated, return early - labels will be updated in next reconcile
updated, err := profilePatcher.PatchStatus(ctx, newProfile, newProfile.Status, existing.Status)
if updated {
return err
}
// Status wasn't updated, now safe to patch labels
syncLabelsFromCluster(newProfile, cluster)
// Patch labels (patcher handles change detection)
_, err = profilePatcher.PatchLabelAnnotations(ctx, newProfile, newProfile.ObjectMeta, existing.ObjectMeta)
return err
}
func syncLabelsFromCluster(profile *cpv1alpha1.ClusterProfile, cluster *v1.ManagedCluster) {
mclLabels := cluster.GetLabels()
mclSetLabel := mclLabels[v1beta2.ClusterSetLabel]
requiredLabels := map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: mclSetLabel,
// Keep the cluster-name label that lifecycle controller added
v1.ClusterNameLabelKey: cluster.Name,
}
modified := false
resourcemerge.MergeMap(&modified, &profile.Labels, requiredLabels)
}
func syncStatusFromCluster(profile *cpv1alpha1.ClusterProfile, cluster *v1.ManagedCluster) {
// Sync version
profile.Status.Version.Kubernetes = cluster.Status.Version.Kubernetes
// Sync properties from cluster claims
cpProperties := []cpv1alpha1.Property{}
for _, claim := range cluster.Status.ClusterClaims {
cpProperties = append(cpProperties, cpv1alpha1.Property{Name: claim.Name, Value: claim.Value})
}
profile.Status.Properties = cpProperties
// Sync conditions
if availableCondition := meta.FindStatusCondition(cluster.Status.Conditions, v1.ManagedClusterConditionAvailable); availableCondition != nil {
meta.SetStatusCondition(&profile.Status.Conditions, metav1.Condition{
Type: cpv1alpha1.ClusterConditionControlPlaneHealthy,
Status: availableCondition.Status,
Reason: availableCondition.Reason,
Message: availableCondition.Message,
})
}
if joinedCondition := meta.FindStatusCondition(cluster.Status.Conditions, v1.ManagedClusterConditionJoined); joinedCondition != nil {
meta.SetStatusCondition(&profile.Status.Conditions, metav1.Condition{
Type: "Joined",
Status: joinedCondition.Status,
Reason: joinedCondition.Reason,
Message: joinedCondition.Message,
})
}
}

View File

@@ -0,0 +1,621 @@
package clusterprofile
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/cache"
cpv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
cpfake "sigs.k8s.io/cluster-inventory-api/client/clientset/versioned/fake"
cpinformers "sigs.k8s.io/cluster-inventory-api/client/informers/externalversions"
clusterfake "open-cluster-management.io/api/client/cluster/clientset/versioned/fake"
clusterinformers "open-cluster-management.io/api/client/cluster/informers/externalversions"
v1 "open-cluster-management.io/api/cluster/v1"
v1beta2 "open-cluster-management.io/api/cluster/v1beta2"
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
)
func TestStatusControllerSync(t *testing.T) {
cluster1 := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Labels: map[string]string{v1beta2.ClusterSetLabel: "set1"},
},
Status: v1.ManagedClusterStatus{
Version: v1.ManagedClusterVersion{
Kubernetes: "v1.28.0",
},
ClusterClaims: []v1.ManagedClusterClaim{
{Name: "platform", Value: "AWS"},
{Name: "region", Value: "us-west-2"},
},
Conditions: []metav1.Condition{
{
Type: v1.ManagedClusterConditionAvailable,
Status: metav1.ConditionTrue,
Reason: "ManagedClusterAvailable",
Message: "Cluster is available",
},
{
Type: v1.ManagedClusterConditionJoined,
Status: metav1.ConditionTrue,
Reason: "ManagedClusterJoined",
Message: "Cluster has joined",
},
},
},
}
profile1Ns1 := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster1",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster1",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
profile1Ns2 := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns2",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster1",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster1",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
profileNotManaged := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns3",
Labels: map[string]string{
v1.ClusterNameLabelKey: "cluster1",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster1",
ClusterManager: cpv1alpha1.ClusterManager{
Name: "other-manager",
},
},
}
clusterWithLabelSelector := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-label-selector",
Labels: map[string]string{
"environment": "production",
"region": "us-west",
},
},
Status: v1.ManagedClusterStatus{
Version: v1.ManagedClusterVersion{
Kubernetes: "v1.29.0",
},
ClusterClaims: []v1.ManagedClusterClaim{
{Name: "platform", Value: "GCP"},
},
Conditions: []metav1.Condition{
{
Type: v1.ManagedClusterConditionAvailable,
Status: metav1.ConditionTrue,
Reason: "Available",
Message: "Cluster is available",
},
},
},
}
profileLabelSelectorNs1 := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-label-selector",
Namespace: "prod-ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster-label-selector",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster-label-selector",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
profileLabelSelectorNs2 := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-label-selector",
Namespace: "region-ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster-label-selector",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster-label-selector",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
clusterMixed := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-mixed",
Labels: map[string]string{
v1beta2.ClusterSetLabel: "set1",
"environment": "production",
},
},
Status: v1.ManagedClusterStatus{
Version: v1.ManagedClusterVersion{
Kubernetes: "v1.30.0",
},
Conditions: []metav1.Condition{
{
Type: v1.ManagedClusterConditionAvailable,
Status: metav1.ConditionTrue,
Reason: "Available",
Message: "Cluster available",
},
},
},
}
profileMixedExclusive := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-mixed",
Namespace: "exclusive-ns",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster-mixed",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster-mixed",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
profileMixedLabelSelector := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-mixed",
Namespace: "labelselector-ns",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster-mixed",
},
},
Spec: cpv1alpha1.ClusterProfileSpec{
DisplayName: "cluster-mixed",
ClusterManager: cpv1alpha1.ClusterManager{
Name: ClusterProfileManagerName,
},
},
}
cases := []struct {
name string
key string
clusters []runtime.Object
existingProfiles []runtime.Object
expectedUpdates int
expectError bool
}{
{
name: "update status for profiles in multiple namespaces",
key: "cluster1",
clusters: []runtime.Object{cluster1},
existingProfiles: []runtime.Object{profile1Ns1, profile1Ns2},
expectedUpdates: 2, // Should update both profiles
expectError: false,
},
{
name: "skip profiles not managed by OCM",
key: "cluster1",
clusters: []runtime.Object{cluster1},
existingProfiles: []runtime.Object{profile1Ns1, profileNotManaged},
expectedUpdates: 1, // Only update OCM-managed profile
expectError: false,
},
{
name: "no profiles exist",
key: "cluster1",
clusters: []runtime.Object{cluster1},
existingProfiles: []runtime.Object{},
expectedUpdates: 0,
expectError: false,
},
{
name: "cluster not found",
key: "nonexistent-cluster",
clusters: []runtime.Object{},
existingProfiles: []runtime.Object{profile1Ns1},
expectedUpdates: 0,
expectError: false,
},
{
name: "cluster selected by LabelSelector - multiple profiles",
key: "cluster-label-selector",
clusters: []runtime.Object{clusterWithLabelSelector},
existingProfiles: []runtime.Object{profileLabelSelectorNs1, profileLabelSelectorNs2},
expectedUpdates: 2, // Update both profiles created by different LabelSelector sets
expectError: false,
},
{
name: "cluster selected by both ExclusiveClusterSetLabel and LabelSelector",
key: "cluster-mixed",
clusters: []runtime.Object{clusterMixed},
existingProfiles: []runtime.Object{profileMixedExclusive, profileMixedLabelSelector},
expectedUpdates: 2, // Update profiles in both namespaces
expectError: false,
},
{
name: "cluster selected by LabelSelector - single profile",
key: "cluster-label-selector",
clusters: []runtime.Object{clusterWithLabelSelector},
existingProfiles: []runtime.Object{profileLabelSelectorNs1},
expectedUpdates: 1,
expectError: false,
},
{
name: "mixed managed and unmanaged profiles for LabelSelector cluster",
key: "cluster-label-selector",
clusters: []runtime.Object{clusterWithLabelSelector},
existingProfiles: []runtime.Object{profileLabelSelectorNs1, profileNotManaged},
expectedUpdates: 1, // Only update OCM-managed profile
expectError: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
clusterClient := clusterfake.NewSimpleClientset(c.clusters...)
clusterInformers := clusterinformers.NewSharedInformerFactory(clusterClient, 0)
cpClient := cpfake.NewSimpleClientset(c.existingProfiles...)
cpInformers := cpinformers.NewSharedInformerFactory(cpClient, 0)
// Populate informers
for _, cluster := range c.clusters {
clusterInformers.Cluster().V1().ManagedClusters().Informer().GetStore().Add(cluster)
}
// Add indexer for profiles
cpInformer := cpInformers.Apis().V1alpha1().ClusterProfiles()
err := cpInformer.Informer().AddIndexers(cache.Indexers{
byClusterName: indexByClusterName,
})
if err != nil {
t.Fatal(err)
}
for _, profile := range c.existingProfiles {
cpInformer.Informer().GetStore().Add(profile)
}
ctrl := &clusterProfileStatusController{
clusterLister: clusterInformers.Cluster().V1().ManagedClusters().Lister(),
clusterProfileClient: cpClient,
clusterProfileLister: cpInformer.Lister(),
clusterProfileIndexer: cpInformer.Informer().GetIndexer(),
}
syncCtx := testingcommon.NewFakeSyncContext(t, c.key)
err = ctrl.sync(context.TODO(), syncCtx, c.key)
if c.expectError && err == nil {
t.Errorf("expected error but got none")
}
if !c.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
}
// Count patch actions (label patch + status patch for each profile)
patchCount := 0
for _, action := range cpClient.Actions() {
if action.GetVerb() == "patch" {
patchCount++
}
}
// We expect 2 patches per profile (labels + status)
// But if nothing changed, there might be fewer patches
// Just verify we got some activity
if c.expectedUpdates == 0 && patchCount != 0 {
t.Errorf("expected no patches but got %d", patchCount)
}
if c.expectedUpdates > 0 && patchCount == 0 {
t.Errorf("expected patches but got none")
}
})
}
}
func TestStatusSyncLabelsFromCluster(t *testing.T) {
cases := []struct {
name string
cluster *v1.ManagedCluster
profile *cpv1alpha1.ClusterProfile
expectedLabels map[string]string
}{
{
name: "cluster with ExclusiveClusterSetLabel",
cluster: &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Labels: map[string]string{v1beta2.ClusterSetLabel: "set1"},
},
},
profile: &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns1",
Labels: map[string]string{},
},
},
expectedLabels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "set1",
v1.ClusterNameLabelKey: "cluster1",
},
},
{
name: "cluster without clusterset label (LabelSelector scenario)",
cluster: &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-no-set",
Labels: map[string]string{
"environment": "production",
"region": "us-west",
},
},
},
profile: &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-no-set",
Namespace: "prod-ns",
Labels: map[string]string{},
},
},
expectedLabels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "", // Empty when no ClusterSetLabel
v1.ClusterNameLabelKey: "cluster-no-set",
},
},
{
name: "cluster with both clusterset and custom labels",
cluster: &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-mixed",
Labels: map[string]string{
v1beta2.ClusterSetLabel: "set1",
"environment": "production",
"tier": "critical",
},
},
},
profile: &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-mixed",
Namespace: "ns1",
Labels: map[string]string{},
},
},
expectedLabels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "set1",
v1.ClusterNameLabelKey: "cluster-mixed",
},
},
{
name: "cluster with empty labels",
cluster: &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-empty",
Labels: map[string]string{},
},
},
profile: &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-empty",
Namespace: "ns1",
Labels: map[string]string{},
},
},
expectedLabels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "", // Empty when no ClusterSetLabel
v1.ClusterNameLabelKey: "cluster-empty",
},
},
{
name: "update existing profile labels",
cluster: &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-update",
Labels: map[string]string{v1beta2.ClusterSetLabel: "new-set"},
},
},
profile: &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-update",
Namespace: "ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterSetKey: "old-set", // Should be updated
"custom-label": "keep-me",
},
},
},
expectedLabels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
cpv1alpha1.LabelClusterSetKey: "new-set",
v1.ClusterNameLabelKey: "cluster-update",
"custom-label": "keep-me", // Custom labels preserved
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
syncLabelsFromCluster(c.profile, c.cluster)
// Verify expected labels
for key, expectedValue := range c.expectedLabels {
actualValue, exists := c.profile.Labels[key]
if !exists {
t.Errorf("expected label %s to exist", key)
continue
}
if actualValue != expectedValue {
t.Errorf("label %s: expected %s, got %s", key, expectedValue, actualValue)
}
}
})
}
}
func TestStatusSyncStatusFromCluster(t *testing.T) {
cluster := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
},
Status: v1.ManagedClusterStatus{
Version: v1.ManagedClusterVersion{
Kubernetes: "v1.28.0",
},
ClusterClaims: []v1.ManagedClusterClaim{
{Name: "platform", Value: "AWS"},
{Name: "region", Value: "us-west-2"},
},
Conditions: []metav1.Condition{
{
Type: v1.ManagedClusterConditionAvailable,
Status: metav1.ConditionTrue,
Reason: "Available",
Message: "Cluster is available",
},
{
Type: v1.ManagedClusterConditionJoined,
Status: metav1.ConditionTrue,
Reason: "Joined",
Message: "Cluster joined",
},
},
},
}
profile := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns1",
},
}
syncStatusFromCluster(profile, cluster)
// Verify version
if profile.Status.Version.Kubernetes != "v1.28.0" {
t.Errorf("expected Kubernetes version v1.28.0, got %s", profile.Status.Version.Kubernetes)
}
// Verify properties
if len(profile.Status.Properties) != 2 {
t.Errorf("expected 2 properties, got %d", len(profile.Status.Properties))
}
// Verify conditions
availableCondition := meta.FindStatusCondition(profile.Status.Conditions, cpv1alpha1.ClusterConditionControlPlaneHealthy)
if availableCondition == nil {
t.Errorf("expected ControlPlaneHealthy condition")
} else if availableCondition.Status != metav1.ConditionTrue {
t.Errorf("expected ControlPlaneHealthy condition to be True, got %s", availableCondition.Status)
}
joinedCondition := meta.FindStatusCondition(profile.Status.Conditions, "Joined")
if joinedCondition == nil {
t.Errorf("expected Joined condition")
} else if joinedCondition.Status != metav1.ConditionTrue {
t.Errorf("expected Joined condition to be True, got %s", joinedCondition.Status)
}
}
func TestStatusControllerQueueKeyMapping(t *testing.T) {
cluster1 := &v1.ManagedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
},
}
profile1 := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
v1.ClusterNameLabelKey: "cluster1",
},
},
}
// Profile without cluster-name label to test fallback
profileNoLabel := &cpv1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Name: "profile-no-label",
Namespace: "ns1",
Labels: map[string]string{
cpv1alpha1.LabelClusterManagerKey: ClusterProfileManagerName,
// v1.ClusterNameLabelKey is intentionally missing to test fallback
},
},
}
t.Run("clusterToQueueKey", func(t *testing.T) {
ctrl := &clusterProfileStatusController{}
keys := ctrl.clusterToQueueKey(cluster1)
if len(keys) != 1 || keys[0] != "cluster1" {
t.Errorf("expected [cluster1], got %v", keys)
}
})
t.Run("profileToQueueKey with cluster-name label", func(t *testing.T) {
ctrl := &clusterProfileStatusController{}
keys := ctrl.profileToQueueKey(profile1)
if len(keys) != 1 || keys[0] != "cluster1" {
t.Errorf("expected [cluster1], got %v", keys)
}
})
t.Run("profileToQueueKey without cluster-name label (fallback to name)", func(t *testing.T) {
ctrl := &clusterProfileStatusController{}
keys := ctrl.profileToQueueKey(profileNoLabel)
if len(keys) != 1 || keys[0] != "profile-no-label" {
t.Errorf("expected [profile-no-label] (fallback to name), got %v", keys)
}
})
}

View File

@@ -23,9 +23,12 @@ import (
)
const (
byClusterSet = "by-clusterset"
// ByClusterSetIndex is the indexer name for ManagedClusterSetBinding by ClusterSet
ByClusterSetIndex = "by-clusterset"
)
const byClusterSet = ByClusterSetIndex // Use exported constant internally
// managedClusterSetController reconciles instances of ManagedClusterSet on the hub.
type managedClusterSetBindingController struct {
clusterClient clientset.Interface

View File

@@ -323,9 +323,18 @@ func (m *HubManagerOptions) RunControllerManagerWithInformers(
)
}
var clusterProfileController factory.Controller
var clusterProfileLifecycleController factory.Controller
var clusterProfileStatusController factory.Controller
if features.HubMutableFeatureGate.Enabled(ocmfeature.ClusterProfile) {
clusterProfileController = clusterprofile.NewClusterProfileController(
clusterProfileLifecycleController = clusterprofile.NewClusterProfileLifecycleController(
clusterInformers.Cluster().V1().ManagedClusters(),
clusterInformers.Cluster().V1beta2().ManagedClusterSets(),
clusterInformers.Cluster().V1beta2().ManagedClusterSetBindings(),
clusterProfileClient,
clusterProfileInformers.Apis().V1alpha1().ClusterProfiles(),
)
clusterProfileStatusController = clusterprofile.NewClusterProfileStatusController(
clusterInformers.Cluster().V1().ManagedClusters(),
clusterProfileClient,
clusterProfileInformers.Apis().V1alpha1().ClusterProfiles(),
@@ -384,7 +393,8 @@ func (m *HubManagerOptions) RunControllerManagerWithInformers(
go globalManagedClusterSetController.Run(ctx, 1)
}
if features.HubMutableFeatureGate.Enabled(ocmfeature.ClusterProfile) {
go clusterProfileController.Run(ctx, 1)
go clusterProfileLifecycleController.Run(ctx, 1)
go clusterProfileStatusController.Run(ctx, 1)
}
if features.HubMutableFeatureGate.Enabled(ocmfeature.ClusterImporter) {
for _, provider := range providers {