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 }