mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	Add pod ambiguous selector check
This commit is contained in:
		@@ -50,6 +50,7 @@ import (
 | 
			
		||||
	"k8s.io/klog/v2"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/controller"
 | 
			
		||||
	metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/controller/util/selectors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -103,6 +104,10 @@ type HorizontalController struct {
 | 
			
		||||
	scaleUpEventsLock   sync.RWMutex
 | 
			
		||||
	scaleDownEvents     map[string][]timestampedScaleEvent
 | 
			
		||||
	scaleDownEventsLock sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	// Storage of HPAs and their selectors.
 | 
			
		||||
	hpaSelectors    *selectors.BiMultimap
 | 
			
		||||
	hpaSelectorsMux sync.Mutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewHorizontalController creates a new HorizontalController.
 | 
			
		||||
@@ -139,6 +144,7 @@ func NewHorizontalController(
 | 
			
		||||
		scaleUpEventsLock:            sync.RWMutex{},
 | 
			
		||||
		scaleDownEvents:              map[string][]timestampedScaleEvent{},
 | 
			
		||||
		scaleDownEventsLock:          sync.RWMutex{},
 | 
			
		||||
		hpaSelectors:                 selectors.NewBiMultimap(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hpaInformer.Informer().AddEventHandlerWithResyncPeriod(
 | 
			
		||||
@@ -203,6 +209,15 @@ func (a *HorizontalController) enqueueHPA(obj interface{}) {
 | 
			
		||||
	// request for the HPA in the queue then a new request is always dropped. Requests spend resync
 | 
			
		||||
	// interval in queue so HPAs are processed every resync interval.
 | 
			
		||||
	a.queue.AddRateLimited(key)
 | 
			
		||||
 | 
			
		||||
	// Register HPA in the hpaSelectors map if it's not present yet. Attaching the Nothing selector
 | 
			
		||||
	// that does not select objects. The actual selector is going to be updated
 | 
			
		||||
	// when it's available during the autoscaler reconciliation.
 | 
			
		||||
	a.hpaSelectorsMux.Lock()
 | 
			
		||||
	defer a.hpaSelectorsMux.Unlock()
 | 
			
		||||
	if hpaKey := selectors.Parse(key); !a.hpaSelectors.SelectorExists(hpaKey) {
 | 
			
		||||
		a.hpaSelectors.PutSelector(hpaKey, labels.Nothing())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *HorizontalController) deleteHPA(obj interface{}) {
 | 
			
		||||
@@ -214,6 +229,11 @@ func (a *HorizontalController) deleteHPA(obj interface{}) {
 | 
			
		||||
 | 
			
		||||
	// TODO: could we leak if we fail to get the key?
 | 
			
		||||
	a.queue.Forget(key)
 | 
			
		||||
 | 
			
		||||
	// Remove HPA and attached selector.
 | 
			
		||||
	a.hpaSelectorsMux.Lock()
 | 
			
		||||
	defer a.hpaSelectorsMux.Unlock()
 | 
			
		||||
	a.hpaSelectors.DeleteSelector(selectors.Parse(key))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *HorizontalController) worker(ctx context.Context) {
 | 
			
		||||
@@ -254,19 +274,10 @@ func (a *HorizontalController) processNextWorkItem(ctx context.Context) bool {
 | 
			
		||||
// all metrics computed.
 | 
			
		||||
func (a *HorizontalController) computeReplicasForMetrics(ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, scale *autoscalingv1.Scale,
 | 
			
		||||
	metricSpecs []autoscalingv2.MetricSpec) (replicas int32, metric string, statuses []autoscalingv2.MetricStatus, timestamp time.Time, err error) {
 | 
			
		||||
	if scale.Status.Selector == "" {
 | 
			
		||||
		errMsg := "selector is required"
 | 
			
		||||
		a.eventRecorder.Event(hpa, v1.EventTypeWarning, "SelectorRequired", errMsg)
 | 
			
		||||
		setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "InvalidSelector", "the HPA target's scale is missing a selector")
 | 
			
		||||
		return 0, "", nil, time.Time{}, fmt.Errorf(errMsg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selector, err := labels.Parse(scale.Status.Selector)
 | 
			
		||||
	selector, err := a.validateAndParseSelector(hpa, scale.Status.Selector)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		errMsg := fmt.Sprintf("couldn't convert selector into a corresponding internal selector object: %v", err)
 | 
			
		||||
		a.eventRecorder.Event(hpa, v1.EventTypeWarning, "InvalidSelector", errMsg)
 | 
			
		||||
		setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "InvalidSelector", errMsg)
 | 
			
		||||
		return 0, "", nil, time.Time{}, fmt.Errorf(errMsg)
 | 
			
		||||
		return 0, "", nil, time.Time{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	specReplicas := scale.Spec.Replicas
 | 
			
		||||
@@ -305,6 +316,80 @@ func (a *HorizontalController) computeReplicasForMetrics(ctx context.Context, hp
 | 
			
		||||
	return replicas, metric, statuses, timestamp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// hpasControllingPodsUnderSelector returns a list of keys of all HPAs that control a given list of pods.
 | 
			
		||||
func (a *HorizontalController) hpasControllingPodsUnderSelector(pods []*v1.Pod) []selectors.Key {
 | 
			
		||||
	a.hpaSelectorsMux.Lock()
 | 
			
		||||
	defer a.hpaSelectorsMux.Unlock()
 | 
			
		||||
 | 
			
		||||
	hpas := map[selectors.Key]struct{}{}
 | 
			
		||||
	for _, p := range pods {
 | 
			
		||||
		podKey := selectors.Key{Name: p.Name, Namespace: p.Namespace}
 | 
			
		||||
		a.hpaSelectors.Put(podKey, p.Labels)
 | 
			
		||||
 | 
			
		||||
		selectingHpas, ok := a.hpaSelectors.ReverseSelect(podKey)
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		for _, hpa := range selectingHpas {
 | 
			
		||||
			hpas[hpa] = struct{}{}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// Clean up all added pods.
 | 
			
		||||
	a.hpaSelectors.KeepOnly([]selectors.Key{})
 | 
			
		||||
 | 
			
		||||
	hpaList := []selectors.Key{}
 | 
			
		||||
	for hpa := range hpas {
 | 
			
		||||
		hpaList = append(hpaList, hpa)
 | 
			
		||||
	}
 | 
			
		||||
	return hpaList
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// validateAndParseSelector verifies that:
 | 
			
		||||
// - selector is not empty;
 | 
			
		||||
// - selector format is valid;
 | 
			
		||||
// - all pods by current selector are controlled by only one HPA.
 | 
			
		||||
// Returns an error if the check has failed or the parsed selector if succeeded.
 | 
			
		||||
// In case of an error the ScalingActive is set to false with the corresponding reason.
 | 
			
		||||
func (a *HorizontalController) validateAndParseSelector(hpa *autoscalingv2.HorizontalPodAutoscaler, selector string) (labels.Selector, error) {
 | 
			
		||||
	if selector == "" {
 | 
			
		||||
		errMsg := "selector is required"
 | 
			
		||||
		a.eventRecorder.Event(hpa, v1.EventTypeWarning, "SelectorRequired", errMsg)
 | 
			
		||||
		setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "InvalidSelector", "the HPA target's scale is missing a selector")
 | 
			
		||||
		return nil, fmt.Errorf(errMsg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parsedSelector, err := labels.Parse(selector)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		errMsg := fmt.Sprintf("couldn't convert selector into a corresponding internal selector object: %v", err)
 | 
			
		||||
		a.eventRecorder.Event(hpa, v1.EventTypeWarning, "InvalidSelector", errMsg)
 | 
			
		||||
		setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "InvalidSelector", errMsg)
 | 
			
		||||
		return nil, fmt.Errorf(errMsg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hpaKey := selectors.Key{Name: hpa.Name, Namespace: hpa.Namespace}
 | 
			
		||||
	a.hpaSelectorsMux.Lock()
 | 
			
		||||
	if a.hpaSelectors.SelectorExists(hpaKey) {
 | 
			
		||||
		// Update HPA selector only if the HPA was registered in enqueueHPA.
 | 
			
		||||
		a.hpaSelectors.PutSelector(hpaKey, parsedSelector)
 | 
			
		||||
	}
 | 
			
		||||
	a.hpaSelectorsMux.Unlock()
 | 
			
		||||
 | 
			
		||||
	pods, err := a.podLister.Pods(hpa.Namespace).List(parsedSelector)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectingHpas := a.hpasControllingPodsUnderSelector(pods)
 | 
			
		||||
	if len(selectingHpas) > 1 {
 | 
			
		||||
		errMsg := fmt.Sprintf("pods by selector %v are controlled by multiple HPAs: %v", selector, selectingHpas)
 | 
			
		||||
		a.eventRecorder.Event(hpa, v1.EventTypeWarning, "AmbiguousSelector", errMsg)
 | 
			
		||||
		setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "AmbiguousSelector", errMsg)
 | 
			
		||||
		return nil, fmt.Errorf(errMsg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return parsedSelector, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Computes the desired number of replicas for a specific hpa and metric specification,
 | 
			
		||||
// returning the metric status and a proposed condition to be set on the HPA object.
 | 
			
		||||
func (a *HorizontalController) computeReplicasForMetric(ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, spec autoscalingv2.MetricSpec,
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ import (
 | 
			
		||||
	autoscalingapiv2 "k8s.io/kubernetes/pkg/apis/autoscaling/v2"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/controller"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/controller/util/selectors"
 | 
			
		||||
	cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
 | 
			
		||||
	emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
 | 
			
		||||
	metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
 | 
			
		||||
@@ -146,6 +147,7 @@ type testCase struct {
 | 
			
		||||
	testScaleClient   *scalefake.FakeScaleClient
 | 
			
		||||
 | 
			
		||||
	recommendations []timestampedRecommendation
 | 
			
		||||
	hpaSelectors    *selectors.BiMultimap
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Needs to be called under a lock.
 | 
			
		||||
@@ -741,6 +743,9 @@ func (tc *testCase) setupController(t *testing.T) (*HorizontalController, inform
 | 
			
		||||
	if tc.recommendations != nil {
 | 
			
		||||
		hpaController.recommendations["test-namespace/test-hpa"] = tc.recommendations
 | 
			
		||||
	}
 | 
			
		||||
	if tc.hpaSelectors != nil {
 | 
			
		||||
		hpaController.hpaSelectors = tc.hpaSelectors
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return hpaController, informerFactory
 | 
			
		||||
}
 | 
			
		||||
@@ -2387,6 +2392,112 @@ func TestConditionInvalidSelectorUnparsable(t *testing.T) {
 | 
			
		||||
	tc.runTest(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConditionNoAmbiguousSelectorWhenNoSelectorOverlapBetweenHPAs(t *testing.T) {
 | 
			
		||||
	hpaSelectors := selectors.NewBiMultimap()
 | 
			
		||||
	hpaSelectors.PutSelector(selectors.Key{Name: "test-hpa-2", Namespace: testNamespace}, labels.SelectorFromSet(labels.Set{"cheddar": "cheese"}))
 | 
			
		||||
 | 
			
		||||
	tc := testCase{
 | 
			
		||||
		minReplicas:             2,
 | 
			
		||||
		maxReplicas:             6,
 | 
			
		||||
		specReplicas:            3,
 | 
			
		||||
		statusReplicas:          3,
 | 
			
		||||
		expectedDesiredReplicas: 5,
 | 
			
		||||
		CPUTarget:               30,
 | 
			
		||||
		reportedLevels:          []uint64{300, 500, 700},
 | 
			
		||||
		reportedCPURequests:     []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
 | 
			
		||||
		useMetricsAPI:           true,
 | 
			
		||||
		hpaSelectors:            hpaSelectors,
 | 
			
		||||
	}
 | 
			
		||||
	tc.runTest(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConditionAmbiguousSelectorWhenFullSelectorOverlapBetweenHPAs(t *testing.T) {
 | 
			
		||||
	hpaSelectors := selectors.NewBiMultimap()
 | 
			
		||||
	hpaSelectors.PutSelector(selectors.Key{Name: "test-hpa-2", Namespace: testNamespace}, labels.SelectorFromSet(labels.Set{"name": podNamePrefix}))
 | 
			
		||||
 | 
			
		||||
	tc := testCase{
 | 
			
		||||
		minReplicas:             2,
 | 
			
		||||
		maxReplicas:             6,
 | 
			
		||||
		specReplicas:            3,
 | 
			
		||||
		statusReplicas:          3,
 | 
			
		||||
		expectedDesiredReplicas: 3,
 | 
			
		||||
		CPUTarget:               30,
 | 
			
		||||
		reportedLevels:          []uint64{300, 500, 700},
 | 
			
		||||
		reportedCPURequests:     []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
 | 
			
		||||
		useMetricsAPI:           true,
 | 
			
		||||
		expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
 | 
			
		||||
			{
 | 
			
		||||
				Type:   autoscalingv2.AbleToScale,
 | 
			
		||||
				Status: v1.ConditionTrue,
 | 
			
		||||
				Reason: "SucceededGetScale",
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Type:   autoscalingv2.ScalingActive,
 | 
			
		||||
				Status: v1.ConditionFalse,
 | 
			
		||||
				Reason: "AmbiguousSelector",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		hpaSelectors: hpaSelectors,
 | 
			
		||||
	}
 | 
			
		||||
	tc.runTest(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConditionAmbiguousSelectorWhenPartialSelectorOverlapBetweenHPAs(t *testing.T) {
 | 
			
		||||
	hpaSelectors := selectors.NewBiMultimap()
 | 
			
		||||
	hpaSelectors.PutSelector(selectors.Key{Name: "test-hpa-2", Namespace: testNamespace}, labels.SelectorFromSet(labels.Set{"cheddar": "cheese"}))
 | 
			
		||||
 | 
			
		||||
	tc := testCase{
 | 
			
		||||
		minReplicas:             2,
 | 
			
		||||
		maxReplicas:             6,
 | 
			
		||||
		specReplicas:            3,
 | 
			
		||||
		statusReplicas:          3,
 | 
			
		||||
		expectedDesiredReplicas: 3,
 | 
			
		||||
		CPUTarget:               30,
 | 
			
		||||
		reportedLevels:          []uint64{300, 500, 700},
 | 
			
		||||
		reportedCPURequests:     []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
 | 
			
		||||
		useMetricsAPI:           true,
 | 
			
		||||
		expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
 | 
			
		||||
			{
 | 
			
		||||
				Type:   autoscalingv2.AbleToScale,
 | 
			
		||||
				Status: v1.ConditionTrue,
 | 
			
		||||
				Reason: "SucceededGetScale",
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Type:   autoscalingv2.ScalingActive,
 | 
			
		||||
				Status: v1.ConditionFalse,
 | 
			
		||||
				Reason: "AmbiguousSelector",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		hpaSelectors: hpaSelectors,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testClient, _, _, _, _ := tc.prepareTestClient(t)
 | 
			
		||||
	tc.testClient = testClient
 | 
			
		||||
 | 
			
		||||
	testClient.PrependReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
 | 
			
		||||
		tc.Lock()
 | 
			
		||||
		defer tc.Unlock()
 | 
			
		||||
 | 
			
		||||
		obj := &v1.PodList{}
 | 
			
		||||
		for i := range tc.reportedCPURequests {
 | 
			
		||||
			pod := v1.Pod{
 | 
			
		||||
				ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
					Name:      fmt.Sprintf("%s-%d", podNamePrefix, i),
 | 
			
		||||
					Namespace: testNamespace,
 | 
			
		||||
					Labels: map[string]string{
 | 
			
		||||
						"name":    podNamePrefix, // selected by the original HPA
 | 
			
		||||
						"cheddar": "cheese",      // selected by test-hpa-2
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			}
 | 
			
		||||
			obj.Items = append(obj.Items, pod)
 | 
			
		||||
		}
 | 
			
		||||
		return true, obj, nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	tc.runTest(t)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConditionFailedGetMetrics(t *testing.T) {
 | 
			
		||||
	targetValue := resource.MustParse("15.0")
 | 
			
		||||
	averageValue := resource.MustParse("15.0")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										380
									
								
								pkg/controller/util/selectors/bimultimap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										380
									
								
								pkg/controller/util/selectors/bimultimap.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,380 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2022 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package selectors
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	pkglabels "k8s.io/apimachinery/pkg/labels"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BiMultimap is an efficient, bi-directional mapping of object
 | 
			
		||||
// keys. Associations are created by putting keys with a selector.
 | 
			
		||||
type BiMultimap struct {
 | 
			
		||||
	mux sync.RWMutex
 | 
			
		||||
 | 
			
		||||
	// Objects.
 | 
			
		||||
	labeledObjects   map[Key]*labeledObject
 | 
			
		||||
	selectingObjects map[Key]*selectingObject
 | 
			
		||||
 | 
			
		||||
	// Associations.
 | 
			
		||||
	labeledBySelecting map[selectorKey]*labeledObjects
 | 
			
		||||
	selectingByLabeled map[labelsKey]*selectingObjects
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBiMultimap creates a map.
 | 
			
		||||
func NewBiMultimap() *BiMultimap {
 | 
			
		||||
	return &BiMultimap{
 | 
			
		||||
		labeledObjects:     make(map[Key]*labeledObject),
 | 
			
		||||
		selectingObjects:   make(map[Key]*selectingObject),
 | 
			
		||||
		labeledBySelecting: make(map[selectorKey]*labeledObjects),
 | 
			
		||||
		selectingByLabeled: make(map[labelsKey]*selectingObjects),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Key is a tuple of name and namespace.
 | 
			
		||||
type Key struct {
 | 
			
		||||
	Name      string
 | 
			
		||||
	Namespace string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parse turns a string in the format namespace/name into a Key.
 | 
			
		||||
func Parse(s string) (key Key) {
 | 
			
		||||
	ns := strings.SplitN(s, "/", 2)
 | 
			
		||||
	if len(ns) == 2 {
 | 
			
		||||
		key.Namespace = ns[0]
 | 
			
		||||
		key.Name = ns[1]
 | 
			
		||||
	} else {
 | 
			
		||||
		key.Name = ns[0]
 | 
			
		||||
	}
 | 
			
		||||
	return key
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (k Key) String() string {
 | 
			
		||||
	return fmt.Sprintf("%v/%v", k.Namespace, k.Name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type selectorKey struct {
 | 
			
		||||
	key       string
 | 
			
		||||
	namespace string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type selectingObject struct {
 | 
			
		||||
	key      Key
 | 
			
		||||
	selector pkglabels.Selector
 | 
			
		||||
	// selectorKey is a stable serialization of selector for
 | 
			
		||||
	// association caching.
 | 
			
		||||
	selectorKey selectorKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type selectingObjects struct {
 | 
			
		||||
	objects  map[Key]*selectingObject
 | 
			
		||||
	refCount int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type labelsKey struct {
 | 
			
		||||
	key       string
 | 
			
		||||
	namespace string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type labeledObject struct {
 | 
			
		||||
	key    Key
 | 
			
		||||
	labels map[string]string
 | 
			
		||||
	// labelsKey is a stable serialization of labels for association
 | 
			
		||||
	// caching.
 | 
			
		||||
	labelsKey labelsKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type labeledObjects struct {
 | 
			
		||||
	objects  map[Key]*labeledObject
 | 
			
		||||
	refCount int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Put inserts or updates an object and the incoming associations
 | 
			
		||||
// based on the object labels.
 | 
			
		||||
func (m *BiMultimap) Put(key Key, labels map[string]string) {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	labelsKey := labelsKey{
 | 
			
		||||
		key:       pkglabels.Set(labels).String(),
 | 
			
		||||
		namespace: key.Namespace,
 | 
			
		||||
	}
 | 
			
		||||
	if l, ok := m.labeledObjects[key]; ok {
 | 
			
		||||
		// Update labeled object.
 | 
			
		||||
		if labelsKey == l.labelsKey {
 | 
			
		||||
			// No change to labels.
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// Delete before readding.
 | 
			
		||||
		m.delete(key)
 | 
			
		||||
	}
 | 
			
		||||
	// Add labeled object.
 | 
			
		||||
	labels = copyLabels(labels)
 | 
			
		||||
	labeledObject := &labeledObject{
 | 
			
		||||
		key:       key,
 | 
			
		||||
		labels:    labels,
 | 
			
		||||
		labelsKey: labelsKey,
 | 
			
		||||
	}
 | 
			
		||||
	m.labeledObjects[key] = labeledObject
 | 
			
		||||
	// Add associations.
 | 
			
		||||
	if _, ok := m.selectingByLabeled[labelsKey]; !ok {
 | 
			
		||||
		// Cache miss. Scan selecting objects.
 | 
			
		||||
		selecting := &selectingObjects{
 | 
			
		||||
			objects: make(map[Key]*selectingObject),
 | 
			
		||||
		}
 | 
			
		||||
		set := pkglabels.Set(labels)
 | 
			
		||||
		for _, s := range m.selectingObjects {
 | 
			
		||||
			if s.key.Namespace != key.Namespace {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if s.selector.Matches(set) {
 | 
			
		||||
				selecting.objects[s.key] = s
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Associate selecting with labeled.
 | 
			
		||||
		m.selectingByLabeled[labelsKey] = selecting
 | 
			
		||||
	}
 | 
			
		||||
	selecting := m.selectingByLabeled[labelsKey]
 | 
			
		||||
	selecting.refCount += 1
 | 
			
		||||
	for _, sObject := range selecting.objects {
 | 
			
		||||
		// Associate labeled with selecting.
 | 
			
		||||
		labeled := m.labeledBySelecting[sObject.selectorKey]
 | 
			
		||||
		labeled.objects[labeledObject.key] = labeledObject
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete removes a labeled object and incoming associations.
 | 
			
		||||
func (m *BiMultimap) Delete(key Key) {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
	m.delete(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *BiMultimap) delete(key Key) {
 | 
			
		||||
	if _, ok := m.labeledObjects[key]; !ok {
 | 
			
		||||
		// Does not exist.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	labeledObject := m.labeledObjects[key]
 | 
			
		||||
	labelsKey := labeledObject.labelsKey
 | 
			
		||||
	defer delete(m.labeledObjects, key)
 | 
			
		||||
	if _, ok := m.selectingByLabeled[labelsKey]; !ok {
 | 
			
		||||
		// No associations.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// Remove associations.
 | 
			
		||||
	for _, selectingObject := range m.selectingByLabeled[labelsKey].objects {
 | 
			
		||||
		selectorKey := selectingObject.selectorKey
 | 
			
		||||
		// Delete selectingObject to labeledObject association.
 | 
			
		||||
		delete(m.labeledBySelecting[selectorKey].objects, key)
 | 
			
		||||
	}
 | 
			
		||||
	m.selectingByLabeled[labelsKey].refCount -= 1
 | 
			
		||||
	// Garbage collect labeledObject to selectingObject associations.
 | 
			
		||||
	if m.selectingByLabeled[labelsKey].refCount == 0 {
 | 
			
		||||
		delete(m.selectingByLabeled, labelsKey)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Exists returns true if the labeled object is present in the map.
 | 
			
		||||
func (m *BiMultimap) Exists(key Key) bool {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	_, exists := m.labeledObjects[key]
 | 
			
		||||
	return exists
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PutSelector inserts or updates an object with a selector. Associations
 | 
			
		||||
// are created or updated based on the selector.
 | 
			
		||||
func (m *BiMultimap) PutSelector(key Key, selector pkglabels.Selector) {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	selectorKey := selectorKey{
 | 
			
		||||
		key:       selector.String(),
 | 
			
		||||
		namespace: key.Namespace,
 | 
			
		||||
	}
 | 
			
		||||
	if s, ok := m.selectingObjects[key]; ok {
 | 
			
		||||
		// Update selecting object.
 | 
			
		||||
		if selectorKey == s.selectorKey {
 | 
			
		||||
			// No change to selector.
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		// Delete before readding.
 | 
			
		||||
		m.deleteSelector(key)
 | 
			
		||||
	}
 | 
			
		||||
	// Add selecting object.
 | 
			
		||||
	selectingObject := &selectingObject{
 | 
			
		||||
		key:         key,
 | 
			
		||||
		selector:    selector,
 | 
			
		||||
		selectorKey: selectorKey,
 | 
			
		||||
	}
 | 
			
		||||
	m.selectingObjects[key] = selectingObject
 | 
			
		||||
	// Add associations.
 | 
			
		||||
	if _, ok := m.labeledBySelecting[selectorKey]; !ok {
 | 
			
		||||
		// Cache miss. Scan labeled objects.
 | 
			
		||||
		labeled := &labeledObjects{
 | 
			
		||||
			objects: make(map[Key]*labeledObject),
 | 
			
		||||
		}
 | 
			
		||||
		for _, l := range m.labeledObjects {
 | 
			
		||||
			if l.key.Namespace != key.Namespace {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			set := pkglabels.Set(l.labels)
 | 
			
		||||
			if selector.Matches(set) {
 | 
			
		||||
				labeled.objects[l.key] = l
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		// Associate labeled with selecting.
 | 
			
		||||
		m.labeledBySelecting[selectorKey] = labeled
 | 
			
		||||
	}
 | 
			
		||||
	labeled := m.labeledBySelecting[selectorKey]
 | 
			
		||||
	labeled.refCount += 1
 | 
			
		||||
	for _, labeledObject := range labeled.objects {
 | 
			
		||||
		// Associate selecting with labeled.
 | 
			
		||||
		selecting := m.selectingByLabeled[labeledObject.labelsKey]
 | 
			
		||||
		selecting.objects[selectingObject.key] = selectingObject
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteSelector deletes a selecting object and associations created by its
 | 
			
		||||
// selector.
 | 
			
		||||
func (m *BiMultimap) DeleteSelector(key Key) {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
	m.deleteSelector(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *BiMultimap) deleteSelector(key Key) {
 | 
			
		||||
	if _, ok := m.selectingObjects[key]; !ok {
 | 
			
		||||
		// Does not exist.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	selectingObject := m.selectingObjects[key]
 | 
			
		||||
	selectorKey := selectingObject.selectorKey
 | 
			
		||||
	defer delete(m.selectingObjects, key)
 | 
			
		||||
	if _, ok := m.labeledBySelecting[selectorKey]; !ok {
 | 
			
		||||
		// No associations.
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// Remove associations.
 | 
			
		||||
	for _, labeledObject := range m.labeledBySelecting[selectorKey].objects {
 | 
			
		||||
		labelsKey := labeledObject.labelsKey
 | 
			
		||||
		// Delete labeledObject to selectingObject association.
 | 
			
		||||
		delete(m.selectingByLabeled[labelsKey].objects, key)
 | 
			
		||||
	}
 | 
			
		||||
	m.labeledBySelecting[selectorKey].refCount -= 1
 | 
			
		||||
	// Garbage collect selectingObjects to labeledObject associations.
 | 
			
		||||
	if m.labeledBySelecting[selectorKey].refCount == 0 {
 | 
			
		||||
		delete(m.labeledBySelecting, selectorKey)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SelectorExists returns true if the selecting object is present in the map.
 | 
			
		||||
func (m *BiMultimap) SelectorExists(key Key) bool {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	_, exists := m.selectingObjects[key]
 | 
			
		||||
	return exists
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// KeepOnly retains only the specified labeled objects and deletes the
 | 
			
		||||
// rest. Like calling Delete for all keys not specified.
 | 
			
		||||
func (m *BiMultimap) KeepOnly(keys []Key) {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	keyMap := make(map[Key]bool)
 | 
			
		||||
	for _, k := range keys {
 | 
			
		||||
		keyMap[k] = true
 | 
			
		||||
	}
 | 
			
		||||
	for k := range m.labeledObjects {
 | 
			
		||||
		if !keyMap[k] {
 | 
			
		||||
			m.delete(k)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// KeepOnlySelectors retains only the specified selecting objects and
 | 
			
		||||
// deletes the rest. Like calling DeleteSelector for all keys not
 | 
			
		||||
// specified.
 | 
			
		||||
func (m *BiMultimap) KeepOnlySelectors(keys []Key) {
 | 
			
		||||
	m.mux.Lock()
 | 
			
		||||
	defer m.mux.Unlock()
 | 
			
		||||
 | 
			
		||||
	keyMap := make(map[Key]bool)
 | 
			
		||||
	for _, k := range keys {
 | 
			
		||||
		keyMap[k] = true
 | 
			
		||||
	}
 | 
			
		||||
	for k := range m.selectingObjects {
 | 
			
		||||
		if !keyMap[k] {
 | 
			
		||||
			m.deleteSelector(k)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Select finds objects associated with a selecting object. If the
 | 
			
		||||
// given key was found in the map `ok` will be true. Otherwise false.
 | 
			
		||||
func (m *BiMultimap) Select(key Key) (keys []Key, ok bool) {
 | 
			
		||||
	m.mux.RLock()
 | 
			
		||||
	defer m.mux.RUnlock()
 | 
			
		||||
 | 
			
		||||
	selectingObject, ok := m.selectingObjects[key]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		// Does not exist.
 | 
			
		||||
		return nil, false
 | 
			
		||||
	}
 | 
			
		||||
	keys = make([]Key, 0)
 | 
			
		||||
	if labeled, ok := m.labeledBySelecting[selectingObject.selectorKey]; ok {
 | 
			
		||||
		for _, labeledObject := range labeled.objects {
 | 
			
		||||
			keys = append(keys, labeledObject.key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return keys, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReverseSelect finds objects selecting the given object. If the
 | 
			
		||||
// given key was found in the map `ok` will be true. Otherwise false.
 | 
			
		||||
func (m *BiMultimap) ReverseSelect(key Key) (keys []Key, ok bool) {
 | 
			
		||||
	m.mux.RLock()
 | 
			
		||||
	defer m.mux.RUnlock()
 | 
			
		||||
 | 
			
		||||
	labeledObject, ok := m.labeledObjects[key]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		// Does not exist.
 | 
			
		||||
		return []Key{}, false
 | 
			
		||||
	}
 | 
			
		||||
	keys = make([]Key, 0)
 | 
			
		||||
	if selecting, ok := m.selectingByLabeled[labeledObject.labelsKey]; ok {
 | 
			
		||||
		for _, selectingObject := range selecting.objects {
 | 
			
		||||
			keys = append(keys, selectingObject.key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return keys, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyLabels(labels map[string]string) map[string]string {
 | 
			
		||||
	l := make(map[string]string)
 | 
			
		||||
	for k, v := range labels {
 | 
			
		||||
		l[k] = v
 | 
			
		||||
	}
 | 
			
		||||
	return l
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										641
									
								
								pkg/controller/util/selectors/bimultimap_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										641
									
								
								pkg/controller/util/selectors/bimultimap_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,641 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2022 The Kubernetes Authors.
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package selectors
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	pkglabels "k8s.io/apimachinery/pkg/labels"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/selection"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAssociations(t *testing.T) {
 | 
			
		||||
	cases := []struct {
 | 
			
		||||
		name                string
 | 
			
		||||
		ops                 []operation
 | 
			
		||||
		want                []expectation
 | 
			
		||||
		testAllPermutations bool
 | 
			
		||||
	}{{
 | 
			
		||||
		name: "single association",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod"), labels("a", "1")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa"), key("pod")),
 | 
			
		||||
			reverseSelect(key("pod"), key("hpa")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "multiple associations from a selecting object",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-2"), labels("a", "1")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa"), key("pod-1"), key("pod-2")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa")),
 | 
			
		||||
			reverseSelect(key("pod-2"), key("hpa")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "multiple associations to a labeled object",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-2"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod"), labels("a", "1")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod")),
 | 
			
		||||
			forwardSelect(key("hpa-2"), key("pod")),
 | 
			
		||||
			reverseSelect(key("pod"), key("hpa-1"), key("hpa-2")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "disjoint association sets",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-2"), selector("a", "2")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-2"), labels("a", "2")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod-1")),
 | 
			
		||||
			forwardSelect(key("hpa-2"), key("pod-2")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa-1")),
 | 
			
		||||
			reverseSelect(key("pod-2"), key("hpa-2")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "separate label cache paths",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1", "b", "2")),
 | 
			
		||||
			putLabeledObject(key("pod-2"), labels("a", "1", "b", "3")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa"), key("pod-1"), key("pod-2")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa")),
 | 
			
		||||
			reverseSelect(key("pod-2"), key("hpa")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "separate selector cache paths",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-2"), selector("b", "2")),
 | 
			
		||||
			putLabeledObject(key("pod"), labels("a", "1", "b", "2")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod")),
 | 
			
		||||
			forwardSelect(key("hpa-2"), key("pod")),
 | 
			
		||||
			reverseSelect(key("pod"), key("hpa-1"), key("hpa-2")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "selection in different namespaces",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putLabeledObject(key("pod-1", "namespace-1"), labels("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1", "namespace-2"), labels("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-1", "namespace-2"), selector("a", "1")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1", "namespace-1")), // selects nothing
 | 
			
		||||
			forwardSelect(key("hpa-1", "namespace-2"), key("pod-1", "namespace-2")),
 | 
			
		||||
			reverseSelect(key("pod-1", "namespace-1")), // selects nothing
 | 
			
		||||
			reverseSelect(key("pod-1", "namespace-2"), key("hpa-1", "namespace-2")),
 | 
			
		||||
		},
 | 
			
		||||
		testAllPermutations: true,
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "update labeled objects",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "2")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "2")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod-1")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa-1")),
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "update selecting objects",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "2")),
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "2")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod-1")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa-1")),
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "keep only labeled objects",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-2"), labels("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-3"), labels("a", "1")),
 | 
			
		||||
			keepOnly(key("pod-1"), key("pod-2")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod-1"), key("pod-2")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa-1")),
 | 
			
		||||
			reverseSelect(key("pod-2"), key("hpa-1")),
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "keep only selecting objects",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-2"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-3"), selector("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1")),
 | 
			
		||||
			keepOnlySelectors(key("hpa-1"), key("hpa-2")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			forwardSelect(key("hpa-1"), key("pod-1")),
 | 
			
		||||
			forwardSelect(key("hpa-2"), key("pod-1")),
 | 
			
		||||
			reverseSelect(key("pod-1"), key("hpa-1"), key("hpa-2")),
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "put multiple associations and delete all",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			putSelectingObject(key("hpa-1"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-2"), selector("a", "1")),
 | 
			
		||||
			putSelectingObject(key("hpa-3"), selector("a", "2")),
 | 
			
		||||
			putSelectingObject(key("hpa-4"), selector("b", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-1"), labels("a", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-2"), labels("a", "2")),
 | 
			
		||||
			putLabeledObject(key("pod-3"), labels("a", "1", "b", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-4"), labels("a", "2", "b", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-5"), labels("b", "1")),
 | 
			
		||||
			putLabeledObject(key("pod-6"), labels("b", "2")),
 | 
			
		||||
			deleteSelecting(key("hpa-1")),
 | 
			
		||||
			deleteSelecting(key("hpa-2")),
 | 
			
		||||
			deleteSelecting(key("hpa-3")),
 | 
			
		||||
			deleteSelecting(key("hpa-4")),
 | 
			
		||||
			deleteLabeled(key("pod-1")),
 | 
			
		||||
			deleteLabeled(key("pod-2")),
 | 
			
		||||
			deleteLabeled(key("pod-3")),
 | 
			
		||||
			deleteLabeled(key("pod-4")),
 | 
			
		||||
			deleteLabeled(key("pod-5")),
 | 
			
		||||
			deleteLabeled(key("pod-6")),
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			emptyMap,
 | 
			
		||||
		},
 | 
			
		||||
	}, {
 | 
			
		||||
		name: "fuzz testing",
 | 
			
		||||
		ops: []operation{
 | 
			
		||||
			randomOperations(10000),
 | 
			
		||||
			deleteAll,
 | 
			
		||||
		},
 | 
			
		||||
		want: []expectation{
 | 
			
		||||
			emptyMap,
 | 
			
		||||
		},
 | 
			
		||||
	}}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		var permutations [][]int
 | 
			
		||||
		if tc.testAllPermutations {
 | 
			
		||||
			// Run test case with all permutations of operations.
 | 
			
		||||
			permutations = indexPermutations(len(tc.ops))
 | 
			
		||||
		} else {
 | 
			
		||||
			// Unless test is order dependent (e.g. includes
 | 
			
		||||
			// deletes) or just too big.
 | 
			
		||||
			var p []int
 | 
			
		||||
			for i := 0; i < len(tc.ops); i++ {
 | 
			
		||||
				p = append(p, i)
 | 
			
		||||
			}
 | 
			
		||||
			permutations = [][]int{p}
 | 
			
		||||
		}
 | 
			
		||||
		for _, permutation := range permutations {
 | 
			
		||||
			name := tc.name + fmt.Sprintf(" permutation %v", permutation)
 | 
			
		||||
			t.Run(name, func(t *testing.T) {
 | 
			
		||||
				multimap := NewBiMultimap()
 | 
			
		||||
				for i := range permutation {
 | 
			
		||||
					tc.ops[i](multimap)
 | 
			
		||||
					// Run consistency check after every operation.
 | 
			
		||||
					err := consistencyCheck(multimap)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						t.Fatalf(err.Error())
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				for _, expect := range tc.want {
 | 
			
		||||
					err := expect(multimap)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						t.Errorf("%v %v", tc.name, err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestEfficientAssociation(t *testing.T) {
 | 
			
		||||
	useOnceSelector := useOnce(selector("a", "1"))
 | 
			
		||||
	m := NewBiMultimap()
 | 
			
		||||
	m.PutSelector(key("hpa-1"), useOnceSelector)
 | 
			
		||||
	m.Put(key("pod-1"), labels("a", "1"))
 | 
			
		||||
 | 
			
		||||
	// Selector is used only during full scan. Second Put will use
 | 
			
		||||
	// cached association or explode.
 | 
			
		||||
	m.Put(key("pod-2"), labels("a", "1"))
 | 
			
		||||
 | 
			
		||||
	err := forwardSelect(key("hpa-1"), key("pod-1"), key("pod-2"))(m)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUseOnceSelector(t *testing.T) {
 | 
			
		||||
	useOnceSelector := useOnce(selector("a", "1"))
 | 
			
		||||
	labels := pkglabels.Set(labels("a", "1"))
 | 
			
		||||
 | 
			
		||||
	// Use once.
 | 
			
		||||
	useOnceSelector.Matches(labels)
 | 
			
		||||
	// Use twice.
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r == nil {
 | 
			
		||||
			t.Errorf("Expected panic when using selector twice.")
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	useOnceSelector.Matches(labels)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestObjectsExist(t *testing.T) {
 | 
			
		||||
	m := NewBiMultimap()
 | 
			
		||||
 | 
			
		||||
	// Nothing exists in the empty map.
 | 
			
		||||
	assert.False(t, m.Exists(key("pod-1")))
 | 
			
		||||
	assert.False(t, m.SelectorExists(key("hpa-1")))
 | 
			
		||||
 | 
			
		||||
	// Adding entries.
 | 
			
		||||
	m.PutSelector(key("hpa-1"), useOnce(selector("a", "1")))
 | 
			
		||||
	m.Put(key("pod-1"), labels("a", "1"))
 | 
			
		||||
 | 
			
		||||
	// Entries exist.
 | 
			
		||||
	assert.True(t, m.Exists(key("pod-1")))
 | 
			
		||||
	assert.True(t, m.SelectorExists(key("hpa-1")))
 | 
			
		||||
 | 
			
		||||
	// Removing the entries.
 | 
			
		||||
	m.DeleteSelector(key("hpa-1"))
 | 
			
		||||
	m.Delete(key("pod-1"))
 | 
			
		||||
 | 
			
		||||
	// They don't exist anymore.
 | 
			
		||||
	assert.False(t, m.Exists(key("pod-1")))
 | 
			
		||||
	assert.False(t, m.SelectorExists(key("hpa-1")))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type useOnceSelector struct {
 | 
			
		||||
	used     bool
 | 
			
		||||
	selector pkglabels.Selector
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func useOnce(s pkglabels.Selector) pkglabels.Selector {
 | 
			
		||||
	return &useOnceSelector{
 | 
			
		||||
		selector: s,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) Matches(l pkglabels.Labels) bool {
 | 
			
		||||
	if u.used {
 | 
			
		||||
		panic("useOnceSelector used more than once")
 | 
			
		||||
	}
 | 
			
		||||
	u.used = true
 | 
			
		||||
	return u.selector.Matches(l)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) Empty() bool {
 | 
			
		||||
	return u.selector.Empty()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) String() string {
 | 
			
		||||
	return u.selector.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) Add(r ...pkglabels.Requirement) pkglabels.Selector {
 | 
			
		||||
	u.selector = u.selector.Add(r...)
 | 
			
		||||
	return u
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) Requirements() (pkglabels.Requirements, bool) {
 | 
			
		||||
	return u.selector.Requirements()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) DeepCopySelector() pkglabels.Selector {
 | 
			
		||||
	u.selector = u.selector.DeepCopySelector()
 | 
			
		||||
	return u
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *useOnceSelector) RequiresExactMatch(label string) (value string, found bool) {
 | 
			
		||||
	v, f := u.selector.RequiresExactMatch(label)
 | 
			
		||||
	return v, f
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func indexPermutations(size int) [][]int {
 | 
			
		||||
	var permute func([]int, []int) [][]int
 | 
			
		||||
	permute = func(placed, remaining []int) (permutations [][]int) {
 | 
			
		||||
		if len(remaining) == 0 {
 | 
			
		||||
			return [][]int{placed}
 | 
			
		||||
		}
 | 
			
		||||
		for i, v := range remaining {
 | 
			
		||||
			r := append([]int(nil), remaining...) // copy remaining
 | 
			
		||||
			r = append(r[:i], r[i+1:]...)         // delete placed index
 | 
			
		||||
			p := permute(append(placed, v), r)    // place index and permute
 | 
			
		||||
			permutations = append(permutations, p...)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var remaining []int
 | 
			
		||||
	for i := 0; i < size; i++ {
 | 
			
		||||
		remaining = append(remaining, i)
 | 
			
		||||
	}
 | 
			
		||||
	return permute(nil, remaining)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type operation func(*BiMultimap)
 | 
			
		||||
 | 
			
		||||
func putLabeledObject(key Key, labels map[string]string) operation {
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		m.Put(key, labels)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func putSelectingObject(key Key, selector pkglabels.Selector) operation {
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		m.PutSelector(key, selector)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteLabeled(key Key) operation {
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		m.Delete(key)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteSelecting(key Key) operation {
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		m.DeleteSelector(key)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteAll(m *BiMultimap) {
 | 
			
		||||
	for key := range m.labeledObjects {
 | 
			
		||||
		m.Delete(key)
 | 
			
		||||
	}
 | 
			
		||||
	for key := range m.selectingObjects {
 | 
			
		||||
		m.DeleteSelector(key)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func keepOnly(keys ...Key) operation {
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		m.KeepOnly(keys)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func keepOnlySelectors(keys ...Key) operation {
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		m.KeepOnlySelectors(keys)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func randomOperations(times int) operation {
 | 
			
		||||
	pods := []Key{
 | 
			
		||||
		key("pod-1"),
 | 
			
		||||
		key("pod-2"),
 | 
			
		||||
		key("pod-3"),
 | 
			
		||||
		key("pod-4"),
 | 
			
		||||
		key("pod-5"),
 | 
			
		||||
		key("pod-6"),
 | 
			
		||||
	}
 | 
			
		||||
	randomPod := func() Key {
 | 
			
		||||
		return pods[rand.Intn(len(pods))]
 | 
			
		||||
	}
 | 
			
		||||
	labels := []map[string]string{
 | 
			
		||||
		labels("a", "1"),
 | 
			
		||||
		labels("a", "2"),
 | 
			
		||||
		labels("b", "1"),
 | 
			
		||||
		labels("b", "2"),
 | 
			
		||||
		labels("a", "1", "b", "1"),
 | 
			
		||||
		labels("a", "2", "b", "2"),
 | 
			
		||||
		labels("a", "3"),
 | 
			
		||||
		labels("c", "1"),
 | 
			
		||||
	}
 | 
			
		||||
	randomLabels := func() map[string]string {
 | 
			
		||||
		return labels[rand.Intn(len(labels))]
 | 
			
		||||
	}
 | 
			
		||||
	hpas := []Key{
 | 
			
		||||
		key("hpa-1"),
 | 
			
		||||
		key("hpa-2"),
 | 
			
		||||
		key("hpa-3"),
 | 
			
		||||
	}
 | 
			
		||||
	randomHpa := func() Key {
 | 
			
		||||
		return hpas[rand.Intn(len(hpas))]
 | 
			
		||||
	}
 | 
			
		||||
	selectors := []pkglabels.Selector{
 | 
			
		||||
		selector("a", "1"),
 | 
			
		||||
		selector("b", "1"),
 | 
			
		||||
		selector("a", "1", "b", "1"),
 | 
			
		||||
		selector("c", "2"),
 | 
			
		||||
	}
 | 
			
		||||
	randomSelector := func() pkglabels.Selector {
 | 
			
		||||
		return selectors[rand.Intn(len(selectors))]
 | 
			
		||||
	}
 | 
			
		||||
	randomOp := func(m *BiMultimap) {
 | 
			
		||||
		switch rand.Intn(4) {
 | 
			
		||||
		case 0:
 | 
			
		||||
			m.Put(randomPod(), randomLabels())
 | 
			
		||||
		case 1:
 | 
			
		||||
			m.PutSelector(randomHpa(), randomSelector())
 | 
			
		||||
		case 2:
 | 
			
		||||
			m.Delete(randomPod())
 | 
			
		||||
		case 3:
 | 
			
		||||
			m.DeleteSelector(randomHpa())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return func(m *BiMultimap) {
 | 
			
		||||
		for i := 0; i < times; i++ {
 | 
			
		||||
			randomOp(m)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type expectation func(*BiMultimap) error
 | 
			
		||||
 | 
			
		||||
func forwardSelect(key Key, want ...Key) expectation {
 | 
			
		||||
	return func(m *BiMultimap) error {
 | 
			
		||||
		got, _ := m.Select(key)
 | 
			
		||||
		if !unorderedEqual(want, got) {
 | 
			
		||||
			return fmt.Errorf("forward select %v wanted %v. got %v.", key, want, got)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func reverseSelect(key Key, want ...Key) expectation {
 | 
			
		||||
	return func(m *BiMultimap) error {
 | 
			
		||||
		got, _ := m.ReverseSelect(key)
 | 
			
		||||
		if !unorderedEqual(want, got) {
 | 
			
		||||
			return fmt.Errorf("reverse select %v wanted %v. got %v.", key, want, got)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func emptyMap(m *BiMultimap) error {
 | 
			
		||||
	if len(m.labeledObjects) != 0 {
 | 
			
		||||
		return fmt.Errorf("Found %v labeledObjects. Wanted none.", len(m.labeledObjects))
 | 
			
		||||
	}
 | 
			
		||||
	if len(m.selectingObjects) != 0 {
 | 
			
		||||
		return fmt.Errorf("Found %v selectingObjects. Wanted none.", len(m.selectingObjects))
 | 
			
		||||
	}
 | 
			
		||||
	if len(m.labeledBySelecting) != 0 {
 | 
			
		||||
		return fmt.Errorf("Found %v cached labeledBySelecting associations. Wanted none.", len(m.labeledBySelecting))
 | 
			
		||||
	}
 | 
			
		||||
	if len(m.selectingByLabeled) != 0 {
 | 
			
		||||
		return fmt.Errorf("Found %v cached selectingByLabeled associations. Wanted none.", len(m.selectingByLabeled))
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func consistencyCheck(m *BiMultimap) error {
 | 
			
		||||
	emptyKey := Key{}
 | 
			
		||||
	emptyLabelsKey := labelsKey{}
 | 
			
		||||
	emptySelectorKey := selectorKey{}
 | 
			
		||||
	for k, v := range m.labeledObjects {
 | 
			
		||||
		if v == nil {
 | 
			
		||||
			return fmt.Errorf("Found nil labeled object for key %q", k)
 | 
			
		||||
		}
 | 
			
		||||
		if k == emptyKey {
 | 
			
		||||
			return fmt.Errorf("Found empty key for labeled object %+v", v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for k, v := range m.selectingObjects {
 | 
			
		||||
		if v == nil {
 | 
			
		||||
			return fmt.Errorf("Found nil selecting object for key %q", k)
 | 
			
		||||
		}
 | 
			
		||||
		if k == emptyKey {
 | 
			
		||||
			return fmt.Errorf("Found empty key for selecting object %+v", v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for k, v := range m.labeledBySelecting {
 | 
			
		||||
		if v == nil {
 | 
			
		||||
			return fmt.Errorf("Found nil labeledBySelecting entry for key %q", k)
 | 
			
		||||
		}
 | 
			
		||||
		if k == emptySelectorKey {
 | 
			
		||||
			return fmt.Errorf("Found empty key for labeledBySelecting object %+v", v)
 | 
			
		||||
		}
 | 
			
		||||
		for k2, v2 := range v.objects {
 | 
			
		||||
			if v2 == nil {
 | 
			
		||||
				return fmt.Errorf("Found nil object in labeledBySelecting under keys %q and %q", k, k2)
 | 
			
		||||
			}
 | 
			
		||||
			if k2 == emptyKey {
 | 
			
		||||
				return fmt.Errorf("Found empty key for object in labeledBySelecting under key %+v", k)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if v.refCount < 1 {
 | 
			
		||||
			return fmt.Errorf("Found labeledBySelecting entry with no references (orphaned) under key %q", k)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for k, v := range m.selectingByLabeled {
 | 
			
		||||
		if v == nil {
 | 
			
		||||
			return fmt.Errorf("Found nil selectingByLabeled entry for key %q", k)
 | 
			
		||||
		}
 | 
			
		||||
		if k == emptyLabelsKey {
 | 
			
		||||
			return fmt.Errorf("Found empty key for selectingByLabeled object %+v", v)
 | 
			
		||||
		}
 | 
			
		||||
		for k2, v2 := range v.objects {
 | 
			
		||||
			if v2 == nil {
 | 
			
		||||
				return fmt.Errorf("Found nil object in selectingByLabeled under keys %q and %q", k, k2)
 | 
			
		||||
			}
 | 
			
		||||
			if k2 == emptyKey {
 | 
			
		||||
				return fmt.Errorf("Found empty key for object in selectingByLabeled under key %+v", k)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if v.refCount < 1 {
 | 
			
		||||
			return fmt.Errorf("Found selectingByLabeled entry with no references (orphaned) under key %q", k)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func key(s string, ss ...string) Key {
 | 
			
		||||
	if len(ss) > 1 {
 | 
			
		||||
		panic("Key requires 1 or 2 parts.")
 | 
			
		||||
	}
 | 
			
		||||
	k := Key{
 | 
			
		||||
		Name: s,
 | 
			
		||||
	}
 | 
			
		||||
	if len(ss) >= 1 {
 | 
			
		||||
		k.Namespace = ss[0]
 | 
			
		||||
	}
 | 
			
		||||
	return k
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func labels(ls ...string) map[string]string {
 | 
			
		||||
	if len(ls)%2 != 0 {
 | 
			
		||||
		panic("labels requires pairs of strings.")
 | 
			
		||||
	}
 | 
			
		||||
	ss := make(map[string]string)
 | 
			
		||||
	for i := 0; i < len(ls); i += 2 {
 | 
			
		||||
		ss[ls[i]] = ls[i+1]
 | 
			
		||||
	}
 | 
			
		||||
	return ss
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func selector(ss ...string) pkglabels.Selector {
 | 
			
		||||
	if len(ss)%2 != 0 {
 | 
			
		||||
		panic("selector requires pairs of strings.")
 | 
			
		||||
	}
 | 
			
		||||
	s := pkglabels.NewSelector()
 | 
			
		||||
	for i := 0; i < len(ss); i += 2 {
 | 
			
		||||
		r, err := pkglabels.NewRequirement(ss[i], selection.Equals, []string{ss[i+1]})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
		s = s.Add(*r)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func unorderedEqual(as, bs []Key) bool {
 | 
			
		||||
	if len(as) != len(bs) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	aMap := make(map[Key]int)
 | 
			
		||||
	for _, a := range as {
 | 
			
		||||
		aMap[a] += 1
 | 
			
		||||
	}
 | 
			
		||||
	bMap := make(map[Key]int)
 | 
			
		||||
	for _, b := range bs {
 | 
			
		||||
		bMap[b] += 1
 | 
			
		||||
	}
 | 
			
		||||
	if len(aMap) != len(bMap) {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	for a, count := range aMap {
 | 
			
		||||
		if bMap[a] != count {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user