mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-02-14 18:09:57 +00:00
🌱 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
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:
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
438
pkg/registration/hub/clusterprofile/lifecycle_controller.go
Normal file
438
pkg/registration/hub/clusterprofile/lifecycle_controller.go
Normal 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
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
259
pkg/registration/hub/clusterprofile/status_controller.go
Normal file
259
pkg/registration/hub/clusterprofile/status_controller.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
621
pkg/registration/hub/clusterprofile/status_controller_test.go
Normal file
621
pkg/registration/hub/clusterprofile/status_controller_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user