github.com/outbrain/consul@v1.4.5/agent/checks/alias_test.go (about) 1 package checks 2 3 import ( 4 "fmt" 5 "reflect" 6 "sync/atomic" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/consul/agent/mock" 11 "github.com/hashicorp/consul/agent/structs" 12 "github.com/hashicorp/consul/api" 13 "github.com/hashicorp/consul/testutil/retry" 14 "github.com/hashicorp/consul/types" 15 //"github.com/stretchr/testify/require" 16 ) 17 18 // Test that we do a backoff on error. 19 func TestCheckAlias_remoteErrBackoff(t *testing.T) { 20 t.Parallel() 21 22 notify := newMockAliasNotify() 23 chkID := types.CheckID("foo") 24 rpc := &mockRPC{} 25 chk := &CheckAlias{ 26 Node: "remote", 27 ServiceID: "web", 28 CheckID: chkID, 29 Notify: notify, 30 RPC: rpc, 31 } 32 33 rpc.Reply.Store(fmt.Errorf("failure")) 34 35 chk.Start() 36 defer chk.Stop() 37 38 time.Sleep(100 * time.Millisecond) 39 if got, want := atomic.LoadUint32(&rpc.Calls), uint32(6); got > want { 40 t.Fatalf("got %d updates want at most %d", got, want) 41 } 42 43 retry.Run(t, func(r *retry.R) { 44 if got, want := notify.State(chkID), api.HealthCritical; got != want { 45 r.Fatalf("got state %q want %q", got, want) 46 } 47 }) 48 } 49 50 // No remote health checks should result in passing on the check. 51 func TestCheckAlias_remoteNoChecks(t *testing.T) { 52 t.Parallel() 53 54 notify := newMockAliasNotify() 55 chkID := types.CheckID("foo") 56 rpc := &mockRPC{} 57 chk := &CheckAlias{ 58 Node: "remote", 59 ServiceID: "web", 60 CheckID: chkID, 61 Notify: notify, 62 RPC: rpc, 63 } 64 65 rpc.Reply.Store(structs.IndexedHealthChecks{}) 66 67 chk.Start() 68 defer chk.Stop() 69 retry.Run(t, func(r *retry.R) { 70 if got, want := notify.State(chkID), api.HealthPassing; got != want { 71 r.Fatalf("got state %q want %q", got, want) 72 } 73 }) 74 } 75 76 // If the node is critical then the check is critical 77 func TestCheckAlias_remoteNodeFailure(t *testing.T) { 78 t.Parallel() 79 80 notify := newMockAliasNotify() 81 chkID := types.CheckID("foo") 82 rpc := &mockRPC{} 83 chk := &CheckAlias{ 84 Node: "remote", 85 ServiceID: "web", 86 CheckID: chkID, 87 Notify: notify, 88 RPC: rpc, 89 } 90 91 rpc.Reply.Store(structs.IndexedHealthChecks{ 92 HealthChecks: []*structs.HealthCheck{ 93 // Should ignore non-matching node 94 &structs.HealthCheck{ 95 Node: "A", 96 ServiceID: "web", 97 Status: api.HealthCritical, 98 }, 99 100 // Node failure 101 &structs.HealthCheck{ 102 Node: "remote", 103 ServiceID: "", 104 Status: api.HealthCritical, 105 }, 106 107 // Match 108 &structs.HealthCheck{ 109 Node: "remote", 110 ServiceID: "web", 111 Status: api.HealthPassing, 112 }, 113 }, 114 }) 115 116 chk.Start() 117 defer chk.Stop() 118 retry.Run(t, func(r *retry.R) { 119 if got, want := notify.State(chkID), api.HealthCritical; got != want { 120 r.Fatalf("got state %q want %q", got, want) 121 } 122 }) 123 } 124 125 // Only passing should result in passing 126 func TestCheckAlias_remotePassing(t *testing.T) { 127 t.Parallel() 128 129 notify := newMockAliasNotify() 130 chkID := types.CheckID("foo") 131 rpc := &mockRPC{} 132 chk := &CheckAlias{ 133 Node: "remote", 134 ServiceID: "web", 135 CheckID: chkID, 136 Notify: notify, 137 RPC: rpc, 138 } 139 140 rpc.Reply.Store(structs.IndexedHealthChecks{ 141 HealthChecks: []*structs.HealthCheck{ 142 // Should ignore non-matching node 143 &structs.HealthCheck{ 144 Node: "A", 145 ServiceID: "web", 146 Status: api.HealthCritical, 147 }, 148 149 // Should ignore non-matching service 150 &structs.HealthCheck{ 151 Node: "remote", 152 ServiceID: "db", 153 Status: api.HealthCritical, 154 }, 155 156 // Match 157 &structs.HealthCheck{ 158 Node: "remote", 159 ServiceID: "web", 160 Status: api.HealthPassing, 161 }, 162 }, 163 }) 164 165 chk.Start() 166 defer chk.Stop() 167 retry.Run(t, func(r *retry.R) { 168 if got, want := notify.State(chkID), api.HealthPassing; got != want { 169 r.Fatalf("got state %q want %q", got, want) 170 } 171 }) 172 } 173 174 // If any checks are critical, it should be critical 175 func TestCheckAlias_remoteCritical(t *testing.T) { 176 t.Parallel() 177 178 notify := newMockAliasNotify() 179 chkID := types.CheckID("foo") 180 rpc := &mockRPC{} 181 chk := &CheckAlias{ 182 Node: "remote", 183 ServiceID: "web", 184 CheckID: chkID, 185 Notify: notify, 186 RPC: rpc, 187 } 188 189 rpc.Reply.Store(structs.IndexedHealthChecks{ 190 HealthChecks: []*structs.HealthCheck{ 191 // Should ignore non-matching node 192 &structs.HealthCheck{ 193 Node: "A", 194 ServiceID: "web", 195 Status: api.HealthCritical, 196 }, 197 198 // Should ignore non-matching service 199 &structs.HealthCheck{ 200 Node: "remote", 201 ServiceID: "db", 202 Status: api.HealthCritical, 203 }, 204 205 // Match 206 &structs.HealthCheck{ 207 Node: "remote", 208 ServiceID: "web", 209 Status: api.HealthPassing, 210 }, 211 212 &structs.HealthCheck{ 213 Node: "remote", 214 ServiceID: "web", 215 Status: api.HealthCritical, 216 }, 217 }, 218 }) 219 220 chk.Start() 221 defer chk.Stop() 222 retry.Run(t, func(r *retry.R) { 223 if got, want := notify.State(chkID), api.HealthCritical; got != want { 224 r.Fatalf("got state %q want %q", got, want) 225 } 226 }) 227 } 228 229 // If no checks are critical and at least one is warning, then it should warn 230 func TestCheckAlias_remoteWarning(t *testing.T) { 231 t.Parallel() 232 233 notify := newMockAliasNotify() 234 chkID := types.CheckID("foo") 235 rpc := &mockRPC{} 236 chk := &CheckAlias{ 237 Node: "remote", 238 ServiceID: "web", 239 CheckID: chkID, 240 Notify: notify, 241 RPC: rpc, 242 } 243 244 rpc.Reply.Store(structs.IndexedHealthChecks{ 245 HealthChecks: []*structs.HealthCheck{ 246 // Should ignore non-matching node 247 &structs.HealthCheck{ 248 Node: "A", 249 ServiceID: "web", 250 Status: api.HealthCritical, 251 }, 252 253 // Should ignore non-matching service 254 &structs.HealthCheck{ 255 Node: "remote", 256 ServiceID: "db", 257 Status: api.HealthCritical, 258 }, 259 260 // Match 261 &structs.HealthCheck{ 262 Node: "remote", 263 ServiceID: "web", 264 Status: api.HealthPassing, 265 }, 266 267 &structs.HealthCheck{ 268 Node: "remote", 269 ServiceID: "web", 270 Status: api.HealthWarning, 271 }, 272 }, 273 }) 274 275 chk.Start() 276 defer chk.Stop() 277 retry.Run(t, func(r *retry.R) { 278 if got, want := notify.State(chkID), api.HealthWarning; got != want { 279 r.Fatalf("got state %q want %q", got, want) 280 } 281 }) 282 } 283 284 // Only passing should result in passing for node-only checks 285 func TestCheckAlias_remoteNodeOnlyPassing(t *testing.T) { 286 t.Parallel() 287 288 notify := newMockAliasNotify() 289 chkID := types.CheckID("foo") 290 rpc := &mockRPC{} 291 chk := &CheckAlias{ 292 Node: "remote", 293 CheckID: chkID, 294 Notify: notify, 295 RPC: rpc, 296 } 297 298 rpc.Reply.Store(structs.IndexedHealthChecks{ 299 HealthChecks: []*structs.HealthCheck{ 300 // Should ignore non-matching node 301 &structs.HealthCheck{ 302 Node: "A", 303 ServiceID: "web", 304 Status: api.HealthCritical, 305 }, 306 307 // Should ignore any services 308 &structs.HealthCheck{ 309 Node: "remote", 310 ServiceID: "db", 311 Status: api.HealthCritical, 312 }, 313 314 // Match 315 &structs.HealthCheck{ 316 Node: "remote", 317 Status: api.HealthPassing, 318 }, 319 }, 320 }) 321 322 chk.Start() 323 defer chk.Stop() 324 retry.Run(t, func(r *retry.R) { 325 if got, want := notify.State(chkID), api.HealthPassing; got != want { 326 r.Fatalf("got state %q want %q", got, want) 327 } 328 }) 329 } 330 331 // Only critical should result in passing for node-only checks 332 func TestCheckAlias_remoteNodeOnlyCritical(t *testing.T) { 333 t.Parallel() 334 335 notify := newMockAliasNotify() 336 chkID := types.CheckID("foo") 337 rpc := &mockRPC{} 338 chk := &CheckAlias{ 339 Node: "remote", 340 CheckID: chkID, 341 Notify: notify, 342 RPC: rpc, 343 } 344 345 rpc.Reply.Store(structs.IndexedHealthChecks{ 346 HealthChecks: []*structs.HealthCheck{ 347 // Should ignore non-matching node 348 &structs.HealthCheck{ 349 Node: "A", 350 ServiceID: "web", 351 Status: api.HealthCritical, 352 }, 353 354 // Should ignore any services 355 &structs.HealthCheck{ 356 Node: "remote", 357 ServiceID: "db", 358 Status: api.HealthCritical, 359 }, 360 361 // Match 362 &structs.HealthCheck{ 363 Node: "remote", 364 Status: api.HealthCritical, 365 }, 366 }, 367 }) 368 369 chk.Start() 370 defer chk.Stop() 371 retry.Run(t, func(r *retry.R) { 372 if got, want := notify.State(chkID), api.HealthCritical; got != want { 373 r.Fatalf("got state %q want %q", got, want) 374 } 375 }) 376 } 377 378 type mockAliasNotify struct { 379 *mock.Notify 380 } 381 382 func newMockAliasNotify() *mockAliasNotify { 383 return &mockAliasNotify{ 384 Notify: mock.NewNotify(), 385 } 386 } 387 388 func (m *mockAliasNotify) AddAliasCheck(chkID types.CheckID, serviceID string, ch chan<- struct{}) error { 389 return nil 390 } 391 392 func (m *mockAliasNotify) RemoveAliasCheck(chkID types.CheckID, serviceID string) { 393 } 394 395 func (m *mockAliasNotify) Checks() map[types.CheckID]*structs.HealthCheck { 396 return nil 397 } 398 399 // mockRPC is an implementation of RPC that can be used for tests. The 400 // atomic.Value fields can be set concurrently and will reflect on the next 401 // RPC call. 402 type mockRPC struct { 403 Calls uint32 // Read-only, number of RPC calls 404 Args atomic.Value // Read-only, the last args sent 405 406 // Write-only, the reply to send. If of type "error" then an error will 407 // be returned from the RPC call. 408 Reply atomic.Value 409 } 410 411 func (m *mockRPC) RPC(method string, args interface{}, reply interface{}) error { 412 atomic.AddUint32(&m.Calls, 1) 413 m.Args.Store(args) 414 415 // We don't adhere to blocking queries, so this helps prevent 416 // too much CPU usage on the check loop. 417 time.Sleep(10 * time.Millisecond) 418 419 // This whole machinery below sets the value of the reply. This is 420 // basically what net/rpc does internally, though much condensed. 421 replyv := reflect.ValueOf(reply) 422 if replyv.Kind() != reflect.Ptr { 423 return fmt.Errorf("RPC reply must be pointer") 424 } 425 replyv = replyv.Elem() // Get pointer value 426 replyv.Set(reflect.Zero(replyv.Type())) // Reset to zero value 427 if v := m.Reply.Load(); v != nil { 428 // Return an error if the reply is an error type 429 if err, ok := v.(error); ok { 430 return err 431 } 432 433 replyv.Set(reflect.ValueOf(v)) // Set to reply value if non-nil 434 } 435 436 return nil 437 } 438 439 // Test that local checks immediately reflect the subject states when added and 440 // don't require an update to the subject before being accurate. 441 func TestCheckAlias_localInitialStatus(t *testing.T) { 442 t.Parallel() 443 444 notify := newMockAliasNotify() 445 chkID := types.CheckID("foo") 446 rpc := &mockRPC{} 447 chk := &CheckAlias{ 448 ServiceID: "web", 449 CheckID: chkID, 450 Notify: notify, 451 RPC: rpc, 452 } 453 454 chk.Start() 455 defer chk.Stop() 456 457 // Don't touch the aliased service or it's checks (there are none but this is 458 // valid and should be consisded "passing"). 459 460 retry.Run(t, func(r *retry.R) { 461 if got, want := notify.State(chkID), api.HealthPassing; got != want { 462 r.Fatalf("got state %q want %q", got, want) 463 } 464 }) 465 }