mirror of
https://github.com/rancher/steve.git
synced 2025-09-04 08:55:55 +00:00
support unwatchables in vai (#458)
* Create and use a synthetic watcher for non-watchable resources. * Write unit tests for the synthetic watcher. * Make the refresh interval for synthetic watchers configurable. The default is to call `client.List(...)` every 5 seconds for each unwatchable GVK. There are currently only 3 such GVKs right now so this will be informative enough but not really noticeable. * Pass the context into the synthetic watch func. * Restore changes lost in rebasing. --------- Co-authored-by: Tom Lebreux <tom.lebreux@suse.com>
This commit is contained in:
190
pkg/sqlcache/informer/synthetic_watcher_test.go
Normal file
190
pkg/sqlcache/informer/synthetic_watcher_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user