github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/boskos/mason/mason_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 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 mason 18 19 import ( 20 "fmt" 21 "reflect" 22 "sort" 23 "strings" 24 "testing" 25 "time" 26 27 "context" 28 29 "k8s.io/test-infra/boskos/common" 30 "k8s.io/test-infra/boskos/ranch" 31 "k8s.io/test-infra/boskos/storage" 32 ) 33 34 var ( 35 errConstruct = fmt.Errorf("failed to construct") 36 ) 37 38 const ( 39 fakeConfigType = "fakeConfig" 40 emptyContent = "empty content" 41 owner = "mason" 42 defaultWaitPeriod = 100 * time.Millisecond 43 ) 44 45 func errorsEqual(a, b error) bool { 46 if a == nil && b == nil { 47 return true 48 } 49 if a != nil && b != nil { 50 return a.Error() == b.Error() 51 } 52 return false 53 } 54 55 type releasedResource struct { 56 name, state string 57 } 58 59 type fakeBoskos struct { 60 ranch *ranch.Ranch 61 releasedResources chan releasedResource 62 } 63 64 type testConfig map[string]struct { 65 resourceNeeds *common.ResourceNeeds 66 count int 67 } 68 69 type fakeConfig struct { 70 sleepTime time.Duration 71 err error 72 } 73 74 func fakeConfigConverter(in string) (Masonable, error) { 75 return &fakeConfig{sleepTime: 0}, nil 76 } 77 78 func failingConfigConverter(in string) (Masonable, error) { 79 return &fakeConfig{sleepTime: 0, err: errConstruct}, nil 80 } 81 82 func timeoutConfigConverter(in string) (Masonable, error) { 83 return &fakeConfig{sleepTime: defaultWaitPeriod}, nil 84 } 85 86 func (fc *fakeConfig) Construct(ctx context.Context, res common.Resource, typeToRes common.TypeToResources) (*common.UserData, error) { 87 // Mess around with data 88 res.Name = "nothingToDo" 89 res.State = "unknown" 90 res.UserData = common.UserDataFromMap(common.UserDataMap{"test": "test"}) 91 for k := range typeToRes { 92 delete(typeToRes, k) 93 } 94 time.Sleep(fc.sleepTime) 95 return common.UserDataFromMap(common.UserDataMap{"fakeConfig": "unused"}), fc.err 96 } 97 98 // Create a fake client 99 func createFakeBoskos(tc testConfig) (*ranch.Storage, *Client, []common.ResourcesConfig, chan releasedResource) { 100 names := make(chan releasedResource, 100) 101 configNames := map[string]bool{} 102 var configs []common.ResourcesConfig 103 s, _ := ranch.NewStorage(storage.NewMemoryStorage(), "") 104 r, _ := ranch.NewRanch("", s) 105 106 for rtype, c := range tc { 107 for i := 0; i < c.count; i++ { 108 res := common.Resource{ 109 Type: rtype, 110 Name: fmt.Sprintf("%s_%d", rtype, i), 111 State: common.Free, 112 UserData: &common.UserData{}, 113 } 114 if c.resourceNeeds != nil { 115 res.State = common.Dirty 116 if _, ok := configNames[rtype]; !ok { 117 configNames[rtype] = true 118 configs = append(configs, common.ResourcesConfig{ 119 Config: common.ConfigType{ 120 Type: fakeConfigType, 121 Content: emptyContent, 122 }, 123 Name: rtype, 124 Needs: *c.resourceNeeds, 125 }) 126 } 127 } 128 s.AddResource(res) 129 } 130 } 131 return s, NewClient(&fakeBoskos{ranch: r, releasedResources: names}), configs, names 132 } 133 134 func (fb *fakeBoskos) Acquire(rtype, state, dest string) (*common.Resource, error) { 135 return fb.ranch.Acquire(rtype, state, dest, owner) 136 } 137 138 func (fb *fakeBoskos) AcquireByState(state, dest string, names []string) ([]common.Resource, error) { 139 return fb.ranch.AcquireByState(state, dest, owner, names) 140 } 141 142 func (fb *fakeBoskos) ReleaseOne(name, dest string) error { 143 fb.releasedResources <- releasedResource{name: name, state: dest} 144 return fb.ranch.Release(name, dest, owner) 145 } 146 147 func (fb *fakeBoskos) UpdateOne(name, state string, userData *common.UserData) error { 148 return fb.ranch.Update(name, owner, state, userData) 149 } 150 151 func (fb *fakeBoskos) UpdateAll(state string) error { 152 // not used in this test 153 return nil 154 } 155 156 func (fb *fakeBoskos) ReleaseAll(state string) error { 157 // not used in this test 158 return nil 159 } 160 161 func (fb *fakeBoskos) SyncAll() error { 162 // not used in this test 163 return nil 164 } 165 166 func TestRecycleLeasedResources(t *testing.T) { 167 tc := testConfig{ 168 "type1": { 169 count: 1, 170 }, 171 "type2": { 172 resourceNeeds: &common.ResourceNeeds{ 173 "type1": 1, 174 }, 175 count: 1, 176 }, 177 } 178 179 rStorage, mClient, configs, _ := createFakeBoskos(tc) 180 res1, _ := rStorage.GetResource("type1_0") 181 res1.State = "type2_0" 182 rStorage.UpdateResource(res1) 183 res2, _ := rStorage.GetResource("type2_0") 184 res2.UserData.Set(LeasedResources, &[]string{"type1_0"}) 185 rStorage.UpdateResource(res2) 186 m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod) 187 m.storage.SyncConfigs(configs) 188 m.RegisterConfigConverter(fakeConfigType, fakeConfigConverter) 189 ctx, cancel := context.WithCancel(context.Background()) 190 m.cancel = cancel 191 m.start(ctx, m.recycleAll) 192 select { 193 case <-m.pending: 194 break 195 case <-time.After(1 * time.Second): 196 t.Errorf("Timeout") 197 } 198 m.Stop() 199 res1, _ = rStorage.GetResource("type1_0") 200 res2, _ = rStorage.GetResource("type2_0") 201 if res2.State != common.Cleaning { 202 t.Errorf("Resource state should be cleaning, found %s", res2.State) 203 } 204 if res1.State != common.Dirty { 205 t.Errorf("Resource state should be dirty, found %s", res1.State) 206 } 207 } 208 209 func TestRecycleNoLeasedResources(t *testing.T) { 210 tc := testConfig{ 211 "type1": { 212 count: 1, 213 }, 214 "type2": { 215 resourceNeeds: &common.ResourceNeeds{ 216 "type1": 1, 217 }, 218 count: 1, 219 }, 220 } 221 222 rStorage, mClient, configs, _ := createFakeBoskos(tc) 223 m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod) 224 m.storage.SyncConfigs(configs) 225 m.RegisterConfigConverter(fakeConfigType, fakeConfigConverter) 226 ctx, cancel := context.WithCancel(context.Background()) 227 m.cancel = cancel 228 m.start(ctx, m.recycleAll) 229 select { 230 case <-m.pending: 231 break 232 case <-time.After(1 * time.Second): 233 t.Errorf("Timeout") 234 } 235 m.Stop() 236 res1, _ := rStorage.GetResource("type1_0") 237 res2, _ := rStorage.GetResource("type2_0") 238 if res2.State != common.Cleaning { 239 t.Errorf("Resource state should be cleaning") 240 } 241 if res1.State != common.Free { 242 t.Errorf("Resource state should be untouched, current %s", mClient.resources["type1_0"].State) 243 } 244 } 245 246 func TestCleanOne(t *testing.T) { 247 testCases := []struct { 248 name string 249 configConvert ConfigConverter 250 err error 251 timeout bool 252 }{ 253 { 254 name: "success", 255 configConvert: fakeConfigConverter, 256 }, 257 { 258 name: "constructFailure", 259 configConvert: failingConfigConverter, 260 err: errConstruct, 261 }, 262 { 263 name: "constructTimeout", 264 configConvert: timeoutConfigConverter, 265 err: fmt.Errorf("context deadline exceeded"), 266 timeout: true, 267 }, 268 } 269 270 config := testConfig{ 271 "type1": { 272 count: 1, 273 }, 274 "type2": { 275 resourceNeeds: &common.ResourceNeeds{ 276 "type1": 1, 277 }, 278 count: 1, 279 }, 280 } 281 282 for _, tc := range testCases { 283 t.Run(tc.name, func(tt *testing.T) { 284 rStorage, mClient, configs, _ := createFakeBoskos(config) 285 m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod) 286 m.storage.SyncConfigs(configs) 287 m.RegisterConfigConverter(fakeConfigType, tc.configConvert) 288 masonRes, err := m.client.Acquire("type2", common.Dirty, common.Cleaning) 289 if err != nil { 290 t.Errorf("unexpected error %v", err) 291 t.FailNow() 292 } 293 req := requirements{ 294 resource: *masonRes, 295 needs: *config["type2"].resourceNeeds, 296 fulfillment: common.TypeToResources{}, 297 } 298 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 299 defer cancel() 300 if err := m.fulfillOne(ctx, &req); err != nil { 301 t.Errorf("unexpected error %v", err) 302 } 303 304 if tc.timeout { 305 ctx, cancel = context.WithTimeout(context.Background(), defaultWaitPeriod/2) 306 defer cancel() 307 } 308 309 err = m.cleanOne(ctx, &req.resource, req.fulfillment) 310 if !errorsEqual(tc.err, err) { 311 tt.Errorf("expected error %v got %v", tc.err, err) 312 } 313 m.garbageCollect(req) 314 resources, err := rStorage.GetResources() 315 if err != nil { 316 t.Errorf("unexpected error %v", err) 317 t.FailNow() 318 } 319 for _, res := range resources { 320 if res.State != common.Dirty { 321 tt.Errorf("resource %v should be released as dirty", res) 322 } 323 324 } 325 }) 326 } 327 } 328 329 func TestFulfillOne(t *testing.T) { 330 tc := testConfig{ 331 "type1": { 332 count: 1, 333 }, 334 "type2": { 335 resourceNeeds: &common.ResourceNeeds{ 336 "type1": 1, 337 }, 338 count: 1, 339 }, 340 } 341 342 rStorage, mClient, configs, _ := createFakeBoskos(tc) 343 m := NewMason(1, mClient.basic, defaultWaitPeriod, defaultWaitPeriod) 344 m.storage.SyncConfigs(configs) 345 res, _ := mClient.basic.Acquire("type2", common.Dirty, common.Cleaning) 346 conf, err := m.storage.GetConfig("type2") 347 if err != nil { 348 t.Error("failed to get config") 349 } 350 req := requirements{ 351 resource: *res, 352 needs: conf.Needs, 353 fulfillment: common.TypeToResources{}, 354 } 355 if err = m.fulfillOne(context.Background(), &req); err != nil { 356 t.Errorf("could not satisfy requirements ") 357 } 358 if len(req.fulfillment) != 1 { 359 t.Errorf("there should be only one type") 360 } 361 if len(req.fulfillment["type1"]) != 1 { 362 t.Errorf("there should be only one resources") 363 } 364 userRes := req.fulfillment["type1"][0] 365 leasedResource, _ := rStorage.GetResource(userRes.Name) 366 if leasedResource.State != common.Leased { 367 t.Errorf("State should be Leased") 368 } 369 *res, _ = rStorage.GetResource(res.Name) 370 var leasedResources common.LeasedResources 371 if res.UserData.Extract(LeasedResources, &leasedResources); err != nil { 372 t.Errorf("unable to extract %s", LeasedResources) 373 } 374 if res.UserData.ToMap()[LeasedResources] != req.resource.UserData.ToMap()[LeasedResources] { 375 t.Errorf( 376 "resource user data from requirement %v should be the same as the one received %v", 377 req.resource.UserData.ToMap()[LeasedResources], res.UserData.ToMap()[LeasedResources]) 378 } 379 if len(leasedResources) != 1 { 380 t.Errorf("there should be one leased resource, found %d", len(leasedResources)) 381 } 382 if leasedResources[0] != leasedResource.Name { 383 t.Errorf("Leased resource don t match") 384 } 385 } 386 387 func TestMason(t *testing.T) { 388 count := 10 389 tc := testConfig{ 390 "type1": { 391 count: count, 392 }, 393 "type2": { 394 resourceNeeds: &common.ResourceNeeds{ 395 "type1": 1, 396 }, 397 count: count, 398 }, 399 } 400 rStorage, mClient, configs, releasedResources := createFakeBoskos(tc) 401 m := NewMason(10, mClient.basic, defaultWaitPeriod, defaultWaitPeriod) 402 m.storage.SyncConfigs(configs) 403 m.RegisterConfigConverter(fakeConfigType, fakeConfigConverter) 404 m.Start() 405 timeout := time.NewTicker(5 * time.Second).C 406 i := 0 407 loop: 408 for { 409 select { 410 case <-timeout: 411 t.Errorf("Test timed ouf") 412 t.FailNow() 413 case rr := <-releasedResources: 414 if strings.HasPrefix(rr.name, "type2_") { 415 if rr.state != common.Free { 416 t.Errorf("resource %s should be at state %s, found %s", rr.name, common.Free, rr.state) 417 } 418 } else if strings.HasPrefix(rr.name, "type1_") { 419 if !strings.HasPrefix(rr.state, "type2_") { 420 t.Errorf("resource %s should be starting with type2_, found %s", rr.name, rr.state) 421 } 422 } 423 i++ 424 if i >= 2*count { 425 break loop 426 } 427 } 428 } 429 430 leasedResourceFromRes := func(r common.Resource) (l []common.Resource) { 431 var leasedResources []string 432 r.UserData.Extract(LeasedResources, &leasedResources) 433 for _, name := range leasedResources { 434 r, _ := rStorage.GetResource(name) 435 l = append(l, r) 436 } 437 return 438 } 439 440 var resourcesToRelease []common.Resource 441 442 for i := 0; i < 10; i++ { 443 res, err := mClient.Acquire("type2", common.Free, "Used") 444 if err != nil { 445 t.Errorf("Count %d: There should be free resources", i) 446 t.FailNow() 447 } 448 leasedResources := leasedResourceFromRes(*res) 449 if len(leasedResources) != 1 { 450 t.Error("there should be 1 resource of type1") 451 } 452 for _, r := range leasedResources { 453 if r.Type != "type1" { 454 t.Error("resource should be of type type1") 455 } 456 } 457 resourcesToRelease = append(resourcesToRelease, *res) 458 } 459 if _, err := mClient.Acquire("type2", common.Free, "Used"); err == nil { 460 t.Error("there should not be any resource left") 461 } 462 m.Stop() 463 for _, res := range resourcesToRelease { 464 if err := mClient.ReleaseOne(res.Name, common.Dirty); err != nil { 465 t.Error("unable to release leased resources") 466 467 } 468 } 469 resources, _ := rStorage.GetResources() 470 for _, r := range resources { 471 if r.State != common.Dirty { 472 t.Errorf("state should be %s, found %s", common.Dirty, r.State) 473 } 474 } 475 } 476 477 func TestMasonStartStop(t *testing.T) { 478 tc := testConfig{ 479 "type1": { 480 count: 10, 481 }, 482 "type2": { 483 resourceNeeds: &common.ResourceNeeds{ 484 "type1": 1, 485 }, 486 count: 10, 487 }, 488 } 489 _, mClient, configs, _ := createFakeBoskos(tc) 490 m := NewMason(5, mClient.basic, defaultWaitPeriod, defaultWaitPeriod) 491 m.storage.SyncConfigs(configs) 492 m.RegisterConfigConverter(fakeConfigType, failingConfigConverter) 493 m.Start() 494 done := make(chan bool) 495 go func() { 496 m.Stop() 497 done <- true 498 }() 499 select { 500 case <-time.After(time.Second): 501 t.Errorf("unable to stop mason") 502 case <-done: 503 } 504 } 505 506 func TestConfig(t *testing.T) { 507 resources, err := ranch.ParseConfig("test-resources.yaml") 508 if err != nil { 509 t.Error(err) 510 } 511 configs, err := ParseConfig("test-configs.yaml") 512 if err != nil { 513 t.Error(err) 514 } 515 if err := ValidateConfig(configs, resources); err == nil { 516 t.Error(err) 517 } 518 } 519 520 func makeFakeConfig(name, cType, content string, needs int) common.ResourcesConfig { 521 c := common.ResourcesConfig{ 522 Name: name, 523 Needs: common.ResourceNeeds{}, 524 Config: common.ConfigType{ 525 Type: cType, 526 Content: content, 527 }, 528 } 529 for i := 0; i < needs; i++ { 530 c.Needs[fmt.Sprintf("type_%d", i)] = i 531 } 532 return c 533 } 534 535 func TestSyncConfig(t *testing.T) { 536 var testcases = []struct { 537 name string 538 oldConfig []common.ResourcesConfig 539 newConfig []common.ResourcesConfig 540 expect []common.ResourcesConfig 541 }{ 542 { 543 name: "empty", 544 }, 545 { 546 name: "deleteAll", 547 oldConfig: []common.ResourcesConfig{ 548 makeFakeConfig("config1", "fakeType", "", 2), 549 makeFakeConfig("config2", "fakeType", "", 3), 550 makeFakeConfig("config3", "fakeType", "", 4), 551 }, 552 }, 553 { 554 name: "new", 555 newConfig: []common.ResourcesConfig{ 556 makeFakeConfig("config1", "fakeType", "", 2), 557 makeFakeConfig("config2", "fakeType", "", 3), 558 makeFakeConfig("config3", "fakeType", "", 4), 559 }, 560 expect: []common.ResourcesConfig{ 561 makeFakeConfig("config1", "fakeType", "", 2), 562 makeFakeConfig("config2", "fakeType", "", 3), 563 makeFakeConfig("config3", "fakeType", "", 4), 564 }, 565 }, 566 { 567 name: "noChange", 568 oldConfig: []common.ResourcesConfig{ 569 makeFakeConfig("config1", "fakeType", "", 2), 570 makeFakeConfig("config2", "fakeType", "", 3), 571 makeFakeConfig("config3", "fakeType", "", 4), 572 }, 573 newConfig: []common.ResourcesConfig{ 574 makeFakeConfig("config1", "fakeType", "", 2), 575 makeFakeConfig("config2", "fakeType", "", 3), 576 makeFakeConfig("config3", "fakeType", "", 4), 577 }, 578 expect: []common.ResourcesConfig{ 579 makeFakeConfig("config1", "fakeType", "", 2), 580 makeFakeConfig("config2", "fakeType", "", 3), 581 makeFakeConfig("config3", "fakeType", "", 4), 582 }, 583 }, 584 { 585 name: "update", 586 oldConfig: []common.ResourcesConfig{ 587 makeFakeConfig("config1", "fakeType", "", 2), 588 makeFakeConfig("config2", "fakeType", "", 3), 589 makeFakeConfig("config3", "fakeType", "", 4), 590 }, 591 newConfig: []common.ResourcesConfig{ 592 makeFakeConfig("config1", "fakeType2", "", 2), 593 makeFakeConfig("config2", "fakeType", "something", 3), 594 makeFakeConfig("config3", "fakeType", "", 5), 595 }, 596 expect: []common.ResourcesConfig{ 597 makeFakeConfig("config1", "fakeType2", "", 2), 598 makeFakeConfig("config2", "fakeType", "something", 3), 599 makeFakeConfig("config3", "fakeType", "", 5), 600 }, 601 }, 602 { 603 name: "delete", 604 oldConfig: []common.ResourcesConfig{ 605 makeFakeConfig("config1", "fakeType", "", 2), 606 makeFakeConfig("config2", "fakeType", "", 3), 607 makeFakeConfig("config3", "fakeType", "", 4), 608 }, 609 newConfig: []common.ResourcesConfig{ 610 makeFakeConfig("config1", "fakeType2", "", 2), 611 makeFakeConfig("config3", "fakeType", "", 5), 612 }, 613 expect: []common.ResourcesConfig{ 614 makeFakeConfig("config1", "fakeType2", "", 2), 615 makeFakeConfig("config3", "fakeType", "", 5), 616 }, 617 }, 618 } 619 620 for _, tc := range testcases { 621 s := newStorage(storage.NewMemoryStorage()) 622 s.SyncConfigs(tc.newConfig) 623 configs, err := s.GetConfigs() 624 if err != nil { 625 t.Errorf("failed to get resources") 626 continue 627 } 628 sort.Stable(common.ResourcesConfigByName(configs)) 629 sort.Stable(common.ResourcesConfigByName(tc.expect)) 630 if !reflect.DeepEqual(configs, tc.expect) { 631 t.Errorf("Test %v: got %v, expect %v", tc.name, configs, tc.expect) 632 } 633 } 634 } 635 636 func TestGetConfig(t *testing.T) { 637 var testcases = []struct { 638 name, configName string 639 exists bool 640 configs []common.ResourcesConfig 641 }{ 642 { 643 name: "exists", 644 exists: true, 645 configName: "test", 646 configs: []common.ResourcesConfig{ 647 { 648 Needs: common.ResourceNeeds{"type1": 1, "type2": 2}, 649 Config: common.ConfigType{ 650 Type: "type3", 651 Content: "content", 652 }, 653 Name: "test", 654 }, 655 }, 656 }, 657 { 658 name: "noConfig", 659 exists: false, 660 configName: "test", 661 }, 662 { 663 name: "existsMultipleConfigs", 664 exists: true, 665 configName: "test1", 666 configs: []common.ResourcesConfig{ 667 { 668 Needs: common.ResourceNeeds{"type1": 1, "type2": 2}, 669 Config: common.ConfigType{ 670 Type: "type3", 671 Content: "content", 672 }, 673 Name: "test", 674 }, 675 { 676 Needs: common.ResourceNeeds{"type1": 1, "type2": 2}, 677 Config: common.ConfigType{ 678 Type: "type3", 679 Content: "content", 680 }, 681 Name: "test1", 682 }, 683 }, 684 }, 685 { 686 name: "noExistMultipleConfigs", 687 exists: false, 688 configName: "test2", 689 configs: []common.ResourcesConfig{ 690 { 691 Needs: common.ResourceNeeds{"type1": 1, "type2": 2}, 692 Config: common.ConfigType{ 693 Type: "type3", 694 Content: "content", 695 }, 696 Name: "test", 697 }, 698 { 699 Needs: common.ResourceNeeds{"type1": 1, "type2": 2}, 700 Config: common.ConfigType{ 701 Type: "type3", 702 Content: "content", 703 }, 704 Name: "test1", 705 }, 706 }, 707 }, 708 } 709 for _, tc := range testcases { 710 s := newStorage(storage.NewMemoryStorage()) 711 for _, config := range tc.configs { 712 s.AddConfig(config) 713 } 714 config, err := s.GetConfig(tc.configName) 715 if !tc.exists { 716 if err == nil { 717 t.Error("client should return an error") 718 } 719 } else { 720 if config.Name != tc.configName { 721 t.Error("config name should match") 722 } 723 } 724 } 725 }