Skip to content

Commit 187911e

Browse files
committed
RB suspension: scheduler and controller change
Signed-off-by: Monokaix <[email protected]>
1 parent b0c94ec commit 187911e

14 files changed

+541
-19
lines changed

Diff for: pkg/apis/work/v1alpha2/binding_types.go

+3
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,9 @@ const (
398398
// FullyApplied represents the condition that the resource referencing by ResourceBinding or ClusterResourceBinding
399399
// has been applied to all scheduled clusters.
400400
FullyApplied string = "FullyApplied"
401+
402+
// SchedulingSuspended represents the condition that the ResourceBinding or ClusterResourceBinding is suspended to schedule.
403+
SchedulingSuspended string = "SchedulingSuspended"
401404
)
402405

403406
// These are reasons for a binding's transition to a Scheduled condition.

Diff for: pkg/controllers/binding/binding_controller.go

+5
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ func (c *ResourceBindingController) Reconcile(ctx context.Context, req controlle
8989
return c.removeFinalizer(ctx, binding)
9090
}
9191

92+
if err := updateBindingDispatchingConditionIfNeeded(ctx, c.Client, c.EventRecorder, binding, apiextensionsv1.NamespaceScoped); err != nil {
93+
klog.ErrorS(err, "Failed to update binding condition.", "name", klog.KObj(binding), "type", workv1alpha2.SchedulingSuspended)
94+
return controllerruntime.Result{}, err
95+
}
96+
9297
return c.syncBinding(ctx, binding)
9398
}
9499

Diff for: pkg/controllers/binding/binding_controller_test.go

