mirror of https://github.com/knative/pkg.git
Add logic to convert the watch.Interface. (#106)
The watch logic returns events containing `unstructured.Unstructured`. This adds a proxy that converts those to the appropriate structured type.
This commit is contained in:
parent
bb5b93d475
commit
fd1e3cc5c9
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 The Knative 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 duck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewProxyWatcher is based on the same concept from Kubernetes apimachinery in 1.12 here:
|
||||||
|
// https://github.com/kubernetes/apimachinery/blob/c6dd271be/pkg/watch/watch.go#L272
|
||||||
|
// Replace this copy once we've update our client libraries.
|
||||||
|
|
||||||
|
// proxyWatcher lets you wrap your channel in watch.Interface. Threadsafe.
|
||||||
|
type proxyWatcher struct {
|
||||||
|
result chan watch.Event
|
||||||
|
stopCh chan struct{}
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
stopped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ watch.Interface = (*proxyWatcher)(nil)
|
||||||
|
|
||||||
|
// NewProxyWatcher creates new proxyWatcher by wrapping a channel
|
||||||
|
func NewProxyWatcher(ch chan watch.Event) watch.Interface {
|
||||||
|
return &proxyWatcher{
|
||||||
|
result: ch,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
stopped: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop implements Interface
|
||||||
|
func (pw *proxyWatcher) Stop() {
|
||||||
|
pw.mutex.Lock()
|
||||||
|
defer pw.mutex.Unlock()
|
||||||
|
if !pw.stopped {
|
||||||
|
pw.stopped = true
|
||||||
|
close(pw.stopCh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stopping returns true if Stop() has been called
|
||||||
|
func (pw *proxyWatcher) Stopping() bool {
|
||||||
|
pw.mutex.Lock()
|
||||||
|
defer pw.mutex.Unlock()
|
||||||
|
return pw.stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultChan implements watch.Interface
|
||||||
|
func (pw *proxyWatcher) ResultChan() <-chan watch.Event {
|
||||||
|
return pw.result
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopChan returns stop channel
|
||||||
|
func (pw *proxyWatcher) StopChan() <-chan struct{} {
|
||||||
|
return pw.stopCh
|
||||||
|
}
|
||||||
|
|
@ -18,12 +18,14 @@ package duck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
||||||
|
|
@ -47,7 +49,7 @@ func (dif *TypedInformerFactory) Get(gvr schema.GroupVersionResource) (cache.Sha
|
||||||
listObj := dif.Type.GetListType()
|
listObj := dif.Type.GetListType()
|
||||||
lw := &cache.ListWatch{
|
lw := &cache.ListWatch{
|
||||||
ListFunc: asStructuredLister(dif.Client.Resource(gvr).List, listObj),
|
ListFunc: asStructuredLister(dif.Client.Resource(gvr).List, listObj),
|
||||||
WatchFunc: dif.Client.Resource(gvr).Watch,
|
WatchFunc: AsStructuredWatcher(dif.Client.Resource(gvr).Watch, dif.Type),
|
||||||
}
|
}
|
||||||
inf := cache.NewSharedIndexInformer(lw, dif.Type, dif.ResyncPeriod, cache.Indexers{
|
inf := cache.NewSharedIndexInformer(lw, dif.Type, dif.ResyncPeriod, cache.Indexers{
|
||||||
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
|
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
|
||||||
|
|
@ -79,3 +81,61 @@ func asStructuredLister(ulist unstructuredLister, listObj runtime.Object) cache.
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsStructuredWatcher is public for testing only.
|
||||||
|
// TODO(mattmoor): Move tests for this to `package duck` and make private.
|
||||||
|
func AsStructuredWatcher(wf cache.WatchFunc, obj runtime.Object) cache.WatchFunc {
|
||||||
|
return func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
uw, err := wf(lo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
structuredCh := make(chan watch.Event)
|
||||||
|
go func() {
|
||||||
|
defer close(structuredCh)
|
||||||
|
unstructuredCh := uw.ResultChan()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ue, ok := <-unstructuredCh:
|
||||||
|
if !ok {
|
||||||
|
// Channel is closed.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unstructuredObj, ok := ue.Object.(*unstructured.Unstructured)
|
||||||
|
if !ok {
|
||||||
|
// If it isn't an unstructured object, then forward the
|
||||||
|
// event as-is. This is likely to happen when the event's
|
||||||
|
// Type is an Error.
|
||||||
|
structuredCh <- ue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
structuredObj := obj.DeepCopyObject()
|
||||||
|
|
||||||
|
err := FromUnstructured(unstructuredObj, structuredObj)
|
||||||
|
if err != nil {
|
||||||
|
// Pass back an error indicating that the object we got
|
||||||
|
// was invalid.
|
||||||
|
structuredCh <- watch.Event{
|
||||||
|
Type: watch.Error,
|
||||||
|
Object: &metav1.Status{
|
||||||
|
Status: metav1.StatusFailure,
|
||||||
|
Code: http.StatusUnprocessableEntity,
|
||||||
|
Reason: metav1.StatusReasonInvalid,
|
||||||
|
Message: err.Error(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Send the structured event.
|
||||||
|
structuredCh <- watch.Event{
|
||||||
|
Type: ue.Type,
|
||||||
|
Object: structuredObj,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return NewProxyWatcher(structuredCh), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,17 @@ limitations under the License.
|
||||||
package duck_test
|
package duck_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/client-go/dynamic/fake"
|
"k8s.io/client-go/dynamic/fake"
|
||||||
|
|
||||||
"github.com/knative/pkg/apis/duck"
|
"github.com/knative/pkg/apis/duck"
|
||||||
|
|
@ -85,3 +91,185 @@ func TestSimpleList(t *testing.T) {
|
||||||
|
|
||||||
// TODO(mattmoor): Access through informer
|
// TODO(mattmoor): Access through informer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAsStructuredWatcherNestedError(t *testing.T) {
|
||||||
|
want := errors.New("this is what we expect")
|
||||||
|
nwf := func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
return nil, want
|
||||||
|
}
|
||||||
|
|
||||||
|
wf := duck.AsStructuredWatcher(nwf, &duckv1alpha1.Generational{})
|
||||||
|
|
||||||
|
_, got := wf(metav1.ListOptions{})
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("WatchFunc() = %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsStructuredWatcherClosedChannel(t *testing.T) {
|
||||||
|
nwf := func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
return watch.NewEmptyWatch(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wf := duck.AsStructuredWatcher(nwf, &duckv1alpha1.Generational{})
|
||||||
|
|
||||||
|
wi, err := wf(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("WatchFunc() = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := wi.ResultChan()
|
||||||
|
|
||||||
|
x, ok := <-ch
|
||||||
|
if ok {
|
||||||
|
t.Errorf("<-ch = %v, wanted closed", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsStructuredWatcherPassThru(t *testing.T) {
|
||||||
|
unstructuredCh := make(chan watch.Event)
|
||||||
|
nwf := func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
return duck.NewProxyWatcher(unstructuredCh), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wf := duck.AsStructuredWatcher(nwf, &duckv1alpha1.Generational{})
|
||||||
|
|
||||||
|
wi, err := wf(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("WatchFunc() = %v", err)
|
||||||
|
}
|
||||||
|
defer wi.Stop()
|
||||||
|
ch := wi.ResultChan()
|
||||||
|
|
||||||
|
// Don't expect a message yet.
|
||||||
|
select {
|
||||||
|
case x, ok := <-ch:
|
||||||
|
t.Errorf("Saw unexpected message on channel: %v, %v.", x, ok)
|
||||||
|
case _ = <-time.After(100 * time.Millisecond):
|
||||||
|
// Expected path.
|
||||||
|
}
|
||||||
|
|
||||||
|
want := watch.Added
|
||||||
|
unstructuredCh <- watch.Event{
|
||||||
|
Type: want,
|
||||||
|
Object: &unstructured.Unstructured{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect a message when we send one though.
|
||||||
|
select {
|
||||||
|
case x, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("<-ch = closed, wanted *duckv1alpha1.Generational{}")
|
||||||
|
}
|
||||||
|
if got := x.Type; got != want {
|
||||||
|
t.Errorf("x.Type = %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
if _, ok := x.Object.(*duckv1alpha1.Generational); !ok {
|
||||||
|
t.Errorf("<-ch = %T, wanted %T", x, &duckv1alpha1.Generational{})
|
||||||
|
}
|
||||||
|
case _ = <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("Didn't see expected message on channel.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsStructuredWatcherPassThruErrors(t *testing.T) {
|
||||||
|
unstructuredCh := make(chan watch.Event)
|
||||||
|
nwf := func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
return duck.NewProxyWatcher(unstructuredCh), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wf := duck.AsStructuredWatcher(nwf, &duckv1alpha1.Generational{})
|
||||||
|
|
||||||
|
wi, err := wf(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("WatchFunc() = %v", err)
|
||||||
|
}
|
||||||
|
defer wi.Stop()
|
||||||
|
ch := wi.ResultChan()
|
||||||
|
|
||||||
|
want := watch.Event{
|
||||||
|
Type: watch.Error,
|
||||||
|
Object: &metav1.Status{
|
||||||
|
Code: 42,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
unstructuredCh <- want
|
||||||
|
|
||||||
|
// Expect a message when we send one though.
|
||||||
|
select {
|
||||||
|
case got, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("<-ch = closed, wanted *metav1.Status{}")
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("<-ch (-want, +got) = %v", diff)
|
||||||
|
}
|
||||||
|
case _ = <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("Didn't see expected message on channel.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsStructuredWatcherErrorConverting(t *testing.T) {
|
||||||
|
unstructuredCh := make(chan watch.Event)
|
||||||
|
nwf := func(lo metav1.ListOptions) (watch.Interface, error) {
|
||||||
|
return duck.NewProxyWatcher(unstructuredCh), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wf := duck.AsStructuredWatcher(nwf, &badObject{})
|
||||||
|
|
||||||
|
wi, err := wf(metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("WatchFunc() = %v", err)
|
||||||
|
}
|
||||||
|
defer wi.Stop()
|
||||||
|
ch := wi.ResultChan()
|
||||||
|
|
||||||
|
unstructuredCh <- watch.Event{
|
||||||
|
Type: watch.Added,
|
||||||
|
Object: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect a message when we send one though.
|
||||||
|
select {
|
||||||
|
case x, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("<-ch = closed, wanted *duckv1alpha1.Generational{}")
|
||||||
|
}
|
||||||
|
if got, want := x.Type, watch.Error; got != want {
|
||||||
|
t.Errorf("<-ch = %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
if status, ok := x.Object.(*metav1.Status); !ok {
|
||||||
|
t.Errorf("<-ch = %T, wanted %T", x, &metav1.Status{})
|
||||||
|
} else if got, want := status.Message, errNoUnmarshal.Error(); got != want {
|
||||||
|
t.Errorf("<-ch = %v, wanted %v", got, want)
|
||||||
|
}
|
||||||
|
case _ = <-time.After(100 * time.Millisecond):
|
||||||
|
t.Errorf("Didn't see expected message on channel.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNoUnmarshal = errors.New("this cannot be unmarshalled")
|
||||||
|
|
||||||
|
type badObject struct {
|
||||||
|
Foo doNotUnmarshal `json:"foo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type doNotUnmarshal struct{}
|
||||||
|
|
||||||
|
var _ json.Unmarshaler = (*doNotUnmarshal)(nil)
|
||||||
|
|
||||||
|
func (*doNotUnmarshal) UnmarshalJSON([]byte) error {
|
||||||
|
return errNoUnmarshal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *badObject) GetObjectKind() schema.ObjectKind {
|
||||||
|
return &metav1.TypeMeta{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *badObject) DeepCopyObject() runtime.Object {
|
||||||
|
return &badObject{}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue