github.com/splunk/dan1-qbec@v0.7.3/internal/rollout/rollout_test.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package rollout
    18  
    19  import (
    20  	"fmt"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/splunk/qbec/internal/model"
    26  	"github.com/splunk/qbec/internal/types"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/watch"
    33  )
    34  
    35  type testEvent struct {
    36  	wait  time.Duration
    37  	event watch.Event
    38  }
    39  
    40  type testWatcher struct {
    41  	ch   chan watch.Event
    42  	once sync.Once
    43  }
    44  
    45  func (t *testWatcher) Stop() {
    46  	t.once.Do(func() {
    47  		close(t.ch)
    48  	})
    49  }
    50  
    51  func (t *testWatcher) emit(events []testEvent) {
    52  	for _, e := range events {
    53  		time.Sleep(e.wait)
    54  		t.ch <- e.event
    55  	}
    56  }
    57  
    58  func (t *testWatcher) ResultChan() <-chan watch.Event {
    59  	return t.ch
    60  }
    61  
    62  func newTestWatcher(events []testEvent) *testWatcher {
    63  	tw := &testWatcher{ch: make(chan watch.Event, 10)}
    64  	go tw.emit(events)
    65  	return tw
    66  }
    67  
    68  func testKey(obj model.K8sMeta) string {
    69  	return fmt.Sprintf("%s/%s %s/%s", obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetNamespace(), obj.GetName())
    70  }
    71  
    72  type watchFactory struct {
    73  	eventsMap map[string][]testEvent
    74  }
    75  
    76  func (w *watchFactory) getWatcher(obj model.K8sMeta) (watch.Interface, error) {
    77  	k := testKey(obj)
    78  	events, ok := w.eventsMap[k]
    79  	if !ok {
    80  		return nil, fmt.Errorf("unable to produce events for %s", k)
    81  	}
    82  	return newTestWatcher(events), nil
    83  }
    84  
    85  type testListener struct {
    86  	t                *testing.T
    87  	l                sync.Mutex
    88  	initCalled       bool
    89  	endCalled        bool
    90  	initObjects      int
    91  	remainingObjects int
    92  	statuses         map[string][]string
    93  	errors           map[string]string
    94  }
    95  
    96  func newTestListener(t *testing.T) *testListener {
    97  	return &testListener{
    98  		t:        t,
    99  		statuses: map[string][]string{},
   100  		errors:   map[string]string{},
   101  	}
   102  }
   103  
   104  func (l *testListener) OnInit(objects []model.K8sMeta) {
   105  	l.l.Lock()
   106  	defer l.l.Unlock()
   107  	l.initObjects = len(objects)
   108  	l.remainingObjects = l.initObjects
   109  	l.initCalled = true
   110  	l.t.Log("init", len(objects), "objects")
   111  }
   112  
   113  func (l *testListener) OnStatusChange(object model.K8sMeta, rs types.RolloutStatus) {
   114  	l.l.Lock()
   115  	defer l.l.Unlock()
   116  	k := testKey(object)
   117  	l.statuses[k] = append(l.statuses[k], rs.Description)
   118  	if rs.Done {
   119  		l.remainingObjects--
   120  	}
   121  	l.t.Log("k=", k, "desc=", rs.Description, "done=", rs.Done)
   122  }
   123  
   124  func (l *testListener) OnError(object model.K8sMeta, err error) {
   125  	l.l.Lock()
   126  	defer l.l.Unlock()
   127  	k := testKey(object)
   128  	l.errors[k] = err.Error()
   129  	l.t.Log("k=", k, "err=", err)
   130  }
   131  
   132  func (l *testListener) OnEnd(err error) {
   133  	l.l.Lock()
   134  	defer l.l.Unlock()
   135  	l.endCalled = true
   136  	l.t.Log("end", "err=", err, "remaining=", l.remainingObjects)
   137  }
   138  
   139  func newObject(kind string, name string, status *types.RolloutStatus, err error) map[string]interface{} {
   140  	anns := map[string]interface{}{}
   141  	ret := map[string]interface{}{
   142  		"apiVersion": "apps/v1",
   143  		"kind":       kind,
   144  		"metadata": map[string]interface{}{
   145  			"namespace":   "test-ns",
   146  			"name":        name,
   147  			"annotations": anns,
   148  		},
   149  	}
   150  	if status != nil {
   151  		anns["status/desc"] = status.Description
   152  		anns["status/done"] = fmt.Sprint(status.Done)
   153  	}
   154  	if err != nil {
   155  		anns["status/error"] = err.Error()
   156  	}
   157  	return ret
   158  }
   159  
   160  func extractStatus(obj *unstructured.Unstructured, _ int64) (*types.RolloutStatus, error) {
   161  	desc := obj.GetAnnotations()["status/desc"]
   162  	errmsg := obj.GetAnnotations()["status/error"]
   163  	done := obj.GetAnnotations()["status/done"]
   164  	if errmsg != "" {
   165  		return nil, fmt.Errorf(errmsg)
   166  	}
   167  	return &types.RolloutStatus{Description: desc, Done: done == "true"}, nil
   168  }
   169  
   170  func testStatusMapper(obj model.K8sMeta) types.RolloutStatusFunc {
   171  	switch obj.GetKind() {
   172  	case "Foo", "Bar":
   173  		return extractStatus
   174  	default:
   175  		return nil
   176  	}
   177  }
   178  
   179  func newTestMeta(kind string, name string) model.K8sMeta {
   180  	return model.NewK8sObject(newObject(kind, name, nil, nil))
   181  }
   182  
   183  func newUnstructured(kind string, name string, status *types.RolloutStatus, err error) *unstructured.Unstructured {
   184  	return &unstructured.Unstructured{Object: newObject(kind, name, status, err)}
   185  }
   186  
   187  func TestWaitUntilComplete(t *testing.T) {
   188  	statusMapper = testStatusMapper
   189  	defer func() {
   190  		statusMapper = types.StatusFuncFor
   191  	}()
   192  
   193  	foo1, foo2 := newTestMeta("Foo", "foo1"), newTestMeta("Foo", "foo2")
   194  	bar1 := newTestMeta("Bar", "bar1")
   195  	baz1 := newTestMeta("Baz", "baz1")
   196  
   197  	wf := &watchFactory{
   198  		eventsMap: map[string][]testEvent{
   199  			testKey(foo1): {
   200  				{
   201  					wait: 0,
   202  					event: watch.Event{
   203  						Type:   watch.Modified,
   204  						Object: newUnstructured(foo1.GetKind(), foo1.GetName(), &types.RolloutStatus{Description: "start"}, nil),
   205  					},
   206  				},
   207  				{
   208  					wait: 10 * time.Millisecond,
   209  					event: watch.Event{
   210  						Type:   watch.Modified,
   211  						Object: newUnstructured(foo1.GetKind(), foo1.GetName(), &types.RolloutStatus{Description: "mid"}, nil),
   212  					},
   213  				},
   214  				{
   215  					wait: 20 * time.Millisecond,
   216  					event: watch.Event{
   217  						Type:   watch.Modified,
   218  						Object: newUnstructured(foo1.GetKind(), foo1.GetName(), &types.RolloutStatus{Description: "end", Done: true}, nil),
   219  					},
   220  				},
   221  			},
   222  			testKey(foo2): {
   223  				{
   224  					wait: 5 * time.Millisecond,
   225  					event: watch.Event{
   226  						Type:   watch.Modified,
   227  						Object: newUnstructured(foo2.GetKind(), foo2.GetName(), &types.RolloutStatus{Description: "done", Done: true}, nil),
   228  					},
   229  				},
   230  			},
   231  			testKey(bar1): {
   232  				{
   233  					wait: 30 * time.Millisecond,
   234  					event: watch.Event{
   235  						Type:   watch.Modified,
   236  						Object: newUnstructured(bar1.GetKind(), bar1.GetName(), &types.RolloutStatus{Description: "done", Done: true}, nil),
   237  					},
   238  				},
   239  			},
   240  		},
   241  	}
   242  	listener := newTestListener(t)
   243  	err := WaitUntilComplete(
   244  		[]model.K8sMeta{foo1, foo2, bar1, baz1},
   245  		wf.getWatcher,
   246  		WaitOptions{Listener: listener, Timeout: time.Second},
   247  	)
   248  	require.Nil(t, err)
   249  	a := assert.New(t)
   250  	a.Equal(3, listener.initObjects)
   251  	a.Equal(0, listener.remainingObjects)
   252  	a.Equal([]string{"start", "mid", "end"}, listener.statuses[testKey(foo1)])
   253  	a.Equal([]string{"done"}, listener.statuses[testKey(foo2)])
   254  	a.Equal([]string{"done"}, listener.statuses[testKey(bar1)])
   255  }
   256  
   257  func TestWaitUntilCompleteDefaultOpts(t *testing.T) {
   258  	statusMapper = testStatusMapper
   259  	defer func() {
   260  		statusMapper = types.StatusFuncFor
   261  	}()
   262  
   263  	bar1 := newTestMeta("Bar", "bar1")
   264  
   265  	wf := &watchFactory{
   266  		eventsMap: map[string][]testEvent{
   267  			testKey(bar1): {
   268  				{
   269  					wait: 30 * time.Millisecond,
   270  					event: watch.Event{
   271  						Type:   watch.Modified,
   272  						Object: newUnstructured(bar1.GetKind(), bar1.GetName(), &types.RolloutStatus{Description: "done", Done: true}, nil),
   273  					},
   274  				},
   275  			},
   276  		},
   277  	}
   278  	err := WaitUntilComplete(
   279  		[]model.K8sMeta{bar1},
   280  		wf.getWatcher,
   281  		WaitOptions{},
   282  	)
   283  	require.Nil(t, err)
   284  }
   285  
   286  type runtimeFoo struct{}
   287  
   288  func (r runtimeFoo) GetObjectKind() schema.ObjectKind {
   289  	bar1 := newTestMeta("Bar", "bar1")
   290  	return newUnstructured(bar1.GetKind(), bar1.GetName(), nil, nil)
   291  }
   292  
   293  func (r runtimeFoo) DeepCopyObject() runtime.Object {
   294  	return r
   295  }
   296  
   297  func TestWaitNegative(t *testing.T) {
   298  	statusMapper = testStatusMapper
   299  	defer func() {
   300  		statusMapper = types.StatusFuncFor
   301  	}()
   302  	foo1, foo2 := newTestMeta("Foo", "foo1"), newTestMeta("Foo", "foo2")
   303  	bar1 := newTestMeta("Bar", "bar1")
   304  	baz1 := newTestMeta("Baz", "baz1")
   305  
   306  	tests := []struct {
   307  		name     string
   308  		objs     []model.K8sMeta
   309  		init     func() *watchFactory
   310  		asserter func(t *testing.T, err error, listener *testListener)
   311  	}{
   312  		{
   313  			name: "timeout",
   314  			objs: []model.K8sMeta{foo1, bar1},
   315  			init: func() *watchFactory {
   316  				return &watchFactory{
   317  					eventsMap: map[string][]testEvent{
   318  						testKey(foo1): {
   319  							{
   320  								wait: 0,
   321  								event: watch.Event{
   322  									Type:   watch.Modified,
   323  									Object: newUnstructured(foo1.GetKind(), foo1.GetName(), &types.RolloutStatus{Description: "start"}, nil),
   324  								},
   325  							},
   326  							{
   327  								wait: 10 * time.Millisecond,
   328  								event: watch.Event{
   329  									Type:   watch.Modified,
   330  									Object: newUnstructured(foo1.GetKind(), foo1.GetName(), &types.RolloutStatus{Description: "mid"}, nil),
   331  								},
   332  							},
   333  						},
   334  						testKey(bar1): {
   335  							{
   336  								wait: 30 * time.Millisecond,
   337  								event: watch.Event{
   338  									Type:   watch.Modified,
   339  									Object: newUnstructured(bar1.GetKind(), bar1.GetName(), &types.RolloutStatus{Description: "done", Done: true}, nil),
   340  								},
   341  							},
   342  						},
   343  					},
   344  				}
   345  			},
   346  			asserter: func(t *testing.T, err error, listener *testListener) {
   347  				require.NotNil(t, err)
   348  				a := assert.New(t)
   349  				a.Equal("wait timed out after 1s", err.Error())
   350  				a.Equal(2, listener.initObjects)
   351  				a.Equal(1, listener.remainingObjects)
   352  				a.Equal([]string{"start", "mid"}, listener.statuses[testKey(foo1)])
   353  				a.Equal([]string{"done"}, listener.statuses[testKey(bar1)])
   354  			},
   355  		},
   356  		{
   357  			name: "watch error event",
   358  			objs: []model.K8sMeta{foo1},
   359  			init: func() *watchFactory {
   360  				return &watchFactory{
   361  					eventsMap: map[string][]testEvent{
   362  						testKey(foo1): {
   363  							{
   364  								wait: 0,
   365  								event: watch.Event{
   366  									Type:   watch.Error,
   367  									Object: newUnstructured(foo1.GetKind(), foo1.GetName(), nil, nil),
   368  								},
   369  							},
   370  						},
   371  					},
   372  				}
   373  			},
   374  			asserter: func(t *testing.T, err error, listener *testListener) {
   375  				require.NotNil(t, err)
   376  				a := assert.New(t)
   377  				a.Equal("1 wait errors", err.Error())
   378  				a.Equal(1, listener.initObjects)
   379  				a.Equal(1, listener.remainingObjects)
   380  				var dummy []string
   381  				a.EqualValues(dummy, listener.statuses[testKey(foo1)])
   382  				e := listener.errors[testKey(foo1)]
   383  				require.NotNil(t, err)
   384  				a.Contains(e, "watch error")
   385  			},
   386  		},
   387  		{
   388  			name: "obj delete",
   389  			objs: []model.K8sMeta{foo1},
   390  			init: func() *watchFactory {
   391  				return &watchFactory{
   392  					eventsMap: map[string][]testEvent{
   393  						testKey(foo1): {
   394  							{
   395  								wait: 0,
   396  								event: watch.Event{
   397  									Type: watch.Deleted,
   398  								},
   399  							},
   400  						},
   401  					},
   402  				}
   403  			},
   404  			asserter: func(t *testing.T, err error, listener *testListener) {
   405  				require.NotNil(t, err)
   406  				a := assert.New(t)
   407  				a.Equal("1 wait errors", err.Error())
   408  				a.Equal(1, listener.initObjects)
   409  				a.Equal(1, listener.remainingObjects)
   410  				var dummy []string
   411  				a.EqualValues(dummy, listener.statuses[testKey(foo1)])
   412  				e := listener.errors[testKey(foo1)]
   413  				require.NotNil(t, err)
   414  				a.Contains(e, "object was deleted")
   415  			},
   416  		},
   417  		{
   418  			name: "get watch error",
   419  			objs: []model.K8sMeta{foo1},
   420  			init: func() *watchFactory {
   421  				return &watchFactory{
   422  					eventsMap: map[string][]testEvent{},
   423  				}
   424  			},
   425  			asserter: func(t *testing.T, err error, listener *testListener) {
   426  				require.NotNil(t, err)
   427  				a := assert.New(t)
   428  				a.Equal("1 wait errors", err.Error())
   429  				a.Equal(1, listener.initObjects)
   430  				a.Equal(1, listener.remainingObjects)
   431  				var dummy []string
   432  				a.EqualValues(dummy, listener.statuses[testKey(foo1)])
   433  				e := listener.errors[testKey(foo1)]
   434  				require.NotNil(t, err)
   435  				a.Contains(e, "unable to produce events")
   436  			},
   437  		},
   438  		{
   439  			name: "status func error",
   440  			objs: []model.K8sMeta{foo1},
   441  			init: func() *watchFactory {
   442  				return &watchFactory{
   443  					eventsMap: map[string][]testEvent{
   444  						testKey(foo1): {
   445  							{
   446  								wait: 0,
   447  								event: watch.Event{
   448  									Type:   watch.Modified,
   449  									Object: newUnstructured(foo1.GetKind(), foo1.GetName(), nil, fmt.Errorf("fubar")),
   450  								},
   451  							},
   452  						},
   453  					},
   454  				}
   455  			},
   456  			asserter: func(t *testing.T, err error, listener *testListener) {
   457  				require.NotNil(t, err)
   458  				a := assert.New(t)
   459  				a.Equal("1 wait errors", err.Error())
   460  				a.Equal(1, listener.initObjects)
   461  				a.Equal(1, listener.remainingObjects)
   462  				var dummy []string
   463  				a.EqualValues(dummy, listener.statuses[testKey(foo1)])
   464  				e := listener.errors[testKey(foo1)]
   465  				require.NotNil(t, err)
   466  				a.Contains(e, "fubar")
   467  			},
   468  		},
   469  		{
   470  			name: "no objects",
   471  			objs: []model.K8sMeta{baz1},
   472  			init: func() *watchFactory {
   473  				return &watchFactory{
   474  					eventsMap: map[string][]testEvent{},
   475  				}
   476  			},
   477  			asserter: func(t *testing.T, err error, listener *testListener) {
   478  				require.Nil(t, err)
   479  				a := assert.New(t)
   480  				a.True(listener.initCalled)
   481  				a.True(listener.endCalled)
   482  			},
   483  		},
   484  		{
   485  			name: "bad object",
   486  			objs: []model.K8sMeta{foo1},
   487  			init: func() *watchFactory {
   488  				return &watchFactory{
   489  					eventsMap: map[string][]testEvent{
   490  						testKey(foo1): {
   491  							{
   492  								wait: 0,
   493  								event: watch.Event{
   494  									Type:   watch.Modified,
   495  									Object: runtimeFoo{},
   496  								},
   497  							},
   498  						},
   499  					},
   500  				}
   501  			},
   502  			asserter: func(t *testing.T, err error, listener *testListener) {
   503  				require.NotNil(t, err)
   504  				a := assert.New(t)
   505  				a.Equal("1 wait errors", err.Error())
   506  				a.True(listener.initCalled)
   507  				a.True(listener.endCalled)
   508  				e := listener.errors[testKey(foo1)]
   509  				require.NotNil(t, err)
   510  				a.Contains(e, "unexpected watch object type")
   511  			},
   512  		},
   513  	}
   514  	t.Log(foo2, baz1)
   515  	for _, test := range tests {
   516  		t.Run(test.name, func(t *testing.T) {
   517  			wf := test.init()
   518  			listener := newTestListener(t)
   519  			err := WaitUntilComplete(
   520  				test.objs,
   521  				wf.getWatcher,
   522  				WaitOptions{Listener: listener, Timeout: time.Second},
   523  			)
   524  			test.asserter(t, err, listener)
   525  		})
   526  	}
   527  }