cli-utils/pkg/kstatus/polling/engine/engine_test.go

248 lines
6.7 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package engine
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
fakemapper "sigs.k8s.io/cli-utils/pkg/testutil"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func TestStatusPollerRunner(t *testing.T) {
testCases := map[string]struct {
identifiers object.ObjMetadataSet
defaultStatusReader StatusReader
expectedEventTypes []event.EventType
}{
"single resource": {
identifiers: object.ObjMetadataSet{
{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "bar",
},
},
defaultStatusReader: &fakeStatusReader{
resourceStatuses: map[schema.GroupKind][]status.Status{
schema.GroupKind{Group: "apps", Kind: "Deployment"}: { //nolint:gofmt
status.InProgressStatus,
status.CurrentStatus,
},
},
resourceStatusCount: make(map[schema.GroupKind]int),
},
expectedEventTypes: []event.EventType{
event.ResourceUpdateEvent,
event.ResourceUpdateEvent,
},
},
"multiple resources": {
identifiers: object.ObjMetadataSet{
{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
Namespace: "default",
},
{
GroupKind: schema.GroupKind{
Group: "",
Kind: "Service",
},
Name: "bar",
Namespace: "default",
},
},
defaultStatusReader: &fakeStatusReader{
resourceStatuses: map[schema.GroupKind][]status.Status{
schema.GroupKind{Group: "apps", Kind: "Deployment"}: { //nolint:gofmt
status.InProgressStatus,
status.CurrentStatus,
},
schema.GroupKind{Group: "", Kind: "Service"}: { //nolint:gofmt
status.InProgressStatus,
status.InProgressStatus,
status.CurrentStatus,
},
},
resourceStatusCount: make(map[schema.GroupKind]int),
},
expectedEventTypes: []event.EventType{
event.ResourceUpdateEvent,
event.ResourceUpdateEvent,
event.ResourceUpdateEvent,
event.ResourceUpdateEvent,
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
identifiers := tc.identifiers
fakeMapper := fakemapper.NewFakeRESTMapper(
appsv1.SchemeGroupVersion.WithKind("Deployment"),
v1.SchemeGroupVersion.WithKind("Service"),
)
engine := PollerEngine{
Mapper: fakeMapper,
}
options := Options{
PollInterval: 2 * time.Second,
ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ object.ObjMetadataSet) (
ClusterReader, error) {
return testutil.NewNoopClusterReader(), nil
},
StatusReadersFactoryFunc: func(_ ClusterReader, _ meta.RESTMapper) (
statusReaders map[schema.GroupKind]StatusReader, defaultStatusReader StatusReader) {
return make(map[schema.GroupKind]StatusReader), tc.defaultStatusReader
},
}
eventChannel := engine.Poll(ctx, identifiers, options)
var eventTypes []event.EventType
for ch := range eventChannel {
eventTypes = append(eventTypes, ch.EventType)
if len(eventTypes) == len(tc.expectedEventTypes) {
cancel()
}
}
assert.Equal(t, tc.expectedEventTypes, eventTypes)
})
}
}
func TestNewStatusPollerRunnerCancellation(t *testing.T) {
identifiers := make(object.ObjMetadataSet, 0)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
timer := time.NewTimer(5 * time.Second)
engine := PollerEngine{}
options := Options{
PollInterval: 2 * time.Second,
ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ object.ObjMetadataSet) (
ClusterReader, error) {
return testutil.NewNoopClusterReader(), nil
},
StatusReadersFactoryFunc: func(_ ClusterReader, _ meta.RESTMapper) (
statusReaders map[schema.GroupKind]StatusReader, defaultStatusReader StatusReader) {
return make(map[schema.GroupKind]StatusReader), nil
},
}
eventChannel := engine.Poll(ctx, identifiers, options)
for {
select {
case <-eventChannel:
timer.Stop()
return
case <-timer.C:
t.Errorf("expected runner to time out, but it didn't")
return
}
}
}
func TestNewStatusPollerRunnerIdentifierValidation(t *testing.T) {
identifiers := object.ObjMetadataSet{
{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Name: "foo",
},
}
engine := PollerEngine{
Mapper: fakemapper.NewFakeRESTMapper(
appsv1.SchemeGroupVersion.WithKind("Deployment"),
),
}
eventChannel := engine.Poll(context.Background(), identifiers, Options{
ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ object.ObjMetadataSet) (
ClusterReader, error) {
return testutil.NewNoopClusterReader(), nil
},
StatusReadersFactoryFunc: func(_ ClusterReader, _ meta.RESTMapper) (
statusReaders map[schema.GroupKind]StatusReader, defaultStatusReader StatusReader) {
return make(map[schema.GroupKind]StatusReader), nil
},
})
timer := time.NewTimer(3 * time.Second)
defer timer.Stop()
select {
case e := <-eventChannel:
if e.EventType != event.ErrorEvent {
t.Errorf("expected an error event, but got %s", e.EventType.String())
return
}
err := e.Error
if !strings.Contains(err.Error(), "namespace is not set") {
t.Errorf("expected error with namespace not set, but got %v", err)
}
return
case <-timer.C:
t.Errorf("expected an error event, but didn't get one")
}
}
type fakeStatusReader struct {
resourceStatuses map[schema.GroupKind][]status.Status
resourceStatusCount map[schema.GroupKind]int
}
func (f *fakeStatusReader) ReadStatus(_ context.Context, identifier object.ObjMetadata) *event.ResourceStatus {
count := f.resourceStatusCount[identifier.GroupKind]
resourceStatusSlice := f.resourceStatuses[identifier.GroupKind]
var resourceStatus status.Status
if len(resourceStatusSlice) > count {
resourceStatus = resourceStatusSlice[count]
} else {
resourceStatus = resourceStatusSlice[len(resourceStatusSlice)-1]
}
f.resourceStatusCount[identifier.GroupKind] = count + 1
return &event.ResourceStatus{
Identifier: identifier,
Status: resourceStatus,
}
}
func (f *fakeStatusReader) ReadStatusForObject(_ context.Context, _ *unstructured.Unstructured) *event.ResourceStatus {
return nil
}