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  }