mirror of
https://github.com/rancher/steve.git
synced 2025-07-01 09:12:12 +00:00
191 lines
5.6 KiB
Go
191 lines
5.6 KiB
Go
|
/*
|
||
|
Copyright 2024 SUSE LLC
|
||
|
*/
|
||
|
|
||
|
package informer
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/stretchr/testify/assert"
|
||
|
"go.uber.org/mock/gomock"
|
||
|
|
||
|
v1 "k8s.io/api/core/v1"
|
||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||
|
"k8s.io/apimachinery/pkg/runtime"
|
||
|
"k8s.io/apimachinery/pkg/watch"
|
||
|
)
|
||
|
|
||
|
func TestSyntheticWatcher(t *testing.T) {
|
||
|
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
|
||
|
var err error
|
||
|
cs1 := v1.ComponentStatus{
|
||
|
TypeMeta: metav1.TypeMeta{
|
||
|
APIVersion: "v1",
|
||
|
Kind: "ComponentStatus",
|
||
|
},
|
||
|
ObjectMeta: metav1.ObjectMeta{
|
||
|
Name: "cs1",
|
||
|
UID: "1",
|
||
|
ResourceVersion: "rv1.1",
|
||
|
},
|
||
|
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs1"}},
|
||
|
}
|
||
|
cs2 := v1.ComponentStatus{
|
||
|
TypeMeta: metav1.TypeMeta{
|
||
|
APIVersion: "v1",
|
||
|
Kind: "ComponentStatus",
|
||
|
},
|
||
|
ObjectMeta: metav1.ObjectMeta{
|
||
|
Name: "cs2",
|
||
|
UID: "2",
|
||
|
ResourceVersion: "rv2.1",
|
||
|
},
|
||
|
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs2"}},
|
||
|
}
|
||
|
cs3 := v1.ComponentStatus{
|
||
|
TypeMeta: metav1.TypeMeta{
|
||
|
APIVersion: "v1",
|
||
|
Kind: "ComponentStatus",
|
||
|
},
|
||
|
ObjectMeta: metav1.ObjectMeta{
|
||
|
Name: "cs3",
|
||
|
UID: "3",
|
||
|
ResourceVersion: "rv3.1",
|
||
|
},
|
||
|
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs3"}},
|
||
|
}
|
||
|
cs4 := v1.ComponentStatus{
|
||
|
TypeMeta: metav1.TypeMeta{
|
||
|
APIVersion: "v1",
|
||
|
Kind: "ComponentStatus",
|
||
|
},
|
||
|
ObjectMeta: metav1.ObjectMeta{
|
||
|
Name: "cs4",
|
||
|
UID: "4",
|
||
|
ResourceVersion: "rv4.1",
|
||
|
},
|
||
|
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs4"}},
|
||
|
}
|
||
|
list, err := makeCSList(cs1, cs2, cs3, cs4)
|
||
|
assert.Nil(t, err)
|
||
|
dynamicClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(list, nil)
|
||
|
// Make copies to avoid modifying objects before the watcher has processed them.
|
||
|
cs1b := cs1.DeepCopy()
|
||
|
cs1b.ObjectMeta.ResourceVersion = "rv1.2"
|
||
|
cs2b := cs2.DeepCopy()
|
||
|
cs2b.ObjectMeta.ResourceVersion = "rv2.2"
|
||
|
list2, err := makeCSList(*cs1b, *cs2b, cs4)
|
||
|
assert.Nil(t, err)
|
||
|
dynamicClient.EXPECT().List(gomock.Any(), gomock.Any()).AnyTimes().Return(list2, nil)
|
||
|
|
||
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
sw := newSyntheticWatcher(ctx, cancel)
|
||
|
pollingInterval := 10 * time.Millisecond
|
||
|
watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
|
||
|
return sw.watch(dynamicClient, options, pollingInterval)
|
||
|
}
|
||
|
options := metav1.ListOptions{}
|
||
|
w, err := watchFunc(options)
|
||
|
assert.Nil(t, err)
|
||
|
errChan := make(chan error)
|
||
|
results := make([]processedObjectInfo, 0)
|
||
|
var wg sync.WaitGroup
|
||
|
wg.Add(2)
|
||
|
go func() {
|
||
|
results, err = handleAnyWatch(w, errChan, sw.stopChan)
|
||
|
wg.Done()
|
||
|
}()
|
||
|
|
||
|
go func() {
|
||
|
time.Sleep(40 * time.Millisecond)
|
||
|
sw.stopChan <- struct{}{}
|
||
|
wg.Done()
|
||
|
}()
|
||
|
wg.Wait()
|
||
|
// Verify we get what we expected to see
|
||
|
assert.Len(t, results, 8)
|
||
|
for i, _ := range list.Items {
|
||
|
assert.Equal(t, "added-result", results[i].eventName)
|
||
|
}
|
||
|
assert.Equal(t, "modified-result", results[len(list.Items)].eventName)
|
||
|
assert.Equal(t, "modified-result", results[len(list.Items)+1].eventName)
|
||
|
assert.Equal(t, "deleted-result", results[len(list.Items)+2].eventName)
|
||
|
assert.Equal(t, "stop", results[7].eventName)
|
||
|
// We can't really assert that the events get the correct timestamps on them
|
||
|
// because they can be held up in the channel for unexpected durations. I did have
|
||
|
// assert.Greater(t, float64(timeDelta), 0.9*float64(pollingInterval))
|
||
|
// but saw a failure -- the interval was actually 0.75 * pollingInterval.
|
||
|
// So there's no point testing that.
|
||
|
assert.Greater(t, results[4].createdAt, results[0].createdAt)
|
||
|
}
|
||
|
|
||
|
func makeCSList(objs ...v1.ComponentStatus) (*unstructured.UnstructuredList, error) {
|
||
|
unList := make([]unstructured.Unstructured, len(objs))
|
||
|
for i, cs := range objs {
|
||
|
unst, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cs)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
unList[i] = unstructured.Unstructured{Object: unst}
|
||
|
}
|
||
|
list := &unstructured.UnstructuredList{
|
||
|
Items: unList,
|
||
|
}
|
||
|
return list, nil
|
||
|
}
|
||
|
|
||
|
type processedObjectInfo struct {
|
||
|
createdAt time.Time
|
||
|
eventName string
|
||
|
payload interface{}
|
||
|
}
|
||
|
|
||
|
func makeProcessedObject(eventName string, payload interface{}) processedObjectInfo {
|
||
|
return processedObjectInfo{
|
||
|
createdAt: time.Now(),
|
||
|
eventName: eventName,
|
||
|
payload: payload,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func handleAnyWatch(w watch.Interface,
|
||
|
errCh chan error,
|
||
|
stopCh <-chan struct{},
|
||
|
) ([]processedObjectInfo, error) {
|
||
|
results := make([]processedObjectInfo, 0)
|
||
|
loop:
|
||
|
for {
|
||
|
select {
|
||
|
case <-stopCh:
|
||
|
results = append(results, makeProcessedObject("stop", nil))
|
||
|
return results, nil
|
||
|
case err := <-errCh:
|
||
|
results = append(results, makeProcessedObject("error", err))
|
||
|
return results, err
|
||
|
case event, ok := <-w.ResultChan():
|
||
|
if !ok {
|
||
|
results = append(results, makeProcessedObject("bad-result", nil))
|
||
|
break loop
|
||
|
}
|
||
|
switch event.Type {
|
||
|
case watch.Added:
|
||
|
results = append(results, makeProcessedObject("added-result", &event))
|
||
|
case watch.Modified:
|
||
|
results = append(results, makeProcessedObject("modified-result", &event))
|
||
|
case watch.Deleted:
|
||
|
results = append(results, makeProcessedObject("deleted-result", &event))
|
||
|
case watch.Bookmark:
|
||
|
results = append(results, makeProcessedObject("bookmark-result", &event))
|
||
|
default:
|
||
|
results = append(results, makeProcessedObject("unexpected-result", &event))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return results, nil
|
||
|
}
|