github.com/thanos-io/thanos@v0.32.5/pkg/reloader/reloader_test.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package reloader 5 6 import ( 7 "context" 8 "fmt" 9 "net" 10 "net/http" 11 "net/url" 12 "os" 13 "path" 14 "path/filepath" 15 "runtime" 16 "strings" 17 "sync" 18 "testing" 19 "time" 20 21 "github.com/go-kit/log" 22 "github.com/prometheus/client_golang/prometheus" 23 promtest "github.com/prometheus/client_golang/prometheus/testutil" 24 "go.uber.org/atomic" 25 "go.uber.org/goleak" 26 27 "github.com/efficientgo/core/testutil" 28 ) 29 30 func TestMain(m *testing.M) { 31 goleak.VerifyTestMain(m) 32 } 33 34 func TestReloader_ConfigApply(t *testing.T) { 35 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 36 defer cancel() 37 38 l, err := net.Listen("tcp", "localhost:0") 39 testutil.Ok(t, err) 40 41 reloads := &atomic.Value{} 42 reloads.Store(0) 43 i := 0 44 srv := &http.Server{} 45 srv.Handler = http.HandlerFunc(func(resp http.ResponseWriter, r *http.Request) { 46 i++ 47 if i%2 == 0 { 48 // Every second request, fail to ensure that retry works. 49 resp.WriteHeader(http.StatusServiceUnavailable) 50 return 51 } 52 53 reloads.Store(reloads.Load().(int) + 1) // The only writer. 54 resp.WriteHeader(http.StatusOK) 55 }) 56 go func() { _ = srv.Serve(l) }() 57 defer func() { testutil.Ok(t, srv.Close()) }() 58 59 reloadURL, err := url.Parse(fmt.Sprintf("http://%s", l.Addr().String())) 60 testutil.Ok(t, err) 61 62 dir := t.TempDir() 63 64 testutil.Ok(t, os.Mkdir(filepath.Join(dir, "in"), os.ModePerm)) 65 testutil.Ok(t, os.Mkdir(filepath.Join(dir, "out"), os.ModePerm)) 66 67 var ( 68 input = filepath.Join(dir, "in", "cfg.yaml.tmpl") 69 output = filepath.Join(dir, "out", "cfg.yaml") 70 ) 71 reloader := New(nil, nil, &Options{ 72 ReloadURL: reloadURL, 73 CfgFile: input, 74 CfgOutputFile: output, 75 WatchedDirs: nil, 76 WatchInterval: 9999 * time.Hour, // Disable interval to test watch logic only. 77 RetryInterval: 100 * time.Millisecond, 78 DelayInterval: 1 * time.Millisecond, 79 }) 80 81 // Fail without config. 82 err = reloader.Watch(ctx) 83 testutil.NotOk(t, err) 84 testutil.Assert(t, strings.HasSuffix(err.Error(), "no such file or directory"), "expect error since there is no input config.") 85 86 testutil.Ok(t, os.WriteFile(input, []byte(` 87 config: 88 a: 1 89 b: $(TEST_RELOADER_THANOS_ENV) 90 c: $(TEST_RELOADER_THANOS_ENV2) 91 `), os.ModePerm)) 92 93 // Fail with config but without unset variables. 94 err = reloader.Watch(ctx) 95 testutil.NotOk(t, err) 96 testutil.Assert(t, strings.HasSuffix(err.Error(), `found reference to unset environment variable "TEST_RELOADER_THANOS_ENV"`), "expect error since there envvars are not set.") 97 98 testutil.Ok(t, os.Setenv("TEST_RELOADER_THANOS_ENV", "2")) 99 testutil.Ok(t, os.Setenv("TEST_RELOADER_THANOS_ENV2", "3")) 100 101 rctx, cancel2 := context.WithCancel(ctx) 102 g := sync.WaitGroup{} 103 g.Add(1) 104 go func() { 105 defer g.Done() 106 testutil.Ok(t, reloader.Watch(rctx)) 107 }() 108 109 reloadsSeen := 0 110 attemptsCnt := 0 111 Outer: 112 for { 113 select { 114 case <-ctx.Done(): 115 break Outer 116 case <-time.After(300 * time.Millisecond): 117 } 118 119 rel := reloads.Load().(int) 120 reloadsSeen = rel 121 122 if reloadsSeen == 1 { 123 // Initial apply seen (without doing nothing). 124 f, err := os.ReadFile(output) 125 testutil.Ok(t, err) 126 testutil.Equals(t, ` 127 config: 128 a: 1 129 b: 2 130 c: 3 131 `, string(f)) 132 133 // Change config, expect reload in another iteration. 134 testutil.Ok(t, os.WriteFile(input, []byte(` 135 config: 136 a: changed 137 b: $(TEST_RELOADER_THANOS_ENV) 138 c: $(TEST_RELOADER_THANOS_ENV2) 139 `), os.ModePerm)) 140 } else if reloadsSeen == 2 { 141 // Another apply, ensure we see change. 142 f, err := os.ReadFile(output) 143 testutil.Ok(t, err) 144 testutil.Equals(t, ` 145 config: 146 a: changed 147 b: 2 148 c: 3 149 `, string(f)) 150 151 // Change the mode so reloader can't read the file. 152 testutil.Ok(t, os.Chmod(input, os.ModeDir)) 153 attemptsCnt++ 154 // That was the second attempt to reload config. All good, break. 155 if attemptsCnt == 2 { 156 break 157 } 158 } 159 } 160 cancel2() 161 g.Wait() 162 163 testutil.Ok(t, os.Unsetenv("TEST_RELOADER_THANOS_ENV")) 164 testutil.Ok(t, os.Unsetenv("TEST_RELOADER_THANOS_ENV2")) 165 } 166 167 func TestReloader_ConfigRollback(t *testing.T) { 168 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 169 defer cancel() 170 171 l, err := net.Listen("tcp", "localhost:0") 172 testutil.Ok(t, err) 173 174 correctConfig := []byte(` 175 config: 176 a: 1 177 `) 178 faultyConfig := []byte(` 179 faulty_config: 180 a: 1 181 `) 182 183 dir := t.TempDir() 184 185 testutil.Ok(t, os.Mkdir(filepath.Join(dir, "in"), os.ModePerm)) 186 testutil.Ok(t, os.Mkdir(filepath.Join(dir, "out"), os.ModePerm)) 187 188 var ( 189 input = filepath.Join(dir, "in", "cfg.yaml.tmpl") 190 output = filepath.Join(dir, "out", "cfg.yaml") 191 ) 192 193 reloads := &atomic.Value{} 194 reloads.Store(0) 195 srv := &http.Server{} 196 197 srv.Handler = http.HandlerFunc(func(resp http.ResponseWriter, r *http.Request) { 198 f, err := os.ReadFile(output) 199 testutil.Ok(t, err) 200 201 if string(f) == string(faultyConfig) { 202 resp.WriteHeader(http.StatusServiceUnavailable) 203 return 204 } 205 206 reloads.Store(reloads.Load().(int) + 1) // The only writer. 207 resp.WriteHeader(http.StatusOK) 208 }) 209 go func() { _ = srv.Serve(l) }() 210 defer func() { testutil.Ok(t, srv.Close()) }() 211 212 reloadURL, err := url.Parse(fmt.Sprintf("http://%s", l.Addr().String())) 213 testutil.Ok(t, err) 214 215 reloader := New(nil, nil, &Options{ 216 ReloadURL: reloadURL, 217 CfgFile: input, 218 CfgOutputFile: output, 219 WatchedDirs: nil, 220 WatchInterval: 10 * time.Second, // 10 seconds to make the reload of faulty config fail quick 221 RetryInterval: 100 * time.Millisecond, 222 DelayInterval: 1 * time.Millisecond, 223 }) 224 225 testutil.Ok(t, os.WriteFile(input, correctConfig, os.ModePerm)) 226 227 rctx, cancel2 := context.WithCancel(ctx) 228 g := sync.WaitGroup{} 229 g.Add(1) 230 go func() { 231 defer g.Done() 232 testutil.Ok(t, reloader.Watch(rctx)) 233 }() 234 235 reloadsSeen := 0 236 faulty := false 237 238 for { 239 select { 240 case <-ctx.Done(): 241 t.Fatalf("Timeout with faulty = %t, reloadsSeen = %d", faulty, reloadsSeen) 242 case <-time.After(300 * time.Millisecond): 243 } 244 245 rel := reloads.Load().(int) 246 reloadsSeen = rel 247 248 if reloadsSeen == 1 && !faulty { 249 // Initial apply seen (without doing anything). 250 f, err := os.ReadFile(output) 251 testutil.Ok(t, err) 252 testutil.Equals(t, string(correctConfig), string(f)) 253 254 // Change to a faulty config 255 testutil.Ok(t, os.WriteFile(input, faultyConfig, os.ModePerm)) 256 faulty = true 257 } else if reloadsSeen == 1 && faulty { 258 // Faulty config will trigger a reload, but reload failed 259 f, err := os.ReadFile(output) 260 testutil.Ok(t, err) 261 testutil.Equals(t, string(faultyConfig), string(f)) 262 263 // Rollback config 264 testutil.Ok(t, os.WriteFile(input, correctConfig, os.ModePerm)) 265 } else if reloadsSeen >= 2 { 266 // Rollback to previous config should trigger a reload 267 f, err := os.ReadFile(output) 268 testutil.Ok(t, err) 269 testutil.Equals(t, string(correctConfig), string(f)) 270 271 break 272 } 273 } 274 cancel2() 275 g.Wait() 276 } 277 278 func TestReloader_DirectoriesApply(t *testing.T) { 279 l, err := net.Listen("tcp", "localhost:0") 280 testutil.Ok(t, err) 281 282 i := 0 283 reloads := 0 284 reloadsMtx := sync.Mutex{} 285 286 srv := &http.Server{} 287 srv.Handler = http.HandlerFunc(func(resp http.ResponseWriter, r *http.Request) { 288 reloadsMtx.Lock() 289 defer reloadsMtx.Unlock() 290 291 i++ 292 if i%2 == 0 { 293 // Fail every second request to ensure that retry works. 294 resp.WriteHeader(http.StatusServiceUnavailable) 295 return 296 } 297 298 reloads++ 299 resp.WriteHeader(http.StatusOK) 300 }) 301 go func() { 302 _ = srv.Serve(l) 303 }() 304 defer func() { testutil.Ok(t, srv.Close()) }() 305 306 reloadURL, err := url.Parse(fmt.Sprintf("http://%s", l.Addr().String())) 307 testutil.Ok(t, err) 308 309 ruleDir := t.TempDir() 310 tempRule1File := path.Join(ruleDir, "rule1.yaml") 311 tempRule3File := path.Join(ruleDir, "rule3.yaml") 312 tempRule4File := path.Join(ruleDir, "rule4.yaml") 313 314 testutil.Ok(t, os.WriteFile(tempRule1File, []byte("rule1-changed"), os.ModePerm)) 315 testutil.Ok(t, os.WriteFile(tempRule3File, []byte("rule3-changed"), os.ModePerm)) 316 testutil.Ok(t, os.WriteFile(tempRule4File, []byte("rule4-changed"), os.ModePerm)) 317 318 dir := t.TempDir() 319 dir2 := t.TempDir() 320 321 // dir 322 // └─ rule-dir -> dir2/rule-dir 323 // dir2 324 // └─ rule-dir 325 testutil.Ok(t, os.Mkdir(path.Join(dir2, "rule-dir"), os.ModePerm)) 326 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule-dir"), path.Join(dir, "rule-dir"))) 327 328 logger := log.NewNopLogger() 329 r := prometheus.NewRegistry() 330 reloader := New( 331 logger, 332 r, 333 &Options{ 334 ReloadURL: reloadURL, 335 CfgFile: "", 336 CfgOutputFile: "", 337 WatchedDirs: []string{dir, path.Join(dir, "rule-dir")}, 338 WatchInterval: 9999 * time.Hour, // Disable interval to test watch logic only. 339 RetryInterval: 100 * time.Millisecond, 340 }) 341 342 // dir 343 // ├─ rule-dir -> dir2/rule-dir 344 // └─ rule1.yaml 345 // dir2 346 // ├─ rule-dir 347 // │ └─ rule4.yaml 348 // ├─ rule3-001.yaml -> rule3-source.yaml 349 // └─ rule3-source.yaml 350 // The reloader watches 2 directories: dir and dir/rule-dir. 351 testutil.Ok(t, os.WriteFile(path.Join(dir, "rule1.yaml"), []byte("rule"), os.ModePerm)) 352 testutil.Ok(t, os.WriteFile(path.Join(dir2, "rule3-source.yaml"), []byte("rule3"), os.ModePerm)) 353 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule3-source.yaml"), path.Join(dir2, "rule3-001.yaml"))) 354 testutil.Ok(t, os.WriteFile(path.Join(dir2, "rule-dir", "rule4.yaml"), []byte("rule4"), os.ModePerm)) 355 356 stepFunc := func(rel int) { 357 t.Log("Performing step number", rel) 358 switch rel { 359 case 0: 360 // Create rule2.yaml. 361 // 362 // dir 363 // ├─ rule-dir -> dir2/rule-dir 364 // ├─ rule1.yaml 365 // └─ rule2.yaml (*) 366 // dir2 367 // ├─ rule-dir 368 // │ └─ rule4.yaml 369 // ├─ rule3-001.yaml -> rule3-source.yaml 370 // └─ rule3-source.yaml 371 testutil.Ok(t, os.WriteFile(path.Join(dir, "rule2.yaml"), []byte("rule2"), os.ModePerm)) 372 case 1: 373 // Update rule1.yaml. 374 // 375 // dir 376 // ├─ rule-dir -> dir2/rule-dir 377 // ├─ rule1.yaml (*) 378 // └─ rule2.yaml 379 // dir2 380 // ├─ rule-dir 381 // │ └─ rule4.yaml 382 // ├─ rule3-001.yaml -> rule3-source.yaml 383 // └─ rule3-source.yaml 384 testutil.Ok(t, os.Rename(tempRule1File, path.Join(dir, "rule1.yaml"))) 385 case 2: 386 // Create dir/rule3.yaml (symlink to rule3-001.yaml). 387 // 388 // dir 389 // ├─ rule-dir -> dir2/rule-dir 390 // ├─ rule1.yaml 391 // ├─ rule2.yaml 392 // └─ rule3.yaml -> dir2/rule3-001.yaml (*) 393 // dir2 394 // ├─ rule-dir 395 // │ └─ rule4.yaml 396 // ├─ rule3-001.yaml -> rule3-source.yaml 397 // └─ rule3-source.yaml 398 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule3-001.yaml"), path.Join(dir2, "rule3.yaml"))) 399 testutil.Ok(t, os.Rename(path.Join(dir2, "rule3.yaml"), path.Join(dir, "rule3.yaml"))) 400 case 3: 401 // Update the symlinked file and replace the symlink file to trigger fsnotify. 402 // 403 // dir 404 // ├─ rule-dir -> dir2/rule-dir 405 // ├─ rule1.yaml 406 // ├─ rule2.yaml 407 // └─ rule3.yaml -> dir2/rule3-002.yaml (*) 408 // dir2 409 // ├─ rule-dir 410 // │ └─ rule4.yaml 411 // ├─ rule3-002.yaml -> rule3-source.yaml (*) 412 // └─ rule3-source.yaml (*) 413 testutil.Ok(t, os.Rename(tempRule3File, path.Join(dir2, "rule3-source.yaml"))) 414 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule3-source.yaml"), path.Join(dir2, "rule3-002.yaml"))) 415 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule3-002.yaml"), path.Join(dir2, "rule3.yaml"))) 416 testutil.Ok(t, os.Rename(path.Join(dir2, "rule3.yaml"), path.Join(dir, "rule3.yaml"))) 417 testutil.Ok(t, os.Remove(path.Join(dir2, "rule3-001.yaml"))) 418 case 4: 419 // Update rule4.yaml in the symlinked directory. 420 // 421 // dir 422 // ├─ rule-dir -> dir2/rule-dir 423 // ├─ rule1.yaml 424 // ├─ rule2.yaml 425 // └─ rule3.yaml -> rule3-source.yaml 426 // dir2 427 // ├─ rule-dir 428 // │ └─ rule4.yaml (*) 429 // └─ rule3-source.yaml 430 testutil.Ok(t, os.Rename(tempRule4File, path.Join(dir2, "rule-dir", "rule4.yaml"))) 431 } 432 } 433 434 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 435 g := sync.WaitGroup{} 436 g.Add(1) 437 go func() { 438 defer g.Done() 439 defer cancel() 440 441 reloadsSeen := 0 442 init := false 443 for { 444 runtime.Gosched() // Ensure during testing on small machine, other go routines have chance to continue. 445 446 select { 447 case <-ctx.Done(): 448 return 449 case <-time.After(500 * time.Millisecond): 450 } 451 452 reloadsMtx.Lock() 453 rel := reloads 454 reloadsMtx.Unlock() 455 if init && rel <= reloadsSeen { 456 continue 457 } 458 459 // Catch up if reloader is step(s) ahead. 460 for skipped := rel - reloadsSeen - 1; skipped > 0; skipped-- { 461 stepFunc(rel - skipped) 462 } 463 464 stepFunc(rel) 465 466 init = true 467 reloadsSeen = rel 468 469 if rel > 4 { 470 // All good. 471 return 472 } 473 } 474 }() 475 err = reloader.Watch(ctx) 476 cancel() 477 g.Wait() 478 479 testutil.Ok(t, err) 480 testutil.Equals(t, 6.0, promtest.ToFloat64(reloader.watcher.watchEvents)) 481 testutil.Equals(t, 0.0, promtest.ToFloat64(reloader.watcher.watchErrors)) 482 testutil.Equals(t, 4.0, promtest.ToFloat64(reloader.reloadErrors)) 483 testutil.Equals(t, 9.0, promtest.ToFloat64(reloader.reloads)) 484 testutil.Equals(t, 5, reloads) 485 } 486 487 func TestReloaderDirectoriesApplyBasedOnWatchInterval(t *testing.T) { 488 l, err := net.Listen("tcp", "localhost:0") 489 testutil.Ok(t, err) 490 491 reloads := &atomic.Value{} 492 reloads.Store(0) 493 srv := &http.Server{} 494 srv.Handler = http.HandlerFunc(func(resp http.ResponseWriter, r *http.Request) { 495 reloads.Store(reloads.Load().(int) + 1) // The only writer. 496 resp.WriteHeader(http.StatusOK) 497 }) 498 go func() { 499 _ = srv.Serve(l) 500 }() 501 defer func() { testutil.Ok(t, srv.Close()) }() 502 503 reloadURL, err := url.Parse(fmt.Sprintf("http://%s", l.Addr().String())) 504 testutil.Ok(t, err) 505 506 dir := t.TempDir() 507 dir2 := t.TempDir() 508 509 // dir 510 // └─ rule-dir -> dir2/rule-dir 511 // dir2 512 // └─ rule-dir 513 testutil.Ok(t, os.Mkdir(path.Join(dir2, "rule-dir"), os.ModePerm)) 514 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule-dir"), path.Join(dir, "rule-dir"))) 515 516 logger := log.NewNopLogger() 517 reloader := New( 518 logger, 519 nil, 520 &Options{ 521 ReloadURL: reloadURL, 522 CfgFile: "", 523 CfgOutputFile: "", 524 WatchedDirs: []string{dir, path.Join(dir, "rule-dir")}, 525 WatchInterval: 1 * time.Second, // use a small watch interval. 526 RetryInterval: 9999 * time.Hour, 527 }, 528 ) 529 530 // dir 531 // ├─ rule-dir -> dir2/rule-dir 532 // └─ rule1.yaml 533 // dir2 534 // ├─ rule-dir 535 // │ └─ rule4.yaml 536 // ├─ rule3-001.yaml -> rule3-source.yaml 537 // └─ rule3-source.yaml 538 // 539 // The reloader watches 2 directories: dir and dir/rule-dir. 540 testutil.Ok(t, os.WriteFile(path.Join(dir, "rule1.yaml"), []byte("rule"), os.ModePerm)) 541 testutil.Ok(t, os.WriteFile(path.Join(dir2, "rule3-source.yaml"), []byte("rule3"), os.ModePerm)) 542 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule3-source.yaml"), path.Join(dir2, "rule3-001.yaml"))) 543 testutil.Ok(t, os.WriteFile(path.Join(dir2, "rule-dir", "rule4.yaml"), []byte("rule4"), os.ModePerm)) 544 545 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 546 g := sync.WaitGroup{} 547 g.Add(1) 548 go func() { 549 defer g.Done() 550 defer cancel() 551 552 reloadsSeen := 0 553 init := false 554 for { 555 runtime.Gosched() // Ensure during testing on small machine, other go routines have chance to continue. 556 557 select { 558 case <-ctx.Done(): 559 return 560 case <-time.After(500 * time.Millisecond): 561 } 562 563 rel := reloads.Load().(int) 564 if init && rel <= reloadsSeen { 565 continue 566 } 567 init = true 568 reloadsSeen = rel 569 570 t.Log("Performing step number", rel) 571 switch rel { 572 case 0: 573 // Create rule3.yaml (symlink to rule3-001.yaml). 574 // 575 // dir 576 // ├─ rule-dir -> dir2/rule-dir 577 // ├─ rule1.yaml 578 // ├─ rule2.yaml 579 // └─ rule3.yaml -> dir2/rule3-001.yaml (*) 580 // dir2 581 // ├─ rule-dir 582 // │ └─ rule4.yaml 583 // ├─ rule3-001.yaml -> rule3-source.yaml 584 // └─ rule3-source.yaml 585 testutil.Ok(t, os.Symlink(path.Join(dir2, "rule3-001.yaml"), path.Join(dir2, "rule3.yaml"))) 586 testutil.Ok(t, os.Rename(path.Join(dir2, "rule3.yaml"), path.Join(dir, "rule3.yaml"))) 587 case 1: 588 // Update the symlinked file but do not replace the symlink in dir. 589 // 590 // fsnotify shouldn't send any event because the change happens 591 // in a directory that isn't watched but the reloader should detect 592 // the update thanks to the watch interval. 593 // 594 // dir 595 // ├─ rule-dir -> dir2/rule-dir 596 // ├─ rule1.yaml 597 // ├─ rule2.yaml 598 // └─ rule3.yaml -> dir2/rule3-001.yaml 599 // dir2 600 // ├─ rule-dir 601 // │ └─ rule4.yaml 602 // ├─ rule3-001.yaml -> rule3-source.yaml 603 // └─ rule3-source.yaml (*) 604 testutil.Ok(t, os.WriteFile(path.Join(dir2, "rule3-source.yaml"), []byte("rule3-changed"), os.ModePerm)) 605 } 606 607 if rel > 1 { 608 // All good. 609 return 610 } 611 } 612 }() 613 err = reloader.Watch(ctx) 614 cancel() 615 g.Wait() 616 617 testutil.Ok(t, err) 618 testutil.Equals(t, 2, reloads.Load().(int)) 619 } 620 621 func TestReloader_ConfigApplyWithWatchIntervalEqualsZero(t *testing.T) { 622 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 623 defer cancel() 624 625 l, err := net.Listen("tcp", "localhost:0") 626 testutil.Ok(t, err) 627 628 reloads := &atomic.Value{} 629 reloads.Store(0) 630 srv := &http.Server{} 631 srv.Handler = http.HandlerFunc(func(resp http.ResponseWriter, r *http.Request) { 632 reloads.Store(reloads.Load().(int) + 1) 633 resp.WriteHeader(http.StatusOK) 634 }) 635 go func() { _ = srv.Serve(l) }() 636 defer func() { testutil.Ok(t, srv.Close()) }() 637 638 reloadURL, err := url.Parse(fmt.Sprintf("http://%s", l.Addr().String())) 639 testutil.Ok(t, err) 640 641 dir := t.TempDir() 642 643 testutil.Ok(t, os.Mkdir(filepath.Join(dir, "in"), os.ModePerm)) 644 testutil.Ok(t, os.Mkdir(filepath.Join(dir, "out"), os.ModePerm)) 645 646 var ( 647 input = filepath.Join(dir, "in", "cfg.yaml.tmpl") 648 output = filepath.Join(dir, "out", "cfg.yaml") 649 ) 650 reloader := New(nil, nil, &Options{ 651 ReloadURL: reloadURL, 652 CfgFile: input, 653 CfgOutputFile: output, 654 WatchedDirs: nil, 655 WatchInterval: 0, // Set WatchInterval equals to 0 656 RetryInterval: 100 * time.Millisecond, 657 DelayInterval: 1 * time.Millisecond, 658 }) 659 660 testutil.Ok(t, os.WriteFile(input, []byte(` 661 config: 662 a: 1 663 b: 2 664 c: 3 665 `), os.ModePerm)) 666 667 rctx, cancel2 := context.WithCancel(ctx) 668 g := sync.WaitGroup{} 669 g.Add(1) 670 go func() { 671 defer g.Done() 672 testutil.Ok(t, reloader.Watch(rctx)) 673 }() 674 675 Outer: 676 for { 677 select { 678 case <-ctx.Done(): 679 break Outer 680 case <-time.After(300 * time.Millisecond): 681 } 682 if reloads.Load().(int) == 0 { 683 // Initial apply seen (without doing nothing). 684 f, err := os.ReadFile(output) 685 testutil.Ok(t, err) 686 testutil.Equals(t, ` 687 config: 688 a: 1 689 b: 2 690 c: 3 691 `, string(f)) 692 break 693 } 694 } 695 cancel2() 696 g.Wait() 697 // Check no reload request made 698 testutil.Equals(t, 0, reloads.Load().(int)) 699 }