github.com/palantir/witchcraft-go-server/v2@v2.76.0/integration/health_test.go (about) 1 // Copyright (c) 2018 Palantir Technologies. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package integration 16 17 import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "net/http" 25 "net/http/httptest" 26 "strings" 27 "sync" 28 "testing" 29 "time" 30 31 "github.com/palantir/pkg/httpserver" 32 "github.com/palantir/pkg/refreshable" 33 "github.com/palantir/witchcraft-go-health/conjure/witchcraft/api/health" 34 "github.com/palantir/witchcraft-go-health/reporter" 35 "github.com/palantir/witchcraft-go-health/sources/periodic" 36 "github.com/palantir/witchcraft-go-server/v2/config" 37 "github.com/palantir/witchcraft-go-server/v2/status" 38 "github.com/palantir/witchcraft-go-server/v2/witchcraft" 39 "github.com/stretchr/testify/assert" 40 "github.com/stretchr/testify/require" 41 ) 42 43 // TestAddHealthCheckSources verifies that custom health check sources report via the health endpoint. 44 func TestAddHealthCheckSources(t *testing.T) { 45 port, err := httpserver.AvailablePort() 46 require.NoError(t, err) 47 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, func(t *testing.T, initFn witchcraft.InitFunc, installCfg config.Install, logOutputBuffer io.Writer) *witchcraft.Server { 48 return createTestServer(t, initFn, installCfg, logOutputBuffer).WithHealth(healthCheckWithType{typ: "FOO"}, healthCheckWithType{typ: "BAR"}) 49 }) 50 51 defer func() { 52 require.NoError(t, server.Close()) 53 }() 54 defer cleanup() 55 56 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 57 require.NoError(t, err) 58 59 bytes, err := ioutil.ReadAll(resp.Body) 60 require.NoError(t, err) 61 62 var healthResults health.HealthStatus 63 err = json.Unmarshal(bytes, &healthResults) 64 require.NoError(t, err) 65 assert.Equal(t, health.HealthStatus{ 66 Checks: map[health.CheckType]health.HealthCheckResult{ 67 health.CheckType("FOO"): { 68 Type: health.CheckType("FOO"), 69 State: health.New_HealthState(health.HealthState_HEALTHY), 70 Message: nil, 71 Params: make(map[string]interface{}), 72 }, 73 health.CheckType("BAR"): { 74 Type: health.CheckType("BAR"), 75 State: health.New_HealthState(health.HealthState_HEALTHY), 76 Message: nil, 77 Params: make(map[string]interface{}), 78 }, 79 health.CheckType("CONFIG_RELOAD"): { 80 Type: health.CheckType("CONFIG_RELOAD"), 81 State: health.New_HealthState(health.HealthState_HEALTHY), 82 Params: make(map[string]interface{}), 83 }, 84 health.CheckType("SERVER_STATUS"): { 85 Type: health.CheckType("SERVER_STATUS"), 86 State: health.New_HealthState(health.HealthState_HEALTHY), 87 Message: nil, 88 Params: make(map[string]interface{}), 89 }, 90 }, 91 }, healthResults) 92 93 select { 94 case err := <-serverErr: 95 require.NoError(t, err) 96 default: 97 } 98 } 99 100 func TestServiceDependencyHealth(t *testing.T) { 101 ctx, cancel := context.WithCancel(context.Background()) 102 defer cancel() 103 104 port, err := httpserver.AvailablePort() 105 require.NoError(t, err) 106 var clients witchcraft.ConfigurableServiceDiscovery 107 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, 108 func(ctx context.Context, info witchcraft.InitInfo) (func(), error) { 109 clients = info.Clients 110 return nil, nil 111 }, 112 ioutil.Discard, 113 createTestServer, 114 ) 115 116 getHealth := func() health.HealthStatus { 117 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 118 require.NoError(t, err) 119 120 bytes, err := ioutil.ReadAll(resp.Body) 121 require.NoError(t, err) 122 123 var healthResults health.HealthStatus 124 err = json.Unmarshal(bytes, &healthResults) 125 require.NoError(t, err) 126 return healthResults 127 } 128 129 defer func() { 130 require.NoError(t, server.Close()) 131 }() 132 defer cleanup() 133 134 okServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 135 rw.WriteHeader(http.StatusOK) 136 })) 137 defer okServer.Close() 138 errServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 139 rw.WriteHeader(http.StatusServiceUnavailable) 140 })) 141 defer errServer.Close() 142 errHost := strings.TrimPrefix(errServer.URL, "http://") 143 stoppedServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 144 rw.WriteHeader(http.StatusServiceUnavailable) 145 })) 146 stoppedServer.Close() 147 stoppedHost := strings.TrimPrefix(stoppedServer.URL, "http://") 148 149 assert.Equal(t, health.HealthStatus{ 150 Checks: map[health.CheckType]health.HealthCheckResult{ 151 health.CheckType("CONFIG_RELOAD"): { 152 Type: health.CheckType("CONFIG_RELOAD"), 153 State: health.New_HealthState(health.HealthState_HEALTHY), 154 Params: make(map[string]interface{}), 155 }, 156 health.CheckType("SERVER_STATUS"): { 157 Type: health.CheckType("SERVER_STATUS"), 158 State: health.New_HealthState(health.HealthState_HEALTHY), 159 Params: make(map[string]interface{}), 160 }, 161 }, 162 }, getHealth()) 163 164 clientA, err := clients.NewHTTPClient(ctx, "serviceA") 165 require.NoError(t, err) 166 _, _ = clientA.CurrentHTTPClient().Get(okServer.URL) 167 168 require.Equal(t, health.HealthStatus{ 169 Checks: map[health.CheckType]health.HealthCheckResult{ 170 health.CheckType("CONFIG_RELOAD"): { 171 Type: health.CheckType("CONFIG_RELOAD"), 172 State: health.New_HealthState(health.HealthState_HEALTHY), 173 Params: make(map[string]interface{}), 174 }, 175 health.CheckType("SERVER_STATUS"): { 176 Type: health.CheckType("SERVER_STATUS"), 177 State: health.New_HealthState(health.HealthState_HEALTHY), 178 Params: make(map[string]interface{}), 179 }, 180 health.CheckType("SERVICE_DEPENDENCY"): { 181 Type: health.CheckType("SERVICE_DEPENDENCY"), 182 Message: stringPtr("All remote services are healthy"), 183 State: health.New_HealthState(health.HealthState_HEALTHY), 184 Params: make(map[string]interface{}), 185 }, 186 }, 187 }, getHealth()) 188 189 // Trigger two errors so failure rate is greater than half. 190 _, _ = clientA.CurrentHTTPClient().Get(errServer.URL) 191 _, _ = clientA.CurrentHTTPClient().Get(errServer.URL) 192 193 require.Equal(t, health.HealthStatus{ 194 Checks: map[health.CheckType]health.HealthCheckResult{ 195 health.CheckType("CONFIG_RELOAD"): { 196 Type: health.CheckType("CONFIG_RELOAD"), 197 State: health.New_HealthState(health.HealthState_HEALTHY), 198 Params: make(map[string]interface{}), 199 }, 200 health.CheckType("SERVER_STATUS"): { 201 Type: health.CheckType("SERVER_STATUS"), 202 State: health.New_HealthState(health.HealthState_HEALTHY), 203 Params: make(map[string]interface{}), 204 }, 205 health.CheckType("SERVICE_DEPENDENCY"): { 206 Type: health.CheckType("SERVICE_DEPENDENCY"), 207 Message: stringPtr("Some nodes of a remote service have a high failure rate"), 208 State: health.New_HealthState(health.HealthState_HEALTHY), 209 Params: map[string]interface{}{ 210 "serviceA": []interface{}{errHost}, 211 }, 212 }, 213 }, 214 }, getHealth()) 215 216 clientB, err := clients.NewHTTPClient(ctx, "serviceB") 217 require.NoError(t, err) 218 _, _ = clientB.CurrentHTTPClient().Get(stoppedServer.URL) 219 220 require.Equal(t, health.HealthStatus{ 221 Checks: map[health.CheckType]health.HealthCheckResult{ 222 health.CheckType("CONFIG_RELOAD"): { 223 Type: health.CheckType("CONFIG_RELOAD"), 224 State: health.New_HealthState(health.HealthState_HEALTHY), 225 Params: make(map[string]interface{}), 226 }, 227 health.CheckType("SERVER_STATUS"): { 228 Type: health.CheckType("SERVER_STATUS"), 229 State: health.New_HealthState(health.HealthState_HEALTHY), 230 Params: make(map[string]interface{}), 231 }, 232 health.CheckType("SERVICE_DEPENDENCY"): { 233 Type: health.CheckType("SERVICE_DEPENDENCY"), 234 Message: stringPtr("All nodes of a remote service have a high failure rate"), 235 State: health.New_HealthState(health.HealthState_WARNING), 236 Params: map[string]interface{}{ 237 "serviceA": []interface{}{errHost}, 238 "serviceB": []interface{}{stoppedHost}, 239 }, 240 }, 241 }, 242 }, getHealth()) 243 244 select { 245 case err := <-serverErr: 246 require.NoError(t, err) 247 default: 248 } 249 } 250 251 // TestHealthReporter verifies the behavior of the reporter package. 252 // We create 4 health components, flip their health/unhealthy states, and ensure the aggregated states 253 // returned by the health endpoint reflect what we have set. 254 func TestHealthReporter(t *testing.T) { 255 healthReporter := reporter.NewHealthReporter() 256 257 port, err := httpserver.AvailablePort() 258 require.NoError(t, err) 259 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, func(t *testing.T, initFn witchcraft.InitFunc, installCfg config.Install, logOutputBuffer io.Writer) *witchcraft.Server { 260 return createTestServer(t, initFn, installCfg, logOutputBuffer).WithHealth(healthReporter) 261 }) 262 263 defer func() { 264 require.NoError(t, server.Close()) 265 }() 266 defer cleanup() 267 268 // Initialize health components and set their health 269 healthyComponents := []string{"COMPONENT_A", "COMPONENT_B"} 270 unhealthyComponents := []string{"COMPONENT_C", "COMPONENT_D"} 271 errString := "Something failed" 272 var wg sync.WaitGroup 273 wg.Add(len(healthyComponents) + len(unhealthyComponents)) 274 for _, n := range healthyComponents { 275 go func(healthReporter reporter.HealthReporter, name string) { 276 defer wg.Done() 277 component, err := healthReporter.InitializeHealthComponent(name) 278 if err != nil { 279 panic(fmt.Errorf("failed to initialize %s health reporter: %v", name, err)) 280 } 281 if component.Status() != health.HealthState_REPAIRING { 282 panic(fmt.Errorf("expected reporter to be in REPAIRING before being marked healthy, got %s", component.Status())) 283 } 284 component.Healthy() 285 if component.Status() != health.HealthState_HEALTHY { 286 panic(fmt.Errorf("expected reporter to be in HEALTHY after being marked healthy, got %s", component.Status())) 287 } 288 }(healthReporter, n) 289 } 290 for _, n := range unhealthyComponents { 291 go func(healthReporter reporter.HealthReporter, name string) { 292 defer wg.Done() 293 component, err := healthReporter.InitializeHealthComponent(name) 294 if err != nil { 295 panic(fmt.Errorf("failed to initialize %s health reporter: %v", name, err)) 296 } 297 if component.Status() != health.HealthState_REPAIRING { 298 panic(fmt.Errorf("expected reporter to be in REPAIRING before being marked healthy, got %s", component.Status())) 299 } 300 component.Error(errors.New(errString)) 301 if component.Status() != health.HealthState_ERROR { 302 panic(fmt.Errorf("expected reporter to be in ERROR after being marked with error, got %s", component.Status())) 303 } 304 }(healthReporter, n) 305 } 306 wg.Wait() 307 308 // Validate GetHealthComponent 309 component, ok := healthReporter.GetHealthComponent(healthyComponents[0]) 310 assert.True(t, ok) 311 assert.Equal(t, health.HealthState_HEALTHY, component.Status()) 312 313 // Validate health 314 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 315 require.NoError(t, err) 316 317 bytes, err := ioutil.ReadAll(resp.Body) 318 require.NoError(t, err) 319 320 var healthResults health.HealthStatus 321 err = json.Unmarshal(bytes, &healthResults) 322 require.NoError(t, err) 323 assert.Equal(t, health.HealthStatus{ 324 Checks: map[health.CheckType]health.HealthCheckResult{ 325 health.CheckType("COMPONENT_A"): { 326 Type: health.CheckType("COMPONENT_A"), 327 State: health.New_HealthState(health.HealthState_HEALTHY), 328 Message: nil, 329 Params: make(map[string]interface{}), 330 }, 331 health.CheckType("COMPONENT_B"): { 332 Type: health.CheckType("COMPONENT_B"), 333 State: health.New_HealthState(health.HealthState_HEALTHY), 334 Message: nil, 335 Params: make(map[string]interface{}), 336 }, 337 health.CheckType("COMPONENT_C"): { 338 Type: health.CheckType("COMPONENT_C"), 339 State: health.New_HealthState(health.HealthState_ERROR), 340 Message: &errString, 341 Params: make(map[string]interface{}), 342 }, 343 health.CheckType("COMPONENT_D"): { 344 Type: health.CheckType("COMPONENT_D"), 345 State: health.New_HealthState(health.HealthState_ERROR), 346 Message: &errString, 347 Params: make(map[string]interface{}), 348 }, 349 health.CheckType("CONFIG_RELOAD"): { 350 Type: health.CheckType("CONFIG_RELOAD"), 351 State: health.New_HealthState(health.HealthState_HEALTHY), 352 Params: make(map[string]interface{}), 353 }, 354 health.CheckType("SERVER_STATUS"): { 355 Type: health.CheckType("SERVER_STATUS"), 356 State: health.New_HealthState(health.HealthState_HEALTHY), 357 Params: make(map[string]interface{}), 358 }, 359 }, 360 }, healthResults) 361 362 select { 363 case err := <-serverErr: 364 require.NoError(t, err) 365 default: 366 } 367 } 368 369 // TestPeriodicHealthSource tests that basic periodic healthcheck wiring works properly. Unit testing covers the grace 370 // period logic - this test covers the plumbing. 371 func TestPeriodicHealthSource(t *testing.T) { 372 inputSource := periodic.Source{ 373 Checks: map[health.CheckType]periodic.CheckFunc{ 374 "HEALTHY_CHECK": func(ctx context.Context) *health.HealthCheckResult { 375 return &health.HealthCheckResult{ 376 Type: "HEALTHY_CHECK", 377 State: health.New_HealthState(health.HealthState_HEALTHY), 378 } 379 }, 380 "ERROR_CHECK": func(ctx context.Context) *health.HealthCheckResult { 381 return &health.HealthCheckResult{ 382 Type: "ERROR_CHECK", 383 State: health.New_HealthState(health.HealthState_ERROR), 384 Message: stringPtr("something went wrong"), 385 Params: map[string]interface{}{"foo": "bar"}, 386 } 387 }, 388 }, 389 } 390 expectedStatus := health.HealthStatus{Checks: map[health.CheckType]health.HealthCheckResult{ 391 "HEALTHY_CHECK": { 392 Type: "HEALTHY_CHECK", 393 State: health.New_HealthState(health.HealthState_HEALTHY), 394 Message: nil, 395 Params: make(map[string]interface{}), 396 }, 397 "ERROR_CHECK": { 398 Type: "ERROR_CHECK", 399 State: health.New_HealthState(health.HealthState_REPAIRING), 400 Message: stringPtr("No successful checks during 1m0s grace period: something went wrong"), 401 Params: map[string]interface{}{"foo": "bar"}, 402 }, 403 health.CheckType("CONFIG_RELOAD"): { 404 Type: health.CheckType("CONFIG_RELOAD"), 405 State: health.New_HealthState(health.HealthState_HEALTHY), 406 Params: make(map[string]interface{}), 407 }, 408 health.CheckType("SERVER_STATUS"): { 409 Type: health.CheckType("SERVER_STATUS"), 410 State: health.New_HealthState(health.HealthState_HEALTHY), 411 Message: nil, 412 Params: make(map[string]interface{}), 413 }, 414 }} 415 periodicHealthCheckSource := periodic.FromHealthCheckSource(context.Background(), time.Second*60, time.Millisecond*1, inputSource) 416 417 port, err := httpserver.AvailablePort() 418 require.NoError(t, err) 419 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, func(t *testing.T, initFn witchcraft.InitFunc, installCfg config.Install, logOutputBuffer io.Writer) *witchcraft.Server { 420 return createTestServer(t, initFn, installCfg, logOutputBuffer).WithHealth(periodicHealthCheckSource) 421 }) 422 423 defer func() { 424 require.NoError(t, server.Close()) 425 }() 426 defer cleanup() 427 428 // Wait for checks to run at least once 429 time.Sleep(5 * time.Millisecond) 430 431 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 432 require.NoError(t, err) 433 434 bytes, err := ioutil.ReadAll(resp.Body) 435 require.NoError(t, err) 436 437 var healthResults health.HealthStatus 438 err = json.Unmarshal(bytes, &healthResults) 439 require.NoError(t, err) 440 assert.Equal(t, expectedStatus, healthResults) 441 442 select { 443 case err := <-serverErr: 444 require.NoError(t, err) 445 default: 446 } 447 } 448 449 // TestHealthSharedSecret verifies that a non-empty health check shared secret is required by the endpoint when configured. 450 // If the secret is not provided or is incorrect, the endpoint returns 401 Unauthorized. 451 func TestHealthSharedSecret(t *testing.T) { 452 port, err := httpserver.AvailablePort() 453 require.NoError(t, err) 454 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, func(t *testing.T, initFn witchcraft.InitFunc, installCfg config.Install, logOutputBuffer io.Writer) *witchcraft.Server { 455 return createTestServer(t, initFn, installCfg, logOutputBuffer). 456 WithHealth(emptyHealthCheckSource{}). 457 WithDisableGoRuntimeMetrics(). 458 WithRuntimeConfig(config.Runtime{ 459 HealthChecks: config.HealthChecksConfig{ 460 SharedSecret: "top-secret", 461 }, 462 }) 463 }) 464 465 defer func() { 466 require.NoError(t, server.Close()) 467 }() 468 defer cleanup() 469 470 client := testServerClient() 471 request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint), nil) 472 require.NoError(t, err) 473 request.Header.Set("Authorization", "Bearer top-secret") 474 resp, err := client.Do(request) 475 require.NoError(t, err) 476 require.Equal(t, http.StatusOK, resp.StatusCode) 477 478 bytes, err := ioutil.ReadAll(resp.Body) 479 require.NoError(t, err) 480 481 var healthResults health.HealthStatus 482 err = json.Unmarshal(bytes, &healthResults) 483 require.NoError(t, err) 484 assert.Equal(t, health.HealthStatus{ 485 Checks: map[health.CheckType]health.HealthCheckResult{ 486 health.CheckType("CONFIG_RELOAD"): { 487 Type: health.CheckType("CONFIG_RELOAD"), 488 State: health.New_HealthState(health.HealthState_HEALTHY), 489 Params: make(map[string]interface{}), 490 }, 491 health.CheckType("SERVER_STATUS"): { 492 Type: health.CheckType("SERVER_STATUS"), 493 State: health.New_HealthState(health.HealthState_HEALTHY), 494 Params: make(map[string]interface{}), 495 }, 496 }, 497 }, healthResults) 498 499 // bad header should return 401 500 request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", "bad-secret")) 501 resp, err = client.Do(request) 502 require.NoError(t, err) 503 require.Equal(t, http.StatusForbidden, resp.StatusCode) 504 505 select { 506 case err := <-serverErr: 507 require.NoError(t, err) 508 default: 509 } 510 } 511 512 // TestRuntimeConfigReloadHealth verifies that runtime configuration that is invalid when strict unmarshal mode is true 513 // does not produces an error health check if strict unmarshal mode is not specified (since default value is false). 514 func TestRuntimeConfigReloadHealthWithStrictUnmarshalFalse(t *testing.T) { 515 port, err := httpserver.AvailablePort() 516 require.NoError(t, err) 517 518 validCfgYML := `logging: 519 level: info 520 ` 521 invalidCfgYML := ` 522 invalid-key: invalid-value 523 ` 524 runtimeConfigRefreshable := refreshable.NewDefaultRefreshable([]byte(validCfgYML)) 525 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, func(t *testing.T, initFn witchcraft.InitFunc, installCfg config.Install, logOutputBuffer io.Writer) *witchcraft.Server { 526 return createTestServer(t, initFn, installCfg, logOutputBuffer). 527 WithRuntimeConfigProvider(runtimeConfigRefreshable). 528 WithDisableGoRuntimeMetrics() 529 }) 530 531 defer func() { 532 require.NoError(t, server.Close()) 533 }() 534 defer cleanup() 535 536 client := testServerClient() 537 request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint), nil) 538 require.NoError(t, err) 539 resp, err := client.Do(request) 540 require.NoError(t, err) 541 542 bytes, err := ioutil.ReadAll(resp.Body) 543 require.NoError(t, err) 544 545 var healthResults health.HealthStatus 546 err = json.Unmarshal(bytes, &healthResults) 547 require.NoError(t, err) 548 assert.Equal(t, health.HealthStatus{ 549 Checks: map[health.CheckType]health.HealthCheckResult{ 550 health.CheckType("CONFIG_RELOAD"): { 551 Type: health.CheckType("CONFIG_RELOAD"), 552 State: health.New_HealthState(health.HealthState_HEALTHY), 553 Params: make(map[string]interface{}), 554 }, 555 health.CheckType("SERVER_STATUS"): { 556 Type: health.CheckType("SERVER_STATUS"), 557 State: health.New_HealthState(health.HealthState_HEALTHY), 558 Params: make(map[string]interface{}), 559 }, 560 }, 561 }, healthResults) 562 563 // write invalid runtime config and observe health check go unhealthy 564 err = runtimeConfigRefreshable.Update([]byte(invalidCfgYML)) 565 require.NoError(t, err) 566 time.Sleep(500 * time.Millisecond) 567 568 request, err = http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint), nil) 569 require.NoError(t, err) 570 resp, err = client.Do(request) 571 require.NoError(t, err) 572 573 bytes, err = ioutil.ReadAll(resp.Body) 574 require.NoError(t, err) 575 576 err = json.Unmarshal(bytes, &healthResults) 577 require.NoError(t, err) 578 assert.Equal(t, health.HealthStatus{ 579 Checks: map[health.CheckType]health.HealthCheckResult{ 580 health.CheckType("CONFIG_RELOAD"): { 581 Type: health.CheckType("CONFIG_RELOAD"), 582 State: health.New_HealthState(health.HealthState_HEALTHY), 583 Params: make(map[string]interface{}), 584 }, 585 health.CheckType("SERVER_STATUS"): { 586 Type: health.CheckType("SERVER_STATUS"), 587 State: health.New_HealthState(health.HealthState_HEALTHY), 588 Params: make(map[string]interface{}), 589 }, 590 }, 591 }, healthResults) 592 593 select { 594 case err := <-serverErr: 595 require.NoError(t, err) 596 default: 597 } 598 } 599 600 // TestRuntimeConfigReloadHealth verifies that runtime configuration that is invalid when strict unmarshal mode is true 601 // produces an error health check. 602 func TestRuntimeConfigReloadHealthWithStrictUnmarshalTrue(t *testing.T) { 603 port, err := httpserver.AvailablePort() 604 require.NoError(t, err) 605 606 validCfgYML := `logging: 607 level: info 608 ` 609 invalidCfgYML := ` 610 invalid-key: invalid-value 611 ` 612 runtimeConfigRefreshable := refreshable.NewDefaultRefreshable([]byte(validCfgYML)) 613 server, serverErr, cleanup := createAndRunCustomTestServer(t, port, port, nil, ioutil.Discard, func(t *testing.T, initFn witchcraft.InitFunc, installCfg config.Install, logOutputBuffer io.Writer) *witchcraft.Server { 614 return createTestServer(t, initFn, installCfg, logOutputBuffer). 615 WithRuntimeConfigProvider(runtimeConfigRefreshable). 616 WithDisableGoRuntimeMetrics(). 617 WithStrictUnmarshalConfig() 618 }) 619 620 defer func() { 621 require.NoError(t, server.Close()) 622 }() 623 defer cleanup() 624 625 client := testServerClient() 626 request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint), nil) 627 require.NoError(t, err) 628 resp, err := client.Do(request) 629 require.NoError(t, err) 630 631 bytes, err := ioutil.ReadAll(resp.Body) 632 require.NoError(t, err) 633 634 var healthResults health.HealthStatus 635 err = json.Unmarshal(bytes, &healthResults) 636 require.NoError(t, err) 637 assert.Equal(t, health.HealthStatus{ 638 Checks: map[health.CheckType]health.HealthCheckResult{ 639 health.CheckType("CONFIG_RELOAD"): { 640 Type: health.CheckType("CONFIG_RELOAD"), 641 State: health.New_HealthState(health.HealthState_HEALTHY), 642 Params: make(map[string]interface{}), 643 }, 644 health.CheckType("SERVER_STATUS"): { 645 Type: health.CheckType("SERVER_STATUS"), 646 State: health.New_HealthState(health.HealthState_HEALTHY), 647 Params: make(map[string]interface{}), 648 }, 649 }, 650 }, healthResults) 651 652 // write invalid runtime config and observe health check go unhealthy 653 err = runtimeConfigRefreshable.Update([]byte(invalidCfgYML)) 654 require.NoError(t, err) 655 time.Sleep(500 * time.Millisecond) 656 657 request, err = http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint), nil) 658 require.NoError(t, err) 659 resp, err = client.Do(request) 660 require.NoError(t, err) 661 662 bytes, err = ioutil.ReadAll(resp.Body) 663 require.NoError(t, err) 664 665 err = json.Unmarshal(bytes, &healthResults) 666 require.NoError(t, err) 667 assert.Equal(t, health.HealthStatus{ 668 Checks: map[health.CheckType]health.HealthCheckResult{ 669 health.CheckType("CONFIG_RELOAD"): { 670 Type: health.CheckType("CONFIG_RELOAD"), 671 State: health.New_HealthState(health.HealthState_ERROR), 672 Params: make(map[string]interface{}), 673 Message: stringPtr("Refreshable validation failed, please look at service logs for more information."), 674 }, 675 health.CheckType("SERVER_STATUS"): { 676 Type: health.CheckType("SERVER_STATUS"), 677 State: health.New_HealthState(health.HealthState_HEALTHY), 678 Params: make(map[string]interface{}), 679 }, 680 }, 681 }, healthResults) 682 683 select { 684 case err := <-serverErr: 685 require.NoError(t, err) 686 default: 687 } 688 } 689 690 type emptyHealthCheckSource struct{} 691 692 func (emptyHealthCheckSource) HealthStatus(ctx context.Context) health.HealthStatus { 693 return health.HealthStatus{} 694 } 695 696 type healthCheckWithType struct { 697 typ health.CheckType 698 } 699 700 func (cwt healthCheckWithType) HealthStatus(_ context.Context) health.HealthStatus { 701 return health.HealthStatus{ 702 Checks: map[health.CheckType]health.HealthCheckResult{ 703 cwt.typ: { 704 Type: cwt.typ, 705 State: health.New_HealthState(health.HealthState_HEALTHY), 706 Message: nil, 707 Params: make(map[string]interface{}), 708 }, 709 }, 710 } 711 } 712 713 func stringPtr(s string) *string { 714 return &s 715 }