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  }