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 }