github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/pkg/setup/health_test.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package setup
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/cenkalti/backoff/v4"
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/google/go-cmp/cmp/cmpopts"
    32  )
    33  
    34  // Used to compare two error message strings, needed because errors.Is(fmt.Errorf(text),fmt.Errorf(text)) == false
    35  type errMatcher struct {
    36  	msg string
    37  }
    38  
    39  func (e errMatcher) Error() string {
    40  	return e.msg
    41  }
    42  
    43  func (e errMatcher) Is(err error) bool {
    44  	return e.Error() == err.Error()
    45  }
    46  
    47  type mockClock struct {
    48  	t time.Time
    49  }
    50  
    51  func (m *mockClock) now() time.Time {
    52  	return m.t
    53  }
    54  
    55  func (m *mockClock) sleep(d time.Duration) {
    56  	m.t = m.t.Add(d)
    57  }
    58  
    59  func TestHealthReporterBasics(t *testing.T) {
    60  	var veryQuick = time.Nanosecond * 1
    61  	tcs := []struct {
    62  		Name               string
    63  		ReportHealth       Health
    64  		ReportMessage      string
    65  		ReportTtl          *time.Duration
    66  		ExpectedHttpStatus int
    67  	}{
    68  
    69  		{
    70  			Name:               "reports error with TTL",
    71  			ReportHealth:       HealthReady,
    72  			ReportMessage:      "should work",
    73  			ReportTtl:          &veryQuick,
    74  			ExpectedHttpStatus: 500,
    75  		},
    76  		{
    77  			Name:               "works without ttl",
    78  			ReportHealth:       HealthReady,
    79  			ReportMessage:      "should work",
    80  			ReportTtl:          nil,
    81  			ExpectedHttpStatus: 200,
    82  		},
    83  	}
    84  	for _, tc := range tcs {
    85  		tc := tc
    86  		t.Run(tc.Name, func(t *testing.T) {
    87  			bo := &mockBackoff{}
    88  			fakeClock := &mockClock{}
    89  			hs := HealthServer{
    90  				parts:          nil,
    91  				mx:             sync.Mutex{},
    92  				BackOffFactory: nil,
    93  				Clock: func() time.Time {
    94  					return fakeClock.now()
    95  				},
    96  			}
    97  			hs.BackOffFactory = func() backoff.BackOff { return bo }
    98  			reporter := hs.Reporter("Clark")
    99  			reporter.ReportHealthTtl(tc.ReportHealth, tc.ReportMessage, tc.ReportTtl)
   100  			//reporter.ReportHealth(HealthFailed, "testing")
   101  			fakeClock.sleep(time.Millisecond * 1)
   102  
   103  			testRequest := httptest.NewRequest("GET", "http://localhost/healthz", nil)
   104  			testResponse := httptest.NewRecorder()
   105  			hs.ServeHTTP(testResponse, testRequest)
   106  			if testResponse.Code != tc.ExpectedHttpStatus {
   107  				t.Errorf("wrong http status, expected %d, got %d", tc.ExpectedHttpStatus, testResponse.Code)
   108  			}
   109  		})
   110  	}
   111  }
   112  
   113  func TestHealthReporter(t *testing.T) {
   114  	tcs := []struct {
   115  		Name               string
   116  		ReportHealth       Health
   117  		ReportMessage      string
   118  		ReportTtl          *time.Duration
   119  		ExpectedHealthBody string
   120  		ExpectedStatus     int
   121  		ExpectedMetricBody string
   122  	}{
   123  		{
   124  			Name:               "reports starting",
   125  			ReportTtl:          nil,
   126  			ExpectedStatus:     500,
   127  			ExpectedHealthBody: `{"a":{"health":"starting"}}`,
   128  			ExpectedMetricBody: `# HELP background_job_ready 
   129  # TYPE background_job_ready gauge
   130  background_job_ready{name="a"} 0
   131  `,
   132  		},
   133  		{
   134  			Name:               "reports ready",
   135  			ReportHealth:       HealthReady,
   136  			ReportMessage:      "running",
   137  			ReportTtl:          nil,
   138  			ExpectedStatus:     200,
   139  			ExpectedHealthBody: `{"a":{"health":"ready","message":"running"}}`,
   140  			ExpectedMetricBody: `# HELP background_job_ready 
   141  # TYPE background_job_ready gauge
   142  background_job_ready{name="a"} 1
   143  `,
   144  		},
   145  		{
   146  			Name:               "reports failed",
   147  			ReportHealth:       HealthFailed,
   148  			ReportMessage:      "didnt work",
   149  			ReportTtl:          nil,
   150  			ExpectedStatus:     500,
   151  			ExpectedHealthBody: `{"a":{"health":"failed","message":"didnt work"}}`,
   152  			ExpectedMetricBody: `# HELP background_job_ready 
   153  # TYPE background_job_ready gauge
   154  background_job_ready{name="a"} 0
   155  `,
   156  		},
   157  	}
   158  	type Deadline struct {
   159  		deadline *time.Time
   160  		hr       *HealthReporter
   161  	}
   162  	for _, tc := range tcs {
   163  		tc := tc
   164  		t.Run(tc.Name, func(t *testing.T) {
   165  			stateChange := make(chan Deadline)
   166  			cfg := ServerConfig{
   167  				HTTP: []HTTPConfig{
   168  					{
   169  						Port: "18883",
   170  					},
   171  				},
   172  				Background: []BackgroundTaskConfig{
   173  					{
   174  						Name: "a",
   175  						Run: func(ctx context.Context, hr *HealthReporter) error {
   176  							actualDeadline := hr.ReportHealthTtl(tc.ReportHealth, tc.ReportMessage, tc.ReportTtl)
   177  							stateChange <- Deadline{deadline: actualDeadline, hr: hr}
   178  							<-ctx.Done()
   179  							return nil
   180  						},
   181  					},
   182  				},
   183  			}
   184  			ctx, cancel := context.WithCancel(context.Background())
   185  			doneCh := make(chan struct{})
   186  			go func() {
   187  				Run(ctx, cfg)
   188  				doneCh <- struct{}{}
   189  			}()
   190  			actualDeadline := <-stateChange
   191  			actualDeadline.hr.server.IsReady(actualDeadline.hr.name)
   192  			status, body := getHttp(t, "http://localhost:18883/healthz")
   193  			if status != tc.ExpectedStatus {
   194  				t.Errorf("wrong http status, expected %d, got %d", tc.ExpectedStatus, status)
   195  			}
   196  
   197  			d := cmp.Diff(body, tc.ExpectedHealthBody)
   198  			if d != "" {
   199  				t.Errorf("wrong body, diff: %s", d)
   200  			}
   201  			_, metricBody := getHttp(t, "http://localhost:18883/metrics")
   202  			if status != tc.ExpectedStatus {
   203  				t.Errorf("wrong http status, expected %d, got %d", tc.ExpectedStatus, status)
   204  			}
   205  			d = cmp.Diff(metricBody, tc.ExpectedMetricBody)
   206  			if d != "" {
   207  				t.Errorf("wrong metric body, diff: %s\ngot:\n%s\nwant:\n%s\n", d, metricBody, tc.ExpectedMetricBody)
   208  			}
   209  			cancel()
   210  			<-doneCh
   211  		})
   212  	}
   213  }
   214  
   215  type mockBackoff struct {
   216  	called   uint
   217  	resetted uint
   218  }
   219  
   220  func (b *mockBackoff) NextBackOff() time.Duration {
   221  	b.called = b.called + 1
   222  	return 1 * time.Nanosecond
   223  }
   224  
   225  func (b *mockBackoff) Reset() {
   226  	b.resetted = b.resetted + 1
   227  	return
   228  }
   229  
   230  func TestHealthReporterRetry(t *testing.T) {
   231  	type step struct {
   232  		ReportHealth  Health
   233  		ReportMessage string
   234  		ReturnError   error
   235  
   236  		ExpectReady         bool
   237  		ExpectBackoffCalled uint
   238  		ExpectResetCalled   uint
   239  	}
   240  	tcs := []struct {
   241  		Name string
   242  
   243  		Steps []step
   244  
   245  		ExpectError error
   246  	}{
   247  		{
   248  			Name: "reports healthy",
   249  			Steps: []step{
   250  				{
   251  					ReportHealth: HealthReady,
   252  
   253  					ExpectReady:       true,
   254  					ExpectResetCalled: 1,
   255  				},
   256  			},
   257  		},
   258  		{
   259  			Name: "reports unhealthy if there is an error",
   260  			Steps: []step{
   261  				{
   262  					ReturnError: fmt.Errorf("no"),
   263  
   264  					ExpectReady:         false,
   265  					ExpectBackoffCalled: 1,
   266  				},
   267  			},
   268  		},
   269  		{
   270  			Name: "doesnt retry permanent errors",
   271  			Steps: []step{
   272  				{
   273  					ReturnError: Permanent(fmt.Errorf("no")),
   274  
   275  					ExpectReady:         false,
   276  					ExpectBackoffCalled: 0,
   277  				},
   278  			},
   279  			ExpectError: errMatcher{"no"},
   280  		},
   281  		{
   282  			Name: "retries some times and resets once it's healthy",
   283  			Steps: []step{
   284  				{
   285  					ReturnError: fmt.Errorf("no"),
   286  
   287  					ExpectReady:         false,
   288  					ExpectBackoffCalled: 1,
   289  				},
   290  				{
   291  					ReturnError: fmt.Errorf("no"),
   292  
   293  					ExpectReady:         false,
   294  					ExpectBackoffCalled: 2,
   295  				},
   296  				{
   297  					ReturnError: fmt.Errorf("no"),
   298  
   299  					ExpectReady:         false,
   300  					ExpectBackoffCalled: 3,
   301  				},
   302  				{
   303  					ReportHealth: HealthReady,
   304  
   305  					ExpectReady:         true,
   306  					ExpectBackoffCalled: 3,
   307  					ExpectResetCalled:   1,
   308  				},
   309  			},
   310  		},
   311  	}
   312  	for _, tc := range tcs {
   313  		tc := tc
   314  		t.Run(tc.Name, func(t *testing.T) {
   315  			stepCh := make(chan step)
   316  			stateChange := make(chan struct{}, len(tc.Steps))
   317  			bo := &mockBackoff{}
   318  			hs := HealthServer{}
   319  			hs.BackOffFactory = func() backoff.BackOff { return bo }
   320  			ctx, cancel := context.WithCancel(context.Background())
   321  			errCh := make(chan error)
   322  			go func() {
   323  				hr := hs.Reporter("a")
   324  				errCh <- hr.Retry(ctx, func() error {
   325  					for {
   326  						select {
   327  						case <-ctx.Done():
   328  							return nil
   329  						case st := <-stepCh:
   330  							if st.ReturnError != nil {
   331  
   332  								stateChange <- struct{}{}
   333  								return st.ReturnError
   334  							}
   335  							hr.ReportHealth(st.ReportHealth, st.ReportMessage)
   336  							stateChange <- struct{}{}
   337  						}
   338  					}
   339  				})
   340  			}()
   341  			for _, st := range tc.Steps {
   342  				stepCh <- st
   343  				<-stateChange
   344  				ready := hs.IsReady("a")
   345  				if st.ExpectReady != ready {
   346  					t.Errorf("expected ready status to %t but got %t", st.ExpectReady, ready)
   347  				}
   348  				if st.ExpectBackoffCalled != bo.called {
   349  					t.Errorf("wrong number of backoffs called, expected %d, but got %d", st.ExpectBackoffCalled, bo.called)
   350  				}
   351  				if st.ExpectResetCalled != bo.resetted {
   352  					t.Errorf("wrong number of backoff resets, expected %d, but got %d", st.ExpectResetCalled, bo.resetted)
   353  				}
   354  
   355  			}
   356  			cancel()
   357  			err := <-errCh
   358  			if diff := cmp.Diff(tc.ExpectError, err, cmpopts.EquateErrors()); diff != "" {
   359  				t.Errorf("error mismatch (-want, +got):\n%s", diff)
   360  			}
   361  			close(stepCh)
   362  
   363  		})
   364  	}
   365  }
   366  
   367  func getHttp(t *testing.T, url string) (int, string) {
   368  	for i := 0; i < 10; i = i + 1 {
   369  		resp, err := http.Get(url)
   370  		if err != nil {
   371  			t.Log(err)
   372  			<-time.After(time.Second)
   373  			continue
   374  		}
   375  		body, _ := io.ReadAll(resp.Body)
   376  		return resp.StatusCode, string(body)
   377  	}
   378  	t.FailNow()
   379  	return 0, ""
   380  }