mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Merge pull request #112011 from pbeschetnov/ambiguous-selectors
Add ambiguous selector check to HPA
This commit is contained in:
commit
c519bc02e8
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user