feat: Introduce a LabelSelector field to DependentObjectReference

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
This commit is contained in:
chaunceyjiang 2023-07-19 19:09:50 +08:00
parent e5277b6317
commit e17eb1a595
7 changed files with 566 additions and 64 deletions

View File

@ -153,6 +153,13 @@ type DependentObjectReference struct {
Namespace string `json:"namespace,omitempty"` Namespace string `json:"namespace,omitempty"`
// Name represents the name of the referent. // Name represents the name of the referent.
// +required // Name and LabelSelector cannot be empty at the same time.
Name string `json:"name"` // +optional
Name string `json:"name,omitempty"`
// LabelSelector represents a label query over a set of resources.
// If name is not empty, labelSelector will be ignored.
// Name and LabelSelector cannot be empty at the same time.
// +optional
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
} }

View File

@ -7,6 +7,7 @@ package v1alpha1
import ( import (
v1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" v1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@ -96,6 +97,11 @@ func (in *DependencyInterpretation) DeepCopy() *DependencyInterpretation {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DependentObjectReference) DeepCopyInto(out *DependentObjectReference) { func (in *DependentObjectReference) DeepCopyInto(out *DependentObjectReference) {
*out = *in *out = *in
if in.LabelSelector != nil {
in, out := &in.LabelSelector, &out.LabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
return return
} }
@ -368,7 +374,9 @@ func (in *ResourceInterpreterResponse) DeepCopyInto(out *ResourceInterpreterResp
if in.Dependencies != nil { if in.Dependencies != nil {
in, out := &in.Dependencies, &out.Dependencies in, out := &in.Dependencies, &out.Dependencies
*out = make([]DependentObjectReference, len(*in)) *out = make([]DependentObjectReference, len(*in))
copy(*out, *in) for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
} }
if in.RawStatus != nil { if in.RawStatus != nil {
in, out := &in.RawStatus, &out.RawStatus in, out := &in.RawStatus, &out.RawStatus

View File

@ -14,7 +14,6 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
@ -54,6 +53,13 @@ const (
bindingDependenciesAnnotationKey = "resourcebinding.karmada.io/dependencies" bindingDependenciesAnnotationKey = "resourcebinding.karmada.io/dependencies"
) )
// LabelsKey is the object key which is a unique identifier under a cluster, across all resources.
type LabelsKey struct {
keys.ClusterWideKey
// Labels is the labels of the referencing object.
Labels map[string]string
}
// DependenciesDistributor is to automatically propagate relevant resources. // DependenciesDistributor is to automatically propagate relevant resources.
// Resource binding will be created when a resource(e.g. deployment) is matched by a propagation policy, we call it independent binding in DependenciesDistributor. // Resource binding will be created when a resource(e.g. deployment) is matched by a propagation policy, we call it independent binding in DependenciesDistributor.
// And when DependenciesDistributor works, it will create or update reference resource bindings of relevant resources(e.g. secret), which we call them attached bindings. // And when DependenciesDistributor works, it will create or update reference resource bindings of relevant resources(e.g. secret), which we call them attached bindings.
@ -111,7 +117,7 @@ func (d *DependenciesDistributor) OnUpdate(oldObj, newObj interface{}) {
klog.V(4).Infof("Ignore update event of object (%s, kind=%s, %s) as specification no change", unstructuredOldObj.GetAPIVersion(), unstructuredOldObj.GetKind(), names.NamespacedKey(unstructuredOldObj.GetNamespace(), unstructuredOldObj.GetName())) klog.V(4).Infof("Ignore update event of object (%s, kind=%s, %s) as specification no change", unstructuredOldObj.GetAPIVersion(), unstructuredOldObj.GetKind(), names.NamespacedKey(unstructuredOldObj.GetNamespace(), unstructuredOldObj.GetName()))
return return
} }
d.OnAdd(oldObj)
d.OnAdd(newObj) d.OnAdd(newObj)
} }
@ -123,7 +129,7 @@ func (d *DependenciesDistributor) OnDelete(obj interface{}) {
// Reconcile performs a full reconciliation for the object referred to by the key. // Reconcile performs a full reconciliation for the object referred to by the key.
// The key will be re-queued if an error is non-nil. // The key will be re-queued if an error is non-nil.
func (d *DependenciesDistributor) reconcile(key util.QueueKey) error { func (d *DependenciesDistributor) reconcile(key util.QueueKey) error {
clusterWideKey, ok := key.(keys.ClusterWideKey) clusterWideKey, ok := key.(*LabelsKey)
if !ok { if !ok {
klog.Error("Invalid key") klog.Error("Invalid key")
return fmt.Errorf("invalid key") return fmt.Errorf("invalid key")
@ -157,7 +163,7 @@ func (d *DependenciesDistributor) reconcile(key util.QueueKey) error {
} }
// dependentObjectReferenceMatches tells if the given object is referred by current resource binding. // dependentObjectReferenceMatches tells if the given object is referred by current resource binding.
func dependentObjectReferenceMatches(objectKey keys.ClusterWideKey, referenceBinding *workv1alpha2.ResourceBinding) bool { func dependentObjectReferenceMatches(objectKey *LabelsKey, referenceBinding *workv1alpha2.ResourceBinding) bool {
dependencies, exist := referenceBinding.Annotations[bindingDependenciesAnnotationKey] dependencies, exist := referenceBinding.Annotations[bindingDependenciesAnnotationKey]
if !exist { if !exist {
return false return false
@ -181,9 +187,17 @@ func dependentObjectReferenceMatches(objectKey keys.ClusterWideKey, referenceBin
for _, dependence := range dependenciesSlice { for _, dependence := range dependenciesSlice {
if objectKey.GroupVersion().String() == dependence.APIVersion && if objectKey.GroupVersion().String() == dependence.APIVersion &&
objectKey.Kind == dependence.Kind && objectKey.Kind == dependence.Kind &&
objectKey.Namespace == dependence.Namespace && objectKey.Namespace == dependence.Namespace {
objectKey.Name == dependence.Name { if len(dependence.Name) != 0 {
return true return dependence.Name == objectKey.Name
}
var selector labels.Selector
if selector, err = metav1.LabelSelectorAsSelector(dependence.LabelSelector); err != nil {
klog.Errorf("Failed to converts the LabelSelector of binding(%s/%s) dependencies(%s): %v",
referenceBinding.Namespace, referenceBinding.Name, dependencies, err)
return false
}
return selector.Matches(labels.Set(objectKey.Labels))
} }
} }
return false return false
@ -239,6 +253,60 @@ func (d *DependenciesDistributor) handleResourceBindingDeletion(namespace, name
return d.removeScheduleResultFromAttachedBindings(namespace, name, attachedBindings) return d.removeScheduleResultFromAttachedBindings(namespace, name, attachedBindings)
} }
func (d *DependenciesDistributor) removeOrphanAttachedBindings(binding *workv1alpha2.ResourceBinding, dependencies []configv1alpha1.DependentObjectReference) error {
// remove orphan attached bindings
orphanBindings, err := d.findOrphanAttachedBindings(binding, dependencies)
if err != nil {
klog.Errorf("Failed to find orphan attached bindings for resourceBinding(%s/%s). Error: %v.",
binding.GetNamespace(), binding.GetName(), err)
return err
}
err = d.removeScheduleResultFromAttachedBindings(binding.Namespace, binding.Name, orphanBindings)
if err != nil {
klog.Errorf("Failed to remove orphan attached bindings by resourceBinding(%s/%s). Error: %v.",
binding.GetNamespace(), binding.GetName(), err)
return err
}
return nil
}
func (d *DependenciesDistributor) handleDependentResource(
binding *workv1alpha2.ResourceBinding,
resource workv1alpha2.ObjectReference,
dependent configv1alpha1.DependentObjectReference) error {
switch {
case len(dependent.Name) != 0:
rawObject, err := helper.FetchResourceTemplate(d.DynamicClient, d.InformerManager, d.RESTMapper, resource)
if err != nil {
// do nothing if resource template not exist.
if apierrors.IsNotFound(err) {
return nil
}
return err
}
attachedBinding := buildAttachedBinding(binding, rawObject)
return d.createOrUpdateAttachedBinding(attachedBinding)
case dependent.LabelSelector != nil:
var selector labels.Selector
var err error
if selector, err = metav1.LabelSelectorAsSelector(dependent.LabelSelector); err != nil {
return err
}
rawObjects, err := helper.FetchResourceTemplateByLabelSelector(d.DynamicClient, d.InformerManager, d.RESTMapper, resource, selector)
if err != nil {
return err
}
for _, rawObject := range rawObjects {
attachedBinding := buildAttachedBinding(binding, rawObject)
if err := d.createOrUpdateAttachedBinding(attachedBinding); err != nil {
return err
}
}
return nil
}
return fmt.Errorf("the Name and LabelSelector in the DependentObjectReference of object (kind=%s %s/%s) cannot be empty at the same time", resource.Kind, resource.Namespace, resource.Name)
}
func (d *DependenciesDistributor) syncScheduleResultToAttachedBindings(binding *workv1alpha2.ResourceBinding, dependencies []configv1alpha1.DependentObjectReference) (err error) { func (d *DependenciesDistributor) syncScheduleResultToAttachedBindings(binding *workv1alpha2.ResourceBinding, dependencies []configv1alpha1.DependentObjectReference) (err error) {
defer func() { defer func() {
if err != nil { if err != nil {
@ -251,19 +319,7 @@ func (d *DependenciesDistributor) syncScheduleResultToAttachedBindings(binding *
if err := d.recordDependencies(binding, dependencies); err != nil { if err := d.recordDependencies(binding, dependencies); err != nil {
return err return err
} }
if err := d.removeOrphanAttachedBindings(binding, dependencies); err != nil {
// remove orphan attached bindings
orphanBindings, err := d.findOrphanAttachedBindings(binding, dependencies)
if err != nil {
klog.Errorf("Failed to find orphan attached bindings for resourceBinding(%s/%s). Error: %v.",
binding.GetNamespace(), binding.GetName(), err)
return err
}
err = d.removeScheduleResultFromAttachedBindings(binding.Namespace, binding.Name, orphanBindings)
if err != nil {
klog.Errorf("Failed to remove orphan attached bindings by resourceBinding(%s/%s). Error: %v.",
binding.GetNamespace(), binding.GetName(), err)
return err return err
} }
@ -286,20 +342,7 @@ func (d *DependenciesDistributor) syncScheduleResultToAttachedBindings(binding *
d.InformerManager.ForResource(gvr, d.eventHandler) d.InformerManager.ForResource(gvr, d.eventHandler)
startInformerManager = true startInformerManager = true
} }
rawObject, err := helper.FetchResourceTemplate(d.DynamicClient, d.InformerManager, d.RESTMapper, resource) errs = append(errs, d.handleDependentResource(binding, resource, dependent))
if err != nil {
// do nothing if resource template not exist.
if apierrors.IsNotFound(err) {
continue
}
errs = append(errs, err)
continue
}
attachedBinding := buildAttachedBinding(binding, rawObject)
if err := d.createOrUpdateAttachedBinding(attachedBinding); err != nil {
errs = append(errs, err)
}
} }
if startInformerManager { if startInformerManager {
d.InformerManager.Start() d.InformerManager.Start()
@ -351,22 +394,72 @@ func (d *DependenciesDistributor) findOrphanAttachedBindings(independentBinding
return nil, err return nil, err
} }
dependenciesSets := sets.NewString() dependenciesMaps := make(map[string][]int, 0)
for _, dependency := range dependencies { for index, dependency := range dependencies {
key := generateDependencyKey(dependency.Kind, dependency.APIVersion, dependency.Namespace, dependency.Name) key := generateDependencyKey(dependency.Kind, dependency.APIVersion, dependency.Namespace)
dependenciesSets.Insert(key) dependenciesMaps[key] = append(dependenciesMaps[key], index)
} }
var orphanAttachedBindings []*workv1alpha2.ResourceBinding var orphanAttachedBindings []*workv1alpha2.ResourceBinding
for _, attachedBinding := range attachedBindings { for _, attachedBinding := range attachedBindings {
key := generateDependencyKey(attachedBinding.Spec.Resource.Kind, attachedBinding.Spec.Resource.APIVersion, attachedBinding.Spec.Resource.Namespace, attachedBinding.Spec.Resource.Name) key := generateDependencyKey(attachedBinding.Spec.Resource.Kind, attachedBinding.Spec.Resource.APIVersion, attachedBinding.Spec.Resource.Namespace)
if !dependenciesSets.Has(key) { dependencyIndexes, exist := dependenciesMaps[key]
if !exist {
orphanAttachedBindings = append(orphanAttachedBindings, attachedBinding)
continue
}
isOrphanAttachedBinding, err := d.isOrphanAttachedBindings(dependencies, dependencyIndexes, attachedBinding)
if err != nil {
return nil, err
}
if isOrphanAttachedBinding {
orphanAttachedBindings = append(orphanAttachedBindings, attachedBinding) orphanAttachedBindings = append(orphanAttachedBindings, attachedBinding)
} }
} }
return orphanAttachedBindings, nil return orphanAttachedBindings, nil
} }
func (d *DependenciesDistributor) isOrphanAttachedBindings(
dependencies []configv1alpha1.DependentObjectReference,
dependencyIndexes []int,
attachedBinding *workv1alpha2.ResourceBinding) (bool, error) {
var resource = attachedBinding.Spec.Resource
for _, idx := range dependencyIndexes {
dependency := dependencies[idx]
switch {
case len(dependency.Name) != 0:
if dependency.Name == resource.Name {
return false, nil
}
case dependency.LabelSelector != nil:
var selector labels.Selector
var err error
if selector, err = metav1.LabelSelectorAsSelector(dependency.LabelSelector); err != nil {
return false, err
}
rawObject, err := helper.FetchResourceTemplate(d.DynamicClient, d.InformerManager, d.RESTMapper, workv1alpha2.ObjectReference{
APIVersion: resource.APIVersion,
Kind: resource.Kind,
Namespace: resource.Namespace,
Name: resource.Name,
})
if err != nil {
// do nothing if resource template not exist.
if apierrors.IsNotFound(err) {
continue
}
return false, err
}
if selector.Matches(labels.Set(rawObject.GetLabels())) {
return false, nil
}
default:
// can not reach here
}
}
return true, nil
}
func (d *DependenciesDistributor) listAttachedBindings(bindingNamespace, bindingName string) (res []*workv1alpha2.ResourceBinding, err error) { func (d *DependenciesDistributor) listAttachedBindings(bindingNamespace, bindingName string) (res []*workv1alpha2.ResourceBinding, err error) {
labelSet := generateBindingDependedLabels(bindingNamespace, bindingName) labelSet := generateBindingDependedLabels(bindingNamespace, bindingName)
selector := labels.SelectorFromSet(labelSet) selector := labels.SelectorFromSet(labelSet)
@ -435,8 +528,21 @@ func (d *DependenciesDistributor) Start(ctx context.Context) error {
klog.Infof("Starting dependencies distributor.") klog.Infof("Starting dependencies distributor.")
d.stopCh = ctx.Done() d.stopCh = ctx.Done()
resourceWorkerOptions := util.Options{ resourceWorkerOptions := util.Options{
Name: "dependencies resource detector", Name: "dependencies resource detector",
KeyFunc: func(obj interface{}) (util.QueueKey, error) { return keys.ClusterWideKeyFunc(obj) }, KeyFunc: func(obj interface{}) (util.QueueKey, error) {
key, err := keys.ClusterWideKeyFunc(obj)
if err != nil {
return nil, err
}
metaInfo, err := meta.Accessor(obj)
if err != nil { // should not happen
return nil, fmt.Errorf("object has no meta: %v", err)
}
return &LabelsKey{
ClusterWideKey: key,
Labels: metaInfo.GetLabels(),
}, nil
},
ReconcileFunc: d.reconcile, ReconcileFunc: d.reconcile,
} }
d.eventHandler = fedinformer.NewHandlerOnEvents(d.OnAdd, d.OnUpdate, d.OnDelete) d.eventHandler = fedinformer.NewHandlerOnEvents(d.OnAdd, d.OnUpdate, d.OnDelete)
@ -506,12 +612,12 @@ func generateBindingDependedLabelKey(bindingNamespace, bindingName string) strin
return fmt.Sprintf(bindingDependedByLabelKeyPrefix + bindHashKey) return fmt.Sprintf(bindingDependedByLabelKeyPrefix + bindHashKey)
} }
func generateDependencyKey(kind, apiVersion, name, namespace string) string { func generateDependencyKey(kind, apiVersion, namespace string) string {
if len(namespace) == 0 { if len(namespace) == 0 {
return kind + "-" + apiVersion + "-" + name return kind + "-" + apiVersion
} }
return kind + "-" + apiVersion + "-" + namespace + "-" + name return kind + "-" + apiVersion + "-" + namespace
} }
func buildAttachedBinding(binding *workv1alpha2.ResourceBinding, object *unstructured.Unstructured) *workv1alpha2.ResourceBinding { func buildAttachedBinding(binding *workv1alpha2.ResourceBinding, object *unstructured.Unstructured) *workv1alpha2.ResourceBinding {

View File

@ -1,17 +1,27 @@
package dependenciesdistributor package dependenciesdistributor
import ( import (
"context"
"reflect"
"testing" "testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/scheme"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
"github.com/karmada-io/karmada/pkg/util/fedinformer/keys" "github.com/karmada-io/karmada/pkg/util/fedinformer/keys"
) )
func Test_dependentObjectReferenceMatches(t *testing.T) { func Test_dependentObjectReferenceMatches(t *testing.T) {
type args struct { type args struct {
objectKey keys.ClusterWideKey objectKey *LabelsKey
referenceBinding *workv1alpha2.ResourceBinding referenceBinding *workv1alpha2.ResourceBinding
} }
tests := []struct { tests := []struct {
@ -22,12 +32,15 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
{ {
name: "test custom resource", name: "test custom resource",
args: args{ args: args{
objectKey: keys.ClusterWideKey{ objectKey: &LabelsKey{
Group: "example-stgzr.karmada.io", ClusterWideKey: keys.ClusterWideKey{
Version: "v1alpha1", Group: "example-stgzr.karmada.io",
Kind: "Foot5zmh", Version: "v1alpha1",
Namespace: "karmadatest-vpvll", Kind: "Foot5zmh",
Name: "cr-fxzq6", Namespace: "karmadatest-vpvll",
Name: "cr-fxzq6",
},
Labels: nil,
}, },
referenceBinding: &workv1alpha2.ResourceBinding{ referenceBinding: &workv1alpha2.ResourceBinding{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
@ -40,12 +53,15 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
{ {
name: "test configmap", name: "test configmap",
args: args{ args: args{
objectKey: keys.ClusterWideKey{ objectKey: &LabelsKey{
Group: "", ClusterWideKey: keys.ClusterWideKey{
Version: "v1", Group: "",
Kind: "ConfigMap", Version: "v1",
Namespace: "karmadatest-h46wh", Kind: "ConfigMap",
Name: "configmap-8w426", Namespace: "karmadatest-h46wh",
Name: "configmap-8w426",
},
Labels: nil,
}, },
referenceBinding: &workv1alpha2.ResourceBinding{ referenceBinding: &workv1alpha2.ResourceBinding{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
@ -55,6 +71,28 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
}, },
want: true, want: true,
}, },
{
name: "test labels",
args: args{
objectKey: &LabelsKey{
ClusterWideKey: keys.ClusterWideKey{
Group: "",
Version: "v1",
Kind: "ConfigMap",
Namespace: "test",
},
Labels: map[string]string{
"app": "test",
},
},
referenceBinding: &workv1alpha2.ResourceBinding{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
bindingDependenciesAnnotationKey: "[{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"namespace\":\"test\",\"labelSelector\":{\"matchExpressions\":[{\"key\":\"app\",\"operator\":\"In\",\"values\":[\"test\"]}]}}]",
}},
},
},
want: true,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -65,3 +103,141 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
}) })
} }
} }
func TestDependenciesDistributor_findOrphanAttachedBindingsByDependencies(t *testing.T) {
type fields struct {
DynamicClient dynamic.Interface
InformerManager genericmanager.SingleClusterInformerManager
RESTMapper meta.RESTMapper
}
type args struct {
dependencies []configv1alpha1.DependentObjectReference
dependencyIndexes []int
attachedBinding *workv1alpha2.ResourceBinding
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
}{
{
name: "can't match labels",
fields: fields{
DynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme),
InformerManager: func() genericmanager.SingleClusterInformerManager {
c := dynamicfake.NewSimpleDynamicClient(scheme.Scheme,
&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "default", Labels: map[string]string{"bar": "bar"}}})
m := genericmanager.NewSingleClusterInformerManager(c, 0, context.TODO().Done())
m.Lister(corev1.SchemeGroupVersion.WithResource("pods"))
m.Start()
m.WaitForCacheSync()
return m
}(),
RESTMapper: func() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
m.Add(corev1.SchemeGroupVersion.WithKind("Pod"), meta.RESTScopeNamespace)
return m
}(),
},
args: args{
dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Secret",
Namespace: "default",
Name: "test",
},
{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{
"bar": "foo",
}},
},
},
dependencyIndexes: []int{1},
attachedBinding: &workv1alpha2.ResourceBinding{
Spec: workv1alpha2.ResourceBindingSpec{
Resource: workv1alpha2.ObjectReference{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
Name: "pod",
},
},
},
},
want: true,
wantErr: false,
},
{
name: "can't match name",
fields: fields{
DynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme),
InformerManager: func() genericmanager.SingleClusterInformerManager {
c := dynamicfake.NewSimpleDynamicClient(scheme.Scheme,
&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "default", Labels: map[string]string{"bar": "foo"}}})
m := genericmanager.NewSingleClusterInformerManager(c, 0, context.TODO().Done())
m.Lister(corev1.SchemeGroupVersion.WithResource("pods"))
m.Start()
m.WaitForCacheSync()
return m
}(),
RESTMapper: func() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
m.Add(corev1.SchemeGroupVersion.WithKind("Pod"), meta.RESTScopeNamespace)
return m
}(),
},
args: args{
dependencies: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "Secret",
Namespace: "default",
Name: "test",
},
{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
Name: "pod",
},
},
dependencyIndexes: []int{1},
attachedBinding: &workv1alpha2.ResourceBinding{
Spec: workv1alpha2.ResourceBindingSpec{
Resource: workv1alpha2.ObjectReference{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
Name: "test2",
},
},
},
},
want: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &DependenciesDistributor{
DynamicClient: tt.fields.DynamicClient,
InformerManager: tt.fields.InformerManager,
RESTMapper: tt.fields.RESTMapper,
}
got, err := d.isOrphanAttachedBindings(tt.args.dependencies, tt.args.dependencyIndexes, tt.args.attachedBinding)
if (err != nil) != tt.wantErr {
t.Errorf("findOrphanAttachedBindingsByDependencies() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("findOrphanAttachedBindingsByDependencies() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1811,16 +1811,23 @@ func schema_pkg_apis_config_v1alpha1_DependentObjectReference(ref common.Referen
}, },
"name": { "name": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "Name represents the name of the referent.", Description: "Name represents the name of the referent. Name and LabelSelector cannot be empty at the same time.",
Default: "",
Type: []string{"string"}, Type: []string{"string"},
Format: "", Format: "",
}, },
}, },
"labelSelector": {
SchemaProps: spec.SchemaProps{
Description: "LabelSelector represents a label query over a set of resources. If name is not empty, labelSelector will be ignored. Name and LabelSelector cannot be empty at the same time.",
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"),
},
},
}, },
Required: []string{"apiVersion", "kind", "name"}, Required: []string{"apiVersion", "kind"},
}, },
}, },
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
} }
} }

