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  }