+122
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@ import (
2222
"reflect"
2323
"testing"
2424

25+
"github.com/stretchr/testify/assert"
2526
appsv1 "k8s.io/api/apps/v1"
2627
corev1 "k8s.io/api/core/v1"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2729
"k8s.io/apimachinery/pkg/api/meta"
2830
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2931
"k8s.io/apimachinery/pkg/runtime"
32+
"k8s.io/apimachinery/pkg/runtime/schema"
3033
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/uuid"
3135
fakedynamic "k8s.io/client-go/dynamic/fake"
3236
"k8s.io/client-go/kubernetes/scheme"
3337
"k8s.io/client-go/tools/record"
38+
"k8s.io/utils/ptr"
3439
controllerruntime "sigs.k8s.io/controller-runtime"
3540
"sigs.k8s.io/controller-runtime/pkg/client"
3641
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -39,6 +44,7 @@ import (
3944
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
4045
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
4146
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
47+
"github.com/karmada-io/karmada/pkg/events"
4248
testing2 "github.com/karmada-io/karmada/pkg/search/proxy/testing"
4349
"github.com/karmada-io/karmada/pkg/util"
4450
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
@@ -445,3 +451,119 @@ func TestResourceBindingController_removeFinalizer(t *testing.T) {
445451
})
446452
}
447453
}
454+
455+
func TestUpdateBindingDispatchingConditionIfNeeded(t *testing.T) {
456+
tests := []struct {
457+
name string
458+
binding *workv1alpha2.ResourceBinding
459+
expectedCondition metav1.Condition
460+
expectedEventCount int
461+
expectEventMessage string
462+
}{
463+
{
464+
name: "Binding scheduling is suspended",
465+
binding: newRb(true, metav1.Condition{}),
466+
expectedCondition: metav1.Condition{
467+
Type: workv1alpha2.SchedulingSuspended,
468+
Status: metav1.ConditionTrue,
469+
},
470+
expectedEventCount: 1,
471+
expectEventMessage: fmt.Sprintf("%s %s %s", corev1.EventTypeNormal, events.EventReasonBindingScheduling, SuspendedSchedulingConditionMessage),
472+
},
473+
{
474+
name: "Binding scheduling is not suspended",
475+
binding: newRb(false, metav1.Condition{
476+
Type: workv1alpha2.SchedulingSuspended,
477+
Status: metav1.ConditionTrue,
478+
Reason: SuspendedSchedulingConditionReason,
479+
Message: SuspendedSchedulingConditionMessage,
480+
}),
481+
expectedCondition: metav1.Condition{
482+
Type: workv1alpha2.SchedulingSuspended,
483+
Status: metav1.ConditionFalse,
484+
},
485+
expectedEventCount: 1,
486+
expectEventMessage: fmt.Sprintf("%s %s %s", corev1.EventTypeNormal, events.EventReasonBindingScheduling, SchedulingConditionMessage),
487+
},
488+
{
489+
name: "Condition already matches, no update needed",
490+
binding: newRb(true, metav1.Condition{
491+
Type: workv1alpha2.SchedulingSuspended,
492+
Status: metav1.ConditionTrue,
493+
Reason: SuspendedSchedulingConditionReason,
494+
Message: SuspendedSchedulingConditionMessage,
495+
}),
496+
expectedCondition: metav1.Condition{
497+
Type: workv1alpha2.SchedulingSuspended,
498+
Status: metav1.ConditionTrue,
499+
},
500+
},
501+
{
502+
name: "No SchedulingSuspended condition and scheduling is not suspended, no update needed",
503+
binding: newRb(false, metav1.Condition{
504+
Type: workv1alpha2.BindingReasonUnschedulable,
505+
Status: metav1.ConditionTrue,
506+
}),
507+
expectedCondition: metav1.Condition{
508+
Type: workv1alpha2.BindingReasonUnschedulable,
509+
Status: metav1.ConditionTrue,
510+
},
511+
},
512+
}
513+
514+
for _, tt := range tests {
515+
t.Run(tt.name, func(t *testing.T) {
516+
eventRecorder := record.NewFakeRecorder(1)
517+
c := newResourceBindingController(tt.binding, eventRecorder)
518+
519+
updatedBinding := &workv1alpha2.ResourceBinding{}
520+
assert.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: tt.binding.Name, Namespace: tt.binding.Namespace}, updatedBinding))
521+
522+
err := updateBindingDispatchingConditionIfNeeded(context.Background(), c.Client, c.EventRecorder, tt.binding, apiextensionsv1.NamespaceScoped)
523+
if err != nil {
524+
t.Errorf("updateBindingDispatchingConditionIfNeeded() returned an error: %v", err)
525+
}
526+
527+
assert.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: tt.binding.Name, Namespace: tt.binding.Namespace}, updatedBinding))
528+
assert.True(t, meta.IsStatusConditionPresentAndEqual(updatedBinding.Status.Conditions, tt.expectedCondition.Type, tt.expectedCondition.Status))
529+
assert.Equal(t, tt.expectedEventCount, len(eventRecorder.Events))
530+
if tt.expectEventMessage != "" {
531+
e := <-eventRecorder.Events
532+
assert.Equal(t, tt.expectEventMessage, e)
533+
}
534+
})
535+
}
536+
}
537+
538+
func newResourceBindingController(binding *workv1alpha2.ResourceBinding, eventRecord record.EventRecorder) ResourceBindingController {
539+
restMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
540+
fakeClient := fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects(binding).WithStatusSubresource(binding).WithRESTMapper(restMapper).Build()
541+
return ResourceBindingController{
542+
Client: fakeClient,
543+
EventRecorder: eventRecord,
544+
}
545+
}
546+
547+
func newRb(suspended bool, condition metav1.Condition) *workv1alpha2.ResourceBinding {
548+
return &workv1alpha2.ResourceBinding{
549+
TypeMeta: metav1.TypeMeta{
550+
Kind: workv1alpha2.ResourceKindResourceBinding,
551+
APIVersion: workv1alpha2.GroupVersion.Version,
552+
},
553+
ObjectMeta: metav1.ObjectMeta{
554+
Name: "test-rb",
555+
Namespace: "default",
556+
UID: uuid.NewUUID(),
557+
},
558+
Spec: workv1alpha2.ResourceBindingSpec{
559+
Suspension: &workv1alpha2.Suspension{
560+
Scheduling: ptr.To(suspended),
561+
},
562+
},
563+
Status: workv1alpha2.ResourceBindingStatus{
564+
Conditions: []metav1.Condition{
565+
condition,
566+
},
567+
},
568+
}
569+
}

