mirror of https://github.com/knative/pkg.git
567 lines
13 KiB
Go
567 lines
13 KiB
Go
/*
|
|
Copyright 2017 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 controller
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"golang.org/x/sync/errgroup"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/client-go/tools/cache"
|
|
"k8s.io/client-go/util/workqueue"
|
|
|
|
. "github.com/knative/pkg/controller/testing"
|
|
. "github.com/knative/pkg/logging/testing"
|
|
. "github.com/knative/pkg/testing"
|
|
)
|
|
|
|
func TestPassNew(t *testing.T) {
|
|
old := "foo"
|
|
new := "bar"
|
|
|
|
PassNew(func(got interface{}) {
|
|
if new != got.(string) {
|
|
t.Errorf("PassNew() = %v, wanted %v", got, new)
|
|
}
|
|
})(old, new)
|
|
}
|
|
|
|
var (
|
|
boolTrue = true
|
|
boolFalse = false
|
|
gvk = schema.GroupVersionKind{
|
|
Group: "pkg.knative.dev",
|
|
Version: "v1meta1",
|
|
Kind: "Parent",
|
|
}
|
|
)
|
|
|
|
func TestFilter(t *testing.T) {
|
|
filter := Filter(gvk)
|
|
|
|
tests := []struct {
|
|
name string
|
|
input interface{}
|
|
want bool
|
|
}{{
|
|
name: "not a metav1.Object",
|
|
input: "foo",
|
|
want: false,
|
|
}, {
|
|
name: "nil",
|
|
input: nil,
|
|
want: false,
|
|
}, {
|
|
name: "no owner reference",
|
|
input: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
},
|
|
},
|
|
want: false,
|
|
}, {
|
|
name: "wrong owner reference, not controller",
|
|
input: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: "another.knative.dev/v1beta3",
|
|
Kind: "Parent",
|
|
Controller: &boolFalse,
|
|
}},
|
|
},
|
|
},
|
|
want: false,
|
|
}, {
|
|
name: "right owner reference, not controller",
|
|
input: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Kind: gvk.Kind,
|
|
Controller: &boolFalse,
|
|
}},
|
|
},
|
|
},
|
|
want: false,
|
|
}, {
|
|
name: "wrong owner reference, but controller",
|
|
input: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: "another.knative.dev/v1beta3",
|
|
Kind: "Parent",
|
|
Controller: &boolTrue,
|
|
}},
|
|
},
|
|
},
|
|
want: false,
|
|
}, {
|
|
name: "right owner reference, is controller",
|
|
input: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Kind: gvk.Kind,
|
|
Controller: &boolTrue,
|
|
}},
|
|
},
|
|
},
|
|
want: true,
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got := filter(test.input)
|
|
if test.want != got {
|
|
t.Errorf("Filter() = %v, wanted %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type NopReconciler struct{}
|
|
|
|
func (nr *NopReconciler) Reconcile(context.Context, string) error {
|
|
return nil
|
|
}
|
|
|
|
func TestEnqueues(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
work func(*Impl)
|
|
wantQueue []string
|
|
}{{
|
|
name: "do nothing",
|
|
work: func(*Impl) {},
|
|
}, {
|
|
name: "enqueue key",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueKey("foo/bar")
|
|
},
|
|
wantQueue: []string{"foo/bar"},
|
|
}, {
|
|
name: "enqueue duplicate key",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueKey("foo/bar")
|
|
impl.EnqueueKey("foo/bar")
|
|
},
|
|
// The queue deduplicates.
|
|
wantQueue: []string{"foo/bar"},
|
|
}, {
|
|
name: "enqueue different keys",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueKey("foo/bar")
|
|
impl.EnqueueKey("foo/baz")
|
|
},
|
|
wantQueue: []string{"foo/bar", "foo/baz"},
|
|
}, {
|
|
name: "enqueue resource",
|
|
work: func(impl *Impl) {
|
|
impl.Enqueue(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
},
|
|
})
|
|
},
|
|
wantQueue: []string{"bar/foo"},
|
|
}, {
|
|
name: "enqueue bad resource",
|
|
work: func(impl *Impl) {
|
|
impl.Enqueue("baz/blah")
|
|
},
|
|
}, {
|
|
name: "enqueue controller of bad resource",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueControllerOf("baz/blah")
|
|
},
|
|
}, {
|
|
name: "enqueue controller of resource without owner",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueControllerOf(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
},
|
|
})
|
|
},
|
|
}, {
|
|
name: "enqueue controller of resource with owner",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueControllerOf(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Kind: gvk.Kind,
|
|
Name: "baz",
|
|
Controller: &boolTrue,
|
|
}},
|
|
},
|
|
})
|
|
},
|
|
wantQueue: []string{"bar/baz"},
|
|
}, {
|
|
name: "enqueue controller of deleted resource with owner",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueControllerOf(cache.DeletedFinalStateUnknown{
|
|
Key: "foo/bar",
|
|
Obj: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
OwnerReferences: []metav1.OwnerReference{{
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Kind: gvk.Kind,
|
|
Name: "baz",
|
|
Controller: &boolTrue,
|
|
}},
|
|
},
|
|
},
|
|
})
|
|
},
|
|
wantQueue: []string{"bar/baz"},
|
|
}, {
|
|
name: "enqueue controller of deleted bad resource",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueControllerOf(cache.DeletedFinalStateUnknown{
|
|
Key: "foo/bar",
|
|
Obj: "bad-resource",
|
|
})
|
|
},
|
|
}, {
|
|
name: "enqueue label of bad resource",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("test-ns", "test-name")("baz/blah")
|
|
},
|
|
}, {
|
|
name: "enqueue label of resource without label",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("ns-key", "name-key")(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
Labels: map[string]string{
|
|
"ns-key": "bar",
|
|
},
|
|
},
|
|
})
|
|
},
|
|
}, {
|
|
name: "enqueue label of resource without namespace label",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("ns-key", "name-key")(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
Labels: map[string]string{
|
|
"name-key": "baz",
|
|
},
|
|
},
|
|
})
|
|
},
|
|
}, {
|
|
name: "enqueue label of resource with labels",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("ns-key", "name-key")(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
Labels: map[string]string{
|
|
"ns-key": "bar",
|
|
"name-key": "baz",
|
|
},
|
|
},
|
|
})
|
|
},
|
|
wantQueue: []string{"bar/baz"},
|
|
}, {
|
|
name: "enqueue label of resource with empty namespace label (cluster-scoped resource)",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("", "name-key")(&Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
Labels: map[string]string{
|
|
"name-key": "baz",
|
|
},
|
|
},
|
|
})
|
|
},
|
|
wantQueue: []string{"baz"},
|
|
}, {
|
|
name: "enqueue label of deleted resource with label",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("ns-key", "name-key")(cache.DeletedFinalStateUnknown{
|
|
Key: "foo/bar",
|
|
Obj: &Resource{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "foo",
|
|
Namespace: "bar",
|
|
Labels: map[string]string{
|
|
"ns-key": "bar",
|
|
"name-key": "baz",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
},
|
|
wantQueue: []string{"bar/baz"},
|
|
}, {
|
|
name: "enqueue controller of deleted bad resource",
|
|
work: func(impl *Impl) {
|
|
impl.EnqueueLabelOf("ns-key", "name-key")(cache.DeletedFinalStateUnknown{
|
|
Key: "foo/bar",
|
|
Obj: "bad-resource",
|
|
})
|
|
},
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
impl := NewImpl(&NopReconciler{}, TestLogger(t), "Testing", &FakeStatsReporter{})
|
|
test.work(impl)
|
|
|
|
// The rate limit on our queue delays when things are added to the queue.
|
|
time.Sleep(50 * time.Millisecond)
|
|
impl.WorkQueue.ShutDown()
|
|
gotQueue := drainWorkQueue(impl.WorkQueue)
|
|
|
|
if diff := cmp.Diff(test.wantQueue, gotQueue); diff != "" {
|
|
t.Errorf("unexpected queue (-want +got): %s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type CountingReconciler struct {
|
|
Count int
|
|
}
|
|
|
|
func (cr *CountingReconciler) Reconcile(context.Context, string) error {
|
|
cr.Count++
|
|
return nil
|
|
}
|
|
|
|
func TestStartAndShutdown(t *testing.T) {
|
|
r := &CountingReconciler{}
|
|
impl := NewImpl(r, TestLogger(t), "Testing", &FakeStatsReporter{})
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
var eg errgroup.Group
|
|
eg.Go(func() error {
|
|
return impl.Run(1, stopCh)
|
|
})
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
close(stopCh)
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
t.Errorf("Wait() = %v", err)
|
|
}
|
|
|
|
if got, want := r.Count, 0; got != want {
|
|
t.Errorf("Count = %v, wanted %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestStartAndShutdownWithWork(t *testing.T) {
|
|
r := &CountingReconciler{}
|
|
reporter := &FakeStatsReporter{}
|
|
impl := NewImpl(r, TestLogger(t), "Testing", reporter)
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
impl.EnqueueKey("foo/bar")
|
|
|
|
var eg errgroup.Group
|
|
eg.Go(func() error {
|
|
return impl.Run(1, stopCh)
|
|
})
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
close(stopCh)
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
t.Errorf("Wait() = %v", err)
|
|
}
|
|
|
|
if got, want := r.Count, 1; got != want {
|
|
t.Errorf("Count = %v, wanted %v", got, want)
|
|
}
|
|
if got, want := impl.WorkQueue.NumRequeues("foo/bar"), 0; got != want {
|
|
t.Errorf("Count = %v, wanted %v", got, want)
|
|
}
|
|
|
|
checkStats(t, reporter, 1, 0, 1, trueString)
|
|
}
|
|
|
|
type ErrorReconciler struct{}
|
|
|
|
func (er *ErrorReconciler) Reconcile(context.Context, string) error {
|
|
return errors.New("I always error")
|
|
}
|
|
|
|
func TestStartAndShutdownWithErroringWork(t *testing.T) {
|
|
r := &ErrorReconciler{}
|
|
reporter := &FakeStatsReporter{}
|
|
impl := NewImpl(r, TestLogger(t), "Testing", reporter)
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
impl.EnqueueKey("foo/bar")
|
|
|
|
var eg errgroup.Group
|
|
eg.Go(func() error {
|
|
return impl.Run(1, stopCh)
|
|
})
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
close(stopCh)
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
t.Errorf("Wait() = %v", err)
|
|
}
|
|
|
|
// Check that the work was requeued.
|
|
if got, want := impl.WorkQueue.NumRequeues("foo/bar"), 1; got != want {
|
|
t.Errorf("Count = %v, wanted %v", got, want)
|
|
}
|
|
|
|
checkStats(t, reporter, 1, 0, 1, falseString)
|
|
}
|
|
|
|
func TestStartAndShutdownWithInvalidWork(t *testing.T) {
|
|
r := &CountingReconciler{}
|
|
reporter := &FakeStatsReporter{}
|
|
impl := NewImpl(r, TestLogger(t), "Testing", reporter)
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
// Add a nonsense work item, which we couldn't ordinarily get into our workqueue.
|
|
thing := struct{}{}
|
|
impl.WorkQueue.AddRateLimited(thing)
|
|
|
|
var eg errgroup.Group
|
|
eg.Go(func() error {
|
|
return impl.Run(1, stopCh)
|
|
})
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
close(stopCh)
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
t.Errorf("Wait() = %v", err)
|
|
}
|
|
|
|
if got, want := r.Count, 0; got != want {
|
|
t.Errorf("Count = %v, wanted %v", got, want)
|
|
}
|
|
if got, want := impl.WorkQueue.NumRequeues(thing), 0; got != want {
|
|
t.Errorf("Count = %v, wanted %v", got, want)
|
|
}
|
|
|
|
checkStats(t, reporter, 1, 0, 1, falseString)
|
|
}
|
|
|
|
func drainWorkQueue(wq workqueue.RateLimitingInterface) (hasQueue []string) {
|
|
for {
|
|
key, shutdown := wq.Get()
|
|
if key == nil && shutdown {
|
|
break
|
|
}
|
|
hasQueue = append(hasQueue, key.(string))
|
|
}
|
|
return
|
|
}
|
|
|
|
type dummyInformer struct {
|
|
cache.SharedInformer
|
|
}
|
|
|
|
type dummyStore struct {
|
|
cache.Store
|
|
}
|
|
|
|
func (*dummyInformer) GetStore() cache.Store {
|
|
return &dummyStore{}
|
|
}
|
|
|
|
var dummyKeys = []string{"foo/bar", "bar/foo", "fizz/buzz"}
|
|
|
|
func (*dummyStore) ListKeys() []string {
|
|
return dummyKeys
|
|
}
|
|
|
|
func TestImplGlobalResync(t *testing.T) {
|
|
r := &CountingReconciler{}
|
|
impl := NewImpl(r, TestLogger(t), "Testing", &FakeStatsReporter{})
|
|
|
|
stopCh := make(chan struct{})
|
|
|
|
var eg errgroup.Group
|
|
eg.Go(func() error {
|
|
return impl.Run(1, stopCh)
|
|
})
|
|
|
|
impl.GlobalResync(&dummyInformer{})
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
close(stopCh)
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
t.Errorf("Wait() = %v", err)
|
|
}
|
|
|
|
if want, got := 3, r.Count; want != got {
|
|
t.Errorf("GlobalResync: want = %v, got = %v", want, got)
|
|
}
|
|
}
|
|
|
|
func checkStats(t *testing.T, r *FakeStatsReporter, reportCount, lastQueueDepth, reconcileCount int, lastReconcileSuccess string) {
|
|
qd := r.GetQueueDepths()
|
|
if got, want := len(qd), reportCount; got != want {
|
|
t.Errorf("Queue depth reports = %v, wanted %v", got, want)
|
|
}
|
|
if got, want := qd[len(qd)-1], int64(lastQueueDepth); got != want {
|
|
t.Errorf("Queue depth report = %v, wanted %v", got, want)
|
|
}
|
|
rd := r.GetReconcileData()
|
|
if got, want := len(rd), 1; got != want {
|
|
t.Errorf("Reconcile reports = %v, wanted %v", got, want)
|
|
}
|
|
if got, want := rd[len(rd)-1].Success, lastReconcileSuccess; got != want {
|
|
t.Errorf("Reconcile success = %v, wanted %v", got, want)
|
|
}
|
|
}
|