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"`
// Name represents the name of the referent.
// +required
Name string `json:"name"`
// Name and LabelSelector cannot be empty at the same time.
// +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 (
v1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
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.
func (in *DependentObjectReference) DeepCopyInto(out *DependentObjectReference) {
*out = *in
if in.LabelSelector != nil {
in, out := &in.LabelSelector, &out.LabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
return
}
@ -368,7 +374,9 @@ func (in *ResourceInterpreterResponse) DeepCopyInto(out *ResourceInterpreterResp
if in.Dependencies != nil {
in, out := &in.Dependencies, &out.Dependencies
*out = make([]DependentObjectReference, len(*in))
copy(*out, *in)
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.RawStatus != nil {
in, out := &in.RawStatus, &out.RawStatus

View File

@ -14,7 +14,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
@ -54,6 +53,13 @@ const (
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.
// 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.
@ -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()))
return
}
d.OnAdd(oldObj)
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.
// The key will be re-queued if an error is non-nil.
func (d *DependenciesDistributor) reconcile(key util.QueueKey) error {
clusterWideKey, ok := key.(keys.ClusterWideKey)
clusterWideKey, ok := key.(*LabelsKey)
if !ok {
klog.Error("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.
func dependentObjectReferenceMatches(objectKey keys.ClusterWideKey, referenceBinding *workv1alpha2.ResourceBinding) bool {
func dependentObjectReferenceMatches(objectKey *LabelsKey, referenceBinding *workv1alpha2.ResourceBinding) bool {
dependencies, exist := referenceBinding.Annotations[bindingDependenciesAnnotationKey]
if !exist {
return false
@ -181,9 +187,17 @@ func dependentObjectReferenceMatches(objectKey keys.ClusterWideKey, referenceBin
for _, dependence := range dependenciesSlice {
if objectKey.GroupVersion().String() == dependence.APIVersion &&
objectKey.Kind == dependence.Kind &&
objectKey.Namespace == dependence.Namespace &&
objectKey.Name == dependence.Name {
return true
objectKey.Namespace == dependence.Namespace {
if len(dependence.Name) != 0 {
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
@ -239,6 +253,60 @@ func (d *DependenciesDistributor) handleResourceBindingDeletion(namespace, name
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) {
defer func() {
if err != nil {
@ -251,19 +319,7 @@ func (d *DependenciesDistributor) syncScheduleResultToAttachedBindings(binding *
if err := d.recordDependencies(binding, dependencies); err != nil {
return err
}
// 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)
if err := d.removeOrphanAttachedBindings(binding, dependencies); err != nil {
return err
}
@ -286,20 +342,7 @@ func (d *DependenciesDistributor) syncScheduleResultToAttachedBindings(binding *
d.InformerManager.ForResource(gvr, d.eventHandler)
startInformerManager = true
}
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) {
continue
}
errs = append(errs, err)
continue
}
attachedBinding := buildAttachedBinding(binding, rawObject)
if err := d.createOrUpdateAttachedBinding(attachedBinding); err != nil {
errs = append(errs, err)
}
errs = append(errs, d.handleDependentResource(binding, resource, dependent))
}
if startInformerManager {
d.InformerManager.Start()
@ -351,22 +394,72 @@ func (d *DependenciesDistributor) findOrphanAttachedBindings(independentBinding
return nil, err
}
dependenciesSets := sets.NewString()
for _, dependency := range dependencies {
key := generateDependencyKey(dependency.Kind, dependency.APIVersion, dependency.Namespace, dependency.Name)
dependenciesSets.Insert(key)
dependenciesMaps := make(map[string][]int, 0)
for index, dependency := range dependencies {
key := generateDependencyKey(dependency.Kind, dependency.APIVersion, dependency.Namespace)
dependenciesMaps[key] = append(dependenciesMaps[key], index)
}
var orphanAttachedBindings []*workv1alpha2.ResourceBinding
for _, attachedBinding := range attachedBindings {
key := generateDependencyKey(attachedBinding.Spec.Resource.Kind, attachedBinding.Spec.Resource.APIVersion, attachedBinding.Spec.Resource.Namespace, attachedBinding.Spec.Resource.Name)
if !dependenciesSets.Has(key) {
key := generateDependencyKey(attachedBinding.Spec.Resource.Kind, attachedBinding.Spec.Resource.APIVersion, attachedBinding.Spec.Resource.Namespace)
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)
}
}
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) {
labelSet := generateBindingDependedLabels(bindingNamespace, bindingName)
selector := labels.SelectorFromSet(labelSet)
@ -436,7 +529,20 @@ func (d *DependenciesDistributor) Start(ctx context.Context) error {
d.stopCh = ctx.Done()
resourceWorkerOptions := util.Options{
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,
}
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)
}
func generateDependencyKey(kind, apiVersion, name, namespace string) string {
func generateDependencyKey(kind, apiVersion, namespace string) string {
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 {

View File

@ -1,17 +1,27 @@
package dependenciesdistributor
import (
"context"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
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"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
"github.com/karmada-io/karmada/pkg/util/fedinformer/keys"
)
func Test_dependentObjectReferenceMatches(t *testing.T) {
type args struct {
objectKey keys.ClusterWideKey
objectKey *LabelsKey
referenceBinding *workv1alpha2.ResourceBinding
}
tests := []struct {
@ -22,13 +32,16 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
{
name: "test custom resource",
args: args{
objectKey: keys.ClusterWideKey{
objectKey: &LabelsKey{
ClusterWideKey: keys.ClusterWideKey{
Group: "example-stgzr.karmada.io",
Version: "v1alpha1",
Kind: "Foot5zmh",
Namespace: "karmadatest-vpvll",
Name: "cr-fxzq6",
},
Labels: nil,
},
referenceBinding: &workv1alpha2.ResourceBinding{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
bindingDependenciesAnnotationKey: "[{\"apiVersion\":\"example-stgzr.karmada.io/v1alpha1\",\"kind\":\"Foot5zmh\",\"namespace\":\"karmadatest-vpvll\",\"name\":\"cr-fxzq6\"}]",
@ -40,13 +53,16 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
{
name: "test configmap",
args: args{
objectKey: keys.ClusterWideKey{
objectKey: &LabelsKey{
ClusterWideKey: keys.ClusterWideKey{
Group: "",
Version: "v1",
Kind: "ConfigMap",
Namespace: "karmadatest-h46wh",
Name: "configmap-8w426",
},
Labels: nil,
},
referenceBinding: &workv1alpha2.ResourceBinding{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
bindingDependenciesAnnotationKey: "[{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"namespace\":\"karmadatest-h46wh\",\"name\":\"configmap-8w426\"}]",
@ -55,6 +71,28 @@ func Test_dependentObjectReferenceMatches(t *testing.T) {
},
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 {
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": {
SchemaProps: spec.SchemaProps{
Description: "Name represents the name of the referent.",
Default: "",
Description: "Name represents the name of the referent. Name and LabelSelector cannot be empty at the same time.",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"apiVersion", "kind", "name"},
"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"},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
}
}

View File

@ -251,6 +251,56 @@ func FetchResourceTemplate(
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.
func GetClusterResourceBindings(c client.Client, ls labels.Set) (*workv1alpha2.ClusterResourceBindingList, error) {
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) {
type args struct {
c client.Client