github.com/splucs/witchcraft-go-server@v1.7.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 "sync" 26 "testing" 27 "time" 28 29 "github.com/palantir/pkg/httpserver" 30 "github.com/palantir/witchcraft-go-server/config" 31 "github.com/palantir/witchcraft-go-server/conjure/witchcraft/api/health" 32 "github.com/palantir/witchcraft-go-server/status" 33 "github.com/palantir/witchcraft-go-server/status/health/periodic" 34 "github.com/palantir/witchcraft-go-server/status/reporter" 35 "github.com/palantir/witchcraft-go-server/witchcraft" 36 "github.com/stretchr/testify/assert" 37 "github.com/stretchr/testify/require" 38 ) 39 40 // TestAddHealthCheckSources verifies that custom health check sources report via the health endpoint. 41 func TestAddHealthCheckSources(t *testing.T) { 42 port, err := httpserver.AvailablePort() 43 require.NoError(t, err) 44 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 { 45 return createTestServer(t, initFn, installCfg, logOutputBuffer).WithHealth(healthCheckWithType{typ: "FOO"}, healthCheckWithType{typ: "BAR"}) 46 }) 47 48 defer func() { 49 require.NoError(t, server.Close()) 50 }() 51 defer cleanup() 52 53 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 54 require.NoError(t, err) 55 56 bytes, err := ioutil.ReadAll(resp.Body) 57 require.NoError(t, err) 58 59 var healthResults health.HealthStatus 60 err = json.Unmarshal(bytes, &healthResults) 61 require.NoError(t, err) 62 assert.Equal(t, health.HealthStatus{ 63 Checks: map[health.CheckType]health.HealthCheckResult{ 64 health.CheckType("FOO"): { 65 Type: health.CheckType("FOO"), 66 State: health.HealthStateHealthy, 67 Message: nil, 68 Params: make(map[string]interface{}), 69 }, 70 health.CheckType("BAR"): { 71 Type: health.CheckType("BAR"), 72 State: health.HealthStateHealthy, 73 Message: nil, 74 Params: make(map[string]interface{}), 75 }, 76 health.CheckType("SERVER_STATUS"): { 77 Type: health.CheckType("SERVER_STATUS"), 78 State: health.HealthStateHealthy, 79 Message: nil, 80 Params: make(map[string]interface{}), 81 }, 82 }, 83 }, healthResults) 84 85 select { 86 case err := <-serverErr: 87 require.NoError(t, err) 88 default: 89 } 90 } 91 92 // TestHealthReporter verifies the behavior of the reporter package. 93 // We create 4 health components, flip their health/unhealthy states, and ensure the aggregated states 94 // returned by the health endpoint reflect what we have set. 95 func TestHealthReporter(t *testing.T) { 96 healthReporter := reporter.NewHealthReporter() 97 98 port, err := httpserver.AvailablePort() 99 require.NoError(t, err) 100 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 { 101 return createTestServer(t, initFn, installCfg, logOutputBuffer).WithHealth(healthReporter) 102 }) 103 104 defer func() { 105 require.NoError(t, server.Close()) 106 }() 107 defer cleanup() 108 109 // Initialize health components and set their health 110 healthyComponents := []string{"COMPONENT_A", "COMPONENT_B"} 111 unhealthyComponents := []string{"COMPONENT_C", "COMPONENT_D"} 112 errString := "Something failed" 113 var wg sync.WaitGroup 114 wg.Add(len(healthyComponents) + len(unhealthyComponents)) 115 for _, n := range healthyComponents { 116 go func(healthReporter reporter.HealthReporter, name string) { 117 defer wg.Done() 118 component, err := healthReporter.InitializeHealthComponent(name) 119 if err != nil { 120 panic(fmt.Errorf("failed to initialize %s health reporter: %v", name, err)) 121 } 122 if component.Status() != reporter.StartingState { 123 panic(fmt.Errorf("expected reporter to be in REPAIRING before being marked healthy, got %s", component.Status())) 124 } 125 component.Healthy() 126 if component.Status() != reporter.HealthyState { 127 panic(fmt.Errorf("expected reporter to be in HEALTHY after being marked healthy, got %s", component.Status())) 128 } 129 }(healthReporter, n) 130 } 131 for _, n := range unhealthyComponents { 132 go func(healthReporter reporter.HealthReporter, name string) { 133 defer wg.Done() 134 component, err := healthReporter.InitializeHealthComponent(name) 135 if err != nil { 136 panic(fmt.Errorf("failed to initialize %s health reporter: %v", name, err)) 137 } 138 if component.Status() != reporter.StartingState { 139 panic(fmt.Errorf("expected reporter to be in REPAIRING before being marked healthy, got %s", component.Status())) 140 } 141 component.Error(errors.New(errString)) 142 if component.Status() != reporter.ErrorState { 143 panic(fmt.Errorf("expected reporter to be in ERROR after being marked with error, got %s", component.Status())) 144 } 145 }(healthReporter, n) 146 } 147 wg.Wait() 148 149 // Validate GetHealthComponent 150 component, ok := healthReporter.GetHealthComponent(healthyComponents[0]) 151 assert.True(t, ok) 152 assert.Equal(t, reporter.HealthyState, component.Status()) 153 154 // Validate health 155 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 156 require.NoError(t, err) 157 158 bytes, err := ioutil.ReadAll(resp.Body) 159 require.NoError(t, err) 160 161 var healthResults health.HealthStatus 162 err = json.Unmarshal(bytes, &healthResults) 163 require.NoError(t, err) 164 assert.Equal(t, health.HealthStatus{ 165 Checks: map[health.CheckType]health.HealthCheckResult{ 166 health.CheckType("COMPONENT_A"): { 167 Type: health.CheckType("COMPONENT_A"), 168 State: reporter.HealthyState, 169 Message: nil, 170 Params: make(map[string]interface{}), 171 }, 172 health.CheckType("COMPONENT_B"): { 173 Type: health.CheckType("COMPONENT_B"), 174 State: reporter.HealthyState, 175 Message: nil, 176 Params: make(map[string]interface{}), 177 }, 178 health.CheckType("COMPONENT_C"): { 179 Type: health.CheckType("COMPONENT_C"), 180 State: reporter.ErrorState, 181 Message: &errString, 182 Params: make(map[string]interface{}), 183 }, 184 health.CheckType("COMPONENT_D"): { 185 Type: health.CheckType("COMPONENT_D"), 186 State: reporter.ErrorState, 187 Message: &errString, 188 Params: make(map[string]interface{}), 189 }, 190 health.CheckType("SERVER_STATUS"): { 191 Type: health.CheckType("SERVER_STATUS"), 192 State: reporter.HealthyState, 193 Params: make(map[string]interface{}), 194 }, 195 }, 196 }, healthResults) 197 198 select { 199 case err := <-serverErr: 200 require.NoError(t, err) 201 default: 202 } 203 } 204 205 // TestPeriodicHealthSource tests that basic periodic healthcheck wiring works properly. Unit testing covers the grace 206 // period logic - this test covers the plumbing. 207 func TestPeriodicHealthSource(t *testing.T) { 208 inputSource := periodic.Source{ 209 Checks: map[health.CheckType]periodic.CheckFunc{ 210 "HEALTHY_CHECK": func(ctx context.Context) *health.HealthCheckResult { 211 return &health.HealthCheckResult{ 212 Type: "HEALTHY_CHECK", 213 State: health.HealthStateHealthy, 214 } 215 }, 216 "ERROR_CHECK": func(ctx context.Context) *health.HealthCheckResult { 217 return &health.HealthCheckResult{ 218 Type: "ERROR_CHECK", 219 State: health.HealthStateError, 220 Message: stringPtr("something went wrong"), 221 Params: map[string]interface{}{"foo": "bar"}, 222 } 223 }, 224 }, 225 } 226 expectedStatus := health.HealthStatus{Checks: map[health.CheckType]health.HealthCheckResult{ 227 "HEALTHY_CHECK": { 228 Type: "HEALTHY_CHECK", 229 State: health.HealthStateHealthy, 230 Message: nil, 231 Params: make(map[string]interface{}), 232 }, 233 "ERROR_CHECK": { 234 Type: "ERROR_CHECK", 235 State: health.HealthStateError, 236 Message: stringPtr("No successful checks during 1m0s grace period: something went wrong"), 237 Params: map[string]interface{}{"foo": "bar"}, 238 }, 239 health.CheckType("SERVER_STATUS"): { 240 Type: health.CheckType("SERVER_STATUS"), 241 State: health.HealthStateHealthy, 242 Message: nil, 243 Params: make(map[string]interface{}), 244 }, 245 }} 246 periodicHealthCheckSource := periodic.FromHealthCheckSource(context.Background(), time.Second*60, time.Millisecond*1, inputSource) 247 248 port, err := httpserver.AvailablePort() 249 require.NoError(t, err) 250 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 { 251 return createTestServer(t, initFn, installCfg, logOutputBuffer).WithHealth(periodicHealthCheckSource) 252 }) 253 254 defer func() { 255 require.NoError(t, server.Close()) 256 }() 257 defer cleanup() 258 259 // Wait for checks to run at least once 260 time.Sleep(5 * time.Millisecond) 261 262 resp, err := testServerClient().Get(fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint)) 263 require.NoError(t, err) 264 265 bytes, err := ioutil.ReadAll(resp.Body) 266 require.NoError(t, err) 267 268 var healthResults health.HealthStatus 269 err = json.Unmarshal(bytes, &healthResults) 270 require.NoError(t, err) 271 assert.Equal(t, expectedStatus, healthResults) 272 273 select { 274 case err := <-serverErr: 275 require.NoError(t, err) 276 default: 277 } 278 } 279 280 // TestHealthSharedSecret verifies that a non-empty health check shared secret is required by the endpoint when configured. 281 // If the secret is not provided or is incorrect, the endpoint returns 401 Unauthorized. 282 func TestHealthSharedSecret(t *testing.T) { 283 port, err := httpserver.AvailablePort() 284 require.NoError(t, err) 285 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 { 286 return createTestServer(t, initFn, installCfg, logOutputBuffer). 287 WithHealth(emptyHealthCheckSource{}). 288 WithDisableGoRuntimeMetrics(). 289 WithRuntimeConfig(config.Runtime{ 290 HealthChecks: config.HealthChecksConfig{ 291 SharedSecret: "top-secret", 292 }, 293 }) 294 }) 295 296 defer func() { 297 require.NoError(t, server.Close()) 298 }() 299 defer cleanup() 300 301 client := testServerClient() 302 request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://localhost:%d/%s/%s", port, basePath, status.HealthEndpoint), nil) 303 require.NoError(t, err) 304 request.Header.Set("Authorization", "Bearer top-secret") 305 resp, err := client.Do(request) 306 require.NoError(t, err) 307 require.Equal(t, http.StatusOK, resp.StatusCode) 308 309 bytes, err := ioutil.ReadAll(resp.Body) 310 require.NoError(t, err) 311 312 var healthResults health.HealthStatus 313 err = json.Unmarshal(bytes, &healthResults) 314 require.NoError(t, err) 315 assert.Equal(t, health.HealthStatus{ 316 Checks: map[health.CheckType]health.HealthCheckResult{ 317 health.CheckType("SERVER_STATUS"): { 318 Type: health.CheckType("SERVER_STATUS"), 319 State: reporter.HealthyState, 320 Params: make(map[string]interface{}), 321 }, 322 }, 323 }, healthResults) 324 325 // bad header should return 401 326 request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", "bad-secret")) 327 resp, err = client.Do(request) 328 require.NoError(t, err) 329 require.Equal(t, http.StatusUnauthorized, resp.StatusCode) 330 331 select { 332 case err := <-serverErr: 333 require.NoError(t, err) 334 default: 335 } 336 } 337 338 type emptyHealthCheckSource struct{} 339 340 func (emptyHealthCheckSource) HealthStatus(ctx context.Context) health.HealthStatus { 341 return health.HealthStatus{} 342 } 343 344 type healthCheckWithType struct { 345 typ health.CheckType 346 } 347 348 func (cwt healthCheckWithType) HealthStatus(_ context.Context) health.HealthStatus { 349 return health.HealthStatus{ 350 Checks: map[health.CheckType]health.HealthCheckResult{ 351 cwt.typ: { 352 Type: cwt.typ, 353 State: health.HealthStateHealthy, 354 Message: nil, 355 Params: make(map[string]interface{}), 356 }, 357 }, 358 } 359 } 360 361 func stringPtr(s string) *string { 362 return &s 363 }