View File

@ -251,6 +251,56 @@ func FetchResourceTemplate(
return unstructuredObj, nil return unstructuredObj, nil
} }
// FetchResourceTemplateByLabelSelector fetches the resource template by label selector to be propagated.
// Any updates to this resource template are not recommended as it may come from the informer cache.
// We should abide by the principle of making a deep copy first and then modifying it.
// See issue: https://github.com/karmada-io/karmada/issues/3878.
func FetchResourceTemplateByLabelSelector(
dynamicClient dynamic.Interface,
informerManager genericmanager.SingleClusterInformerManager,
restMapper meta.RESTMapper,
resource workv1alpha2.ObjectReference,
selector labels.Selector,
) ([]*unstructured.Unstructured, error) {
gvr, err := restmapper.GetGroupVersionResource(restMapper, schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind))
if err != nil {
klog.Errorf("Failed to get GVR from GVK(%s/%s), Error: %v", resource.APIVersion, resource.Kind, err)
return nil, err
}
var objectList []runtime.Object
if len(resource.Namespace) == 0 {
// cluster-scoped resource
objectList, err = informerManager.Lister(gvr).List(selector)
} else {
objectList, err = informerManager.Lister(gvr).ByNamespace(resource.Namespace).List(selector)
}
var objects []*unstructured.Unstructured
if err != nil || len(objectList) == 0 {
// fall back to call api server in case the cache has not been synchronized yet
klog.Warningf("Failed to get resource template (%s/%s/%s) from cache, Error: %v. Fall back to call api server.",
resource.Kind, resource.Namespace, resource.Name, err)
unstructuredList, err := dynamicClient.Resource(gvr).Namespace(resource.Namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
klog.Errorf("Failed to get resource template (%s/%s/%s) from api server, Error: %v",
resource.Kind, resource.Namespace, resource.Name, err)
return nil, err
}
for i := range unstructuredList.Items {
objects = append(objects, &unstructuredList.Items[i])
}
}
for i := range objectList {
unstructuredObj, err := ToUnstructured(objectList[i])
if err != nil {
klog.Errorf("Failed to transform object(%s/%s), Error: %v", resource.Namespace, resource.Name, err)
return nil, err
}
objects = append(objects, unstructuredObj)
}
return objects, nil
}
// GetClusterResourceBindings returns a ClusterResourceBindingList by labels. // GetClusterResourceBindings returns a ClusterResourceBindingList by labels.
func GetClusterResourceBindings(c client.Client, ls labels.Set) (*workv1alpha2.ClusterResourceBindingList, error) { func GetClusterResourceBindings(c client.Client, ls labels.Set) (*workv1alpha2.ClusterResourceBindingList, error) {
bindings := &workv1alpha2.ClusterResourceBindingList{} bindings := &workv1alpha2.ClusterResourceBindingList{}

View File

@ -801,6 +801,154 @@ func TestFetchWorkload(t *testing.T) {
} }
} }
func TestFetchWorkloadByLabelSelector(t *testing.T) {
type args struct {
dynamicClient dynamic.Interface
informerManager func(stopCh <-chan struct{}) genericmanager.SingleClusterInformerManager
restMapper meta.RESTMapper
resource workv1alpha2.ObjectReference
selector *metav1.LabelSelector
}
tests := []struct {
name string
args args
want int
wantErr bool
}{
{
name: "kind is not registered",
args: args{
dynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme),
informerManager: func(stopCh <-chan struct{}) genericmanager.SingleClusterInformerManager {
return genericmanager.NewSingleClusterInformerManager(dynamicfake.NewSimpleDynamicClient(scheme.Scheme), 0, stopCh)
},
restMapper: meta.NewDefaultRESTMapper(nil),
resource: workv1alpha2.ObjectReference{APIVersion: "v1", Kind: "Pod"},
},
want: 0,
wantErr: true,
},
{
name: "namespace scope: get from client",
args: args{
dynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme,
&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "default", Labels: map[string]string{"foo": "foo"}}}),
informerManager: func(stopCh <-chan struct{}) genericmanager.SingleClusterInformerManager {
return genericmanager.NewSingleClusterInformerManager(dynamicfake.NewSimpleDynamicClient(scheme.Scheme), 0, stopCh)
},
restMapper: func() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
m.Add(corev1.SchemeGroupVersion.WithKind("Pod"), meta.RESTScopeNamespace)
return m
}(),
resource: workv1alpha2.ObjectReference{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
},
selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "foo"}},
},
want: 1,
wantErr: false,
},
{
name: "namespace scope: get from cache",
args: args{
dynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme),
informerManager: func(stopCh <-chan struct{}) genericmanager.SingleClusterInformerManager {
c := dynamicfake.NewSimpleDynamicClient(scheme.Scheme,
&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "default", Labels: map[string]string{"bar": "foo"}}})
m := genericmanager.NewSingleClusterInformerManager(c, 0, stopCh)
m.Lister(corev1.SchemeGroupVersion.WithResource("pods"))
m.Start()
m.WaitForCacheSync()
return m
},
restMapper: func() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
m.Add(corev1.SchemeGroupVersion.WithKind("Pod"), meta.RESTScopeNamespace)
return m
}(),
resource: workv1alpha2.ObjectReference{
APIVersion: "v1",
Kind: "Pod",
Namespace: "default",
Name: "pod",
},
selector: &metav1.LabelSelector{MatchLabels: map[string]string{"bar": "foo"}},
},
want: 1,
wantErr: false,
},
{
name: "cluster scope: get from client",
args: args{
dynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme,
&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node", Labels: map[string]string{"bar": "bar"}}}),
informerManager: func(stopCh <-chan struct{}) genericmanager.SingleClusterInformerManager {
return genericmanager.NewSingleClusterInformerManager(dynamicfake.NewSimpleDynamicClient(scheme.Scheme), 0, stopCh)
},
restMapper: func() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
m.Add(corev1.SchemeGroupVersion.WithKind("Node"), meta.RESTScopeRoot)
return m
}(),
resource: workv1alpha2.ObjectReference{
APIVersion: "v1",
Kind: "Node",
Name: "node",
},
selector: &metav1.LabelSelector{MatchLabels: map[string]string{"bar": "bar"}},
},
want: 1,
wantErr: false,
},
{
name: "cluster scope: get from cache",
args: args{
dynamicClient: dynamicfake.NewSimpleDynamicClient(scheme.Scheme),
informerManager: func(stopCh <-chan struct{}) genericmanager.SingleClusterInformerManager {
c := dynamicfake.NewSimpleDynamicClient(scheme.Scheme,
&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node", Labels: map[string]string{"bar": "foo"}}})
m := genericmanager.NewSingleClusterInformerManager(c, 0, stopCh)
m.Lister(corev1.SchemeGroupVersion.WithResource("nodes"))
m.Start()
m.WaitForCacheSync()
return m
},
restMapper: func() meta.RESTMapper {
m := meta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
m.Add(corev1.SchemeGroupVersion.WithKind("Node"), meta.RESTScopeRoot)
return m
}(),
resource: workv1alpha2.ObjectReference{
APIVersion: "v1",
Kind: "Node",
Name: "node",
},
selector: &metav1.LabelSelector{MatchLabels: map[string]string{"bar": "foo"}},
},
want: 1,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stopCh := make(chan struct{})
mgr := tt.args.informerManager(stopCh)
selector, _ := metav1.LabelSelectorAsSelector(tt.args.selector)
got, err := FetchResourceTemplateByLabelSelector(tt.args.dynamicClient, mgr, tt.args.restMapper, tt.args.resource, selector)
if (err != nil) != tt.wantErr {
t.Errorf("FetchResourceTemplate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(got) != tt.want {
t.Errorf("FetchResourceTemplate() got = %v, want %v", got, tt.want)
}
})
}
}
func TestDeleteWorkByRBNamespaceAndName(t *testing.T) { func TestDeleteWorkByRBNamespaceAndName(t *testing.T) {
type args struct { type args struct {
c client.Client c client.Client