mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-14 02:20:53 +00:00
Add APIs for computing status based on fetching resource info from a
cluster
This commit is contained in:
561
kstatus/wait/wait_test.go
Normal file
561
kstatus/wait/wait_test.go
Normal file
@@ -0,0 +1,561 @@
|
||||
package wait
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "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/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/kustomize/kstatus/status"
|
||||
)
|
||||
|
||||
const (
|
||||
testTimeout = 1 * time.Minute
|
||||
testPollInterval = 1 * time.Second
|
||||
)
|
||||
|
||||
func TestFetchAndResolve(t *testing.T) {
|
||||
type result struct {
|
||||
status status.Status
|
||||
error bool
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
resources []runtime.Object
|
||||
expectedResults []result
|
||||
}{
|
||||
"no resources": {
|
||||
resources: []runtime.Object{},
|
||||
expectedResults: []result{},
|
||||
},
|
||||
"single resource": {
|
||||
resources: []runtime.Object{
|
||||
&appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Generation: 1,
|
||||
Name: "myDeployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResults: []result{
|
||||
{
|
||||
status: status.InProgressStatus,
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"multiple resources": {
|
||||
resources: []runtime.Object{
|
||||
&appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Generation: 1,
|
||||
Name: "myStatefulSet",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
|
||||
Type: appsv1.OnDeleteStatefulSetStrategyType,
|
||||
},
|
||||
},
|
||||
Status: appsv1.StatefulSetStatus{
|
||||
ObservedGeneration: 1,
|
||||
},
|
||||
},
|
||||
&corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Generation: 1,
|
||||
Name: "mySecret",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResults: []result{
|
||||
{
|
||||
status: status.CurrentStatus,
|
||||
error: false,
|
||||
},
|
||||
{
|
||||
status: status.CurrentStatus,
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, tc.resources...)
|
||||
resolver := NewResolver(fakeClient, testPollInterval)
|
||||
resolver.statusComputeFunc = status.Compute
|
||||
|
||||
var identifiers []ResourceIdentifier
|
||||
for _, resource := range tc.resources {
|
||||
gvk := resource.GetObjectKind().GroupVersionKind()
|
||||
r := resource.(metav1.Object)
|
||||
identifiers = append(identifiers, &resourceKey{
|
||||
name: r.GetName(),
|
||||
namespace: r.GetNamespace(),
|
||||
apiVersion: gvk.GroupVersion().String(),
|
||||
kind: gvk.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
results := resolver.FetchAndResolve(context.TODO(), identifiers)
|
||||
for i, res := range results {
|
||||
id := identifiers[i]
|
||||
expectedRes := tc.expectedResults[i]
|
||||
rid := fmt.Sprintf("%s/%s", id.GetNamespace(), id.GetName())
|
||||
if expectedRes.error {
|
||||
if res.Error == nil {
|
||||
t.Errorf("expected error for resource %s, but didn't get one", rid)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
t.Errorf("didn't expected error for resource %s, but got %v", rid, res.Error)
|
||||
}
|
||||
|
||||
if got, want := res.Result.Status, expectedRes.status; got != want {
|
||||
t.Errorf("expected status %s for resources %s, but got %s", want, rid, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAndResolveUnknownResource(t *testing.T) {
|
||||
fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme)
|
||||
resolver := NewResolver(fakeClient, testPollInterval)
|
||||
results := resolver.FetchAndResolve(context.TODO(), []ResourceIdentifier{
|
||||
&resourceKey{
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deploymnet",
|
||||
name: "myDeployment",
|
||||
namespace: "default",
|
||||
},
|
||||
})
|
||||
|
||||
if want, got := 1, len(results); want != got {
|
||||
t.Errorf("expected %d results, but got %d", want, got)
|
||||
}
|
||||
|
||||
res := results[0]
|
||||
|
||||
if want, got := status.CurrentStatus, res.Result.Status; got != want {
|
||||
t.Errorf("expected status %s, but got %s", want, got)
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
t.Errorf("expected no error, but got %v", res.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAndResolveWithFetchError(t *testing.T) {
|
||||
expectedError := errors.New("failed to fetch resource")
|
||||
resolver := NewResolver(
|
||||
&fakeReader{
|
||||
Err: expectedError,
|
||||
},
|
||||
testPollInterval,
|
||||
)
|
||||
results := resolver.FetchAndResolve(context.TODO(), []ResourceIdentifier{
|
||||
&resourceKey{
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deploymnet",
|
||||
name: "myDeployment",
|
||||
namespace: "default",
|
||||
},
|
||||
})
|
||||
|
||||
if want, got := 1, len(results); want != got {
|
||||
t.Errorf("expected %d results, but got %d", want, got)
|
||||
}
|
||||
|
||||
res := results[0]
|
||||
|
||||
if want, got := status.UnknownStatus, res.Result.Status; got != want {
|
||||
t.Errorf("expected status %s, but got %s", want, got)
|
||||
}
|
||||
|
||||
if want, got := expectedError, errors.Cause(res.Error); got != want {
|
||||
t.Errorf("expected error %v, but got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAndResolveComputeStatusError(t *testing.T) {
|
||||
expectedError := errors.New("this is a test")
|
||||
resource := &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Generation: 1,
|
||||
Name: "myDeployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, resource)
|
||||
resolver := NewResolver(fakeClient, testPollInterval)
|
||||
|
||||
resolver.statusComputeFunc = func(u *unstructured.Unstructured) (*status.Result, error) {
|
||||
return &status.Result{
|
||||
Status: status.UnknownStatus,
|
||||
Message: "Got an error",
|
||||
}, expectedError
|
||||
}
|
||||
results := resolver.FetchAndResolve(context.TODO(), []ResourceIdentifier{
|
||||
&resourceKey{
|
||||
apiVersion: resource.APIVersion,
|
||||
kind: resource.Kind,
|
||||
name: resource.GetName(),
|
||||
namespace: resource.GetNamespace(),
|
||||
},
|
||||
})
|
||||
|
||||
if want, got := 1, len(results); want != got {
|
||||
t.Errorf("expected %d results, but got %d", want, got)
|
||||
}
|
||||
|
||||
res := results[0]
|
||||
if want, got := expectedError, res.Error; got != want {
|
||||
t.Errorf("expected error %v, but got %v", want, got)
|
||||
}
|
||||
|
||||
if want, got := status.UnknownStatus, res.Result.Status; got != want {
|
||||
t.Errorf("expected status %s, but got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeReader struct {
|
||||
Called int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (f *fakeReader) Get(ctx context.Context, key client.ObjectKey, obj runtime.Object) error {
|
||||
f.Called += 1
|
||||
return f.Err
|
||||
}
|
||||
|
||||
func (f *fakeReader) List(ctx context.Context, list runtime.Object, opts ...client.ListOption) error {
|
||||
return errors.New("list not used")
|
||||
}
|
||||
|
||||
func TestWaitForStatus(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
resources map[runtime.Object][]*status.Result
|
||||
expectedResourceStatuses map[runtime.Object][]status.Status
|
||||
expectedAggregateStatuses []status.Status
|
||||
}{
|
||||
"no resources": {
|
||||
resources: map[runtime.Object][]*status.Result{},
|
||||
expectedResourceStatuses: map[runtime.Object][]status.Status{},
|
||||
expectedAggregateStatuses: []status.Status{
|
||||
status.CurrentStatus,
|
||||
},
|
||||
},
|
||||
"single resource": {
|
||||
resources: map[runtime.Object][]*status.Result{
|
||||
deploymentResource: {
|
||||
{
|
||||
Status: status.InProgressStatus,
|
||||
Message: "FirstInProgress",
|
||||
},
|
||||
{
|
||||
Status: status.InProgressStatus,
|
||||
Message: "SecondInProgress",
|
||||
},
|
||||
{
|
||||
Status: status.CurrentStatus,
|
||||
Message: "CurrentProgress",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResourceStatuses: map[runtime.Object][]status.Status{
|
||||
deploymentResource: {
|
||||
status.InProgressStatus,
|
||||
status.InProgressStatus,
|
||||
status.CurrentStatus,
|
||||
},
|
||||
},
|
||||
expectedAggregateStatuses: []status.Status{
|
||||
status.InProgressStatus,
|
||||
status.InProgressStatus,
|
||||
status.CurrentStatus,
|
||||
status.CurrentStatus,
|
||||
},
|
||||
},
|
||||
"multiple resource": {
|
||||
resources: map[runtime.Object][]*status.Result{
|
||||
statefulSetResource: {
|
||||
{
|
||||
Status: status.InProgressStatus,
|
||||
Message: "FirstUnknown",
|
||||
},
|
||||
{
|
||||
Status: status.InProgressStatus,
|
||||
Message: "SecondInProgress",
|
||||
},
|
||||
{
|
||||
Status: status.CurrentStatus,
|
||||
Message: "CurrentProgress",
|
||||
},
|
||||
},
|
||||
serviceResource: {
|
||||
{
|
||||
Status: status.CurrentStatus,
|
||||
Message: "CurrentImmediately",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResourceStatuses: map[runtime.Object][]status.Status{
|
||||
statefulSetResource: {
|
||||
status.InProgressStatus,
|
||||
status.InProgressStatus,
|
||||
status.CurrentStatus,
|
||||
},
|
||||
serviceResource: {
|
||||
status.CurrentStatus,
|
||||
},
|
||||
},
|
||||
expectedAggregateStatuses: []status.Status{
|
||||
status.UnknownStatus,
|
||||
status.InProgressStatus,
|
||||
status.InProgressStatus,
|
||||
status.CurrentStatus,
|
||||
status.CurrentStatus,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
var objs []runtime.Object
|
||||
statusResults := make(map[resourceKey][]*status.Result)
|
||||
var identifiers []ResourceIdentifier
|
||||
|
||||
for obj, statuses := range tc.resources {
|
||||
objs = append(objs, obj)
|
||||
identifier := keyFromObject(obj)
|
||||
identifiers = append(identifiers, &identifier)
|
||||
statusResults[identifier] = statuses
|
||||
}
|
||||
|
||||
statusComputer := statusComputer{
|
||||
results: statusResults,
|
||||
resourceCallCount: make(map[resourceKey]int),
|
||||
}
|
||||
|
||||
resolver := &Resolver{
|
||||
client: fake.NewFakeClientWithScheme(scheme.Scheme, objs...),
|
||||
statusComputeFunc: statusComputer.Compute,
|
||||
pollInterval: testPollInterval,
|
||||
}
|
||||
|
||||
eventChan := resolver.WaitForStatus(context.TODO(), identifiers)
|
||||
|
||||
var events []Event
|
||||
timer := time.NewTimer(testTimeout)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-eventChan:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
events = append(events, event)
|
||||
case <-timer.C:
|
||||
t.Fatalf("timeout waiting for resources to reach current status")
|
||||
}
|
||||
}
|
||||
|
||||
var aggregateStatuses []status.Status
|
||||
resourceStatuses := make(map[resourceKey][]status.Status)
|
||||
for _, e := range events {
|
||||
aggregateStatuses = append(aggregateStatuses, e.AggregateStatus)
|
||||
if e.EventResource != nil {
|
||||
identifier := keyFromResourceIdentifier(e.EventResource.Identifier)
|
||||
resourceStatuses[identifier] = append(resourceStatuses[identifier], e.EventResource.Status)
|
||||
}
|
||||
}
|
||||
|
||||
for resource, expectedStatuses := range tc.expectedResourceStatuses {
|
||||
identifier := keyFromObject(resource)
|
||||
actualStatuses := resourceStatuses[identifier]
|
||||
if !reflect.DeepEqual(expectedStatuses, actualStatuses) {
|
||||
t.Errorf("expected statuses %v for resource %s/%s, but got %v", expectedStatuses, identifier.namespace, identifier.name, actualStatuses)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.expectedAggregateStatuses, aggregateStatuses) {
|
||||
t.Errorf("expected aggregate statuses %v, but got %v", tc.expectedAggregateStatuses, aggregateStatuses)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForStatusDeletedResources(t *testing.T) {
|
||||
statusComputer := statusComputer{
|
||||
results: make(map[resourceKey][]*status.Result),
|
||||
resourceCallCount: make(map[resourceKey]int),
|
||||
}
|
||||
|
||||
resolver := &Resolver{
|
||||
client: fake.NewFakeClientWithScheme(scheme.Scheme),
|
||||
statusComputeFunc: statusComputer.Compute,
|
||||
pollInterval: testPollInterval,
|
||||
}
|
||||
|
||||
depResourceIdentifier := keyFromObject(deploymentResource)
|
||||
serviceResourceIdentifier := keyFromObject(serviceResource)
|
||||
identifiers := []ResourceIdentifier{
|
||||
&depResourceIdentifier,
|
||||
&serviceResourceIdentifier,
|
||||
}
|
||||
|
||||
eventChan := resolver.WaitForStatus(context.TODO(), identifiers)
|
||||
|
||||
var events []Event
|
||||
timer := time.NewTimer(testTimeout)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-eventChan:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
events = append(events, event)
|
||||
case <-timer.C:
|
||||
t.Fatalf("timeout waiting for resources to reach current status")
|
||||
}
|
||||
}
|
||||
|
||||
expectedEvents := []struct {
|
||||
aggregateStatus status.Status
|
||||
hasResource bool
|
||||
resourceStatus status.Status
|
||||
}{
|
||||
{
|
||||
aggregateStatus: status.UnknownStatus,
|
||||
hasResource: true,
|
||||
resourceStatus: status.CurrentStatus,
|
||||
},
|
||||
{
|
||||
aggregateStatus: status.CurrentStatus,
|
||||
hasResource: true,
|
||||
resourceStatus: status.CurrentStatus,
|
||||
},
|
||||
{
|
||||
aggregateStatus: status.CurrentStatus,
|
||||
hasResource: false,
|
||||
},
|
||||
}
|
||||
|
||||
if want, got := len(expectedEvents), len(events); got != want {
|
||||
t.Errorf("expected %d events, but got %d", want, got)
|
||||
}
|
||||
|
||||
for i, e := range events {
|
||||
ee := expectedEvents[i]
|
||||
if want, got := ee.aggregateStatus, e.AggregateStatus; got != want {
|
||||
t.Errorf("expected event %d to be %s, but got %s", i, want, got)
|
||||
}
|
||||
|
||||
if ee.hasResource {
|
||||
if want, got := ee.resourceStatus, e.EventResource.Status; want != got {
|
||||
t.Errorf("expected resource event %d to be %s, but got %s", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type statusComputer struct {
|
||||
t *testing.T
|
||||
|
||||
results map[resourceKey][]*status.Result
|
||||
resourceCallCount map[resourceKey]int
|
||||
}
|
||||
|
||||
func (s *statusComputer) Compute(u *unstructured.Unstructured) (*status.Result, error) {
|
||||
identifier := resourceKey{
|
||||
apiVersion: u.GetAPIVersion(),
|
||||
kind: u.GetKind(),
|
||||
name: u.GetName(),
|
||||
namespace: u.GetNamespace(),
|
||||
}
|
||||
|
||||
resourceResults, ok := s.results[identifier]
|
||||
if !ok {
|
||||
s.t.Fatalf("No results available for resource %s/%s", u.GetNamespace(), u.GetName())
|
||||
}
|
||||
callCount := s.resourceCallCount[identifier]
|
||||
|
||||
var res *status.Result
|
||||
if len(resourceResults) <= callCount {
|
||||
res = resourceResults[len(resourceResults)-1]
|
||||
} else {
|
||||
res = resourceResults[callCount]
|
||||
}
|
||||
s.resourceCallCount[identifier] = callCount + 1
|
||||
return res, nil
|
||||
}
|
||||
|
||||
var deploymentResource = &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myDeployment",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
var statefulSetResource = &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "StatefulSet",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myStatefulSet",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
var serviceResource = &corev1.Service{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Service",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myService",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user