@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "fmt"
2222 "reflect"
23+ "strings"
2324 "testing"
2425 "time"
2526
@@ -34,6 +35,7 @@ import (
3435 "k8s.io/client-go/dynamic"
3536 dynamicfake "k8s.io/client-go/dynamic/fake"
3637 "k8s.io/client-go/kubernetes/scheme"
38+ "k8s.io/client-go/tools/record"
3739 "k8s.io/utils/ptr"
3840 "sigs.k8s.io/controller-runtime/pkg/client"
3941 "sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -42,6 +44,7 @@ import (
4244 configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
4345 policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
4446 workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
47+ "github.com/karmada-io/karmada/pkg/events"
4548 "github.com/karmada-io/karmada/pkg/util"
4649 "github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
4750 "github.com/karmada-io/karmada/pkg/util/fedinformer/keys"
@@ -3432,3 +3435,136 @@ func Test_deleteBindingFromSnapshot(t *testing.T) {
34323435 })
34333436 }
34343437}
3438+
3439+ type dependencyPolicyTestEnv struct {
3440+ distributor * DependenciesDistributor
3441+ client client.Client
3442+ recorder * record.FakeRecorder
3443+ existing * workv1alpha2.ResourceBinding
3444+ dependent * unstructured.Unstructured
3445+ }
3446+
3447+ func newDependencyPolicyTestEnv (t * testing.T ) * dependencyPolicyTestEnv {
3448+ t .Helper ()
3449+
3450+ scheme := runtime .NewScheme ()
3451+ utilruntime .Must (workv1alpha2 .Install (scheme ))
3452+
3453+ dependent := & unstructured.Unstructured {}
3454+ dependent .SetAPIVersion ("v1" )
3455+ dependent .SetKind ("ConfigMap" )
3456+ dependent .SetNamespace ("default" )
3457+ dependent .SetName ("app-config" )
3458+ dependent .SetUID (types .UID ("cm-uid" ))
3459+
3460+ parentOne := & workv1alpha2.ResourceBinding {
3461+ ObjectMeta : metav1.ObjectMeta {
3462+ Name : "workload-a" ,
3463+ Namespace : "default" ,
3464+ Labels : map [string ]string {
3465+ workv1alpha2 .ResourceBindingPermanentIDLabel : "rb-uid-a" ,
3466+ },
3467+ },
3468+ Spec : workv1alpha2.ResourceBindingSpec {
3469+ Clusters : []workv1alpha2.TargetCluster {{Name : "member1" }},
3470+ ConflictResolution : policyv1alpha1 .ConflictOverwrite ,
3471+ PreserveResourcesOnDeletion : ptr .To (true ),
3472+ },
3473+ }
3474+
3475+ attachedFromParentOne := buildAttachedBinding (parentOne , dependent )
3476+ existing := attachedFromParentOne .DeepCopy ()
3477+
3478+ fakeClient := fake .NewClientBuilder ().WithScheme (scheme ).WithObjects (existing ).Build ()
3479+ recorder := record .NewFakeRecorder (10 )
3480+
3481+ distributor := & DependenciesDistributor {
3482+ Client : fakeClient ,
3483+ EventRecorder : recorder ,
3484+ }
3485+
3486+ return & dependencyPolicyTestEnv {
3487+ distributor : distributor ,
3488+ client : fakeClient ,
3489+ recorder : recorder ,
3490+ existing : existing ,
3491+ dependent : dependent ,
3492+ }
3493+ }
3494+
3495+ func waitForEvents (t * testing.T , recorder * record.FakeRecorder , want int , timeout time.Duration ) []string {
3496+ t .Helper ()
3497+
3498+ eventList := make ([]string , 0 , want )
3499+ timeoutCh := time .After (timeout )
3500+ for len (eventList ) < want {
3501+ select {
3502+ case evt := <- recorder .Events :
3503+ eventList = append (eventList , evt )
3504+ case <- timeoutCh :
3505+ t .Fatalf ("timed out waiting for %d events, got %v" , want , eventList )
3506+ }
3507+ }
3508+ return eventList
3509+ }
3510+
3511+ func findEventByReason (t * testing.T , eventList []string , reason string ) string {
3512+ t .Helper ()
3513+
3514+ for _ , evt := range eventList {
3515+ if strings .Contains (evt , reason ) {
3516+ return evt
3517+ }
3518+ }
3519+ t .Fatalf ("expected event reason %s in %v" , reason , eventList )
3520+ return ""
3521+ }
3522+
3523+ func assertEventMessageContains (t * testing.T , event string , substrings ... string ) {
3524+ t .Helper ()
3525+
3526+ for _ , substring := range substrings {
3527+ if ! strings .Contains (event , substring ) {
3528+ t .Errorf ("event %q missing substring %q" , event , substring )
3529+ }
3530+ }
3531+ }
3532+
3533+ func TestCreateOrUpdateAttachedBinding_RecordsConflictEvent (t * testing.T ) {
3534+ env := newDependencyPolicyTestEnv (t )
3535+
3536+ parentTwo := & workv1alpha2.ResourceBinding {
3537+ ObjectMeta : metav1.ObjectMeta {
3538+ Name : "workload-b" ,
3539+ Namespace : "default" ,
3540+ Labels : map [string ]string {
3541+ workv1alpha2 .ResourceBindingPermanentIDLabel : "rb-uid-b" ,
3542+ },
3543+ },
3544+ Spec : workv1alpha2.ResourceBindingSpec {
3545+ Clusters : []workv1alpha2.TargetCluster {{Name : "member2" }},
3546+ ConflictResolution : policyv1alpha1 .ConflictAbort ,
3547+ PreserveResourcesOnDeletion : ptr .To (false ),
3548+ },
3549+ }
3550+ attachedFromParentTwo := buildAttachedBinding (parentTwo , env .dependent )
3551+
3552+ if err := env .distributor .createOrUpdateAttachedBinding (attachedFromParentTwo ); err != nil {
3553+ t .Fatalf ("createOrUpdateAttachedBinding() error = %v" , err )
3554+ }
3555+
3556+ updated := & workv1alpha2.ResourceBinding {}
3557+ if err := env .client .Get (context .TODO (), client .ObjectKeyFromObject (env .existing ), updated ); err != nil {
3558+ t .Fatalf ("failed to fetch updated binding: %v" , err )
3559+ }
3560+ if len (updated .Spec .RequiredBy ) != 2 {
3561+ t .Fatalf ("expected 2 parents after aggregation, got %d" , len (updated .Spec .RequiredBy ))
3562+ }
3563+
3564+ eventList := waitForEvents (t , env .recorder , 1 , time .Second )
3565+
3566+ conflictEvent := findEventByReason (t , eventList , events .EventReasonDependencyPolicyConflictResolved )
3567+ assertEventMessageContains (t , conflictEvent ,
3568+ "Dependency policy conflict detected and resolved by default rules." ,
3569+ )
3570+ }
0 commit comments