Diff for: pkg/controllers/binding/cluster_resource_binding_controller.go

+5
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ func (c *ClusterResourceBindingController) Reconcile(ctx context.Context, req co
8989
return c.removeFinalizer(ctx, clusterResourceBinding)
9090
}
9191

92+
if err := updateBindingDispatchingConditionIfNeeded(ctx, c.Client, c.EventRecorder, clusterResourceBinding, apiextensionsv1.ClusterScoped); err != nil {
93+
klog.ErrorS(err, "Failed to update binding condition.", "name", klog.KObj(clusterResourceBinding), "type", workv1alpha2.SchedulingSuspended)
94+
return controllerruntime.Result{}, err
95+
}
96+
9297
return c.syncBinding(ctx, clusterResourceBinding)
9398
}
9499

Diff for: pkg/controllers/binding/cluster_resource_binding_controller_test.go

+122
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@ import (
2222
"reflect"
2323
"testing"
2424

25+
"github.com/stretchr/testify/assert"
2526
appsv1 "k8s.io/api/apps/v1"
2627
corev1 "k8s.io/api/core/v1"
28+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2729
"k8s.io/apimachinery/pkg/api/meta"
2830
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2931
"k8s.io/apimachinery/pkg/runtime"
32+
"k8s.io/apimachinery/pkg/runtime/schema"
3033
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/uuid"
3135
fakedynamic "k8s.io/client-go/dynamic/fake"
3236
"k8s.io/client-go/kubernetes/scheme"
3337
"k8s.io/client-go/tools/record"
38+
"k8s.io/utils/ptr"
3439
controllerruntime "sigs.k8s.io/controller-runtime"
3540
"sigs.k8s.io/controller-runtime/pkg/client"
3641
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -39,6 +44,7 @@ import (
3944
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
4045
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
4146
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
47+
"github.com/karmada-io/karmada/pkg/events"
4248
testing2 "github.com/karmada-io/karmada/pkg/search/proxy/testing"
4349
"github.com/karmada-io/karmada/pkg/util"
4450
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
@@ -439,3 +445,119 @@ func TestClusterResourceBindingController_newOverridePolicyFunc(t *testing.T) {
439445
})
440446
}
441447
}
448+
449+
func TestUpdateClusterBindingDispatchingConditionIfNeeded(t *testing.T) {
450+
tests := []struct {
451+
name string
452+
binding *workv1alpha2.ClusterResourceBinding
453+
expectedCondition metav1.Condition
454+
expectedEventCount int
455+
expectEventMessage string
456+
}{
457+
{
458+
name: "Binding scheduling is suspended",
459+
binding: newCrb(true, metav1.Condition{}),
460+
expectedCondition: metav1.Condition{
461+
Type: workv1alpha2.SchedulingSuspended,
462+
Status: metav1.ConditionTrue,
463+
},
464+
expectedEventCount: 1,
465+
expectEventMessage: fmt.Sprintf("%s %s %s", corev1.EventTypeNormal, events.EventReasonBindingScheduling, SuspendedSchedulingConditionMessage),
466+
},
467+
{
468+
name: "Binding scheduling is not suspended",
469+
binding: newCrb(false, metav1.Condition{
470+
Type: workv1alpha2.SchedulingSuspended,
471+
Status: metav1.ConditionTrue,
472+
Reason: SuspendedSchedulingConditionReason,
473+
Message: SuspendedSchedulingConditionMessage,
474+
}),
475+
expectedCondition: metav1.Condition{
476+
Type: workv1alpha2.SchedulingSuspended,
477+
Status: metav1.ConditionFalse,
478+
},
479+
expectedEventCount: 1,
480+
expectEventMessage: fmt.Sprintf("%s %s %s", corev1.EventTypeNormal, events.EventReasonBindingScheduling, SchedulingConditionMessage),
481+
},
482+
{
483+
name: "Condition already matches, no update needed",
484+
binding: newCrb(true, metav1.Condition{
485+
Type: workv1alpha2.SchedulingSuspended,
486+
Status: metav1.ConditionTrue,
487+
Reason: SuspendedSchedulingConditionReason,
488+
Message: SuspendedSchedulingConditionMessage,
489+
}),
490+
expectedCondition: metav1.Condition{
491+
Type: workv1alpha2.SchedulingSuspended,
492+
Status: metav1.ConditionTrue,
493+
},
494+
},
495+
{
496+
name: "No SchedulingSuspended condition and scheduling is not suspended, no update needed",
497+
binding: newCrb(false, metav1.Condition{
498+
Type: workv1alpha2.BindingReasonUnschedulable,
499+
Status: metav1.ConditionTrue,
500+
}),
501+
expectedCondition: metav1.Condition{
502+
Type: workv1alpha2.BindingReasonUnschedulable,
503+
Status: metav1.ConditionTrue,
504+
},
505+
},
506+
}
507+
508+
for _, tt := range tests {
509+
t.Run(tt.name, func(t *testing.T) {
510+
eventRecorder := record.NewFakeRecorder(1)
511+
c := newClusterResourceBindingController(tt.binding, eventRecorder)
512+
513+
updatedBinding := &workv1alpha2.ClusterResourceBinding{}
514+
assert.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: tt.binding.Name, Namespace: tt.binding.Namespace}, updatedBinding))
515+
516+
err := updateBindingDispatchingConditionIfNeeded(context.Background(), c.Client, c.EventRecorder, tt.binding, apiextensionsv1.ClusterScoped)
517+
if err != nil {
518+
t.Errorf("updateBindingDispatchingConditionIfNeeded() returned an error: %v", err)
519+
}
520+
521+
assert.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: tt.binding.Name, Namespace: tt.binding.Namespace}, updatedBinding))
522+
assert.True(t, meta.IsStatusConditionPresentAndEqual(updatedBinding.Status.Conditions, tt.expectedCondition.Type, tt.expectedCondition.Status))
523+
assert.Equal(t, tt.expectedEventCount, len(eventRecorder.Events))
524+
if tt.expectEventMessage != "" {
525+
e := <-eventRecorder.Events
526+
assert.Equal(t, tt.expectEventMessage, e)
527+
}
528+
})
529+
}
530+
}
531+
532+
func newClusterResourceBindingController(binding *workv1alpha2.ClusterResourceBinding, eventRecord record.EventRecorder) ClusterResourceBindingController {
533+
restMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
534+
fakeClient := fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects(binding).WithStatusSubresource(binding).WithRESTMapper(restMapper).Build()
535+
return ClusterResourceBindingController{
536+
Client: fakeClient,
537+
EventRecorder: eventRecord,
538+
}
539+
}
540+
541+
func newCrb(suspended bool, condition metav1.Condition) *workv1alpha2.ClusterResourceBinding {
542+
return &workv1alpha2.ClusterResourceBinding{
543+
TypeMeta: metav1.TypeMeta{
544+
Kind: workv1alpha2.ResourceKindResourceBinding,
545+
APIVersion: workv1alpha2.GroupVersion.Version,
546+
},
547+
ObjectMeta: metav1.ObjectMeta{
548+
Name: "test-rb",
549+
Namespace: "default",
550+
UID: uuid.NewUUID(),
551+
},
552+
Spec: workv1alpha2.ResourceBindingSpec{
553+
Suspension: &workv1alpha2.Suspension{
554+
Scheduling: ptr.To(suspended),
555+
},
556+
},
557+
Status: workv1alpha2.ResourceBindingStatus{
558+
Conditions: []metav1.Condition{
559+
condition,
560+
},
561+
},
562+
}
563+
}

0 commit comments

Comments
 (0)