mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-23 22:26:49 +00:00
Scorecard supply-chain security / Scorecard analysis (push) Failing after 1m24s
Post / coverage (push) Failing after 7m11s
Post / images (amd64, registration) (push) Failing after 45s
Post / images (amd64, registration-operator) (push) Failing after 42s
Post / images (amd64, placement) (push) Failing after 7m50s
Post / images (amd64, work) (push) Failing after 42s
Post / images (arm64, placement) (push) Failing after 42s
Post / images (arm64, registration) (push) Failing after 40s
Post / images (arm64, registration-operator) (push) Failing after 38s
Post / images (arm64, work) (push) Failing after 42s
Post / images (amd64, addon-manager) (push) Failing after 14m28s
Post / images (arm64, addon-manager) (push) Failing after 7m10s
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
Refactored the removeClusterRbac function into separate functions to handle different RBAC resource cleanup scenarios: - removeClusterRBACResources: orchestrates full RBAC cleanup when cluster is deleted - removeClusterSpecificRBAC: removes ClusterRole and ClusterRoleBinding - removeClusterSpecificRoleBindings: removes registration and work RoleBindings When hubAcceptsClient is false (cluster denied), only RoleBindings are removed while ClusterRole and ClusterRoleBinding are preserved and updated. This ensures proper RBAC state for denied clusters without deleting cluster-scoped resources. Added unit test to verify that when a cluster is denied, only RoleBindings are deleted while ClusterRole and ClusterRoleBinding remain intact. Signed-off-by: Zhiwei Yin <zyin@redhat.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
395 lines
17 KiB
Go
395 lines
17 KiB
Go
package managedcluster
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
corev1 "k8s.io/api/core/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
kubeinformers "k8s.io/client-go/informers"
|
|
kubefake "k8s.io/client-go/kubernetes/fake"
|
|
clienttesting "k8s.io/client-go/testing"
|
|
|
|
clusterfake "open-cluster-management.io/api/client/cluster/clientset/versioned/fake"
|
|
clusterinformers "open-cluster-management.io/api/client/cluster/informers/externalversions"
|
|
fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake"
|
|
workinformers "open-cluster-management.io/api/client/work/informers/externalversions"
|
|
v1 "open-cluster-management.io/api/cluster/v1"
|
|
ocmfeature "open-cluster-management.io/api/feature"
|
|
workv1 "open-cluster-management.io/api/work/v1"
|
|
"open-cluster-management.io/sdk-go/pkg/patcher"
|
|
|
|
"open-cluster-management.io/ocm/pkg/common/apply"
|
|
testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
|
|
"open-cluster-management.io/ocm/pkg/features"
|
|
testinghelpers "open-cluster-management.io/ocm/pkg/registration/helpers/testing"
|
|
"open-cluster-management.io/ocm/pkg/registration/register"
|
|
)
|
|
|
|
func TestSyncManagedCluster(t *testing.T) {
|
|
const (
|
|
testCustomLabel = "custom-label"
|
|
testCustomLabelValue = "custom-value"
|
|
testCustomLabel2 = "custom-label2"
|
|
testCustomLabelValue2 = "custom-value2"
|
|
)
|
|
cases := []struct {
|
|
name string
|
|
autoApprovalEnabled bool
|
|
roleBindings []runtime.Object
|
|
manifestWorks []runtime.Object
|
|
startingObjects []runtime.Object
|
|
labels map[string]string
|
|
validateClusterActions func(t *testing.T, actions []clienttesting.Action)
|
|
validateKubeActions func(t *testing.T, actions []clienttesting.Action)
|
|
}{
|
|
{
|
|
name: "sync a deleted spoke cluster",
|
|
startingObjects: []runtime.Object{},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertNoActions(t, actions)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"delete", // clusterrole
|
|
"delete", // clusterrolebinding
|
|
"delete", // registration rolebinding
|
|
"delete") // work rolebinding
|
|
},
|
|
},
|
|
{
|
|
name: "create a new spoke cluster(not accepted before, no accept condition)",
|
|
startingObjects: []runtime.Object{testinghelpers.NewManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertNoActions(t, actions)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertNoActions(t, actions)
|
|
},
|
|
},
|
|
{
|
|
name: "accept a spoke cluster",
|
|
startingObjects: []runtime.Object{testinghelpers.NewAcceptingManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
expectedCondition := metav1.Condition{
|
|
Type: v1.ManagedClusterConditionHubAccepted,
|
|
Status: metav1.ConditionTrue,
|
|
Reason: "HubClusterAdminAccepted",
|
|
Message: "Accepted by hub cluster admin",
|
|
}
|
|
testingcommon.AssertActions(t, actions, "patch")
|
|
patch := actions[0].(clienttesting.PatchAction).GetPatch()
|
|
managedCluster := &v1.ManagedCluster{}
|
|
err := json.Unmarshal(patch, managedCluster)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testingcommon.AssertCondition(t, managedCluster.Status.Conditions, expectedCondition)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"get", "create", // namespace
|
|
"create", // clusterrole
|
|
"create", // clusterrolebinding
|
|
"create", // registration rolebinding
|
|
"create") // work rolebinding
|
|
},
|
|
},
|
|
{
|
|
name: "sync an accepted spoke cluster",
|
|
startingObjects: []runtime.Object{testinghelpers.NewAcceptedManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertNoActions(t, actions)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"get", "create", // namespace
|
|
"create", // clusterrole
|
|
"create", // clusterrolebinding
|
|
"create", // registration rolebinding
|
|
"create") // work rolebinding
|
|
},
|
|
},
|
|
{
|
|
name: "deny an accepted spoke cluster",
|
|
startingObjects: []runtime.Object{testinghelpers.NewDeniedManagedCluster("True")},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
expectedCondition := metav1.Condition{
|
|
Type: v1.ManagedClusterConditionHubAccepted,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: "HubClusterAdminDenied",
|
|
Message: "Denied by hub cluster admin",
|
|
}
|
|
testingcommon.AssertActions(t, actions, "patch")
|
|
patch := actions[0].(clienttesting.PatchAction).GetPatch()
|
|
managedCluster := &v1.ManagedCluster{}
|
|
err := json.Unmarshal(patch, managedCluster)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testingcommon.AssertCondition(t, managedCluster.Status.Conditions, expectedCondition)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"create", // clusterrole
|
|
"create", // clusterrolebinding
|
|
"delete", // registration rolebinding
|
|
"delete") // work rolebinding
|
|
},
|
|
},
|
|
{
|
|
name: "deny a spoke cluster - verify only rolebindings deleted not clusterrole/clusterrolebinding",
|
|
startingObjects: []runtime.Object{testinghelpers.NewDeniedManagedCluster("True")},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
expectedCondition := metav1.Condition{
|
|
Type: v1.ManagedClusterConditionHubAccepted,
|
|
Status: metav1.ConditionFalse,
|
|
Reason: "HubClusterAdminDenied",
|
|
Message: "Denied by hub cluster admin",
|
|
}
|
|
testingcommon.AssertActions(t, actions, "patch")
|
|
patch := actions[0].(clienttesting.PatchAction).GetPatch()
|
|
managedCluster := &v1.ManagedCluster{}
|
|
err := json.Unmarshal(patch, managedCluster)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testingcommon.AssertCondition(t, managedCluster.Status.Conditions, expectedCondition)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
// Verify the sequence of actions
|
|
testingcommon.AssertActions(t, actions,
|
|
"create", // clusterrole - created/updated, not deleted
|
|
"create", // clusterrolebinding - created/updated, not deleted
|
|
"delete", // registration rolebinding - deleted
|
|
"delete") // work rolebinding - deleted
|
|
|
|
// Verify that clusterrole and clusterrolebinding are created (not deleted)
|
|
clusterRoleAction := actions[0].(clienttesting.CreateAction)
|
|
if clusterRoleAction.GetVerb() != "create" {
|
|
t.Errorf("expected clusterrole to be created, got %s", clusterRoleAction.GetVerb())
|
|
}
|
|
clusterRoleBindingAction := actions[1].(clienttesting.CreateAction)
|
|
if clusterRoleBindingAction.GetVerb() != "create" {
|
|
t.Errorf("expected clusterrolebinding to be created, got %s", clusterRoleBindingAction.GetVerb())
|
|
}
|
|
|
|
// Verify that only rolebindings are deleted
|
|
regRoleBindingAction := actions[2].(clienttesting.DeleteAction)
|
|
if regRoleBindingAction.GetVerb() != "delete" {
|
|
t.Errorf("expected registration rolebinding to be deleted, got %s", regRoleBindingAction.GetVerb())
|
|
}
|
|
workRoleBindingAction := actions[3].(clienttesting.DeleteAction)
|
|
if workRoleBindingAction.GetVerb() != "delete" {
|
|
t.Errorf("expected work rolebinding to be deleted, got %s", workRoleBindingAction.GetVerb())
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "delete a spoke cluster without manifestworks",
|
|
roleBindings: []runtime.Object{testinghelpers.NewRoleBinding(testinghelpers.TestManagedClusterName,
|
|
workRoleBindingName(testinghelpers.TestManagedClusterName), []string{workv1.ManifestWorkFinalizer},
|
|
nil, false)},
|
|
startingObjects: []runtime.Object{testinghelpers.NewDeletingManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions, "patch")
|
|
patch := actions[0].(clienttesting.PatchAction).GetPatch()
|
|
managedCluster := &v1.ManagedCluster{}
|
|
err := json.Unmarshal(patch, managedCluster)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(managedCluster.Finalizers) != 0 {
|
|
t.Errorf("expected no finalizer")
|
|
}
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"delete", // clusterrole
|
|
"delete", // clusterrolebinding
|
|
"delete", // registration rolebinding
|
|
"delete", // work rolebinding
|
|
"patch") // work rolebinding
|
|
patch := actions[4].(clienttesting.PatchAction).GetPatch()
|
|
roleBinding := &rbacv1.RoleBinding{}
|
|
err := json.Unmarshal(patch, roleBinding)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(roleBinding.Finalizers) != 0 {
|
|
t.Errorf("expected no finalizer")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "delete a spoke cluster with manifestworks",
|
|
startingObjects: []runtime.Object{testinghelpers.NewDeletingManagedCluster()},
|
|
manifestWorks: []runtime.Object{testinghelpers.NewManifestWork(testinghelpers.TestManagedClusterName,
|
|
"test", nil, nil, nil, nil)},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertNoActions(t, actions)
|
|
},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"delete", // clusterrole
|
|
"delete", // clusterrolebinding
|
|
"delete", // registration rolebinding
|
|
"delete") // work rolebinding
|
|
},
|
|
},
|
|
{
|
|
name: "should accept the clusters when auto approval is enabled",
|
|
autoApprovalEnabled: true,
|
|
startingObjects: []runtime.Object{testinghelpers.NewManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions, "patch")
|
|
},
|
|
},
|
|
{
|
|
name: "should add the auto approval annotation to an accepted cluster when auto approval is enabled",
|
|
autoApprovalEnabled: true,
|
|
startingObjects: []runtime.Object{testinghelpers.NewAcceptedManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions, "patch")
|
|
patch := actions[0].(clienttesting.PatchAction).GetPatch()
|
|
managedCluster := &v1.ManagedCluster{}
|
|
err := json.Unmarshal(patch, managedCluster)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := managedCluster.Annotations[clusterAcceptedAnnotationKey]; !ok {
|
|
t.Errorf("expected auto approval annotation, but failed")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "create resources with labels",
|
|
startingObjects: []runtime.Object{testinghelpers.NewAcceptedManagedCluster()},
|
|
validateClusterActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertNoActions(t, actions)
|
|
},
|
|
labels: map[string]string{testCustomLabel: testCustomLabelValue, testCustomLabel2: testCustomLabelValue2},
|
|
validateKubeActions: func(t *testing.T, actions []clienttesting.Action) {
|
|
testingcommon.AssertActions(t, actions,
|
|
"get", "create", // namespace
|
|
"create", // clusterrole
|
|
"create", // clusterrolebinding
|
|
"create", // registration rolebinding
|
|
"create") // work rolebinding
|
|
|
|
// Validate labels on namespace
|
|
namespace := actions[1].(clienttesting.CreateAction).GetObject().(*corev1.Namespace)
|
|
if namespace.Labels[testCustomLabel] != testCustomLabelValue {
|
|
t.Errorf("expected label '%s=%s' on namespace, but got: %v", testCustomLabel, testCustomLabelValue, namespace.Labels)
|
|
}
|
|
if namespace.Labels[testCustomLabel2] != testCustomLabelValue2 {
|
|
t.Errorf("expected label '%s=%s' on namespace, but got: %v", testCustomLabel2, testCustomLabelValue2, namespace.Labels)
|
|
}
|
|
|
|
// Validate labels on clusterrole
|
|
clusterRole := actions[2].(clienttesting.CreateAction).GetObject().(*rbacv1.ClusterRole)
|
|
if clusterRole.Labels[testCustomLabel] != testCustomLabelValue {
|
|
t.Errorf("expected label '%s=%s' on clusterrole, but got: %v", testCustomLabel, testCustomLabelValue, clusterRole.Labels)
|
|
}
|
|
if clusterRole.Labels[testCustomLabel2] != testCustomLabelValue2 {
|
|
t.Errorf("expected label '%s=%s' on clusterrole, but got: %v", testCustomLabel2, testCustomLabelValue2, clusterRole.Labels)
|
|
}
|
|
|
|
// Validate labels on clusterrolebinding
|
|
clusterRoleBinding := actions[3].(clienttesting.CreateAction).GetObject().(*rbacv1.ClusterRoleBinding)
|
|
if clusterRoleBinding.Labels[testCustomLabel] != testCustomLabelValue {
|
|
t.Errorf("expected label '%s=%s' on clusterrolebinding, but got: %v", testCustomLabel, testCustomLabelValue, clusterRoleBinding.Labels)
|
|
}
|
|
if clusterRoleBinding.Labels[testCustomLabel2] != testCustomLabelValue2 {
|
|
t.Errorf("expected label '%s=%s' on clusterrolebinding, but got: %v", testCustomLabel2, testCustomLabelValue2, clusterRoleBinding.Labels)
|
|
}
|
|
|
|
// Validate labels on registration rolebinding
|
|
registrationRoleBinding := actions[4].(clienttesting.CreateAction).GetObject().(*rbacv1.RoleBinding)
|
|
if registrationRoleBinding.Labels[testCustomLabel] != testCustomLabelValue {
|
|
t.Errorf("expected label '%s=%s' on registration rolebinding, but got: %v", testCustomLabel, testCustomLabelValue, registrationRoleBinding.Labels)
|
|
}
|
|
if registrationRoleBinding.Labels[testCustomLabel2] != testCustomLabelValue2 {
|
|
t.Errorf("expected label '%s=%s' on registration rolebinding, but got: %v", testCustomLabel2, testCustomLabelValue2, registrationRoleBinding.Labels)
|
|
}
|
|
|
|
// Validate labels on work rolebinding
|
|
workRoleBinding := actions[5].(clienttesting.CreateAction).GetObject().(*rbacv1.RoleBinding)
|
|
if workRoleBinding.Labels[testCustomLabel] != testCustomLabelValue {
|
|
t.Errorf("expected label '%s=%s' on work rolebinding, but got: %v", testCustomLabel, testCustomLabelValue, workRoleBinding.Labels)
|
|
}
|
|
if workRoleBinding.Labels[testCustomLabel2] != testCustomLabelValue2 {
|
|
t.Errorf("expected label '%s=%s' on work rolebinding, but got: %v", testCustomLabel2, testCustomLabelValue2, workRoleBinding.Labels)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
features.HubMutableFeatureGate.Add(ocmfeature.DefaultHubRegistrationFeatureGates)
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
clusterClient := clusterfake.NewSimpleClientset(c.startingObjects...)
|
|
kubeClient := kubefake.NewSimpleClientset(c.roleBindings...)
|
|
|
|
kubeInformer := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, time.Minute*10)
|
|
roleBindingStore := kubeInformer.Rbac().V1().RoleBindings().Informer().GetStore()
|
|
for _, roleBinding := range c.roleBindings {
|
|
if err := roleBindingStore.Add(roleBinding); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
clusterInformerFactory := clusterinformers.NewSharedInformerFactory(clusterClient, time.Minute*10)
|
|
clusterStore := clusterInformerFactory.Cluster().V1().ManagedClusters().Informer().GetStore()
|
|
for _, cluster := range c.startingObjects {
|
|
if err := clusterStore.Add(cluster); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
workClient := fakeworkclient.NewSimpleClientset(c.manifestWorks...)
|
|
workInformerFactory := workinformers.NewSharedInformerFactory(workClient, time.Minute*10)
|
|
workStore := workInformerFactory.Work().V1().ManifestWorks().Informer().GetStore()
|
|
for _, work := range c.manifestWorks {
|
|
if err := workStore.Add(work); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
features.HubMutableFeatureGate.Set(fmt.Sprintf("%s=%v", ocmfeature.ManagedClusterAutoApproval, c.autoApprovalEnabled))
|
|
ctrl := managedClusterController{
|
|
kubeClient,
|
|
clusterClient,
|
|
kubeInformer.Rbac().V1().RoleBindings().Lister(),
|
|
workInformerFactory.Work().V1().ManifestWorks().Lister(),
|
|
clusterInformerFactory.Cluster().V1().ManagedClusters().Lister(),
|
|
apply.NewPermissionApplier(
|
|
kubeClient,
|
|
kubeInformer.Rbac().V1().Roles().Lister(),
|
|
kubeInformer.Rbac().V1().RoleBindings().Lister(),
|
|
kubeInformer.Rbac().V1().ClusterRoles().Lister(),
|
|
kubeInformer.Rbac().V1().ClusterRoleBindings().Lister(),
|
|
),
|
|
patcher.NewPatcher[*v1.ManagedCluster, v1.ManagedClusterSpec, v1.ManagedClusterStatus](clusterClient.ClusterV1().ManagedClusters()),
|
|
register.NewNoopHubDriver(),
|
|
c.labels}
|
|
syncErr := ctrl.sync(context.TODO(), testingcommon.NewFakeSyncContext(t, testinghelpers.TestManagedClusterName), testinghelpers.TestManagedClusterName)
|
|
if syncErr != nil && !errors.Is(syncErr, requeueError) {
|
|
t.Errorf("unexpected err: %v", syncErr)
|
|
}
|
|
|
|
c.validateClusterActions(t, clusterClient.Actions())
|
|
if c.validateKubeActions != nil {
|
|
c.validateKubeActions(t, kubeClient.Actions())
|
|
}
|
|
})
|
|
}
|
|
}
|