3dfb02eb14
This patch allows to wait for certain state when applying particular resources. Change-Id: I064cb49c8971f1edee870bc6c3c3dd1e428c73f0 Signed-off-by: Ruslan Aliev <raliev@mirantis.com> Closes: #624
319 lines
9.2 KiB
Go
Executable File
319 lines
9.2 KiB
Go
Executable File
/*
|
|
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
|
|
|
|
https://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 poller_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
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"
|
|
"k8s.io/cli-runtime/pkg/resource"
|
|
"k8s.io/client-go/discovery"
|
|
"k8s.io/client-go/dynamic"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/client-go/tools/clientcmd"
|
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/kubectl/pkg/util/openapi"
|
|
"k8s.io/kubectl/pkg/validation"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
|
|
kstatustestutil "sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
|
|
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
|
|
"sigs.k8s.io/cli-utils/pkg/object"
|
|
"sigs.k8s.io/cli-utils/pkg/testutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"opendev.org/airship/airshipctl/pkg/k8s/poller"
|
|
)
|
|
|
|
func TestNewStatusPoller(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
factory cmdutil.Factory
|
|
expectedError bool
|
|
}{
|
|
"failed rest config": {
|
|
factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
|
|
return nil, errors.New("rest config error")
|
|
}},
|
|
expectedError: true,
|
|
},
|
|
"failed rest mapper": {
|
|
factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
|
|
return nil, nil
|
|
},
|
|
MockToRESTMapper: func() (meta.RESTMapper, error) {
|
|
return nil, errors.New("rest mapper error")
|
|
}},
|
|
expectedError: true,
|
|
},
|
|
"failed new client": {
|
|
factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
|
|
return nil, nil
|
|
},
|
|
MockToRESTMapper: func() (meta.RESTMapper, error) {
|
|
return nil, nil
|
|
}},
|
|
expectedError: true,
|
|
},
|
|
"success new poller": {
|
|
factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
|
|
return &rest.Config{}, nil
|
|
},
|
|
MockToRESTMapper: func() (meta.RESTMapper, error) {
|
|
return testutil.NewFakeRESTMapper(), nil
|
|
}},
|
|
expectedError: false,
|
|
},
|
|
}
|
|
|
|
for tn, tc := range testCases {
|
|
t.Run(tn, func(t *testing.T) {
|
|
p, err := poller.NewStatusPoller(tc.factory)
|
|
if tc.expectedError {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, p)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatusPollerRun(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
identifiers []object.ObjMetadata
|
|
ClusterReaderFactoryFunc engine.ClusterReaderFactoryFunc
|
|
StatusReadersFactoryFunc engine.StatusReadersFactoryFunc
|
|
defaultStatusReader engine.StatusReader
|
|
expectedEventTypes []event.EventType
|
|
}{
|
|
"single resource": {
|
|
identifiers: []object.ObjMetadata{
|
|
{
|
|
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,
|
|
},
|
|
ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ []object.ObjMetadata) (
|
|
engine.ClusterReader, error) {
|
|
return kstatustestutil.NewNoopClusterReader(), nil
|
|
},
|
|
StatusReadersFactoryFunc: func(_ engine.ClusterReader, _ meta.RESTMapper) (
|
|
statusReaders map[schema.GroupKind]engine.StatusReader, defaultStatusReader engine.StatusReader) {
|
|
return make(map[schema.GroupKind]engine.StatusReader), &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),
|
|
}
|
|
},
|
|
},
|
|
"multiple resources": {
|
|
identifiers: []object.ObjMetadata{
|
|
{
|
|
GroupKind: schema.GroupKind{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
},
|
|
Name: "foo",
|
|
Namespace: "default",
|
|
},
|
|
{
|
|
GroupKind: schema.GroupKind{
|
|
Group: "",
|
|
Kind: "Service",
|
|
},
|
|
Name: "bar",
|
|
Namespace: "default",
|
|
},
|
|
},
|
|
ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ []object.ObjMetadata) (
|
|
engine.ClusterReader, error) {
|
|
return kstatustestutil.NewNoopClusterReader(), nil
|
|
},
|
|
StatusReadersFactoryFunc: func(_ engine.ClusterReader, _ meta.RESTMapper) (
|
|
statusReaders map[schema.GroupKind]engine.StatusReader, defaultStatusReader engine.StatusReader) {
|
|
return make(map[schema.GroupKind]engine.StatusReader), &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 := testutil.NewFakeRESTMapper(
|
|
appsv1.SchemeGroupVersion.WithKind("Deployment"),
|
|
v1.SchemeGroupVersion.WithKind("Service"),
|
|
)
|
|
|
|
e := poller.StatusPoller{
|
|
ClusterReaderFactoryFunc: tc.ClusterReaderFactoryFunc,
|
|
StatusReadersFactoryFunc: tc.StatusReadersFactoryFunc,
|
|
Engine: &engine.PollerEngine{Mapper: fakeMapper},
|
|
}
|
|
|
|
options := polling.Options{PollInterval: time.Second, UseCache: true}
|
|
|
|
eventChannel := e.Poll(ctx, identifiers, options)
|
|
|
|
var eventTypes []event.EventType
|
|
for ch := range eventChannel {
|
|
eventTypes = append(eventTypes, ch.EventType)
|
|
if len(eventTypes) == len(tc.expectedEventTypes) {
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
require.Equal(t, tc.expectedEventTypes, eventTypes)
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var _ cmdutil.Factory = &MockCmdUtilFactory{}
|
|
|
|
type MockCmdUtilFactory struct {
|
|
MockToRESTConfig func() (*rest.Config, error)
|
|
MockToRESTMapper func() (meta.RESTMapper, error)
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) ToRESTConfig() (*rest.Config, error) {
|
|
return n.MockToRESTConfig()
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) ToRESTMapper() (meta.RESTMapper, error) {
|
|
return n.MockToRESTMapper()
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
|
return nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) DynamicClient() (dynamic.Interface, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) KubernetesClientSet() (*kubernetes.Clientset, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) RESTClient() (*rest.RESTClient, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) NewBuilder() *resource.Builder {
|
|
return nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) ClientForMapping(_ *meta.RESTMapping) (resource.RESTClient, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) UnstructuredClientForMapping(_ *meta.RESTMapping) (resource.RESTClient, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) Validator(_ bool) (validation.Schema, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) OpenAPISchema() (openapi.Resources, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *MockCmdUtilFactory) OpenAPIGetter() discovery.OpenAPISchemaInterface {
|
|
return nil
|
|
}
|